
⭐️在这个怀疑的年代,我们依然需要信仰。
个人主页 :YYYing.
⭐️C++大型项目系列专栏:C++大型项目之高性能服务器框架
系列上期内容:【C++项目之高性能服务器框架 (七) 】Socket与ByteArray模块
系列下期内容:暂无
目录
3.3 readFixSize() —— 读取固定长度数据(stream.cc)
3.4 readFixSize(ByteArray::ptr) —— 读取固定长度到 ByteArray(stream.cc)
3.5 writeFixSize() —— 写入固定长度数据(stream.cc)
3.6 writeFixSize(ByteArray::ptr) —— 写入固定长度 ByteArray(stream.cc)
4.1 read(void* buffer, size_t length)(socket_stream.cc)
4.2 read(ByteArray::ptr ba, size_t length)(socket_stream.cc)
4.3 write(const void* buffer, size_t length)(socket_stream.cc)
4.4 write(ByteArray::ptr ba, size_t length)(socket_stream.cc)
5.1 isConnected()(socket_stream.cc)
3.2 YAML 配置 ↔ 结构体转换(tcp_server.h)
六、start / stop / startAccept 详解
6.1 start() —— 启动服务(tcp_server.cc)
6.2 startAccept() —— accept 协程循环(tcp_server.cc)
6.3 stop() —— 停止服务(tcp_server.cc)
6.4 handleClient() —— 业务处理虚函数(tcp_server.cc)
7.2 loadCertificates()(tcp_server.cc)

前言:
上一篇我们说完了Socket与ByteArray的内容,我们封装了socket的各种api,并且搭配了超时管理与io管理,提供了基础类型的序列化与反序列化功能,那么这一篇我们开始学习提供字节流读写的流结构与Tcp服务器模块,废话不多说,我们开始。
Stream流抽象接口
一、设计与目的
流结构,提供字节流读写接口。
所有的流结构都继承自抽象类Stream,Stream类规定了一个流必须具备read/write接口和readFixSize/writeFixSize接口,继承自Stream的类必须实现这些接口。
那为什么要抽象 Stream 接口?
在传统的网络编程中,协议层代码往往直接调用 send() / recv() 等 Socket API。这导致:
-
协议与传输强耦合:HTTP 解析器只能工作在 TCP Socket 上,无法直接用于文件或内存。
-
测试困难:想要对协议层做单元测试,必须真的启动网络服务。
-
扩展困难:增加 SSL 加密、压缩传输时,需要修改所有协议代码。
Stream 抽象如何解决这些问题?
| 问题 | 解决方案 |
|---|---|
| 协议与传输耦合 | 协议层只依赖 Stream::ptr,底层可以是 Socket、文件、内存 |
| 测试困难 | 可以用 MemoryStream 模拟数据,无需真实网络 |
| 扩展困难 | 新增传输方式只需继承 Stream,协议代码零改动 |
| 读写不完整性 | readFixSize / writeFixSize 自动处理短读/短写,上层无需关心 |
设计哲学:面向接口编程
// 坏设计:协议层直接依赖 Socket
void handleHttp(int sockfd); // 只能用于网络 Socket
// 好设计:协议层依赖 Stream 抽象
void handleHttp(Stream::ptr stream); // 可用于 Socket、文件、内存、SSL
sylar 框架中的 HttpConnection、RockProtocol 等协议类,内部全部持有 Stream::ptr,这是框架分层架构的核心体现。
二、整体架构

