当前位置: 首页 > news >正文

[muduo] Buffer缓冲区 | TcpServer | Reactor模式

第七章:Buffer(缓冲区)

欢迎回到Muduo库的探索之旅!

在第六章《TcpConnection》中,我们了解了TcpConnection类如何表示单个已建立的网络连接,并通过MessageCallback等回调接口处理输入数据,使用send()等方法发送输出数据。

但"数据"究竟是什么?

网络数据以原始字节形式到达,可能以不完整的块状形式到达。

  • 我们需要一种高效存储、管理和处理这些字节的方式——既要处理来自网络尚未处理的输入数据,也要管理准备发送但未完全传输的输出数据。

    使用固定大小的字符数组(char[])或原始指针处理这些数据不仅繁琐且易出错,尤其在处理变长消息或部分读写时。频繁的内存重新分配或数据拷贝会导致效率低下

这正是muduo::net::Buffer类存在的意义。


Buffer解决的问题

当数据到达网络套接字时,操作系统会将其存入内核缓冲区。

TcpConnection调用read()时,数据从内核缓冲区拷贝到应用层内存中的缓冲区

类似地,调用write()时,数据从应用层缓冲区拷贝到内核发送缓冲区。

Buffer类是Muduo用于管理应用层网络I/O缓冲区的灵活高效工具,专门设计用于处理:

  • 追加输入数据:数据到达时直接追加到缓冲区尾部
  • 提取已处理数据:从前端高效移除已处理数据
  • 队列化输出数据:发送前将数据暂存于输出缓冲区
  • 最小化拷贝操作:使用readv(2)和内存管理技术避免不必要拷贝
  • 动态容量调整:按需扩容以适应大数据量
  • 便捷操作:提供窥视数据、查找模式(如换行符)、处理网络字节序等功能

可以Buffer视为灵活可扩展的字节管道或队列——数据从一端进入(通常通过套接字读取),从另一端消耗(处理或发送时)。


Buffer:灵活的字节队列

muduo::net::Buffer本质上是一个带有位置指针的动态字节数组,通过指针标记可读数据范围和可写空间位置。

关键设计要点:

  1. 内部std::vector<char>:使用动态数组存储字节,支持自动扩容
  2. 双索引机制
    • readerIndex_:标记可读数据起始位置
    • writerIndex_:标记可读数据结束位置(也是可写空间起始位置)
  3. 缓冲区区域划分
    • 可预留空间(Prependable Bytes)readerIndex_前的空间,用于预置数据(如消息长度头)
    • 可读数据区(Readable Bytes)readerIndex_writerIndex_之间的实际内容
    • 可写空间(Writable Bytes)writerIndex_到缓冲区末尾的空闲区域

在这里插入图片描述

