【C++大型项目之高性能服务器框架 (八) 】Stream流和TcpServer模块

⭐️在这个怀疑的年代,我们依然需要信仰。

个人主页 :YYYing.

⭐️C++大型项目系列专栏:C++大型项目之高性能服务器框架

系列上期内容:【C++项目之高性能服务器框架 (七) 】Socket与ByteArray模块

系列下期内容:暂无


目录

前言:

Stream流抽象接口

一、设计与目的

二、整体架构

三、Stream 类详解

3.1 定义(stream.h)

3.2 纯虚接口语义表

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)

四、Stream 在框架中的分层位置

五、完整调用链梳理

六、线程安全总结

七、学习验证清单

SocketStream详解

一、设计与目的

二、整体架构

三、SocketStream 类详解

3.1 定义(socket_stream.h)

3.2 成员变量表

3.3 构造函数(socket_stream.cc)

3.4 析构函数(socket_stream.cc)

四、read/write 实现详解

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)

4.5 close()(socket_stream.cc)

五、辅助方法详解

5.1 isConnected()(socket_stream.cc)

5.2 地址获取方法(socket_stream.cc)

六、ByteArray 与 iovec 的配合原理

七、完整调用链梳理

八、线程安全总结

九、学习验证清单

TCPServer详解

一、设计与目的

三、TcpServerConf 配置结构体详解

3.1 定义(tcp_server.h)

3.2 YAML 配置 ↔ 结构体转换(tcp_server.h)

四、TcpServer 类详解

4.1 定义(tcp_server.h)

4.2 成员变量表

4.3 构造函数(tcp_server.cc)

4.4 析构函数(tcp_server.cc)

五、bind 详解

5.1 单地址绑定(tcp_server.cc)

5.2 多地址绑定(tcp_server.cc)

六、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.1 setConf()(tcp_server.cc)

7.2 loadCertificates()(tcp_server.cc)

7.3 toString()(tcp_server.cc)

八、多调度器分离详解

九、完整调用链梳理

十、线程安全总结

十一、学习验证清单

结语

---⭐️封面自取⭐️---



前言:

        上一篇我们说完了Socket与ByteArray的内容,我们封装了socket的各种api,并且搭配了超时管理与io管理,提供了基础类型的序列化与反序列化功能,那么这一篇我们开始学习提供字节流读写的流结构与Tcp服务器模块,废话不多说,我们开始。

Stream流抽象接口

一、设计与目的

流结构,提供字节流读写接口。

        所有的流结构都继承自抽象类Stream,Stream类规定了一个流必须具备read/write接口和readFixSize/writeFixSize接口,继承自Stream的类必须实现这些接口。

        那为什么要抽象 Stream 接口?

在传统的网络编程中,协议层代码往往直接调用 send() / recv() 等 Socket API。这导致:

  1. 协议与传输强耦合:HTTP 解析器只能工作在 TCP Socket 上,无法直接用于文件或内存。

  2. 测试困难:想要对协议层做单元测试,必须真的启动网络服务。

  3. 扩展困难:增加 SSL 加密、压缩传输时,需要修改所有协议代码。

        Stream 抽象如何解决这些问题?

问题解决方案
协议与传输耦合协议层只依赖 Stream::ptr,底层可以是 Socket、文件、内存
测试困难可以用 MemoryStream 模拟数据,无需真实网络
扩展困难新增传输方式只需继承 Stream,协议代码零改动
读写不完整性readFixSize / writeFixSize 自动处理短读/短写,上层无需关心

        设计哲学:面向接口编程

// 坏设计:协议层直接依赖 Socket
void handleHttp(int sockfd);   // 只能用于网络 Socket
​
// 好设计:协议层依赖 Stream 抽象
void handleHttp(Stream::ptr stream);   // 可用于 Socket、文件、内存、SSL

sylar 框架中的 HttpConnectionRockProtocol 等协议类,内部全部持有 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;
};

类设计要点:

  • ptrstd::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;
}