设计意图:
-
解耦:协议层代码只依赖
Stream::ptr,不关心底层是 Socket、文件还是内存。 -
统一:所有 IO 操作都通过
read/write/close三个核心语义完成。 -
扩展:新增传输方式(如 SSL 流、压缩流)只需继承
Stream,无需修改协议代码。
三、Stream 类详解
3.1 定义(stream.h)
/**
* @brief 流结构
*/
class Stream {
public:
typedef std::shared_ptr<Stream> ptr;
/**
* @brief 析构函数
*/
virtual ~Stream() {}
/**
* @brief 读数据
* @param[out] buffer 接收数据的内存
* @param[in] length 接收数据的内存大小
* @return
* @retval >0 返回接收到的数据的实际大小
* @retval =0 被关闭
* @retval <0 出现流错误
*/
virtual int read(void* buffer, size_t length) = 0;
/**
* @brief 读数据
* @param[out] ba 接收数据的ByteArray
* @param[in] length 接收数据的内存大小
* @return
* @retval >0 返回接收到的数据的实际大小
* @retval =0 被关闭
* @retval <0 出现流错误
*/
virtual int read(ByteArray::ptr ba, size_t length) = 0;
/**
* @brief 读固定长度的数据
* @param[out] buffer 接收数据的内存
* @param[in] length 接收数据的内存大小
* @return
* @retval >0 返回接收到的数据的实际大小
* @retval =0 被关闭
* @retval <0 出现流错误
*/
virtual int readFixSize(void* buffer, size_t length);
/**
* @brief 读固定长度的数据
* @param[out] ba 接收数据的ByteArray
* @param[in] length 接收数据的内存大小
* @return
* @retval >0 返回接收到的数据的实际大小
* @retval =0 被关闭
* @retval <0 出现流错误
*/
virtual int readFixSize(ByteArray::ptr ba, size_t length);
/**
* @brief 写数据
* @param[in] buffer 写数据的内存
* @param[in] length 写入数据的内存大小
* @return
* @retval >0 返回写入到的数据的实际大小
* @retval =0 被关闭
* @retval <0 出现流错误
*/
virtual int write(const void* buffer, size_t length) = 0;
/**
* @brief 写数据
* @param[in] ba 写数据的ByteArray
* @param[in] length 写入数据的内存大小
* @return
* @retval >0 返回写入到的数据的实际大小
* @retval =0 被关闭
* @retval <0 出现流错误
*/
virtual int write(ByteArray::ptr ba, size_t length) = 0;
/**
* @brief 写固定长度的数据
* @param[in] buffer 写数据的内存
* @param[in] length 写入数据的内存大小
* @return
* @retval >0 返回写入到的数据的实际大小
* @retval =0 被关闭
* @retval <0 出现流错误
*/
virtual int writeFixSize(const void* buffer, size_t length);
/**
* @brief 写固定长度的数据
* @param[in] ba 写数据的ByteArray
* @param[in] length 写入数据的内存大小
* @return
* @retval >0 返回写入到的数据的实际大小
* @retval =0 被关闭
* @retval <0 出现流错误
*/
virtual int writeFixSize(ByteArray::ptr ba, size_t length);
/**
* @brief 关闭流
*/
virtual void close() = 0;
};
类设计要点:
-
ptr:std::shared_ptr<Stream>的类型别名,框架中所有传递流的地方都用Stream::ptr,实现多态。 -
析构函数是
virtual的:确保通过基类指针删除派生类对象时能正确调用派生类析构函数。 -
没有非虚成员变量:
Stream是一个纯接口类,状态全部下沉到派生类。
3.2 纯虚接口语义表
| 接口 | 参数 | 返回值 | 语义 |
|---|---|---|---|
read(void* buffer, size_t length) | buffer:接收缓冲区;length:缓冲区大小 | >0:实际读取字节数;=0:对端关闭;<0:出错 | 从流中读取数据到内存缓冲区 |
read(ByteArray::ptr ba, size_t length) | ba:接收数据的 ByteArray;length:期望读取长度 | 同上 | 从流中读取数据到 ByteArray |
write(const void* buffer, size_t length) | buffer:待发送数据;length:数据长度 | >0:实际写入字节数;=0:对端关闭;<0:出错 | 将内存缓冲区数据写入流 |
write(ByteArray::ptr ba, size_t length) | ba:待发送数据的 ByteArray;length:期望写入长度 | 同上 | 将 ByteArray 数据写入流 |
close() | 无 | 无 | 关闭流,释放底层资源 |
为什么同时提供 void* 和 ByteArray::ptr 两种重载?
-
void*版本:适用于 C 风格缓冲区、栈数组等简单场景。 -
ByteArray::ptr版本:适用于需要序列化/反序列化的场景,支持自动扩容、零拷贝iovec传输。
3.3 readFixSize() —— 读取固定长度数据(stream.cc)
int Stream::readFixSize(void* buffer, size_t length) {
size_t offset = 0;
int64_t left = length;
while(left > 0) {
int64_t len = read((char*)buffer + offset, left);
if(len <= 0) {
return len;
}
offset += len;
left -= len;
}
return length;
}
逐行拆解:
-
size_t offset = 0:已写入缓冲区的偏移量,初始为 0。 -
int64_t left = length:还剩多少字节需要读取。用int64_t而非size_t是因为read()返回int(有符号),混用无符号类型会导致比较问题。 -
while(left > 0):循环直到读取够length字节,或遇到错误/关闭。 -
read((char*)buffer + offset, left):调用虚函数read(),从当前偏移位置继续读取。-
(char*)buffer + offset:将void*转为char*做指针算术,因为void*不支持直接加减。
-
-
if(len <= 0) { return len; }:-
len == 0:对端关闭(EOF),直接返回 0。 -
len < 0:发生错误,将错误码向上传递。 -
注意:这里不返回已读取的部分长度,而是直接返回错误码。因为调用者期望的是"要么读完固定长度,要么失败"。
-
-
offset += len; left -= len;:更新偏移和剩余量。 -
return length;:循环正常结束,说明正好读取了length字节。
为什么需要 readFixSize?
-
操作系统底层的
read()/recv()不保证一次读完请求的长度(短读)。 -
网络 IO 中,一个 1024 字节的数据包可能被拆成多次到达。
-
readFixSize内部循环调用read(),自动处理短读,对上层透明。 -
配合 Hook 模块,
read()内部会自动 Yield 协程、等待数据就绪,循环不会空转。
3.4 readFixSize(ByteArray::ptr) —— 读取固定长度到 ByteArray(stream.cc)
int Stream::readFixSize(ByteArray::ptr ba, size_t length) {
int64_t left = length;
while(left > 0) {
int64_t len = read(ba, left);
if(len <= 0) {
return len;
}
left -= len;
}
return length;
}
与 void* 版本的区别:
-
不需要
offset变量,因为ByteArray内部维护m_position,每次read(ba, left)后会自动推进位置。 -
ba会自动扩容(如果底层ByteArray实现支持)。
3.5 writeFixSize() —— 写入固定长度数据(stream.cc)
int Stream::writeFixSize(const void* buffer, size_t length) {
size_t offset = 0;
int64_t left = length;
while(left > 0) {
int64_t len = write((const char*)buffer + offset, left);
if(len <= 0) {
return len;
}
offset += len;
left -= len;
}
return length;
}
与 readFixSize 对称:
-
同样的循环逻辑,处理写操作的短写(
EAGAIN/ 发送缓冲区满)。 -
write()被 Hook 后,如果发送缓冲区满,协程会自动 Yield,等待 socket 可写时恢复。
3.6 writeFixSize(ByteArray::ptr) —— 写入固定长度 ByteArray(stream.cc)
int Stream::writeFixSize(ByteArray::ptr ba, size_t length) {
int64_t left = length;
while(left > 0) {
int64_t len = write(ba, left);
if(len <= 0) {
return len;
}
left -= len;
}
return length;
}
-
同样依赖
ByteArray内部的位置推进机制。
四、Stream 在框架中的分层位置