(Buffer结构示意图)

  1. 追加(append:向可写空间尾部添加数据,自动推进writerIndex_
  2. 提取(retrieve:从可读区头部移除数据,推进readerIndex_
  3. 窥视peek:获取可读数据起始指针而不修改缓冲区状态
  4. 高效读取(readFd:使用readv(2)和栈缓冲区优化FD读取
  5. 数据查找findCRLF()findEOL()等方法定位消息边界
  6. 字节序处理appendInt32readInt32等方法自动处理网络字节序

Buffer使用示例

MessageCallback(处理输入缓冲区)和调用send()前(处理输出缓冲区)常与Buffer交互:

#include "muduo/net/Buffer.h"
#include <cstdio>int main() {muduo::net::Buffer buffer;printf("初始状态: 可读字节=%zu, 可写字节=%zu, 可预留空间=%zu\n",buffer.readableBytes(), buffer.writableBytes(), buffer.prependableBytes());// 追加数据const char* data1 = "你好,";buffer.append(data1, strlen(data1));printf("追加'你好,'后: 可读=%zu, 可写=%zu, 可预留=%zu\n",buffer.readableBytes(), buffer.writableBytes(), buffer.prependableBytes());const char* data2 = "世界!\n";buffer.append(data2, strlen(data2));printf("追加'世界!\\n'后: 可读=%zu, 可写=%zu, 可预留=%zu\n",buffer.readableBytes(), buffer.writableBytes(), buffer.prependableBytes());// 窥视数据printf("窥视数据: %s (首%zu字节)\n", buffer.peek(), buffer.readableBytes());// 提取部分数据size_t 提取长度 = 7; // "你好,"的字节长度std::string 部分数据(buffer.peek(), 提取长度);buffer.retrieve(提取长度);printf("提取%zu字节: '%s'\n", 提取长度, 部分数据.c_str());printf("提取后: 可读=%zu, 可写=%zu, 可预留=%zu\n",buffer.readableBytes(), buffer.writableBytes(), buffer.prependableBytes());// 提取剩余数据std::string 剩余数据 = buffer.retrieveAllAsString();printf("提取剩余数据: '%s'\n", 剩余数据.c_str());printf("全部提取后: 可读=%zu, 可写=%zu, 可预留=%zu\n",buffer.readableBytes(), buffer.writableBytes(), buffer.prependableBytes());return 0;
}

(需包含Muduo头文件并链接库)

作用

网络编程中需要处理数据收发的不对称性,发送方可能一次性写入大量数据,而接收方可能分多次读取

缓冲区作为中间容器,平衡这种速度差异,防止数据丢失或阻塞。

代码

初始化时缓冲区状态:

  • 可读字节(readableBytes):0(无数据可读)
  • 可写字节(writableBytes):默认容量(通常1024字节)
  • 可预留空间(prependableBytes):8字节(用于协议头等场景)

数据追加演示:

buffer.append("你好,", 7);  // 写入7字节中文
// 可读字节+7,可写字节-7
buffer.append("世界!\n", 8); // 再写入8字节
// 可读字节增至15,可写空间继续减少

数据读取过程:

buffer.peek();  // 查看数据但不移动读指针
buffer.retrieve(7);  // 取出前7字节("你好,")
// 读指针后移7字节,可读字节减至8
buffer.retrieveAllAsString();  // 取出剩余"世界!\n"
// 读指针重置,可读字节归零

特点

内存管理优化:

  • 内部使用vector自动扩容,避免频繁内存分配
  • 读指针移动时不会立即清理内存,而是复用空间
  • 预留空间允许在数据头部插入元信息(如包长度)

性能指标:

  • append操作时间复杂度O(1)(不考虑扩容时)
  • retrieve操作仅移动指针,无数据拷贝
  • 连续内存访问利于CPU缓存命中

典型应用场景:

  • 接收HTTP请求时暂存不完整报文
  • 发送大文件时分批写入缓冲区
  • 处理TCP粘包时临时保存半包数据

调试技巧:

  • 使用readableBytes()判断是否收到完整消息
  • prependableBytes检查协议头空间是否充足
  • writableBytes监控内存使用情况

运行
在这里插入图片描述

处理行式数据的典型模式:

void onMessage(const muduo::net::TcpConnectionPtr& conn,muduo::net::Buffer* buf,muduo::Timestamp receiveTime) {while (buf->readableBytes() > 0) {const char* 换行符 = buf->findEOL(); // 查找换行符if (换行符) {std::string 行数据(buf->peek(), 换行符 - buf->peek() + 1);buf->retrieveUntil(换行符 + 1); // 提取至换行符(含)} else {break; // 等待更多数据}}
}

Buffer内存管理机制

核心实现基于std::vector<char>和双索引机制:

class Buffer : public muduo::copyable {public:static const size_t kCheapPrepend = 8; // 头部预留空间static const size_t kInitialSize = 1024; // 初始缓冲区大小explicit Buffer(size_t initialSize = kInitialSize): buffer_(kCheapPrepend + initialSize),readerIndex_(kCheapPrepend),writerIndex_(kCheapPrepend) {}// 核心方法实现void makeSpace(size_t len) {if (可写空间 + 可预留空间 < len + kCheapPrepend) {buffer_.resize(writerIndex_ + len); // 扩容} else {std::copy(begin() + readerIndex_, begin() + writerIndex_, begin() + kCheapPrepend); // 数据前移复用空间readerIndex_ = kCheapPrepend;writerIndex_ = readerIndex_ + 可读字节数;}}ssize_t readFd(int fd, int* savedErrno) {char 栈缓冲区[65536];struct iovec vec[2];vec[0].iov_base = begin() + writerIndex_;vec[0].iov_len = 可写字节数;vec[1].iov_base = 栈缓冲区;vec[1].iov_len = sizeof(栈缓冲区);ssize_t n = sockets::readv(fd, vec, (可写空间 < sizeof(栈缓冲区)) ? 2 : 1);if (n > 可写空间) {append(栈缓冲区, n - 可写空间); // 处理栈缓冲数据}return n;}
};

(简化后的核心实现)

核心数据结构

使用std::vector<char>作为底层存储容器,包含三个关键变量:

  • buffer_:动态数组,实际存储数据
  • readerIndex_:当前可读数据起始位置
  • writerIndex_:当前可写数据起始位置

空间管理

头部预留8字节(kCheapPrepend),用于后续协议处理(如添加消息长度字段)。

初始缓冲区大小为1024字节(kInitialSize),可通过构造函数调整。

扩容

当需要写入数据时,通过makeSpace()方法处理空间不足的情况:

  • 总剩余空间不足时:直接扩容vector
  • 剩余空间足够但分散时:将已读数据前移复用头部空间

数学表达扩容条件:

剩余空间 = (buffer_.size() - writerIndex_) + (readerIndex_ - kCheapPrepend)

高效I/O操作

readFd()实现零拷贝优化:

  • 使用iovec结构体数组,同时指向缓冲区剩余空间和栈临时空间
  • 当缓冲区空间不足时,自动将超额数据存入栈缓冲区后追加到主缓冲区

在这里插入图片描述

readFd方法通过readv(2)系统调用实现高效读取:

  1. 优先填充缓冲区可写空间
  2. 超额数据存入栈缓冲区后追加
  3. 最小化内存拷贝和系统调用次数

总结

功能描述技术优势
动态数组基于std::vector<char>实现自动扩容,适应变长消息
双索引机制readerIndex_writerIndex_管理数据范围高效数据追加/提取,避免内存拷贝
空间预分配头部预留8字节(kCheapPrepend)方便添加协议头
读操作优化readFd使用readv(2)和栈缓冲区单次系统调用读取多块内存,降低上下文切换
零拷贝处理peek()直接返回数据指针避免临时内存分配
字节序转换appendInt32/readInt32自动转换网络字节序简化协议编解码
消息边界处理findCRLF()查找特定分隔符支持基于分隔符的协议解析

结论

muduo::net::Buffer通过动态数组、双索引机制和系统调用优化,实现了高效网络数据管理。

作为TcpConnection的核心数据载体,它既承载着输入数据的暂存处理,也负责输出数据队列化管理,是多线程网络编程中不可或缺的基础组件。

下一章我们将深入探讨TcpServer类,了解其如何管理连接池和多线程事件循环

第八章:TcpServer


第八章:TcpServer(TCP服务器)

我们已经掌握了核心组件:

  • 作为单线程调度器EventLoop(第一章:事件循环)

  • 用于并发运行EventLoopThread(第二章:线程)

  • 将文件描述符与EventLoop关联Channel(第三章:通道)

  • 高效等待事件的Poller(第四章:轮询器)

  • 以及处理定时器TimerQueue(第五章:定时器队列)

  • 今天我们还学习了管理单个TCP连接TcpConnection(第六章:TCP连接)

  • 及其内部数据处理Buffer(第七章:缓冲区)

现在我们需要将这些组件组合起来,构建一个能够监听入站连接、接受连接并同时管理多个TcpConnection服务器

如何编写程序在特定网络地址和端口监听客户端连接,并高效处理多客户端并发请求?

手动操作套接字(socketbindlistenaccept)、为每个连接创建线程(这在客户端量大时效率低下)以及分配工作非常复杂。

这正是muduo::net::TcpServer要解决的问题。


TcpServer解决的问题

TcpServer类是Muduo中处理TCP网络服务端功能的高级抽象,主要职责包括:

  1. 监听:绑定到指定IP地址和端口,将套接字置于监听状态
  2. 接收:检测新客户端连接到达并通过accept(2)系统调用接收,获得该客户端的新套接字描述符
  3. 分发:将新客户端套接字分配给可用EventLoop(通常来自线程池)
  4. 连接管理:为每个接受的客户端套接字创建TcpConnection对象并跟踪所有活动连接
  5. 生命周期管理:处理客户端连接/断开时的对象创建与销毁

可以将TcpServer视为服务的"前台"

  • 监听门铃(新连接)
  • 开门(接受连接)
  • 将访客引导至特定"房间"(分配到EventLoop线程)
  • 并维护当前服务中的客户名单(connections_)。

TcpServer:服务门户

muduo::net::TcpServer是构建TCP服务端应用的核心类,关键设计要素包括:

  1. 地址/端口监听:绑定到指定的muduo::net::InetAddress(IP+端口)
  2. 内部接收器(Acceptor):管理监听套接字并通过Channel在主EventLoop上接收新连接通知
  3. 接收器循环:运行AcceptorEventLoop(称为"主循环"或"接收器循环"),专门处理新连接接收
  4. 事件循环线程池:通过EventLoopThreadPool(第九章:线程池)管理多个工作EventLoop线程
  5. 连接分发策略:采用轮询等算法将新连接分配给线程池中的EventLoop
  6. 连接映射表:维护connections_哈希表,以唯一名称(如"服务名-IP:Port#连接ID")映射到TcpConnectionPtr
  7. 回调工厂:为每个新连接设置用户定义的ConnectionCallbackMessageCallback
  8. 启动方法:调用start()启动监听和线程池

使用TcpServer构建回声服务器

以下示例展示如何构建回声服务器(将接收数据原样返回):

#include "muduo/net/TcpServer.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/InetAddress.h"
#include "muduo/base/Logging.h"
#include <cstdio>// 连接状态回调
void onConnection(const muduo::net::TcpConnectionPtr& conn) {if (conn->connected()) {LOG_INFO << "连接建立: " << conn->name();} else {LOG_INFO << "连接断开: " << conn->name();}
}// 消息处理回调(回声逻辑)
void onMessage(const muduo::net::TcpConnectionPtr& conn,muduo::net::Buffer* buf,muduo::Timestamp receiveTime) {LOG_INFO << "从连接" << conn->name() << "收到" << buf->readableBytes() << "字节";conn->send(buf); // 高效回传数据
}int main() {LOG_INFO << "主进程ID: " << getpid();muduo::net::EventLoop loop; // 接收器循环muduo::net::InetAddress listenAddr(9988); // 监听本地9988端口muduo::net::TcpServer server(&loop, listenAddr, "EchoServer");// 配置服务器server.setConnectionCallback(onConnection);server.setMessageCallback(onMessage);server.setThreadNum(3); // 使用3个I/O线程printf("启动回声服务器(端口9988,3个I/O线程)...\n");server.start();printf("运行接收器循环...\n");loop.loop(); // 阻塞运行printf("服务器停止.\n");return 0;
}

(需包含Muduo头文件并链接库)

这是一个基于Muduo网络库实现的TCP回声服务器,核心功能是将客户端发送的数据原样返回

解析

网络库依赖
使用Muduo库提供的TcpServerEventLoop等类实现高并发网络通信。

该库采用Reactor模式处理I/O事件,通过多线程提升吞吐量。

核心

连接状态管理
onConnection函数在客户端连接/断开时触发,通过日志记录连接状态变化:

LOG_INFO << "连接建立: " << conn->name();  // 连接成功
LOG_INFO << "连接断开: " << conn->name();  // 连接断开

数据回传机制
onMessage函数实现回声核心逻辑:

conn->send(buf);  // 将接收缓冲区数据直接发回客户端

该操作零拷贝,直接引用接收缓冲区提升性能。

服务器配置

初始化参数

  • 监听本地9988端口:InetAddress listenAddr(9988)
  • 设置3个I/O线程:server.setThreadNum(3)

事件循环
loop.loop()启动事件监听,持续处理连接请求和数据传输,直到程序终止

技术

  1. 非阻塞I/O:通过事件驱动模型实现高并发
  2. 线程池:多I/O线程并行处理请求
  3. 零拷贝传输:避免数据内存复制开销

运行

程序启动后持续监听端口,每个新连接会触发日志记录。

客户端发送数据时,服务器立即返回相同内容,并在日志中记录字节数。

在这里插入图片描述

运行说明:

  1. 编译运行服务器程序
  2. 使用telnetnetcat连接127.0.0.1:9988
  3. 输入消息将收到相同回复
  4. 服务器日志显示连接状态和数据接收情况

Reactor 模式

Reactor 模式是一种事件驱动的高并发处理方案,核心思想是用单线程/少量线程集中管理 I/O 事件,实现资源高效复用

它的工作原理类似餐厅服务员:
服务员(Reactor)负责监听所有顾客(客户端)的请求,再将具体的点菜、上菜工作(数据处理)分发给对应的厨师(处理器)

三要素

  1. 事件监听员(Reactor)
    不停扫描所有网络连接,发现可读/可写等事件后立刻标记,但不自己处理数据(如同服务员发现顾客举手后记录需求,但不亲自做菜)。

  2. 事件处理器池(Handlers)
    预先准备好各种处理模块:
    连接处理器负责接待新顾客
    读处理器解析数据
    写处理器发送响应(类似餐厅有专职的接待员、厨师和传菜员)。

  3. 事件分发机制
    采用「非阻塞 I/O + 事件队列」组合,避免线程因等待数据而空转(如同服务员用对讲机通知后厨,而不是站在厨房门口干等)

特点

  • 同步非阻塞:应用需主动读写数据,但通过 I/O 多路复用技术(如 epoll)实现非阻塞
  • 低资源消耗1 个线程可处理上万连接,对比传统「每连接一线程」模式资源消耗下降 90%+
  • 高响应速度:事件触发即处理,无进程/线程创建销毁开销

典型应用场景

▸ Web 服务器(Nginx)
▸ 实时通信系统(Zoom 的音视频传输层)
▸ 数据库中间件(Redis 单线程却能支撑 10W+ QPS 的关键)

📌 理解:
传统模式 = 每桌顾客配专属服务员 → 人海战术成本高
Reactor 模式 = 1 个前台 + N 个专项服务员 → 资源利用率最大化
核心思想:又加了一层,实现了内聚各司其职

TcpServer内部机制

在这里插入图片描述

关键源码解析(简化版):

// TcpServer构造函数
TcpServer::TcpServer(EventLoop* loop, const InetAddress& listenAddr, const string& name): loop_(loop),acceptor_(new Acceptor(loop, listenAddr)),threadPool_(new EventLoopThreadPool(loop, name)) 
{acceptor_->setNewConnectionCallback(bind(&TcpServer::newConnection, this, _1, _2));
}// 新连接处理
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr) {EventLoop* ioLoop = threadPool_->getNextLoop(); // 获取I/O循环TcpConnectionPtr conn(new TcpConnection(ioLoop, connName, sockfd, localAddr, peerAddr));connections_[connName] = conn; // 加入连接映射表conn->setConnectionCallback(connectionCallback_); // 设置用户回调conn->setCloseCallback(bind(&TcpServer::removeConnection, this, _1));ioLoop->runInLoop(bind(&TcpConnection::connectEstablished, conn)); // 启动连接
}// 连接移除
void TcpServer::removeConnectionInLoop(const TcpConnectionPtr& conn) {connections_.erase(conn->name()); // 从映射表移除conn->getLoop()->queueInLoop(bind(&TcpConnection::connectDestroyed, conn)); // 销毁连接
}

功能

功能描述技术优势
监听套接字通过内部接收器管理专注高效接收新连接
接收器循环独立EventLoop处理连接请求避免I/O操作阻塞接收流程
I/O线程池多EventLoop线程处理数据读写充分利用多核资源,实现高并发
连接分发轮询算法分配新连接到不同I/O循环实现负载均衡,避免单线程瓶颈
连接映射表哈希表维护所有活动连接便于连接追踪和生命周期管理
回调工厂模式统一设置用户定义的回调函数保证业务逻辑一致性
线程安全移除通过跨线程任务队列安全移除连接避免多线程竞争问题

共享指针

共享指针(shared_ptr)是一种智能指针,自动管理对象的生命周期。

当多个线程需要访问同一对象时,共享指针通过引用计数机制确保对象在所有使用它的线程释放前不会被销毁,避免悬垂指针或内存泄漏。

跨线程队列 (生–消)

跨线程队列(如BlockingQueue)是线程间安全传递数据的管道。

它通过内部锁或无锁设计,保证多线程同时入队或出队时数据不会混乱,遵循“生产者-消费者”模式。

结合使用的线程安全性

共享指针解决对象所有权问题,跨线程队列解决数据传递问题。两者结合时:

  • 共享指针确保对象在跨线程传递时存活
  • 队列的线程安全机制保证指针传递过程无竞争
    这种组合常用于任务调度、事件处理等场景。

结论

muduo::net::TcpServer通过接收器循环与I/O线程池的协同工作,实现了高性能TCP服务端架构。

其核心优势体现在:

  1. 高效连接接收:专用接收器循环确保快速响应新连接
  2. 智能负载分发线程池机制充分利用多核CPU资源
  3. 安全连接管理通过共享指针跨线程队列确保线程安全
  4. 灵活扩展性:用户回调机制支持各类业务逻辑实现

下一章我们将深入探讨EventLoopThreadPool的实现细节,了解线程池如何管理多个事件循环。

第九章:线程池

相关文章:

  • 在 `setup` 函数中实现路由跳转:Vue3与Vue Router 4的集成
  • Python 数据分析与可视化 Day 3 - Pandas 数据筛选与排序操作
  • vivado工具配置(二)
  • Python 的内置函数 hasattr
  • 网络编程及原理(六):三次握手、四次挥手
  • 【软考高级系统架构论文】论软件设计方法及其应用
  • modelscope设置默认模型路径
  • app Router VS pages Router(Next.js学习笔记)
  • 车载CAN总线数据采集与故障诊断装置设计与实现
  • Spring Boot邮件发送终极指南:从基础到高级应用
  • 纯跟踪算法本质解密:航向角偏差=预瞄角?数学证明与工程实践
  • vscode搭建spring boot项目
  • 【软考高级系统架构论文】论多源数据集成及应用
  • CSS 制作学成在线网页
  • 【LeetCode#第198题】打家劫舍(一维dp)
  • 使用ccs生成bin
  • Vue.js核心概念与实践指南:从实例绑定到数据代理
  • 基于深度学习的侧信道分析(DLSCA)Python实现(带测试)
  • Vscode 编写Markdown支持 plantuml书写
  • 「Linux文件及目录管理」输入输出重定向与管道
  • 太原网站建设工作室/关键词seo如何优化
  • 我想带货怎么找货源/百度排名优化
  • 西安英文网站建设/新闻稿件代发平台
  • 企业网站建设 总结/中国关键词官网
  • 美国建网站的价格/关键词优化排名软件怎么样
  • 深圳软件开发公司在哪里/淮南网站seo