逐行拆解:

  1. size_t offset = 0:已写入缓冲区的偏移量,初始为 0。

  2. int64_t left = length:还剩多少字节需要读取。用 int64_t 而非 size_t 是因为 read() 返回 int(有符号),混用无符号类型会导致比较问题。

  3. while(left > 0):循环直到读取够 length 字节,或遇到错误/关闭。

  4. read((char*)buffer + offset, left):调用虚函数 read(),从当前偏移位置继续读取。

    • (char*)buffer + offset:将 void* 转为 char* 做指针算术,因为 void* 不支持直接加减。

  5. if(len <= 0) { return len; }

    • len == 0:对端关闭(EOF),直接返回 0。

    • len < 0:发生错误,将错误码向上传递。

    • 注意:这里不返回已读取的部分长度,而是直接返回错误码。因为调用者期望的是"要么读完固定长度,要么失败"。

  6. offset += len; left -= len;:更新偏移和剩余量。

  7. 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::recvRequestStream::readSocketStream::readSocket::recvHook::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 的接口。


二、整体架构

设计意图:

  • SocketStreamStream 抽象在网络 Socket 场景下的具体实现。

  • 将 Socket 的 send/recv 语义包装为 Streamread/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_socketSocket::ptr构造时传入底层 Socket 智能指针
m_ownerbooltrue是否拥有 Socket 的生命周期控制权

3.3 构造函数(socket_stream.cc)

SocketStream::SocketStream(Socket::ptr sock, bool owner)
    :m_socket(sock)
    ,m_owner(owner) {
}

逐参数拆解:

参数类型说明
sockSocket::ptr底层 Socket,必须已连接或已绑定/监听
ownerbooltrue: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();
    }
}

逻辑:

  1. if(m_owner):只有拥有控制权时才关闭。

  2. if(m_socket):防止空指针解引用。

  3. 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);
}

逐行拆解:

  1. if(!isConnected()) { return -1; }

    • 先检查 Socket 是否已连接。如果未连接,直接返回 -1(错误)。

    • 避免对无效 fd 调用 recv() 产生未定义行为。

  2. 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;
}

逐行拆解:

  1. std::vector<iovec> iovs;

    • iovec 是 POSIX 标准的分散/聚集 IO结构体,包含 { iov_base, iov_len }

    • 允许一次 readv/writev 系统调用读写多个不连续的内存块。

  2. ba->getWriteBuffers(iovs, length);

    • ByteArray 内部由多个 Node(内存块链表)组成。

    • getWriteBuffersByteArray 中可写入的内存区域转换为 iovec 数组。

    • 如果 ByteArray 容量不足,内部会自动扩容(创建新的 Node)。

  3. m_socket->recv(&iovs[0], iovs.size());

    • 调用 Socket::recv(iovec*, size_t),底层使用 ::readv() 系统调用。

    • 零拷贝:数据直接从内核缓冲区写入 ByteArray 的各个 Node,无需先拷贝到中间栈缓冲区。

  4. 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/writeisConnected() 会返回 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_socketm_owner无显式锁,但通常单个 SocketStream 只被一个协程操作
Socket 内部fd 操作由 Hook 和 IOManager 的 epoll 机制保证串行化
ByteArraym_positionm_size如果多个协程共享同一个 ByteArray,需要外部加锁

关键设计:

  • SocketStream 本身不加锁,因为它的使用模式通常是"一个连接对应一个协程/线程"。

  • 如果需要在多个协程间共享 SocketStream,需要由调用方保证互斥(如每个协程有独立的 ByteArray 缓冲区)。


九、学习验证清单

学完后,你应该能:

  • 解释 m_owner 参数的作用,并举出 owner = false 的两种使用场景。
  • 说明 read(ByteArray::ptr) 相比 read(void*) 的优势(零拷贝、自动扩容)。
  • 解释 getWriteBuffersgetReadBuffers 的区别(前者获取空闲区域用于读入,后者获取有数据区域用于写出)。
  • 说明 iovec + readv/writev 如何配合 ByteArray 的链表结构实现零拷贝。
  • 解释为什么 SocketStream::close() 后,m_socket 智能指针仍然非空。
  • 画出 SocketStream::readByteArray::getWriteBuffersSocket::recv(iovec)::readvHook 的完整调用链。