关键理解:
-
HttpConnection等协议类内部持有Stream::ptr,不关心底层是 TCP Socket、SSL Socket 还是 Unix Domain Socket。 -
这种抽象使得同一个 HTTP 解析器可以工作在各种传输层之上。
五、完整调用链梳理
以 stream->readFixSize(buf, 1024) 为例:
步骤 1:调用 readFixSize
stream->readFixSize(buf, 1024);
步骤 2:Stream::readFixSize 内部循环
while(left > 0) {
int64_t len = read((char*)buffer + offset, left); // 调用虚函数
...
}
步骤 3:派生类实现(以 SocketStream 为例)
int SocketStream::read(void* buffer, size_t length) {
if(!isConnected()) return -1;
return m_socket->recv(buffer, length); // 进入 Socket 层
}
步骤 4:Socket::recv(被 Hook 后)
int Socket::recv(void* buffer, size_t length, int flags) {
// 如果没有数据可读,Hook 会让当前协程 Yield
// 数据到达后,IOManager 将该协程重新 schedule
return ::recv(m_sock, buffer, length, flags);
}
步骤 5:如果数据不足,循环继续
// readFixSize 中:
offset += len; // 更新已读位置
left -= len; // 更新剩余量
// while 继续,直到 left == 0
六、线程安全总结
| 对象 | 保护内容 | 说明 |
|---|---|---|
Stream 基类 | 无成员变量 | 纯接口,无状态,线程安全 |
readFixSize / writeFixSize | 局部变量 | 栈上的 offset / left,无共享状态 |
关键设计:
-
Stream抽象基类本身无锁、无状态,线程安全责任下沉到派生类。 -
SocketStream的线程安全由底层Socket保证(单个 fd 的操作通常串行在单个协程/线程中)。
七、学习验证清单
学完后,你应该能:
- 解释
Stream抽象在框架中的分层位置(协议层 vs 传输层)。 - 说明为什么
read/write要设计为纯虚函数,而readFixSize/writeFixSize是默认实现。 - 解释
readFixSize中遇到len <= 0时为什么直接返回,而不是返回已读取的部分长度。 - 说明
readFixSize配合 Hook 模块后,短读时协程的行为(Yield → 等待 → 恢复)。 - 解释为什么
Stream同时提供void*和ByteArray::ptr两种重载。 - 画出
HttpConnection::recvRequest→Stream::read→SocketStream::read→Socket::recv→Hook::recv的完整调用链。
SocketStream详解
一、设计与目的
套接字流结构,将套接字封装成流结构,以支持Stream接口规范,除此外,SocketStream还支持套接字关闭操作以及获取本地/远端地址的操作。
为什么需要 SocketStream?
sylar 框架中已经有了功能完善的 Socket 类(封装了 send/recv/bind/listen/accept 等),为什么还要再包装一层 SocketStream?
原因 1:统一接口
// Socket 提供的是网络 API
int Socket::send(const void* buffer, size_t length);
int Socket::recv(void* buffer, size_t length);
// Stream 提供的是流式 IO 抽象
int Stream::write(const void* buffer, size_t length);
int Stream::read(void* buffer, size_t length);
-
Socket的语义偏向"网络操作",Stream的语义偏向"数据流"。 -
协议层关心的是"读取/写入数据",而不是"从 Socket 收/发"。
原因 2:生命周期管理
{
SocketStream::ptr stream(new SocketStream(sock, true));
// 使用 stream 读写数据...
} // stream 析构,自动 close(sock) —— RAII
-
m_owner机制让调用方选择是否由SocketStream管理 Socket 生命周期。 -
避免"谁创建谁关闭"的责任混乱。
原因 3:零拷贝与 ByteArray 集成
-
Socket::send/recv只支持连续内存(void*)。 -
SocketStream::read/write(ByteArray::ptr)通过iovec+readv/writev实现了链表缓冲区的零拷贝传输。 -
这是
Socket类本身没有直接提供的便利。
在框架中的位置
协议层(HTTP/RPC)
│ 使用 Stream::ptr
▼
SocketStream —— 网络传输适配器
│ 使用 Socket::ptr
▼
Socket —— 系统调用封装
│ 使用 fd
▼
Kernel —— 内核网络栈
SocketStream 是适配器模式的典型应用:将 Socket 的 API 适配为 Stream 的接口。
二、整体架构

