【C++大型项目之高性能服务器框架 (七) 】Socket与ByteArray模块

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

个人主页 :YYYing.

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

系列上期内容:【C++项目之高性能服务器框架 (六) 】hook、FD管理与Address模块

系列下期内容:暂无


目录

前言:

Sokect模块

一、设计与目的

二、整体架构

三、枚举与类型定义

[3.1 Socket::Type ------ Socket类型(socket.h)](#3.1 Socket::Type —— Socket类型(socket.h))

[3.2 Socket::Family ------ 协议簇(socket.h)](#3.2 Socket::Family —— 协议簇(socket.h))

[四、Socket 类详解](#四、Socket 类详解)

[4.1 定义(socket.h)](#4.1 定义(socket.h))

[4.2 成员变量表](#4.2 成员变量表)

[4.3 构造函数(socket.cc)](#4.3 构造函数(socket.cc))

[4.4 析构函数(socket.cc)](#4.4 析构函数(socket.cc))

五、工厂方法详解

[5.1 CreateTCP / CreateUDP(socket.cc)](#5.1 CreateTCP / CreateUDP(socket.cc))

[5.2 便捷工厂(socket.cc)](#5.2 便捷工厂(socket.cc))

[六、核心 API 详解](#六、核心 API 详解)

[6.1 newSock() ------ 创建底层 fd(socket.cc)](#6.1 newSock() —— 创建底层 fd(socket.cc))

[6.2 initSock() ------ 初始化 socket 选项(socket.cc)](#6.2 initSock() —— 初始化 socket 选项(socket.cc))

[6.3 init() ------ 用已有 fd 初始化 Socket(socket.cc)](#6.3 init() —— 用已有 fd 初始化 Socket(socket.cc))

[6.4 bind() ------ 绑定地址(socket.cc)](#6.4 bind() —— 绑定地址(socket.cc))

[6.5 connect() ------ 连接远端(socket.cc)](#6.5 connect() —— 连接远端(socket.cc))

[6.6 listen() / accept()(socket.cc)](#6.6 listen() / accept()(socket.cc))

[6.7 close()(socket.cc)](#6.7 close()(socket.cc))

[七、数据收发 API 详解](#七、数据收发 API 详解)

[7.1 send() 单缓冲区(socket.cc)](#7.1 send() 单缓冲区(socket.cc))

[7.2 send() 多缓冲区(socket.c)](#7.2 send() 多缓冲区(socket.c))

[7.3 sendTo() 系列(socket.cc)](#7.3 sendTo() 系列(socket.cc))

[7.4 recv() / recvFrom() 系列(socket.cc)](#7.4 recv() / recvFrom() 系列(socket.cc))

八、地址与选项管理

[8.1 getLocalAddress() / getRemoteAddress()(socket.cc)](#8.1 getLocalAddress() / getRemoteAddress()(socket.cc))

[8.2 getOption() / setOption()(socket.cc)](#8.2 getOption() / setOption()(socket.cc))

[8.3 超时管理(socket.cc)](#8.3 超时管理(socket.cc))

[九、IOManager 集成 ------ 事件取消](#九、IOManager 集成 —— 事件取消)

[9.1 cancelRead() / cancelWrite() / cancelAccept() / cancelAll()(socket.cc)](#9.1 cancelRead() / cancelWrite() / cancelAccept() / cancelAll()(socket.cc))

[十、SSLSocket 详解](#十、SSLSocket 详解)

[10.1 定义(socket.h)](#10.1 定义(socket.h))

[10.2 connect() ------ SSL 握手(socket.cc)](#10.2 connect() —— SSL 握手(socket.cc))

[10.3 init() ------ 服务端 SSL 接受(socket.cc)](#10.3 init() —— 服务端 SSL 接受(socket.cc))

[10.4 send() / recv() ------ SSL 读写(socket.cc)](#10.4 send() / recv() —— SSL 读写(socket.cc))

[10.5 证书加载(socket.cc)](#10.5 证书加载(socket.cc))

十一、完整调用链梳理

[以 TCP 客户端连接并发送数据为例:](#以 TCP 客户端连接并发送数据为例:)

[步骤 1:创建 Socket](#步骤 1:创建 Socket)

[步骤 2:连接服务端](#步骤 2:连接服务端)

[步骤 3:发送数据](#步骤 3:发送数据)

[步骤 4:关闭连接](#步骤 4:关闭连接)

[以 TCP 服务端接受连接为例:](#以 TCP 服务端接受连接为例:)

[步骤 1:创建 → Bind → Listen](#步骤 1:创建 → Bind → Listen)

[步骤 2:Accept](#步骤 2:Accept)

十二、设计要点总结

十三、学习验证清单

ByteArray模块

一、设计与目的

[三、Node 结构详解](#三、Node 结构详解)

[3.1 定义(bytearray.h)](#3.1 定义(bytearray.h))

[3.2 构造函数(bytearray.cc)](#3.2 构造函数(bytearray.cc))

[3.3 析构函数(bytearray.cc)](#3.3 析构函数(bytearray.cc))

[四、ByteArray 类详解](#四、ByteArray 类详解)

[4.1 定义(bytearray.h)](#4.1 定义(bytearray.h))

[4.2 成员变量表](#4.2 成员变量表)

[4.3 构造函数与析构函数(bytearray.cc)](#4.3 构造函数与析构函数(bytearray.cc))

五、字节序处理

[5.1 依赖:endian.h](#5.1 依赖:endian.h)

[5.2 byteswap 模板(endian.h)](#5.2 byteswap 模板(endian.h))

[5.3 字节序转换规则(bytearray.cc)](#5.3 字节序转换规则(bytearray.cc))

六、固定长度读写详解

[6.1 固定长度整数(bytearray.cc)](#6.1 固定长度整数(bytearray.cc))

[6.2 Float / Double(bytearray.cc)](#6.2 Float / Double(bytearray.cc))

[七、Varint 编码详解](#七、Varint 编码详解)

[7.1 什么是 Varint?](#7.1 什么是 Varint?)

[7.2 ZigZag 编码 ------ 解决负数问题](#7.2 ZigZag 编码 —— 解决负数问题)

[7.3 writeUint32 ------ 无符号 Varint 编码(bytearray.cc)](#7.3 writeUint32 —— 无符号 Varint 编码(bytearray.cc))

[7.4 readUint32 ------ 无符号 Varint 解码(bytearray.cc)](#7.4 readUint32 —— 无符号 Varint 解码(bytearray.cc))

[7.5 writeInt32 / readInt32 ------ 有符号 Varint(bytearray.cc)](#7.5 writeInt32 / readInt32 —— 有符号 Varint(bytearray.cc))

八、字符串读写详解

[8.1 五种字符串编码方式](#8.1 五种字符串编码方式)

[8.2 writeStringF16(bytearray.cc)](#8.2 writeStringF16(bytearray.cc))

[8.3 readStringF16(bytearray.cc)](#8.3 readStringF16(bytearray.cc))

九、底层读写核心

[9.1 write() ------ 向 ByteArray 写入原始字节(bytearray.cc)](#9.1 write() —— 向 ByteArray 写入原始字节(bytearray.cc))

[9.2 read() ------ 从 ByteArray 读取原始字节(bytearray.cc)](#9.2 read() —— 从 ByteArray 读取原始字节(bytearray.cc))

[9.3 read(pos) ------ 指定位置读取(bytearray.cc)](#9.3 read(pos) —— 指定位置读取(bytearray.cc))

[9.4 addCapacity() ------ 扩容(bytearray.cc)](#9.4 addCapacity() —— 扩容(bytearray.cc))

十、工具函数详解

[10.1 clear() ------ 清空(bytearray.cc)](#10.1 clear() —— 清空(bytearray.cc))

[10.2 setPosition() ------ 设置位置(bytearray.cc)](#10.2 setPosition() —— 设置位置(bytearray.cc))

[10.3 toString() / toHexString()(bytearray.cc)](#10.3 toString() / toHexString()(bytearray.cc))

[10.4 文件读写(bytearray.cc)](#10.4 文件读写(bytearray.cc))

[十一、iovec 支持 ------ 零拷贝对接系统调用](#十一、iovec 支持 —— 零拷贝对接系统调用)

[11.1 getReadBuffers()(bytearray.cc)](#11.1 getReadBuffers()(bytearray.cc))

[11.2 getWriteBuffers()(bytearray.cc)](#11.2 getWriteBuffers()(bytearray.cc))

十二、完整调用链梳理

[以写入一个 int32_t 为例:](#以写入一个 int32_t 为例:)

[步骤 1:调用写入函数](#步骤 1:调用写入函数)

[步骤 2:字节序转换](#步骤 2:字节序转换)

[步骤 3:写入底层缓冲区](#步骤 3:写入底层缓冲区)

[以写入一个 Varint 为例:](#以写入一个 Varint 为例:)

[步骤 1:调用写入函数](#步骤 1:调用写入函数)

[步骤 2:ZigZag 编码](#步骤 2:ZigZag 编码)

[步骤 3:Varint 编码](#步骤 3:Varint 编码)

[以读取一个 String 为例:](#以读取一个 String 为例:)

[步骤 1:调用读取函数](#步骤 1:调用读取函数)

[步骤 2:读取长度(Varint)](#步骤 2:读取长度(Varint))

[步骤 3:读取内容](#步骤 3:读取内容)

十三、设计要点总结

十四、学习验证清单

结语

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



前言:

这一篇的难度跟上一篇差不了太多,也就是学起来不会那么吃力(除了可能个别几个算法有点复杂),但同样也很重要,废话不多说,开始我们今天的内容。

Sokect模块

一、设计与目的

套接字类,表示一个套接字对象。

对于套接字类,需要关注以下属性:

  1. 文件描述符
  2. 地址类型(AF_INET, AF_INET6等)
  3. 套接字类型(SOCK_STREAM, SOCK_DGRAM等)
  4. 协议类型(这项其实可以忽略)
  5. 是否连接(针对TCP套接字,如果是UDP套接字,则默认已连接)
  6. 本地地址和对端的地址

套接字类应提供以下方法:

  1. 创建各种类型的套接字对象的方法(TCP套接字,UDP套接字,Unix域套接字)
  2. 设置套接字选项,比如超时参数
  3. bind/connect/listen方法,实现绑定地址、发起连接、发起监听功能
  4. accept方法,返回连入的套接字对象
  5. 发送、接收数据的方法
  6. 获取本地地址、远端地址的方法
  7. 获取套接字类型、地址类型、协议类型的方法
  8. 取消套接字读、写的方法

二、整体架构


三、枚举与类型定义

3.1 Socket::Type ------ Socket类型(socket.h)

cpp 复制代码
enum Type {
    TCP = SOCK_STREAM,   // 流式套接字
    UDP = SOCK_DGRAM     // 数据报套接字
};
  • 直接映射到 POSIX 标准宏:SOCK_STREAM = 1SOCK_DGRAM = 2

  • 用于 socket() 系统调用的第二个参数。

3.2 Socket::Family ------ 协议簇(socket.h)

cpp 复制代码
enum Family {
    IPv4 = AF_INET,      // IPv4 地址族
    IPv6 = AF_INET6,     // IPv6 地址族
    UNIX = AF_UNIX,      // Unix Domain Socket
};
  • 映射到 POSIX 标准宏:AF_INET = 2AF_INET6 = 10AF_UNIX = 1(Linux)。

  • 用于 socket() 的第一个参数,决定地址格式。


四、Socket 类详解

4.1 定义(socket.h)

cpp 复制代码
/**
 * @brief Socket封装类
 */
class Socket : public std::enable_shared_from_this<Socket>, Noncopyable {
public:
    typedef std::shared_ptr<Socket> ptr;
    typedef std::weak_ptr<Socket> weak_ptr;

    /**
     * @brief Socket类型
     */
    enum Type {
        /// TCP类型
        TCP = SOCK_STREAM,
        /// UDP类型
        UDP = SOCK_DGRAM
    };

    /**
     * @brief Socket协议簇
     */
    enum Family {
        /// IPv4 socket
        IPv4 = AF_INET,
        /// IPv6 socket
        IPv6 = AF_INET6,
        /// Unix socket
        UNIX = AF_UNIX,
    };

    /**
     * @brief 创建TCP Socket(满足地址类型)
     * @param[in] address 地址
     */
    static Socket::ptr CreateTCP(sylar::Address::ptr address);

    /**
     * @brief 创建UDP Socket(满足地址类型)
     * @param[in] address 地址
     */
    static Socket::ptr CreateUDP(sylar::Address::ptr address);

    /**
     * @brief 创建IPv4的TCP Socket
     */
    static Socket::ptr CreateTCPSocket();

    /**
     * @brief 创建IPv4的UDP Socket
     */
    static Socket::ptr CreateUDPSocket();

    /**
     * @brief 创建IPv6的TCP Socket
     */
    static Socket::ptr CreateTCPSocket6();

    /**
     * @brief 创建IPv6的UDP Socket
     */
    static Socket::ptr CreateUDPSocket6();

    /**
     * @brief 创建Unix的TCP Socket
     */
    static Socket::ptr CreateUnixTCPSocket();

    /**
     * @brief 创建Unix的UDP Socket
     */
    static Socket::ptr CreateUnixUDPSocket();

    /**
     * @brief Socket构造函数
     * @param[in] family 协议簇
     * @param[in] type 类型
     * @param[in] protocol 协议
     */
    Socket(int family, int type, int protocol = 0);

    /**
     * @brief 析构函数
     */
    virtual ~Socket();

    /**
     * @brief 获取发送超时时间(毫秒)
     */
    int64_t getSendTimeout();

    /**
     * @brief 设置发送超时时间(毫秒)
     */
    void setSendTimeout(int64_t v);

    /**
     * @brief 获取接受超时时间(毫秒)
     */
    int64_t getRecvTimeout();

    /**
     * @brief 设置接受超时时间(毫秒)
     */
    void setRecvTimeout(int64_t v);

    /**
     * @brief 获取sockopt @see getsockopt
     */
    bool getOption(int level, int option, void* result, socklen_t* len);

    /**
     * @brief 获取sockopt模板 @see getsockopt
     */
    template<class T>
    bool getOption(int level, int option, T& result) {
        socklen_t length = sizeof(T);
        return getOption(level, option, &result, &length);
    }

    /**
     * @brief 设置sockopt @see setsockopt
     */
    bool setOption(int level, int option, const void* result, socklen_t len);

    /**
     * @brief 设置sockopt模板 @see setsockopt
     */
    template<class T>
    bool setOption(int level, int option, const T& value) {
        return setOption(level, option, &value, sizeof(T));
    }

    /**
     * @brief 接收connect链接
     * @return 成功返回新连接的socket,失败返回nullptr
     * @pre Socket必须 bind , listen  成功
     */
    virtual Socket::ptr accept();

    /**
     * @brief 绑定地址
     * @param[in] addr 地址
     * @return 是否绑定成功
     */
    virtual bool bind(const Address::ptr addr);

    /**
     * @brief 连接地址
     * @param[in] addr 目标地址
     * @param[in] timeout_ms 超时时间(毫秒)
     */
    virtual bool connect(const Address::ptr addr, uint64_t timeout_ms = -1);

    virtual bool reconnect(uint64_t timeout_ms = -1);

    /**
     * @brief 监听socket
     * @param[in] backlog 未完成连接队列的最大长度
     * @result 返回监听是否成功
     * @pre 必须先 bind 成功
     */
    virtual bool listen(int backlog = SOMAXCONN);

    /**
     * @brief 关闭socket
     */
    virtual bool close();

    /**
     * @brief 发送数据
     * @param[in] buffer 待发送数据的内存
     * @param[in] length 待发送数据的长度
     * @param[in] flags 标志字
     * @return
     *      @retval >0 发送成功对应大小的数据
     *      @retval =0 socket被关闭
     *      @retval <0 socket出错
     */
    virtual int send(const void* buffer, size_t length, int flags = 0);

    /**
     * @brief 发送数据
     * @param[in] buffers 待发送数据的内存(iovec数组)
     * @param[in] length 待发送数据的长度(iovec长度)
     * @param[in] flags 标志字
     * @return
     *      @retval >0 发送成功对应大小的数据
     *      @retval =0 socket被关闭
     *      @retval <0 socket出错
     */
    virtual int send(const iovec* buffers, size_t length, int flags = 0);

    /**
     * @brief 发送数据
     * @param[in] buffer 待发送数据的内存
     * @param[in] length 待发送数据的长度
     * @param[in] to 发送的目标地址
     * @param[in] flags 标志字
     * @return
     *      @retval >0 发送成功对应大小的数据
     *      @retval =0 socket被关闭
     *      @retval <0 socket出错
     */
    virtual int sendTo(const void* buffer, size_t length, const Address::ptr to, int flags = 0);

    /**
     * @brief 发送数据
     * @param[in] buffers 待发送数据的内存(iovec数组)
     * @param[in] length 待发送数据的长度(iovec长度)
     * @param[in] to 发送的目标地址
     * @param[in] flags 标志字
     * @return
     *      @retval >0 发送成功对应大小的数据
     *      @retval =0 socket被关闭
     *      @retval <0 socket出错
     */
    virtual int sendTo(const iovec* buffers, size_t length, const Address::ptr to, int flags = 0);

    /**
     * @brief 接受数据
     * @param[out] buffer 接收数据的内存
     * @param[in] length 接收数据的内存大小
     * @param[in] flags 标志字
     * @return
     *      @retval >0 接收到对应大小的数据
     *      @retval =0 socket被关闭
     *      @retval <0 socket出错
     */
    virtual int recv(void* buffer, size_t length, int flags = 0);

    /**
     * @brief 接受数据
     * @param[out] buffers 接收数据的内存(iovec数组)
     * @param[in] length 接收数据的内存大小(iovec数组长度)
     * @param[in] flags 标志字
     * @return
     *      @retval >0 接收到对应大小的数据
     *      @retval =0 socket被关闭
     *      @retval <0 socket出错
     */
    virtual int recv(iovec* buffers, size_t length, int flags = 0);

    /**
     * @brief 接受数据
     * @param[out] buffer 接收数据的内存
     * @param[in] length 接收数据的内存大小
     * @param[out] from 发送端地址
     * @param[in] flags 标志字
     * @return
     *      @retval >0 接收到对应大小的数据
     *      @retval =0 socket被关闭
     *      @retval <0 socket出错
     */
    virtual int recvFrom(void* buffer, size_t length, Address::ptr from, int flags = 0);

    /**
     * @brief 接受数据
     * @param[out] buffers 接收数据的内存(iovec数组)
     * @param[in] length 接收数据的内存大小(iovec数组长度)
     * @param[out] from 发送端地址
     * @param[in] flags 标志字
     * @return
     *      @retval >0 接收到对应大小的数据
     *      @retval =0 socket被关闭
     *      @retval <0 socket出错
     */
    virtual int recvFrom(iovec* buffers, size_t length, Address::ptr from, int flags = 0);

    /**
     * @brief 获取远端地址
     */
    Address::ptr getRemoteAddress();

    /**
     * @brief 获取本地地址
     */
    Address::ptr getLocalAddress();

    /**
     * @brief 获取协议簇
     */
    int getFamily() const { return m_family;}

    /**
     * @brief 获取类型
     */
    int getType() const { return m_type;}

    /**
     * @brief 获取协议
     */
    int getProtocol() const { return m_protocol;}

    /**
     * @brief 返回是否连接
     */
    bool isConnected() const { return m_isConnected;}

    /**
     * @brief 是否有效(m_sock != -1)
     */
    bool isValid() const;

    /**
     * @brief 返回Socket错误
     */
    int getError();

    /**
     * @brief 输出信息到流中
     */
    virtual std::ostream& dump(std::ostream& os) const;

    virtual std::string toString() const;

    /**
     * @brief 返回socket句柄
     */
    int getSocket() const { return m_sock;}

    /**
     * @brief 取消读
     */
    bool cancelRead();

    /**
     * @brief 取消写
     */
    bool cancelWrite();

    /**
     * @brief 取消accept
     */
    bool cancelAccept();

    /**
     * @brief 取消所有事件
     */
    bool cancelAll();
protected:
    /**
     * @brief 初始化socket
     */
    void initSock();

    /**
     * @brief 创建socket
     */
    void newSock();

    /**
     * @brief 初始化sock
     */
    virtual bool init(int sock);
protected:
    /// socket句柄
    int m_sock;
    /// 协议簇
    int m_family;
    /// 类型
    int m_type;
    /// 协议
    int m_protocol;
    /// 是否连接
    bool m_isConnected;
    /// 本地地址
    Address::ptr m_localAddress;
    /// 远端地址
    Address::ptr m_remoteAddress;
};

继承关系:

  • std::enable_shared_from_this<Socket>:允许在成员函数中安全获取 shared_ptrshared_from_this())。

  • Noncopyable:禁止拷贝构造和拷贝赋值,避免 fd 重复关闭。


4.2 成员变量表

变量名 类型 默认值 含义
m_sock int -1 socket 文件描述符,-1 表示无效
m_family int 构造传入 协议簇(AF_INET/AF_INET6/AF_UNIX)
m_type int 构造传入 类型(SOCK_STREAM/SOCK_DGRAM)
m_protocol int 0 协议编号,通常由内核自动选择
m_isConnected bool false 连接状态标记
m_localAddress Address::ptr nullptr 本地地址,首次获取时懒加载
m_remoteAddress Address::ptr nullptr 远端地址,首次获取时懒加载

4.3 构造函数(socket.cc

cpp 复制代码
Socket::Socket(int family, int type, int protocol)
    :m_sock(-1)
    ,m_family(family)
    ,m_type(type)
    ,m_protocol(protocol)
    ,m_isConnected(false) {
}

设计要点:

  • 构造时不立即创建 fd ,只保存参数。fd 在 newSock()bind()/connect() 时才真正创建。

  • m_sock = -1:表示尚未分配有效的 socket 描述符。


4.4 析构函数(socket.cc

cpp 复制代码
Socket::~Socket() {
    close();
}
  • RAII 设计:对象销毁时自动关闭 socket,防止 fd 泄漏。

五、工厂方法详解

5.1 CreateTCP / CreateUDPsocket.cc

cpp 复制代码
Socket::ptr Socket::CreateTCP(sylar::Address::ptr address) {
    Socket::ptr sock(new Socket(address->getFamily(), TCP, 0));
    return sock;
}
​
Socket::ptr Socket::CreateUDP(sylar::Address::ptr address) {
    Socket::ptr sock(new Socket(address->getFamily(), UDP, 0));
    sock->newSock();
    sock->m_isConnected = true;
    return sock;
}

TCP vs UDP 的差异:

  • TCP :只构造对象,不创建 fd(延迟到 bind()connect())。

  • UDP :立即调用 newSock() 创建 fd,并设置 m_isConnected = true

    • UDP 是无连接协议,"connected" 在这里表示 fd 已准备好收发数据。

    • UDP 也可以调用 connect() 绑定默认对端地址,但这里的 m_isConnected 只是标记 fd 有效。


5.2 便捷工厂(socket.cc

cpp 复制代码
Socket::ptr Socket::CreateTCPSocket();      // IPv4 + TCP
Socket::ptr Socket::CreateUDPSocket();      // IPv4 + UDP
Socket::ptr Socket::CreateTCPSocket6();     // IPv6 + TCP
Socket::ptr Socket::CreateUDPSocket6();     // IPv6 + UDP
Socket::ptr Socket::CreateUnixTCPSocket();  // Unix + TCP
Socket::ptr Socket::CreateUnixUDPSocket();  // Unix + UDP
  • 这些是对 CreateTCP/CreateUDP 的固定参数版本,方便用户快速创建常见组合。

六、核心 API 详解

6.1 newSock() ------ 创建底层 fd(socket.cc

cpp 复制代码
void Socket::newSock() {
    m_sock = socket(m_family, m_type, m_protocol);
    if(SYLAR_LIKELY(m_sock != -1)) {
        initSock();
    } else {
        SYLAR_LOG_ERROR(g_logger) << "socket(...) errno=" << errno;
    }
}
  • 调用 POSIX socket() 创建 fd。

  • SYLAR_LIKELY:分支预测提示,告诉编译器成功是大概率事件。

  • 成功后调用 initSock() 设置 socket 选项。


6.2 initSock() ------ 初始化 socket 选项(socket.cc

cpp 复制代码
void Socket::initSock() {
    int val = 1;
    setOption(SOL_SOCKET, SO_REUSEADDR, val);
    if(m_type == SOCK_STREAM) {
        setOption(IPPROTO_TCP, TCP_NODELAY, val);
    }
}

设置的两个关键选项:

选项 层级 作用
SO_REUSEADDR SOL_SOCKET 允许重用本地地址,解决 "Address already in use"
TCP_NODELAY IPPROTO_TCP 禁用 Nagle 算法,降低延迟(仅 TCP)

6.3 init() ------ 用已有 fd 初始化 Socket(socket.cc

cpp 复制代码
bool Socket::init(int sock) {
    FdCtx::ptr ctx = FdMgr::GetInstance()->get(sock);
    if(ctx && ctx->isSocket() && !ctx->isClose()) {
        m_sock = sock;
        m_isConnected = true;
        initSock();
        getLocalAddress();
        getRemoteAddress();
        return true;
    }
    return false;
}

使用场景:

  • accept() 返回的新 fd 需要包装成 Socket 对象。

  • 通过 FdManager 检查 fd 是否真的是 socket 且未关闭,防止传入非法 fd。

  • 自动获取本地和远端地址。


6.4 bind() ------ 绑定地址(socket.cc

cpp 复制代码
bool Socket::bind(const Address::ptr addr) {
    if(!isValid()) {
        newSock();
        if(SYLAR_UNLIKELY(!isValid())) {
            return false;
        }
    }
​
    if(SYLAR_UNLIKELY(addr->getFamily() != m_family)) {
        // 协议簇不匹配,记录错误日志
        return false;
    }
​
    // Unix Domain Socket 特殊处理:如果路径已存在,先删除
    UnixAddress::ptr uaddr = std::dynamic_pointer_cast<UnixAddress>(addr);
    if(uaddr) {
        Socket::ptr sock = Socket::CreateUnixTCPSocket();
        if(sock->connect(uaddr)) {
            return false;   // 已有服务在监听
        } else {
            sylar::FSUtil::Unlink(uaddr->getPath(), true);
        }
    }
​
    if(::bind(m_sock, addr->getAddr(), addr->getAddrLen())) {
        // bind 失败
        return false;
    }
    getLocalAddress();
    return true;
}

逐段拆解:

第一段:延迟创建 fd

  • 如果 socket 未创建(m_sock == -1),先调用 newSock()

  • SYLAR_UNLIKELY:失败是小概率事件,优化分支预测。

第二段:协议簇校验

  • 确保传入的地址与 socket 的 m_family 一致。例如不能用 IPv4 地址去 bind IPv6 socket。

第三段:Unix Domain 特殊处理

  • 如果路径文件已存在,尝试 connect() 检查是否有服务在运行。

  • 如果连接成功,说明已有服务,bind 失败。

  • 如果连接失败,删除旧文件后重新 bind。

第四段:调用 ::bind()

  • ::bind() 是全局命名空间的 POSIX bind,避免与类成员 bind 冲突。

  • 成功后调用 getLocalAddress() 缓存本地地址。


6.5 connect() ------ 连接远端(socket.cc

cpp 复制代码
bool Socket::connect(const Address::ptr addr, uint64_t timeout_ms) {
    m_remoteAddress = addr;
    if(!isValid()) {
        newSock();
        if(SYLAR_UNLIKELY(!isValid())) {
            return false;
        }
    }
​
    if(SYLAR_UNLIKELY(addr->getFamily() != m_family)) {
        return false;
    }
​
    if(timeout_ms == (uint64_t)-1) {
        // 阻塞模式,无限等待
        if(::connect(m_sock, addr->getAddr(), addr->getAddrLen())) {
            close();
            return false;
        }
    } else {
        // 带超时的 connect(hook 后的函数)
        if(::connect_with_timeout(m_sock, addr->getAddr(), addr->getAddrLen(), timeout_ms)) {
            close();
            return false;
        }
    }
    m_isConnected = true;
    getRemoteAddress();
    getLocalAddress();
    return true;
}

两种连接模式:

模式 条件 说明
阻塞模式 timeout_ms == -1 调用标准 ::connect(),可能永久阻塞
超时模式 timeout_ms != -1 调用 ::connect_with_timeout()(sylar hook 的函数)
  • connect_with_timeout 是 sylar 通过 hook 技术替换的函数,内部使用 IOManager 实现协程级别的超时控制。

  • 连接失败时调用 close() 回收 fd,防止泄漏。


6.6 listen() / accept()socket.cc

cpp 复制代码
bool Socket::listen(int backlog) {
    if(!isValid()) { return false; }
    if(::listen(m_sock, backlog)) { return false; }
    return true;
}
​
Socket::ptr Socket::accept() {
    Socket::ptr sock(new Socket(m_family, m_type, m_protocol));
    int newsock = ::accept(m_sock, nullptr, nullptr);
    if(newsock == -1) { return nullptr; }
    if(sock->init(newsock)) { return sock; }
    return nullptr;
}
  • listen()backlog 默认 SOMAXCONN(通常为 128 或 4096)。

  • accept()

    • 创建新的 Socket 对象(继承当前 socket 的 family/type/protocol)。

    • ::accept() 返回新 fd。

    • 调用 init(newsock) 完成初始化。


6.7 close()socket.cc

cpp 复制代码
bool Socket::close() {
    if(!m_isConnected && m_sock == -1) {
        return true;
    }
    m_isConnected = false;
    if(m_sock != -1) {
        ::close(m_sock);
        m_sock = -1;
    }
    return false;
}
  • 幂等设计:多次调用不会出错。

  • 关闭后 m_sock = -1m_isConnected = false


七、数据收发 API 详解

7.1 send() 单缓冲区(socket.cc

cpp 复制代码
int Socket::send(const void* buffer, size_t length, int flags) {
    if(isConnected()) {
        return ::send(m_sock, buffer, length, flags);
    }
    return -1;
}
  • 直接映射到 POSIX send()

  • 未连接时返回 -1

  • 返回值:

    • >0:实际发送字节数(可能小于 length

    • =0:对端关闭

    • <0:出错,需检查 errno


7.2 send() 多缓冲区(socket.c)

cpp 复制代码
int Socket::send(const iovec* buffers, size_t length, int flags) {
    if(isConnected()) {
        msghdr msg;
        memset(&msg, 0, sizeof(msg));
        msg.msg_iov = (iovec*)buffers;
        msg.msg_iovlen = length;
        return ::sendmsg(m_sock, &msg, flags);
    }
    return -1;
}
  • 使用 sendmsg() 实现 scatter/gather I/O,一次发送多个不连续缓冲区。

  • iovec 是 POSIX 定义的结构体: { void* iov_base; size_t iov_len; }


7.3 sendTo() 系列(socket.cc

cpp 复制代码
int Socket::sendTo(const void* buffer, size_t length, const Address::ptr to, int flags);
int Socket::sendTo(const iovec* buffers, size_t length, const Address::ptr to, int flags);
  • UDP 专用:每次发送都要指定目标地址。

  • 内部调用 sendto() / sendmsg()


7.4 recv() / recvFrom() 系列(socket.cc

cpp 复制代码
int Socket::recv(void* buffer, size_t length, int flags);
int Socket::recv(iovec* buffers, size_t length, int flags);
int Socket::recvFrom(void* buffer, size_t length, Address::ptr from, int flags);
int Socket::recvFrom(iovec* buffers, size_t length, Address::ptr from, int flags);
  • send/sendTo 对称。

  • recvFrom 用于 UDP,获取数据来源地址。

  • flags 常用值:MSG_PEEK(窥视数据不移除)、MSG_DONTWAIT(非阻塞)。


八、地址与选项管理

8.1 getLocalAddress() / getRemoteAddress()socket.cc

cpp 复制代码
Address::ptr Socket::getRemoteAddress() {
    if(m_remoteAddress) { return m_remoteAddress; }
​
    Address::ptr result;
    switch(m_family) {
        case AF_INET:   result.reset(new IPv4Address()); break;
        case AF_INET6:  result.reset(new IPv6Address()); break;
        case AF_UNIX:   result.reset(new UnixAddress()); break;
        default:        result.reset(new UnknownAddress(m_family)); break;
    }
    socklen_t addrlen = result->getAddrLen();
    if(getpeername(m_sock, result->getAddr(), &addrlen)) {
        return Address::ptr(new UnknownAddress(m_family));
    }
    // ... UnixAddress 特殊处理
    m_remoteAddress = result;
    return m_remoteAddress;
}

设计要点:

  • 懒加载(Lazy Loading) :首次访问时才调用 getpeername()/getsockname()

  • 根据 m_family 创建对应类型的 Address 对象。

  • 获取失败后返回 UnknownAddress,不会返回 nullptr


8.2 getOption() / setOption()socket.cc

cpp 复制代码
bool Socket::getOption(int level, int option, void* result, socklen_t* len);
bool Socket::setOption(int level, int option, const void* result, socklen_t len);
  • 直接封装 getsockopt() / setsockopt()

  • 提供模板重载,简化调用:

cpp 复制代码
template<class T>
bool getOption(int level, int option, T& result) {
    socklen_t length = sizeof(T);
    return getOption(level, option, &result, &length);
}

使用示例:

cpp 复制代码
int sndbuf;
sock->getOption(SOL_SOCKET, SO_SNDBUF, sndbuf);

8.3 超时管理(socket.cc

cpp 复制代码
int64_t Socket::getSendTimeout() {
    FdCtx::ptr ctx = FdMgr::GetInstance()->get(m_sock);
    if(ctx) { return ctx->getTimeout(SO_SNDTIMEO); }
    return -1;
}
​
void Socket::setSendTimeout(int64_t v) {
    struct timeval tv{int(v / 1000), int(v % 1000 * 1000)};
    setOption(SOL_SOCKET, SO_SNDTIMEO, tv);
}
  • 获取超时 :通过 FdManager 查询 fd 上下文中的超时记录。

  • 设置超时 :调用 setsockopt(SO_SNDTIMEO/SO_RCVTIMEO),参数是 timeval 结构。

  • 超时单位是毫秒 ,内部转换为 timeval(秒 + 微秒)。


九、IOManager 集成 ------ 事件取消

9.1 cancelRead() / cancelWrite() / cancelAccept() / cancelAll()socket.cc

cpp 复制代码
bool Socket::cancelRead() {
    return IOManager::GetThis()->cancelEvent(m_sock, sylar::IOManager::READ);
}
bool Socket::cancelWrite() {
    return IOManager::GetThis()->cancelEvent(m_sock, sylar::IOManager::WRITE);
}
bool Socket::cancelAccept() {
    return IOManager::GetThis()->cancelEvent(m_sock, sylar::IOManager::READ);
}
bool Socket::cancelAll() {
    return IOManager::GetThis()->cancelAll(m_sock);
}

作用:

  • 取消注册在 IOManager 上的 IO 事件。

  • cancelReadcancelAccept 都是取消 READ 事件(accept 本质上是等待可读事件)。

  • 这些函数通常在关闭 socket 前调用,确保没有协程还在等待该 fd 的事件。


十、SSLSocket 详解

10.1 定义(socket.h)

cpp 复制代码
class SSLSocket : public Socket {
public:
    typedef std::shared_ptr<SSLSocket> ptr;

    static SSLSocket::ptr CreateTCP(sylar::Address::ptr address);
    static SSLSocket::ptr CreateTCPSocket();
    static SSLSocket::ptr CreateTCPSocket6();

    SSLSocket(int family, int type, int protocol = 0);
    virtual Socket::ptr accept() override;
    virtual bool bind(const Address::ptr addr) override;
    virtual bool connect(const Address::ptr addr, uint64_t timeout_ms = -1) override;
    virtual bool listen(int backlog = SOMAXCONN) override;
    virtual bool close() override;
    virtual int send(const void* buffer, size_t length, int flags = 0) override;
    virtual int send(const iovec* buffers, size_t length, int flags = 0) override;
    virtual int sendTo(const void* buffer, size_t length, const Address::ptr to, int flags = 0) override;
    virtual int sendTo(const iovec* buffers, size_t length, const Address::ptr to, int flags = 0) override;
    virtual int recv(void* buffer, size_t length, int flags = 0) override;
    virtual int recv(iovec* buffers, size_t length, int flags = 0) override;
    virtual int recvFrom(void* buffer, size_t length, Address::ptr from, int flags = 0) override;
    virtual int recvFrom(iovec* buffers, size_t length, Address::ptr from, int flags = 0) override;

    bool loadCertificates(const std::string& cert_file, const std::string& key_file);
    virtual std::ostream& dump(std::ostream& os) const override;
protected:
    virtual bool init(int sock) override;
private:
    std::shared_ptr<SSL_CTX> m_ctx;
    std::shared_ptr<SSL> m_ssl;
};
  • 只支持 TCP(SSL 建立在可靠传输之上,不支持 UDP)。

  • SSL_CTXSSL 使用 shared_ptr 管理,自定义 deleter(SSL_CTX_free / SSL_free)。


10.2 connect() ------ SSL 握手(socket.cc

cpp 复制代码
bool SSLSocket::connect(const Address::ptr addr, uint64_t timeout_ms) {
    bool v = Socket::connect(addr, timeout_ms);   // 先建立 TCP 连接
    if(v) {
        m_ctx.reset(SSL_CTX_new(SSLv23_client_method()), SSL_CTX_free);
        m_ssl.reset(SSL_new(m_ctx.get()), SSL_free);
        SSL_set_fd(m_ssl.get(), m_sock);
        v = (SSL_connect(m_ssl.get()) == 1);   // SSL 握手
    }
    return v;
}

流程:

  1. 先调用父类 Socket::connect() 完成 TCP 三次握手。

  2. 创建客户端 SSL 上下文(SSLv23_client_method())。

  3. 创建 SSL 对象并绑定 fd。

  4. 执行 SSL_connect() 完成 SSL/TLS 握手。


10.3 init() ------ 服务端 SSL 接受(socket.cc

cpp 复制代码
bool SSLSocket::init(int sock) {
    bool v = Socket::init(sock);   // 先完成 Socket 初始化
    if(v) {
        m_ssl.reset(SSL_new(m_ctx.get()), SSL_free);
        SSL_set_fd(m_ssl.get(), m_sock);
        v = (SSL_accept(m_ssl.get()) == 1);   // SSL 握手(服务端)
    }
    return v;
}
  • accept() 中调用,完成服务端 SSL 握手。

  • m_ctx 是从监听 socket 继承来的(见 SSLSocket::accept() 第 493 行:sock->m_ctx = m_ctx;)。


10.4 send() / recv() ------ SSL 读写(socket.cc

cpp 复制代码
int SSLSocket::send(const void* buffer, size_t length, int flags) {
    if(m_ssl) { return SSL_write(m_ssl.get(), buffer, length); }
    return -1;
}
​
int SSLSocket::recv(void* buffer, size_t length, int flags) {
    if(m_ssl) { return SSL_read(m_ssl.get(), buffer, length); }
    return -1;
}
  • 单缓冲区直接映射到 SSL_write / SSL_read

  • 多缓冲区(iovec)版本循环逐个写入/读取。


10.5 证书加载(socket.cc

cpp 复制代码
bool SSLSocket::loadCertificates(const std::string& cert_file, const std::string& key_file) {
    m_ctx.reset(SSL_CTX_new(SSLv23_server_method()), SSL_CTX_free);
    if(SSL_CTX_use_certificate_chain_file(m_ctx.get(), cert_file.c_str()) != 1) { return false; }
    if(SSL_CTX_use_PrivateKey_file(m_ctx.get(), key_file.c_str(), SSL_FILETYPE_PEM) != 1) { return false; }
    if(SSL_CTX_check_private_key(m_ctx.get()) != 1) { return false; }
    return true;
}

服务端三件套:

  1. 加载证书链文件。

  2. 加载私钥文件。

  3. 校验证书和私钥是否匹配。


十一、完整调用链梳理

以 TCP 客户端连接并发送数据为例:

步骤 1:创建 Socket
cpp 复制代码
Socket::ptr sock = Socket::CreateTCP(addr);
// → new Socket(AF_INET, SOCK_STREAM, 0)
// → m_sock = -1, m_isConnected = false
步骤 2:连接服务端
cpp 复制代码
sock->connect(addr, 5000);
// 1. newSock() → socket(AF_INET, SOCK_STREAM, 0) → initSock()
// 2. ::connect_with_timeout(m_sock, addr, addrlen, 5000)
// 3. m_isConnected = true
// 4. getRemoteAddress() / getLocalAddress()
步骤 3:发送数据
cpp 复制代码
sock->send(buffer, length);
// → ::send(m_sock, buffer, length, 0)
步骤 4:关闭连接
cpp 复制代码
sock->close();
// → ::close(m_sock), m_sock = -1

以 TCP 服务端接受连接为例:

步骤 1:创建 → Bind → Listen
cpp 复制代码
Socket::ptr sock = Socket::CreateTCPSocket();
sock->bind(address);   // newSock() → ::bind()
sock->listen();
步骤 2:Accept
cpp 复制代码
Socket::ptr client = sock->accept();
// → ::accept(m_sock, nullptr, nullptr) → init(newsock)
// → getLocalAddress() / getRemoteAddress()

十二、设计要点总结

设计 说明
延迟创建 fd 构造时不创建 socket,首次需要时才 newSock(),减少资源占用
懒加载地址 m_localAddress / m_remoteAddress 首次访问时才查询
RAII 管理 析构时自动 close(),防止 fd 泄漏
非拷贝 继承 Noncopyable,避免 fd 被重复关闭
shared_from_this 允许安全获取 shared_ptr,便于回调中保持对象存活
模板 option getOption<T> / setOption<T> 简化调用
UDP 即连即发 UDP 工厂方法直接 newSock() 并标记 m_isConnected = true

十三、学习验证清单

学完后,你应该能:

  • 解释为什么 Socket 构造时不立即创建 fd,而是在 bind()/connect() 时才创建。
  • 说明 initSock()SO_REUSEADDRTCP_NODELAY 的作用。
  • 解释 bind() 中对 Unix Domain Socket 的特殊处理逻辑。
  • 说明 connect()timeout_ms == -1timeout_ms != -1 的区别。
  • 解释 accept() 返回的 Socket::ptr 是如何通过 init() 完成初始化的。
  • 说明 getLocalAddress() 的懒加载机制。
  • 解释 cancelRead() 是如何与 IOManager 协作的。
  • 描述 SSLSocket::connect() 中 TCP 连接和 SSL 握手的先后顺序。
  • 说明 SSLSocket::accept()m_ctx 是如何传递给新连接的。

ByteArray模块

一、设计与目的

字节数组容器,提供基础类型的序列化与反序列化功能。

ByteArray的底层存储是固定大小的块,以链表形式组织。每次写入数据时,将数据写入到链表最后一个块中,如果最后一个块不足以容纳数据,则分配一个新的块并添加到链表结尾,再写入数据。ByteArray会记录当前的操作位置,每次写入数据时,该操作位置按写入大小往后偏移,如果要读取数据,则必须调用setPosition重新设置当前的操作位置。

ByteArray支持基础类型的序列化与反序列化功能,并且支持将序列化的结果写入文件,以及从文件中读取内容进行反序列化。ByteArray支持以下类型的序列化与反序列化:

  1. 固定长度的有符号/无符号8位、16位、32位、64位整数
  2. 不固定长度的有符号/无符号32位、64位整数
  3. float、double类型
  4. 字符串,包含字符串长度,长度范围支持16位、32位、64位。
  5. 字符串,不包含长度。

以上所有的类型都支持读写。

ByteArray还支持设置序列化时的大小端顺序。


三、Node 结构详解

3.1 定义(bytearray.h)

cpp 复制代码
struct Node {
    Node(size_t s);   // 分配 s 字节内存
    Node();           // 空节点(无内存分配)
    ~Node();          // 释放 ptr
​
    char* ptr;        // 内存块地址
    Node* next;       // 下一个节点
    size_t size;      // 内存块大小
};

3.2 构造函数(bytearray.cc

cpp 复制代码
ByteArray::Node::Node(size_t s)
    :ptr(new char[s])
    ,next(nullptr)
    ,size(s) {
}
​
ByteArray::Node::Node()
    :ptr(nullptr)
    ,next(nullptr)
    ,size(0) {
}

3.3 析构函数(bytearray.cc

cpp 复制代码
ByteArray::Node::~Node() {
    if(ptr) {
        delete[] ptr;
    }
}

设计要点:

  • 使用链表而非连续数组:避免大内存拷贝和重新分配,支持高效追加。

  • 每个 Node 大小固定为 m_baseSize(默认 4096 字节),便于内存管理。

  • m_root 是链表头,m_cur 是当前操作节点,避免每次从头遍历。


四、ByteArray 类详解

4.1 定义(bytearray.h)

cpp 复制代码
/**
 * @brief 二进制数组,提供基础类型的序列化,反序列化功能
 */
class ByteArray {
public:
    typedef std::shared_ptr<ByteArray> ptr;

    // write 系列(写入数据)...
    // read 系列(读取数据)...
    // 工具函数...
    // 实在有点多,我这边直接简写了,想看细节去github

private:
    
    /**
     * @brief 扩容ByteArray,使其可以容纳size个数据(如果原本可以可以容纳,则不扩容)
     */
    void addCapacity(size_t size);

    /**
     * @brief 获取当前的可写入容量
     */
    size_t getCapacity() const { return m_capacity - m_position;}
private:
    /// 内存块的大小
    size_t m_baseSize;
    /// 当前操作位置
    size_t m_position;
    /// 当前的总容量
    size_t m_capacity;
    /// 当前数据的大小
    size_t m_size;
    /// 字节序,默认大端
    int8_t m_endian;
    /// 第一个内存块指针
    Node* m_root;
    /// 当前操作的内存块指针
    Node* m_cur;
};

4.2 成员变量表

变量名 类型 默认值 含义
m_baseSize size_t 4096 每个 Node 的内存块大小
m_position size_t 0 当前读写位置(逻辑偏移)
m_capacity size_t base_size 总容量(所有 Node 之和)
m_size size_t 0 有效数据大小(已写入的最远位置)
m_endian int8_t SYLAR_BIG_ENDIAN 字节序:1=小端, 2=大端
m_root Node* 新Node 链表头节点
m_cur Node* m_root 当前操作节点

4.3 构造函数与析构函数(bytearray.cc

cpp 复制代码
ByteArray::ByteArray(size_t base_size)
    :m_baseSize(base_size)
    ,m_position(0)
    ,m_capacity(base_size)
    ,m_size(0)
    ,m_endian(SYLAR_BIG_ENDIAN)
    ,m_root(new Node(base_size))
    ,m_cur(m_root) {
}
​
ByteArray::~ByteArray() {
    Node* tmp = m_root;
    while(tmp) {
        m_cur = tmp;
        tmp = tmp->next;
        delete m_cur;
    }
}
  • 构造时创建第一个 Node,容量等于 base_size

  • 析构时遍历链表释放所有 Node。

  • 默认大端序:网络协议通常使用大端(网络字节序)。


五、字节序处理

5.1 依赖:endian.h

cpp 复制代码
#define SYLAR_LITTLE_ENDIAN 1
#define SYLAR_BIG_ENDIAN    2
​
// 根据编译器宏判断主机字节序
#if BYTE_ORDER == BIG_ENDIAN
    #define SYLAR_BYTE_ORDER SYLAR_BIG_ENDIAN
#else
    #define SYLAR_BYTE_ORDER SYLAR_LITTLE_ENDIAN
#endif
  • SYLAR_BYTE_ORDER:表示当前主机的实际字节序(x86/x64 通常是小端)。

  • m_endian:表示 ByteArray 使用的目标字节序(默认大端,即网络字节序)。


5.2 byteswap 模板(endian.h)

cpp 复制代码
template<class T>
typename std::enable_if<sizeof(T) == sizeof(uint64_t), T>::type
byteswap(T value) {
    return (T)bswap_64((uint64_t)value);
}
  • 使用 SFINAE 根据类型大小选择对应的 bswap_16/32/64

  • 只有 2/4/8 字节的类型支持字节序转换。


5.3 字节序转换规则(bytearray.cc

cpp 复制代码
void ByteArray::writeFint16(int16_t value) {
    if(m_endian != SYLAR_BYTE_ORDER) {
        value = byteswap(value);   // 主机序 ≠ 目标序,需要转换
    }
    write(&value, sizeof(value));
}

逻辑:

  • 如果主机字节序和 ByteArray 的目标字节序不一致 ,则调用 byteswap()

  • 例如:x86 小端机器上,默认写入大端数据,需要 swap。

  • 如果一致,直接原样写入,避免不必要的转换。


六、固定长度读写详解

6.1 固定长度整数(bytearray.cc

函数 类型 写入大小 字节序转换
writeFint8 / readFint8 int8_t 1字节 无需转换
writeFuint8 / readFuint8 uint8_t 1字节 无需转换
writeFint16 / readFint16 int16_t 2字节 需要转换
writeFuint16 / readFuint16 uint16_t 2字节 需要转换
writeFint32 / readFint32 int32_t 4字节 需要转换
writeFuint32 / readFuint32 uint32_t 4字节 需要转换
writeFint64 / readFint64 int64_t 8字节 需要转换
writeFuint64 / readFuint64 uint64_t 8字节 需要转换

readFint16 等使用宏简化(bytearray.cc):

cpp 复制代码
#define XX(type) \
    type v; \
    read(&v, sizeof(v)); \
    if(m_endian == SYLAR_BYTE_ORDER) { \
        return v; \
    } else { \
        return byteswap(v); \
    }
​
int16_t ByteArray::readFint16() { XX(int16_t); }
uint16_t ByteArray::readFuint16() { XX(uint16_t); }
// ...
#undef XX

6.2 Float / Double(bytearray.cc

cpp 复制代码
void ByteArray::writeFloat(float value) {
    uint32_t v;
    memcpy(&v, &value, sizeof(value));   // 按位拷贝到整数
    writeFuint32(v);                      // 当作 uint32 写入
}
​
float ByteArray::readFloat() {
    uint32_t v = readFuint32();
    float value;
    memcpy(&value, &v, sizeof(v));        // 按位拷贝回 float
    return value;
}
  • 不能直接对浮点数做 byteswap,因为浮点数的字节序转换要通过整数中转。

  • memcpy 保证按位拷贝,避免编译器优化导致的问题。


七、Varint 编码详解

7.1 什么是 Varint?

Varint(Variable Length Integer)是一种变长整数编码:

  • 小数值占用少字节,大数值占用多字节。

  • 每个字节的最高位是** continuation bit**:1 表示后面还有字节,0 表示这是最后一个字节。

  • 低 7 位存储数据。

示例:编码 300

bash 复制代码
300 = 0b00000001 00101100
      按7位分组:0101100 0000010
      小端序排列:10101100 00000010
      (最高位1表示继续,0表示结束)

7.2 ZigZag 编码 ------ 解决负数问题

如果直接用 Varint 编码有符号数,负数(补码表示)的高位全是 1,会导致占用大量字节。

ZigZag 编码:将有符号数映射到无符号数

cpp 复制代码
static uint32_t EncodeZigzag32(const int32_t& v) {
    if(v < 0) {
        return ((uint32_t)(-v)) * 2 - 1;
    } else {
        return v * 2;
    }
}
​
static int32_t DecodeZigzag32(const uint32_t& v) {
    return (v >> 1) ^ -(v & 1);
}
原始值 ZigZag 编码
0 0
-1 1
1 2
-2 3
2 4
  • 负数映射为奇数,正数映射为偶数。

  • 绝对值小的负数也只需要很少字节。

此处如果不知道,请移至传送门:小而巧的数字压缩算法:zigzag_简单的老王-CSDN博客_zigzag编码


7.3 writeUint32 ------ 无符号 Varint 编码(bytearray.cc

cpp 复制代码
void ByteArray::writeUint32(uint32_t value) {
    uint8_t tmp[5];       // uint32 最多需要 5 字节(32/7 = 5)
    uint8_t i = 0;
    while(value >= 0x80) {
        tmp[i++] = (value & 0x7F) | 0x80;   // 取低7位,最高位置1
        value >>= 7;
    }
    tmp[i++] = value;     // 最后一个字节,最高位为0
    write(tmp, i);
}

逐行拆解:

  1. tmp[5]:uint32 最大需要 ceil(32/7) = 5 字节。

  2. while(value >= 0x80):如果值 >= 128,说明还需要更多字节。

  3. (value & 0x7F) | 0x80

    • & 0x7F:取低 7 位数据。

    • | 0x80:最高位置 1,表示后面还有字节。

  4. value >>= 7:右移 7 位,处理下一段。

  5. 循环结束后,tmp[i++] = value:写入最后一个字节(最高位为 0)。


7.4 readUint32 ------ 无符号 Varint 解码(bytearray.cc

cpp 复制代码
uint32_t ByteArray::readUint32() {
    uint32_t result = 0;
    for(int i = 0; i < 32; i += 7) {
        uint8_t b = readFuint8();
        if(b < 0x80) {
            result |= ((uint32_t)b) << i;
            break;
        } else {
            result |= (((uint32_t)(b & 0x7f)) << i);
        }
    }
    return result;
}
  • 最多循环 5 次(i = 0, 7, 14, 21, 28)。

  • b < 0x80:最高位为 0,这是最后一个字节。

  • b & 0x7f:去掉 continuation bit,取有效数据。

  • << i:按小端序排列,第 N 个字节放到对应的位位置上。


7.5 writeInt32 / readInt32 ------ 有符号 Varint(bytearray.cc

cpp 复制代码
void ByteArray::writeInt32(int32_t value) {
    writeUint32(EncodeZigzag32(value));
}
​
int32_t ByteArray::readInt32() {
    return DecodeZigzag32(readUint32());
}
  • 先 ZigZag 编码/解码,再用无符号 Varint 编码/解码。

  • Int64 / Uint64 同理,最多需要 10 字节。


八、字符串读写详解

8.1 五种字符串编码方式

函数 长度字段 长度大小 适用场景
writeStringF16 / readStringF16 uint16_t 2字节 短字符串(<64K)
writeStringF32 / readStringF32 uint32_t 4字节 中等字符串
writeStringF64 / readStringF64 uint64_t 8字节 超长字符串
writeStringVint / readStringVint Varint64 1~10字节 长度变化大,省空间
writeStringWithoutLength 0字节 定长协议或外部已知长度

8.2 writeStringF16bytearray.cc

cpp 复制代码
void ByteArray::writeStringF16(const std::string& value) {
    writeFuint16(value.size());     // 先写2字节长度
    write(value.c_str(), value.size());   // 再写字符串内容
}

8.3 readStringF16bytearray.cc

cpp 复制代码
std::string ByteArray::readStringF16() {
    uint16_t len = readFuint16();    // 先读2字节长度
    std::string buff;
    buff.resize(len);
    read(&buff[0], len);             // 再读内容到 string
    return buff;
}

九、底层读写核心

9.1 write() ------ 向 ByteArray 写入原始字节(bytearray.cc

cpp 复制代码
void ByteArray::write(const void* buf, size_t size) {
    if(size == 0) { return; }
    addCapacity(size);   // 确保容量足够
​
    size_t npos = m_position % m_baseSize;   // 在当前Node内的偏移
    size_t ncap = m_cur->size - npos;         // 当前Node剩余容量
    size_t bpos = 0;                          // buf中的偏移
​
    while(size > 0) {
        if(ncap >= size) {
            // 当前Node足够容纳剩余数据
            memcpy(m_cur->ptr + npos, (const char*)buf + bpos, size);
            if(m_cur->size == (npos + size)) {
                m_cur = m_cur->next;   // 正好写满,移动到下一个Node
            }
            m_position += size;
            bpos += size;
            size = 0;
        } else {
            // 当前Node不够,先写满它
            memcpy(m_cur->ptr + npos, (const char*)buf + bpos, ncap);
            m_position += ncap;
            bpos += ncap;
            size -= ncap;
            m_cur = m_cur->next;       // 移动到下一个Node
            ncap = m_cur->size;
            npos = 0;
        }
    }
​
    if(m_position > m_size) {
        m_size = m_position;   // 更新有效数据大小
    }
}

核心逻辑:

  1. addCapacity(size):如果容量不够,扩容(追加 Node)。

  2. npos = m_position % m_baseSize:计算在当前 Node 内的偏移。

  3. ncap = m_cur->size - npos:计算当前 Node 剩余可写空间。

  4. 循环写入:如果一个 Node 写不下,写满后移动到下一个 Node 继续。

  5. 最后更新 m_size(有效数据大小)。


9.2 read() ------ 从 ByteArray 读取原始字节(bytearray.cc

cpp 复制代码
void ByteArray::read(void* buf, size_t size) {
    if(size > getReadSize()) {
        throw std::out_of_range("not enough len");
    }
​
    size_t npos = m_position % m_baseSize;
    size_t ncap = m_cur->size - npos;
    size_t bpos = 0;
    while(size > 0) {
        if(ncap >= size) {
            memcpy((char*)buf + bpos, m_cur->ptr + npos, size);
            if(m_cur->size == (npos + size)) {
                m_cur = m_cur->next;
            }
            m_position += size;
            bpos += size;
            size = 0;
        } else {
            memcpy((char*)buf + bpos, m_cur->ptr + npos, ncap);
            m_position += ncap;
            bpos += ncap;
            size -= ncap;
            m_cur = m_cur->next;
            ncap = m_cur->size;
            npos = 0;
        }
    }
}
  • 逻辑与 write() 对称,方向相反。

  • 先检查 getReadSize() = m_size - m_position,确保有足够数据可读。

  • 不足时抛出 std::out_of_range 异常。


9.3 read(pos) ------ 指定位置读取(bytearray.cc

cpp 复制代码
void ByteArray::read(void* buf, size_t size, size_t position) const;
  • 不修改 m_positionm_cur,从指定位置读取。

  • 内部临时遍历链表到对应位置。

  • 用于 toString() 等需要重复读取的场景。


9.4 addCapacity() ------ 扩容(bytearray.cc

cpp 复制代码
void ByteArray::addCapacity(size_t size) {
    if(size == 0) { return; }
    size_t old_cap = getCapacity();
    if(old_cap >= size) { return; }   // 容量足够,无需扩容
​
    size = size - old_cap;
    size_t count = ceil(1.0 * size / m_baseSize);   // 需要新增的Node数
​
    Node* tmp = m_root;
    while(tmp->next) { tmp = tmp->next; }   // 找到链表尾部
​
    Node* first = NULL;
    for(size_t i = 0; i < count; ++i) {
        tmp->next = new Node(m_baseSize);
        if(first == NULL) { first = tmp->next; }
        tmp = tmp->next;
        m_capacity += m_baseSize;
    }
​
    if(old_cap == 0) {
        m_cur = first;   // 特殊情况:当前Node恰好写满,需要移动
    }
}
  • 按需追加 Node,每个新 Node 大小为 m_baseSize

  • first 记录第一个新 Node,用于处理 old_cap == 0 的特殊情况。


十、工具函数详解

10.1 clear() ------ 清空(bytearray.cc

cpp 复制代码
void ByteArray::clear() {
    m_position = m_size = 0;
    m_capacity = m_baseSize;
    Node* tmp = m_root->next;
    while(tmp) {
        m_cur = tmp;
        tmp = tmp->next;
        delete m_cur;
    }
    m_cur = m_root;
    m_root->next = NULL;
}
  • 重置位置和大小,释放除第一个 Node 外的所有 Node。

  • 保留 m_root,避免下次写入时重新分配。


10.2 setPosition() ------ 设置位置(bytearray.cc

cpp 复制代码
void ByteArray::setPosition(size_t v) {
    if(v > m_capacity) {
        throw std::out_of_range("set_position out of range");
    }
    m_position = v;
    if(m_position > m_size) {
        m_size = m_position;   // 允许 seek 超出当前数据范围
    }
    m_cur = m_root;
    while(v > m_cur->size) {
        v -= m_cur->size;
        m_cur = m_cur->next;
    }
    if(v == m_cur->size) {
        m_cur = m_cur->next;
    }
}
  • 支持随机访问定位。

  • 遍历链表找到对应 Node,更新 m_cur

  • 如果 v > m_size,会扩展 m_size(类似文件的稀疏扩展)。


10.3 toString() / toHexString()bytearray.cc

cpp 复制代码
std::string ByteArray::toString() const {
    std::string str;
    str.resize(getReadSize());
    if(str.empty()) { return str; }
    read(&str[0], str.size(), m_position);
    return str;
}
​
std::string ByteArray::toHexString() const {
    std::string str = toString();
    std::stringstream ss;
    for(size_t i = 0; i < str.size(); ++i) {
        if(i > 0 && i % 32 == 0) { ss << std::endl; }
        ss << std::setw(2) << std::setfill('0') << std::hex
           << (int)(uint8_t)str[i] << " ";
    }
    return ss.str();
}
  • toString():将剩余可读数据转为 std::string

  • toHexString():转为 16 进制格式(如 48 65 6C 6C 6F),每 32 字节换行。


10.4 文件读写(bytearray.cc

cpp 复制代码
bool ByteArray::writeToFile(const std::string& name) const;
bool ByteArray::readFromFile(const std::string& name);
  • writeToFile:遍历链表,将剩余可读数据写入文件。

  • readFromFile:分块读取文件,每次读 m_baseSize,调用 write() 写入 ByteArray。


十一、iovec 支持 ------ 零拷贝对接系统调用

11.1 getReadBuffers()bytearray.cc

cpp 复制代码
uint64_t ByteArray::getReadBuffers(std::vector<iovec>& buffers, uint64_t len) const {
    len = len > getReadSize() ? getReadSize() : len;
    if(len == 0) { return 0; }
​
    uint64_t size = len;
    size_t npos = m_position % m_baseSize;
    size_t ncap = m_cur->size - npos;
    struct iovec iov;
    Node* cur = m_cur;
​
    while(len > 0) {
        if(ncap >= len) {
            iov.iov_base = cur->ptr + npos;
            iov.iov_len = len;
            len = 0;
        } else {
            iov.iov_base = cur->ptr + npos;
            iov.iov_len = ncap;
            len -= ncap;
            cur = cur->next;
            ncap = cur->size;
            npos = 0;
        }
        buffers.push_back(iov);
    }
    return size;
}

作用:

  • 将 ByteArray 中可读的数据区域转换为 iovec 数组。

  • 可以直接传给 readv() / writev() / sendmsg() 等系统调用,实现零拷贝

  • 每个 iovec 指向一个 Node 中的有效数据区域。


11.2 getWriteBuffers()bytearray.cc

cpp 复制代码
uint64_t ByteArray::getWriteBuffers(std::vector<iovec>& buffers, uint64_t len);
  • getReadBuffers 对称,返回可写入的内存区域。

  • 先调用 addCapacity(len) 确保容量足够。

  • 用于从 socket 直接读取到 ByteArray(配合 recvmsg)。


十二、完整调用链梳理

以写入一个 int32_t 为例:

步骤 1:调用写入函数
cpp 复制代码
ba->writeFint32(12345);
步骤 2:字节序转换
cpp 复制代码
if(m_endian != SYLAR_BYTE_ORDER) {
    value = byteswap(value);   // x86小端 → 大端
}
步骤 3:写入底层缓冲区
cpp 复制代码
write(&value, sizeof(value));
// 1. addCapacity(4) 确保容量
// 2. 计算 npos = m_position % m_baseSize
// 3. memcpy 到对应 Node 的 ptr + npos
// 4. 更新 m_position,如果超过 m_size 则更新 m_size

以写入一个 Varint 为例:

步骤 1:调用写入函数
cpp 复制代码
ba->writeInt32(-5);
步骤 2:ZigZag 编码
cpp 复制代码
EncodeZigzag32(-5) → 9
步骤 3:Varint 编码
cpp 复制代码
tmp[0] = (9 & 0x7F) | 0x80 = 0x89   // 等等,9 < 128,所以直接 tmp[0] = 9
write(tmp, 1);

以读取一个 String 为例:

步骤 1:调用读取函数
cpp 复制代码
std::string s = ba->readStringVint();
步骤 2:读取长度(Varint)
cpp 复制代码
uint64_t len = readUint64();
步骤 3:读取内容
cpp 复制代码
std::string buff; buff.resize(len); read(&buff[0], len);

十三、设计要点总结

设计 说明
链表结构 避免大内存连续分配和拷贝,追加数据效率高
固定大小 Node 默认 4096 字节,平衡内存碎片和分配频率
懒扩容 addCapacity 按需追加 Node,不预分配过多内存
字节序抽象 m_endianSYLAR_BYTE_ORDER 比较,自动 swap
Varint + ZigZag 小整数省空间,负数也能高效编码
iovec 导出 与 POSIX scatter/gather I/O 无缝对接,支持零拷贝
大端默认 符合网络协议惯例,减少上层代码关注字节序

十四、学习验证清单

学完后,你应该能:

  • 解释 ByteArray 为什么使用链表(Node)而不是单一连续数组。
  • 说明 m_positionm_sizem_capacity 三者的关系和区别。
  • 解释字节序转换中 m_endian != SYLAR_BYTE_ORDER 的判断逻辑。
  • 手写 writeUint32 的 Varint 编码过程(以 300 为例)。
  • 解释 ZigZag 编码如何解决负数 Varint 编码过长的问题。
  • 说明 write()nposncapbpos 三个变量的含义。
  • 解释 addCapacity()count = ceil(size / m_baseSize) 的计算逻辑。
  • 说明 getReadBuffers() 返回的 iovec 数组如何用于零拷贝 IO。
  • 解释 writeFloat 为什么要用 memcpy 而不是直接类型转换。

结语

这一篇的内容确实有点杂而多,但是难度还行,相信学完还是有不少收获的,这块序列化和反序列化用处还是蛮多的,希望这篇文章能帮到你。

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

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


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

相关推荐
青山木5 小时前
一把 Redis 分布式锁,踩透四个坑:锁争抢、僵尸锁、锁过期、锁丢失
java·数据库·redis·后端
神奇小汤圆6 小时前
雪花算法ID重复了?惨痛教训:请勿轻易造轮子!
后端
Java内核笔记6 小时前
万字避坑!Spring Boot 3.x 升 4.0 最全升级指南(附 Migration Checklist,建议收藏⭐)
java·后端
从零开始的代码生活_6 小时前
C++ list 原理与实践:双向链表、迭代器与简化实现
开发语言·c++·后端·学习·算法·链表·list
初学AI的小高6 小时前
FastAPI 工程实践:LLM 应用的 API 设计之道
后端·fastapi
charlie1145141916 小时前
现代C++工程实践——WeakPtr 第二关:核心骨架与控制块
开发语言·c++
摇滚侠6 小时前
Linux 零基础教程 09、11、77
linux·运维·服务器
袋鱼不重6 小时前
乐观锁与悲观锁:原理、选型与前后端实践
前端·后端
2401_869769596 小时前
内容9 string 1
c++