TCPServer详解

一、设计与目的

TCP服务器封装。

        TcpServer类支持同时绑定多个地址进行监听,只需要在绑定时传入地址数组即可。TcpServer还可以分别指定接收客户端和处理客户端的协程调度器。

为什么要封装 TcpServer?

在传统的网络编程中,编写一个 TCP 服务器需要手动处理:

  1. socket()bind()listen()accept() 的完整流程。

  2. 多线程/多进程处理并发连接。

  3. 服务器的启动、停止、资源清理。

  4. 多端口监听、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;
    }
};

成员变量表:

变量名类型默认值含义
addressvector<string>监听地址列表,如 "0.0.0.0:8080""[::]:8080"
keepaliveint0TCP keepalive 开关
timeoutint120000(2分钟)连接读取超时时间(毫秒)
sslint0是否启用 SSL(HTTPS)
idstring""服务器唯一标识
typestring"http"服务器类型:httpwsrock
namestring""服务器名称
cert_filestring""SSL 证书文件路径
key_filestring""SSL 私钥文件路径
accept_workerstring""accept 协程运行的调度器名称
io_workerstring""IO 协程运行的调度器名称
process_workerstring""业务处理协程运行的调度器名称
argsmap<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_socksvector<Socket::ptr>监听 Socket 数组,支持多地址绑定
m_workerIOManager*IOManager::GetThis()主工作调度器(通用任务)
m_ioWorkerIOManager*IOManager::GetThis()新连接的 IO 处理调度器
m_acceptWorkerIOManager*IOManager::GetThis()接收连接的调度器
m_recvTimeoutuint64_t配置系统默认值(2分钟)客户端连接读取超时(毫秒)
m_namestring"sylar/1.0.0"服务器名称
m_typestring"tcp"服务器类型标识
m_isStopbooltrue是否已停止(构造时默认为停止状态)
m_sslboolfalse是否启用 SSL
m_confTcpServerConf::ptrnullptr配置对象

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) {
}

逐行拆解:

  1. m_worker(worker):主工作调度器。默认可用 IOManager::GetThis() 获取当前线程的调度器。

  2. m_ioWorker(io_worker):IO 处理调度器。新连接的数据读写在此调度器上执行。

  3. m_acceptWorker(accept_worker):accept 调度器。专门执行 accept() 循环。

  4. m_recvTimeout(g_tcp_server_read_timeout->getValue())

    • g_tcp_server_read_timeout 是配置系统定义的变量,默认值为 60 * 1000 * 2(2分钟)。

    • 每个新连接的 Socket 会继承这个超时值。

  5. m_name("sylar/1.0.0"):默认服务器名称。

  6. 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

  1. m_ssl = ssl;:记录全局 SSL 标志。

  2. ssl ? SSLSocket::CreateTCP(addr) : Socket::CreateTCP(addr)

    • 如果启用 SSL,创建 SSLSocket;否则创建普通 Socket

    • CreateTCP(addr) 会根据地址类型(IPv4/IPv6/Unix)自动选择合适的协议簇。

第二段:bind 和 listen

  1. sock->bind(addr):绑定地址。失败则记录错误日志,将地址加入 fails,继续处理下一个地址。

  2. sock->listen():开始监听。失败处理同上。

  3. m_socks.push_back(sock):成功则加入监听 Socket 数组。

第三段:失败回滚

  1. if(!fails.empty()) { m_socks.clear(); return false; }

    • 关键设计:只要有一个地址绑定/监听失败,清空所有已成功的 Socket,返回失败。

    • 这是原子性保证:要么全部成功,要么全部失败,避免部分监听导致的不一致状态。

第四段:成功日志

  1. 遍历 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;
}