设计意图:
-
SocketStream是Stream抽象在网络 Socket 场景下的具体实现。 -
将 Socket 的
send/recv语义包装为Stream的read/write语义,使协议层代码不直接依赖 Socket API。 -
通过
m_owner参数灵活控制 Socket 生命周期。
三、SocketStream 类详解
3.1 定义(socket_stream.h)
/**
* @brief Socket流
*/
class SocketStream : public Stream {
public:
typedef std::shared_ptr<SocketStream> ptr;
/**
* @brief 构造函数
* @param[in] sock Socket类
* @param[in] owner 是否完全控制
*/
SocketStream(Socket::ptr sock, bool owner = true);
/**
* @brief 析构函数
* @details 如果m_owner=true,则close
*/
~SocketStream();
/**
* @brief 读取数据
* @param[out] buffer 待接收数据的内存
* @param[in] length 待接收数据的内存长度
* @return
* @retval >0 返回实际接收到的数据长度
* @retval =0 socket被远端关闭
* @retval <0 socket错误
*/
virtual int read(void* buffer, size_t length) override;
/**
* @brief 读取数据
* @param[out] ba 接收数据的ByteArray
* @param[in] length 待接收数据的内存长度
* @return
* @retval >0 返回实际接收到的数据长度
* @retval =0 socket被远端关闭
* @retval <0 socket错误
*/
virtual int read(ByteArray::ptr ba, size_t length) override;
/**
* @brief 写入数据
* @param[in] buffer 待发送数据的内存
* @param[in] length 待发送数据的内存长度
* @return
* @retval >0 返回实际接收到的数据长度
* @retval =0 socket被远端关闭
* @retval <0 socket错误
*/
virtual int write(const void* buffer, size_t length) override;
/**
* @brief 写入数据
* @param[in] ba 待发送数据的ByteArray
* @param[in] length 待发送数据的内存长度
* @return
* @retval >0 返回实际接收到的数据长度
* @retval =0 socket被远端关闭
* @retval <0 socket错误
*/
virtual int write(ByteArray::ptr ba, size_t length) override;
/**
* @brief 关闭socket
*/
virtual void close() override;
/**
* @brief 返回Socket类
*/
Socket::ptr getSocket() const { return m_socket;}
/**
* @brief 返回是否连接
*/
bool isConnected() const;
Address::ptr getRemoteAddress();
Address::ptr getLocalAddress();
std::string getRemoteAddressString();
std::string getLocalAddressString();
protected:
/// Socket类
Socket::ptr m_socket;
/// 是否主控
bool m_owner;
};
继承关系:
-
public Stream:必须实现基类定义的 5 个纯虚接口(read(x2)、write(x2)、close)。 -
Stream::readFixSize/writeFixSize是默认实现,派生类无需重写。
3.2 成员变量表
| 变量名 | 类型 | 默认值 | 含义 |
|---|---|---|---|
m_socket | Socket::ptr | 构造时传入 | 底层 Socket 智能指针 |
m_owner | bool | true | 是否拥有 Socket 的生命周期控制权 |
3.3 构造函数(socket_stream.cc)
SocketStream::SocketStream(Socket::ptr sock, bool owner)
:m_socket(sock)
,m_owner(owner) {
}
逐参数拆解:
| 参数 | 类型 | 说明 |
|---|---|---|
sock | Socket::ptr | 底层 Socket,必须已连接或已绑定/监听 |
owner | bool | true:SocketStream 析构时自动 close() Socket;false:不关闭,由外部管理 |
owner = false 的使用场景:
-
HTTP 服务器收到请求后,可能将 Socket 交给
HttpSession处理。HttpSession持有SocketStream,但 Socket 的生命周期由上层服务器管理。 -
连接池中的 Socket 被复用时,取出的
SocketStream不应关闭 Socket。
3.4 析构函数(socket_stream.cc)
SocketStream::~SocketStream() {
if(m_owner && m_socket) {
m_socket->close();
}
}
逻辑:
-
if(m_owner):只有拥有控制权时才关闭。 -
if(m_socket):防止空指针解引用。 -
m_socket->close():调用Socket::close(),关闭 fd、清理 epoll 事件等。
为什么析构函数不设置 virtual?
-
基类
Stream的析构函数已经是virtual,派生类自动继承该属性,无需重复声明。
四、read/write 实现详解
4.1 read(void* buffer, size_t length)(socket_stream.cc)
int SocketStream::read(void* buffer, size_t length) {
if(!isConnected()) {
return -1;
}
return m_socket->recv(buffer, length);
}
逐行拆解:
-
if(!isConnected()) { return -1; }:-
先检查 Socket 是否已连接。如果未连接,直接返回 -1(错误)。
-
避免对无效 fd 调用
recv()产生未定义行为。
-
-
m_socket->recv(buffer, length):-
转发到
Socket::recv(),进入 Socket 层。 -
如果 Socket 被 Hook,
recv()内部会在无数据时 Yield 当前协程,数据到达后自动恢复。
-
4.2 read(ByteArray::ptr ba, size_t length)(socket_stream.cc)
int SocketStream::read(ByteArray::ptr ba, size_t length) {
if(!isConnected()) {
return -1;
}
std::vector<iovec> iovs;
ba->getWriteBuffers(iovs, length);
int rt = m_socket->recv(&iovs[0], iovs.size());
if(rt > 0) {
ba->setPosition(ba->getPosition() + rt);
}
return rt;
}
逐行拆解:
-
std::vector<iovec> iovs;:-
iovec是 POSIX 标准的分散/聚集 IO结构体,包含{ iov_base, iov_len }。 -
允许一次
readv/writev系统调用读写多个不连续的内存块。
-
-
ba->getWriteBuffers(iovs, length);:-
ByteArray内部由多个Node(内存块链表)组成。 -
getWriteBuffers将ByteArray中可写入的内存区域转换为iovec数组。 -
如果
ByteArray容量不足,内部会自动扩容(创建新的 Node)。
-
-
m_socket->recv(&iovs[0], iovs.size());:-
调用
Socket::recv(iovec*, size_t),底层使用::readv()系统调用。 -
零拷贝:数据直接从内核缓冲区写入
ByteArray的各个 Node,无需先拷贝到中间栈缓冲区。
-
-
if(rt > 0) { ba->setPosition(ba->getPosition() + rt); }:-
rt是实际读取的字节数。 -
更新
ByteArray的当前位置(m_position),使后续写入/读取从正确位置开始。
-
为什么用 readv 而不是多次 read?
-
减少系统调用次数(一次
readv代替多次read)。 -
避免中间拷贝,数据直接落到最终目的地。
4.3 write(const void* buffer, size_t length)(socket_stream.cc)
int SocketStream::write(const void* buffer, size_t length) {
if(!isConnected()) {
return -1;
}
return m_socket->send(buffer, length);
}
-
与
read(void*)对称,转发到Socket::send()。 -
如果发送缓冲区满,Hook 后的
send()会让协程 Yield,等待 socket 可写时恢复。
4.4 write(ByteArray::ptr ba, size_t length)(socket_stream.cc)
int SocketStream::write(ByteArray::ptr ba, size_t length) {
if(!isConnected()) {
return -1;
}
std::vector<iovec> iovs;
ba->getReadBuffers(iovs, length);
int rt = m_socket->send(&iovs[0], iovs.size());
if(rt > 0) {
ba->setPosition(ba->getPosition() + rt);
}
return rt;
}
与 read(ByteArray) 的对称性:
-
getReadBuffers(iovs, length):获取ByteArray中已有数据的内存区域(用于读取并发送)。 -
send(&iovs[0], iovs.size()):底层使用::writev(),一次性发送多个不连续内存块。 -
发送成功后,
setPosition推进读取位置。
4.5 close()(socket_stream.cc)
void SocketStream::close() {
if(m_socket) {
m_socket->close();
}
}
-
显式关闭流,转发到
Socket::close()。 -
注意:
close()后m_socket智能指针仍然持有对象(引用计数不为 0),只是 fd 被关闭了。 -
如果之后再次调用
read/write,isConnected()会返回false,直接返回 -1。
五、辅助方法详解
5.1 isConnected()(socket_stream.cc)
bool SocketStream::isConnected() const {
return m_socket && m_socket->isConnected();
}
-
双重检查:
m_socket非空,且底层 Socket 处于已连接状态。 -
所有
read/write操作的前置条件。
5.2 地址获取方法(socket_stream.cc)
Address::ptr SocketStream::getRemoteAddress() {
if(m_socket) {
return m_socket->getRemoteAddress();
}
return nullptr;
}
Address::ptr SocketStream::getLocalAddress() {
if(m_socket) {
return m_socket->getLocalAddress();
}
return nullptr;
}
std::string SocketStream::getRemoteAddressString() {
auto addr = getRemoteAddress();
if(addr) {
return addr->toString();
}
return "";
}
std::string SocketStream::getLocalAddressString() {
auto addr = getLocalAddress();
if(addr) {
return addr->toString();
}
return "";
}
设计要点:
-
getRemoteAddress()/getLocalAddress()返回Address::ptr(地址对象的智能指针)。 -
getRemoteAddressString()/getLocalAddressString()返回字符串,方便日志输出。 -
都做空指针检查,防止
m_socket为空时崩溃。
六、ByteArray 与 iovec 的配合原理

关键理解:
-
ByteArray是链表式缓冲区,避免了大块连续内存的分配压力。 -
getReadBuffers/getWriteBuffers将链表节点转换为iovec数组。 -
readv/writev系统调用支持一次操作多个缓冲区,完美匹配ByteArray的链表结构。 -
整个过程无需将数据拷贝到连续的栈缓冲区。
七、完整调用链梳理
以 stream->read(ba, 4096) 为例:
步骤 1:SocketStream::read(ByteArray)
std::vector<iovec> iovs;
ba->getWriteBuffers(iovs, 4096); // ByteArray 准备可写入区域
int rt = m_socket->recv(&iovs[0], iovs.size());
步骤 2:Socket::recv(iovec)
int Socket::recv(iovec* buffers, size_t length, int flags) {
msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = buffers;
msg.msg_iovlen = length;
return ::recvmsg(m_sock, &msg, flags); // 或 readv
}
步骤 3:Hook 后的 recvmsg
// Hook 拦截 recvmsg
if(当前协程 && 无数据可读) {
// 将 fd 注册到 IOManager 的 epoll 中(READ 事件)
// Yield 当前协程
// 数据到达后,IOManager 恢复该协程
}
步骤 4:返回后更新 ByteArray 位置
if(rt > 0) {
ba->setPosition(ba->getPosition() + rt);
}
八、线程安全总结
| 对象 | 保护内容 | 说明 |
|---|---|---|
SocketStream 对象 | m_socket、m_owner | 无显式锁,但通常单个 SocketStream 只被一个协程操作 |
Socket 内部 | fd 操作 | 由 Hook 和 IOManager 的 epoll 机制保证串行化 |
ByteArray | m_position、m_size | 如果多个协程共享同一个 ByteArray,需要外部加锁 |
关键设计:
-
SocketStream本身不加锁,因为它的使用模式通常是"一个连接对应一个协程/线程"。 -
如果需要在多个协程间共享
SocketStream,需要由调用方保证互斥(如每个协程有独立的ByteArray缓冲区)。
九、学习验证清单
学完后,你应该能:
- 解释
m_owner参数的作用,并举出owner = false的两种使用场景。 - 说明
read(ByteArray::ptr)相比read(void*)的优势(零拷贝、自动扩容)。 - 解释
getWriteBuffers和getReadBuffers的区别(前者获取空闲区域用于读入,后者获取有数据区域用于写出)。 - 说明
iovec+readv/writev如何配合ByteArray的链表结构实现零拷贝。 - 解释为什么
SocketStream::close()后,m_socket智能指针仍然非空。 - 画出
SocketStream::read→ByteArray::getWriteBuffers→Socket::recv(iovec)→::readv→Hook的完整调用链。
TCPServer详解
一、设计与目的
TCP服务器封装。
TcpServer类支持同时绑定多个地址进行监听,只需要在绑定时传入地址数组即可。TcpServer还可以分别指定接收客户端和处理客户端的协程调度器。
为什么要封装 TcpServer?
在传统的网络编程中,编写一个 TCP 服务器需要手动处理:
-
socket()→bind()→listen()→accept()的完整流程。 -
多线程/多进程处理并发连接。
-
服务器的启动、停止、资源清理。
-
多端口监听、SSL 支持、配置化管理。
这些代码在每个服务器项目中都要重复编写,容易出错且难以维护。
TcpServer 如何解决这些问题?
| 传统问题 | TcpServer 的解决方案 |
|---|---|
| 重复编写 bind/listen/accept | 基类统一实现,子类只关心业务逻辑 |
| 并发模型设计困难 | 基于协程 + IOManager,自动实现高并发 |
| 启动/停止逻辑复杂 | start() / stop() 两阶段接口,资源自动清理 |
| 单端口无法满足需求 | m_socks 数组支持多地址同时监听 |
| IO 与业务逻辑争抢线程 | accept_worker / io_worker / worker 三调度器分离 |
| 配置与代码耦合 | TcpServerConf + LexicalCast 支持 YAML 配置化启动 |
模板方法模式的应用
class TcpServer {
// 骨架:由基类实现,子类不可改
bool bind(Address::ptr addr);
bool start();
void stop();
void startAccept(Socket::ptr sock);
// 扩展点:虚函数,子类必须重写
virtual void handleClient(Socket::ptr client);
};
-
bind/start/stop/startAccept是算法骨架,定义了服务器的标准生命周期。 -
handleClient()是扩展点,子类通过重写它注入具体业务逻辑。 -
这是经典的模板方法模式(Template Method Pattern),它的HandleClient是交由继承类来实现的。使用TcpServer时,必须从TcpServer派生一个新类,并重新实现子类的handleClient操作,这点可以参考test_tcp_server.cc。
多调度器分离的设计哲学