逐行拆解:

  1. if(!m_isStop) { return true; }:幂等设计,如果已经在运行,直接返回成功。

  2. m_isStop = false;:标记为运行状态。

  3. 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);
        }
    }
}

逐行拆解:

  1. while(!m_isStop)

    • 循环接受连接,直到服务器被 stop()

    • m_isStopvolatile 语义(实际为普通 bool,由 stop() 修改)。

  2. Socket::ptr client = sock->accept();

    • 调用 Socket::accept(),阻塞等待新连接。

    • 被 Hook 后:如果没有新连接,当前协程 Yield,线程去执行其他任务;有新连接时,IOManager 恢复该协程。

  3. if(client)

    • accept() 成功,返回新的客户端 Socket。

    • client->setRecvTimeout(m_recvTimeout):为新连接设置读取超时。

  4. m_ioWorker->schedule(...)

    • handleClient(client) 投递到 IO 调度器执行。

    • 使用 shared_from_this() 延长 TcpServer 生命周期。

    • 使用 std::bind 绑定 client Socket。

  5. 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();
    });
}

逐行拆解:

  1. m_isStop = true;:设置停止标志。所有 startAccept 循环会在下一次迭代时退出。

  2. auto self = shared_from_this();

    • 获取 shared_ptr,防止 lambda 执行期间 TcpServer 被销毁。

  3. m_acceptWorker->schedule([this, self]() { ... })

    • 将关闭操作投递到 acceptWorker 执行。

    • 为什么要在 acceptWorker 上执行? 因为 startAccept 协程运行在该调度器上,且可能阻塞在 accept()。先 cancelAll() 取消 epoll 事件,唤醒阻塞的 accept(),使其返回错误,然后才能安全 close()

  4. sock->cancelAll();

    • 取消 Socket 上注册的所有 epoll 事件(READ、WRITE、ACCEPT)。

    • 这会唤醒阻塞在该 Socket 上的协程。

  5. sock->close();:关闭 fd。

  6. 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_confshared_ptrreset(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_socksm_isStop无显式锁,但 m_socks 只在 bind() / stop() 中修改,运行期只读
Socket::accept()内核管理accept() 本身是线程安全的,多线程同时 accept 同一个 fd 由内核排队
m_ioWorker->schedule()任务队列IOManager 内部有锁保护任务队列
shared_from_this()引用计数shared_ptr 的原子引用计数保证线程安全

关键设计:

  • TcpServer 本身无锁,状态变更(bindstartstop)通常由主线程串行执行。

  • startAccept 循环运行在 acceptWorker 上,handleClient 运行在 ioWorker 上,天然分离。

  • m_isStop 是简单 bool,如果需要在运行期频繁读写,建议加 std::atomic(当前版本未加,因为 stop/start 通常由控制线程调用)。


十一、学习验证清单

学完后,你应该能:

  • 解释 TcpServer 的两阶段启动(bindstart),以及为什么 m_isStop 初始为 true
  • 说明 bind() 中"任一地址失败则全部回滚"的设计意图。
  • 解释 startAcceptaccept() 被 Hook 后的协程行为(Yield → 新连接到达 → Resume → schedule handleClient)。
  • 说明 stop() 中为什么要先 cancelAll()close()
  • 解释 accept_workerio_workerworker 三个调度器分离的意义和适用场景。
  • 说明 handleClient 为什么是虚函数,以及子类应该如何重写它。
  • 解释 shared_from_this()start() / stop() / startAccept() 中的作用。
  • 说明 TcpServerConf 如何接入配置系统(LexicalCast 特化)。

结语

        今天我们学完了Stream流和TcpServer模块,我们知道了为什么需要这两个东西,也知道了这两个东西怎么去设计,怎么去与我们之前模块进行搭配,想必还是非常清楚的。

我是YYYing,后面还有更精彩的内容,希望各位能多多关注支持一下主包。

无限进步,我们下次再见!


---⭐️封面自取⭐️---

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值