-
accept 轻量:
accept()只是从内核队列取出一个已完成的连接,几乎不耗 CPU。 -
IO 密集型:
read/write会频繁 Yield/Resume,需要足够的线程容纳等待中的协程。 -
CPU 密集:业务逻辑(JSON 解析、数据库查询)消耗 CPU,应与 IO 分离防止相互阻塞。
默认行为:三个调度器都是 IOManager::GetThis(),即单调度器模式。简单场景下无需分离,代码简洁;生产环境可按需拆分。
配置化启动的意义
servers:
- name: api_gateway
type: http
address:
- "0.0.0.0:8080"
- "[::]:8080"
timeout: 30000
ssl: 1
cert_file: /etc/ssl/server.crt
key_file: /etc/ssl/server.key
io_worker: io_pool
accept_worker: accept_pool
-
服务器行为通过 YAML 配置,无需重新编译代码。
-
支持配置热更新(配合
ConfigVar的监听器机制)。 -
同一套代码可以部署为开发环境(单端口、无 SSL)和生产环境(多端口、SSL、多调度器)。
三、TcpServerConf 配置结构体详解
3.1 定义(tcp_server.h)
struct TcpServerConf {
typedef std::shared_ptr<TcpServerConf> ptr;
std::vector<std::string> address;
int keepalive = 0;
int timeout = 1000 * 2 * 60;
int ssl = 0;
std::string id;
/// 服务器类型,http, ws, rock
std::string type = "http";
std::string name;
std::string cert_file;
std::string key_file;
std::string accept_worker;
std::string io_worker;
std::string process_worker;
std::map<std::string, std::string> args;
bool isValid() const {
return !address.empty();
}
bool operator==(const TcpServerConf& oth) const {
return address == oth.address
&& keepalive == oth.keepalive
&& timeout == oth.timeout
&& name == oth.name
&& ssl == oth.ssl
&& cert_file == oth.cert_file
&& key_file == oth.key_file
&& accept_worker == oth.accept_worker
&& io_worker == oth.io_worker
&& process_worker == oth.process_worker
&& args == oth.args
&& id == oth.id
&& type == oth.type;
}
};
成员变量表:
| 变量名 | 类型 | 默认值 | 含义 |
|---|---|---|---|
address | vector<string> | 空 | 监听地址列表,如 "0.0.0.0:8080"、"[::]:8080" |
keepalive | int | 0 | TCP keepalive 开关 |
timeout | int | 120000(2分钟) | 连接读取超时时间(毫秒) |
ssl | int | 0 | 是否启用 SSL(HTTPS) |
id | string | "" | 服务器唯一标识 |
type | string | "http" | 服务器类型:http、ws、rock |
name | string | "" | 服务器名称 |
cert_file | string | "" | SSL 证书文件路径 |
key_file | string | "" | SSL 私钥文件路径 |
accept_worker | string | "" | accept 协程运行的调度器名称 |
io_worker | string | "" | IO 协程运行的调度器名称 |
process_worker | string | "" | 业务处理协程运行的调度器名称 |
args | map<string,string> | 空 | 扩展参数,供子类使用 |
isValid():
-
唯一校验规则:
address不能为空。只要配置了监听地址,就认为是有效配置。
operator==:
-
用于配置系统的变更检测。当配置文件热更新时,通过比较新旧
TcpServerConf是否相等,判断是否需要重建服务器。
3.2 YAML 配置 ↔ 结构体转换(tcp_server.h)
template<>
class LexicalCast<std::string, TcpServerConf> {
public:
TcpServerConf operator()(const std::string& v) {
YAML::Node node = YAML::Load(v);
TcpServerConf conf;
conf.id = node["id"].as<std::string>(conf.id);
conf.type = node["type"].as<std::string>(conf.type);
...
return conf;
}
};
template<>
class LexicalCast<TcpServerConf, std::string> {
public:
std::string operator()(const TcpServerConf& conf) {
YAML::Node node;
node["id"] = conf.id;
...
return ss.str();
}
};
设计要点:
-
特化
LexicalCast模板,使TcpServerConf可以接入 sylar 的配置系统。 -
string → TcpServerConf:从 YAML 配置文件加载服务器配置。 -
TcpServerConf → string:将当前配置序列化为 YAML 字符串(用于日志、调试)。 -
node["key"].as<T>(default):YAML-CPP 的语法,如果配置文件中不存在该 key,使用默认值。
四、TcpServer 类详解
4.1 定义(tcp_server.h)
/**
* @brief TCP服务器封装
*/
class TcpServer : public std::enable_shared_from_this<TcpServer>
, Noncopyable {
public:
typedef std::shared_ptr<TcpServer> ptr;
/**
* @brief 构造函数
* @param[in] worker socket客户端工作的协程调度器
* @param[in] accept_worker 服务器socket执行接收socket连接的协程调度器
*/
TcpServer(sylar::IOManager* worker = sylar::IOManager::GetThis()
,sylar::IOManager* io_woker = sylar::IOManager::GetThis()
,sylar::IOManager* accept_worker = sylar::IOManager::GetThis());
/**
* @brief 析构函数
*/
virtual ~TcpServer();
/**
* @brief 绑定地址
* @return 返回是否绑定成功
*/
virtual bool bind(sylar::Address::ptr addr, bool ssl = false);
/**
* @brief 绑定地址数组
* @param[in] addrs 需要绑定的地址数组
* @param[out] fails 绑定失败的地址
* @return 是否绑定成功
*/
virtual bool bind(const std::vector<Address::ptr>& addrs
,std::vector<Address::ptr>& fails
,bool ssl = false);
bool loadCertificates(const std::string& cert_file, const std::string& key_file);
/**
* @brief 启动服务
* @pre 需要bind成功后执行
*/
virtual bool start();
/**
* @brief 停止服务
*/
virtual void stop();
/**
* @brief 返回读取超时时间(毫秒)
*/
uint64_t getRecvTimeout() const { return m_recvTimeout;}
/**
* @brief 返回服务器名称
*/
std::string getName() const { return m_name;}
/**
* @brief 设置读取超时时间(毫秒)
*/
void setRecvTimeout(uint64_t v) { m_recvTimeout = v;}
/**
* @brief 设置服务器名称
*/
virtual void setName(const std::string& v) { m_name = v;}
/**
* @brief 是否停止
*/
bool isStop() const { return m_isStop;}
TcpServerConf::ptr getConf() const { return m_conf;}
void setConf(TcpServerConf::ptr v) { m_conf = v;}
void setConf(const TcpServerConf& v);
virtual std::string toString(const std::string& prefix = "");
std::vector<Socket::ptr> getSocks() const { return m_socks;}
protected:
/**
* @brief 处理新连接的Socket类
*/
virtual void handleClient(Socket::ptr client);
/**
* @brief 开始接受连接
*/
virtual void startAccept(Socket::ptr sock);
protected:
/// 监听Socket数组
std::vector<Socket::ptr> m_socks;
/// 新连接的Socket工作的调度器
IOManager* m_worker;
IOManager* m_ioWorker;
/// 服务器Socket接收连接的调度器
IOManager* m_acceptWorker;
/// 接收超时时间(毫秒)
uint64_t m_recvTimeout;
/// 服务器名称
std::string m_name;
/// 服务器类型
std::string m_type = "tcp";
/// 服务是否停止
bool m_isStop;
bool m_ssl = false;
TcpServerConf::ptr m_conf;
};
多重继承关系:
-
std::enable_shared_from_this<TcpServer>:允许在成员函数中通过shared_from_this()获取自身的shared_ptr。 -
Noncopyable:禁止拷贝构造和拷贝赋值(服务器对象通常不应被拷贝)。
4.2 成员变量表
| 变量名 | 类型 | 默认值 | 含义 |
|---|---|---|---|
m_socks | vector<Socket::ptr> | 空 | 监听 Socket 数组,支持多地址绑定 |
m_worker | IOManager* | IOManager::GetThis() | 主工作调度器(通用任务) |
m_ioWorker | IOManager* | IOManager::GetThis() | 新连接的 IO 处理调度器 |
m_acceptWorker | IOManager* | IOManager::GetThis() | 接收连接的调度器 |
m_recvTimeout | uint64_t | 配置系统默认值(2分钟) | 客户端连接读取超时(毫秒) |
m_name | string | "sylar/1.0.0" | 服务器名称 |
m_type | string | "tcp" | 服务器类型标识 |
m_isStop | bool | true | 是否已停止(构造时默认为停止状态) |
m_ssl | bool | false | 是否启用 SSL |
m_conf | TcpServerConf::ptr | nullptr | 配置对象 |
4.3 构造函数(tcp_server.cc)
TcpServer::TcpServer(sylar::IOManager* worker,
sylar::IOManager* io_worker,
sylar::IOManager* accept_worker)
:m_worker(worker)
,m_ioWorker(io_worker)
,m_acceptWorker(accept_worker)
,m_recvTimeout(g_tcp_server_read_timeout->getValue())
,m_name("sylar/1.0.0")
,m_isStop(true) {
}
逐行拆解:
-
m_worker(worker):主工作调度器。默认可用IOManager::GetThis()获取当前线程的调度器。 -
m_ioWorker(io_worker):IO 处理调度器。新连接的数据读写在此调度器上执行。 -
m_acceptWorker(accept_worker):accept 调度器。专门执行accept()循环。 -
m_recvTimeout(g_tcp_server_read_timeout->getValue()):-
g_tcp_server_read_timeout是配置系统定义的变量,默认值为60 * 1000 * 2(2分钟)。 -
每个新连接的 Socket 会继承这个超时值。
-
-
m_name("sylar/1.0.0"):默认服务器名称。 -
m_isStop(true):构造时默认为停止状态,必须调用start()才会开始接受连接。
为什么 m_isStop 初始为 true?
-
防止对象构造未完成时就被外部使用。
-
必须先
bind()成功,再start(),这是明确的两阶段启动。
4.4 析构函数(tcp_server.cc)
TcpServer::~TcpServer() {
for(auto& i : m_socks) {
i->close();
}
m_socks.clear();
}
-
遍历所有监听 Socket,逐个关闭。
-
m_socks.clear():释放shared_ptr,如果引用计数降为 0,底层 Socket 对象被销毁。
五、bind 详解
5.1 单地址绑定(tcp_server.cc)
bool TcpServer::bind(sylar::Address::ptr addr, bool ssl) {
std::vector<Address::ptr> addrs;
std::vector<Address::ptr> fails;
addrs.push_back(addr);
return bind(addrs, fails, ssl);
}
-
包装函数,将单个地址转为
vector,委托给多地址版本。 -
fails是输出参数,用于返回绑定失败的地址列表。
5.2 多地址绑定(tcp_server.cc)
bool TcpServer::bind(const std::vector<Address::ptr>& addrs
,std::vector<Address::ptr>& fails
,bool ssl) {
m_ssl = ssl;
for(auto& addr : addrs) {
Socket::ptr sock = ssl ? SSLSocket::CreateTCP(addr) : Socket::CreateTCP(addr);
if(!sock->bind(addr)) {
SYLAR_LOG_ERROR(g_logger) << "bind fail errno="
<< errno << " errstr=" << strerror(errno)
<< " addr=[" << addr->toString() << "]";
fails.push_back(addr);
continue;
}
if(!sock->listen()) {
SYLAR_LOG_ERROR(g_logger) << "listen fail errno="
<< errno << " errstr=" << strerror(errno)
<< " addr=[" << addr->toString() << "]";
fails.push_back(addr);
continue;
}
m_socks.push_back(sock);
}
if(!fails.empty()) {
m_socks.clear();
return false;
}
for(auto& i : m_socks) {
SYLAR_LOG_INFO(g_logger) << "type=" << m_type
<< " name=" << m_name
<< " ssl=" << m_ssl
<< " server bind success: " << *i;
}
return true;
}
逐段拆解:
第一段:遍历地址创建 Socket
-
m_ssl = ssl;:记录全局 SSL 标志。 -
ssl ? SSLSocket::CreateTCP(addr) : Socket::CreateTCP(addr):-
如果启用 SSL,创建
SSLSocket;否则创建普通Socket。 -
CreateTCP(addr)会根据地址类型(IPv4/IPv6/Unix)自动选择合适的协议簇。
-
第二段:bind 和 listen
-
sock->bind(addr):绑定地址。失败则记录错误日志,将地址加入fails,继续处理下一个地址。 -
sock->listen():开始监听。失败处理同上。 -
m_socks.push_back(sock):成功则加入监听 Socket 数组。
第三段:失败回滚
-
if(!fails.empty()) { m_socks.clear(); return false; }:-
关键设计:只要有一个地址绑定/监听失败,清空所有已成功的 Socket,返回失败。
-
这是原子性保证:要么全部成功,要么全部失败,避免部分监听导致的不一致状态。
-
第四段:成功日志
-
遍历
m_socks,输出绑定成功日志,包含服务器类型、名称、SSL 状态和 Socket 信息。
六、start / stop / startAccept 详解
6.1 start() —— 启动服务(tcp_server.cc)
bool TcpServer::start() {
if(!m_isStop) {
return true;
}
m_isStop = false;
for(auto& sock : m_socks) {
m_acceptWorker->schedule(std::bind(&TcpServer::startAccept,
shared_from_this(), sock));
}
return true;
}
逐行拆解:
-
if(!m_isStop) { return true; }:幂等设计,如果已经在运行,直接返回成功。 -
m_isStop = false;:标记为运行状态。 -
m_acceptWorker->schedule(...):-
为每个监听 Socket 投递一个协程任务。
-
任务函数是
TcpServer::startAccept(sock),执行accept()循环。 -
使用
std::bind绑定shared_from_this()和sock:-
shared_from_this()获取shared_ptr<TcpServer>,确保协程执行期间TcpServer对象不会被销毁。 -
sock是当前监听 Socket。
-
-
为什么每个监听 Socket 需要一个独立的 startAccept 协程?
-
因为
accept()是阻塞调用(被 Hook 后变为协程让出)。 -
多个 Socket 不能共享一个
accept()调用,需要各自循环等待连接。
6.2 startAccept() —— accept 协程循环(tcp_server.cc)
void TcpServer::startAccept(Socket::ptr sock) {
while(!m_isStop) {
Socket::ptr client = sock->accept();
if(client) {
client->setRecvTimeout(m_recvTimeout);
m_ioWorker->schedule(std::bind(&TcpServer::handleClient,
shared_from_this(), client));
} else {
SYLAR_LOG_ERROR(g_logger) << "accept errno=" << errno
<< " errstr=" << strerror(errno);
}
}
}
逐行拆解:
-
while(!m_isStop):-
循环接受连接,直到服务器被
stop()。 -
m_isStop是volatile语义(实际为普通 bool,由stop()修改)。
-
-
Socket::ptr client = sock->accept();:-
调用
Socket::accept(),阻塞等待新连接。 -
被 Hook 后:如果没有新连接,当前协程 Yield,线程去执行其他任务;有新连接时,IOManager 恢复该协程。
-
-
if(client):-
accept()成功,返回新的客户端 Socket。 -
client->setRecvTimeout(m_recvTimeout):为新连接设置读取超时。
-
-
m_ioWorker->schedule(...):-
将
handleClient(client)投递到 IO 调度器执行。 -
使用
shared_from_this()延长TcpServer生命周期。 -
使用
std::bind绑定clientSocket。
-
-
else分支:-
accept()失败(返回nullptr),记录错误日志。 -
注意:这里不退出循环,继续下一次
accept()。因为错误可能是暂时的(如信号中断)。
-
6.3 stop() —— 停止服务(tcp_server.cc)
void TcpServer::stop() {
m_isStop = true;
auto self = shared_from_this();
m_acceptWorker->schedule([this, self]() {
for(auto& sock : m_socks) {
sock->cancelAll();
sock->close();
}
m_socks.clear();
});
}
逐行拆解:
-
m_isStop = true;:设置停止标志。所有startAccept循环会在下一次迭代时退出。 -
auto self = shared_from_this();:-
获取
shared_ptr,防止 lambda 执行期间TcpServer被销毁。
-
-
m_acceptWorker->schedule([this, self]() { ... }):-
将关闭操作投递到
acceptWorker执行。 -
为什么要在 acceptWorker 上执行? 因为
startAccept协程运行在该调度器上,且可能阻塞在accept()。先cancelAll()取消 epoll 事件,唤醒阻塞的accept(),使其返回错误,然后才能安全close()。
-
-
sock->cancelAll();:-
取消 Socket 上注册的所有 epoll 事件(READ、WRITE、ACCEPT)。
-
这会唤醒阻塞在该 Socket 上的协程。
-
-
sock->close();:关闭 fd。 -
m_socks.clear();:清空监听 Socket 数组。
6.4 handleClient() —— 业务处理虚函数(tcp_server.cc)
void TcpServer::handleClient(Socket::ptr client) {
SYLAR_LOG_INFO(g_logger) << "handleClient: " << *client;
}
-
基类默认实现:只打印一条日志,什么都不做。
-
子类必须重写:如
HttpServer::handleClient会创建SocketStream,解析 HTTP 请求,调用业务回调。 -
参数
client是新连接的客户端 Socket,已由accept()创建并连接成功。
七、辅助方法详解
7.1 setConf()(tcp_server.cc)
void TcpServer::setConf(const TcpServerConf& v) {
m_conf.reset(new TcpServerConf(v));
}
-
从值拷贝创建新的配置对象。
-
m_conf是shared_ptr,reset(new ...)释放旧对象,指向新对象。
7.2 loadCertificates()(tcp_server.cc)
bool TcpServer::loadCertificates(const std::string& cert_file,
const std::string& key_file) {
for(auto& i : m_socks) {
auto ssl_socket = std::dynamic_pointer_cast<SSLSocket>(i);
if(ssl_socket) {
if(!ssl_socket->loadCertificates(cert_file, key_file)) {
return false;
}
}
}
return true;
}
-
遍历所有监听 Socket,如果是
SSLSocket,加载 SSL 证书和私钥。 -
std::dynamic_pointer_cast:将Socket::ptr安全向下转型为SSLSocket::ptr。 -
如果任一 Socket 加载失败,立即返回
false。
7.3 toString()(tcp_server.cc)
std::string TcpServer::toString(const std::string& prefix) {
std::stringstream ss;
ss << prefix << "[type=" << m_type
<< " name=" << m_name << " ssl=" << m_ssl
<< " worker=" << (m_worker ? m_worker->getName() : "")
<< " accept=" << (m_acceptWorker ? m_acceptWorker->getName() : "")
<< " recv_timeout=" << m_recvTimeout << "]" << std::endl;
std::string pfx = prefix.empty() ? " " : prefix;
for(auto& i : m_socks) {
ss << pfx << pfx << *i << std::endl;
}
return ss.str();
}
-
输出服务器状态信息,用于调试和日志。
-
遍历
m_socks,逐个输出监听 Socket 的详细信息。
八、多调度器分离详解

分离的意义:
-
acceptWorker:只执行accept(),非常轻量。如果和业务混在同一个调度器,大量并发连接时accept()可能被饿死。 -
ioWorker:执行 Socket 读写。IO 操作会频繁 Yield/Resume,需要足够的线程处理并发。 -
worker/processWorker:执行业务逻辑(如 HTTP 解析、数据库查询)。CPU 密集型任务需要更多线程。
默认行为:
-
三个参数默认都是
IOManager::GetThis(),即单调度器模式,适合简单场景。
九、完整调用链梳理
以 EchoServer 启动并接受一个连接为例:
步骤 1:创建并绑定
EchoServer::ptr server(new EchoServer(&iom));
server->bind(addr); // Socket::CreateTCP → bind → listen → m_socks.push_back
步骤 2:启动服务器
server->start();
// m_isStop = false
// 对每个 sock in m_socks:
// m_acceptWorker->schedule(startAccept(sock))
步骤 3:startAccept 协程运行
void TcpServer::startAccept(Socket::ptr sock) {
while(!m_isStop) {
Socket::ptr client = sock->accept(); // 无连接时 Yield
if(client) {
client->setRecvTimeout(m_recvTimeout);
m_ioWorker->schedule(handleClient, client);
}
}
}
步骤 4:handleClient 被调度
void EchoServer::handleClient(Socket::ptr client) {
char buf[1024];
while(true) {
int rt = client->recv(buf, sizeof(buf)); // Hook 后:无数据则 Yield
if(rt <= 0) break;
client->send(buf, rt);
}
}
步骤 5:停止服务器
server->stop();
// m_isStop = true
// acceptWorker 执行:cancelAll → close → m_socks.clear()
// startAccept 循环退出
十、线程安全总结
| 对象 | 保护内容 | 说明 |
|---|---|---|
TcpServer 对象 | m_socks、m_isStop | 无显式锁,但 m_socks 只在 bind() / stop() 中修改,运行期只读 |
Socket::accept() | 内核管理 | accept() 本身是线程安全的,多线程同时 accept 同一个 fd 由内核排队 |
m_ioWorker->schedule() | 任务队列 | IOManager 内部有锁保护任务队列 |
shared_from_this() | 引用计数 | shared_ptr 的原子引用计数保证线程安全 |
关键设计:
-
TcpServer本身无锁,状态变更(bind、start、stop)通常由主线程串行执行。 -
startAccept循环运行在acceptWorker上,handleClient运行在ioWorker上,天然分离。 -
m_isStop是简单 bool,如果需要在运行期频繁读写,建议加std::atomic(当前版本未加,因为 stop/start 通常由控制线程调用)。
十一、学习验证清单
学完后,你应该能:
- 解释
TcpServer的两阶段启动(bind→start),以及为什么m_isStop初始为true。 - 说明
bind()中"任一地址失败则全部回滚"的设计意图。 - 解释
startAccept中accept()被 Hook 后的协程行为(Yield → 新连接到达 → Resume → schedule handleClient)。 - 说明
stop()中为什么要先cancelAll()再close()。 - 解释
accept_worker、io_worker、worker三个调度器分离的意义和适用场景。 - 说明
handleClient为什么是虚函数,以及子类应该如何重写它。 - 解释
shared_from_this()在start()/stop()/startAccept()中的作用。 - 说明
TcpServerConf如何接入配置系统(LexicalCast特化)。
结语
今天我们学完了Stream流和TcpServer模块,我们知道了为什么需要这两个东西,也知道了这两个东西怎么去设计,怎么去与我们之前模块进行搭配,想必还是非常清楚的。
我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。
无限进步,我们下次再见!
---⭐️封面自取⭐️---


1303

被折叠的 条评论
为什么被折叠?



