Asio——socket的创建和连接


网络编程基本流程

📌服务端

核心函数 (API) 主要职责与底层逻辑** **形象比喻
socket() 在内核中创建一个 Socket 描述符,分配系统资源。此时它还只是一个通用的代号。 买了一部崭新的客服电话机
bind() 将这个 Socket 绑定到指定的本机 IP 地址和端口(Port)上。 给这部电话机申请并固定一个官方客服号码(如 95533)。
listen() 将 Socket 设置为被动监听状态,并配置全连接/半连接队列(Backlog)。开始等待客户端连接。 把电话线插上,客服人员坐在电话前,让电话处于随时可呼入的状态
accept() (核心) 阻塞等待客户端呼入。当有连接来时,在内核中分裂并返回一个新的通信 Socket。原监听 Socket 继续回去 listen。 前台总机 接到客户电话后,为了不占用总机线,把这个客户的电话转接给专门的客服小哥分机
read() / write() 使用 <font style="color:rgb(68, 71, 70);">accept</font> 返回的那个新通信 Socket 与该客户端进行数据的收发。 客服小哥分机与该客户开始私密、专属的业务对谈。

📌客户端

核心函数 (API) 主要职责与底层逻辑 形象比喻
socket() 在内核中创建一个 Socket 描述符,分配系统资源。此时它只是一个通用的代号,尚未具备网络通信能力。 买了一部崭新的用户电话机。
connect() (核心 ) 向服务端发起连接请求(三次握手)。内核会分配一个临时端口,并向指定的服务端 IP 和端口发送 SYN 包 拿起电话机,拨打官方客服号码(如 95533),等待对方接听。
write() / read() 连接建立后,使用 Socket 描述符进行数据的发送和接收。 电话接通后,用户与客服小哥开始私密、专属的业务对谈。
close() 关闭 Socket,释放系统资源,断开连接(四次挥手)。 说完"再见",挂断电话,把电话机放回原处。

终端节点的创建

所谓终端节点就是用来通信的端对端的节点,可以通过ip地址和端口构造,其的节点可以连接这个终端节点做通信。 简单来说,它就是**网络通信中的"通信地址"**。

如果是客户端,可以通过对端的ip和端口构造一个endpoint,用这个endpoint和服务器通信。

如果是服务端,则只需根据本地地址绑定就可以生成endpoint,用这个 endpoint 来监听和连接客户端。

cpp 复制代码
#include <iostream>
#include <string>
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>

// 方便使用 asio 命名空间
namespace asio = boost::asio;

// 明确去连接哪一个特定的远程服务器
int client_end_point() {
    // =========================================================================
    // 步骤 1. 准备原始的地址数据。
    // 这里我们假设客户端应用程序已经通过配置文件、用户输入或 DNS 解析
    // 拿到了目标服务器的 IP 地址(字符串形式)和协议端口号。
    // =========================================================================
    std::string raw_ip_address = "127.0.0.1"; // 目标服务器的 IP(这里用本地环回地址测试)
    unsigned short port_num = 3333;           // 目标服务器监听的端口号

    // =========================================================================
    // 步骤 2. 解析 IP 地址字符串。
    // 网络传输不能直接用 "127.0.0.1" 这种文本字符串,必须转换成结构化的二进制网络地址。
    // =========================================================================
    
    // 创建一个错误码对象,用来接收解析过程中可能发生的错误(比如 IP 格式写错了)。
    // 使用错误码(error_code)可以防止代码直接抛出异常,让程序控制流更安全。
    boost::system::error_code ec;
    
    // 使用现代 Asio 推荐的全局自由函数 make_address。
    // 它是一个与 IP 协议版本无关(IP-version independent)的工具,
    // 它能聪明地自动识别传入的是 IPv4(如 127.0.0.1)还是 IPv6(如 ::1),并返回对应的 address 对象。
    asio::ip::address ip_address =
        asio::ip::make_address(raw_ip_address, ec);

    // 检查安全阀:判断地址解析是否成功
    if (ec.value() != 0) {
        // 如果 ec.value() 不为 0,说明传入的 IP 字符串格式非法(例如写成了 "256.0.0.1" 或 "abc")
        std::cout
            << "解析 IP 地址失败. 错误码 = " << ec.value() 
            << ". 错误信息: " << ec.message() << std::endl;
        
        // 返回错误码,中断当前函数的后续执行
        return ec.value();
    }

    // =========================================================================
    // 步骤 3. 构造终端节点(Endpoint)对象。
    // =========================================================================
    
    // 将解析好的、计算机通用的 IP 地址与目标端口号进行"打包/绑定"。
    // 因为我们要进行 TCP 通信,所以这里使用的是 asio::ip::tcp 命名空间下的 endpoint。
    // 到这一步,网络通信的"目的地绝对地址"就真正创建好并存储在 ep 中了。
    asio::ip::tcp::endpoint ep(ip_address, port_num);

    // =========================================================================
    // 步骤 4. 终端节点已就绪。
    // =========================================================================
    
    // 打印验证一下,确保 Endpoint 内部的数据与我们预期的一致。
    // ep.address().to_string() 可以把内部的二进制地址再安全地转回字符串打印出来。
    std::cout << "成功创建 Endpoint: " 
              << ep.address().to_string() << ":" << ep.port() << std::endl;

    // 此时的 ep 已经可以作为参数,传递给客户端套接字的连接方法:
    // 例如:socket.connect(ep); 从而真正向服务器发起三次握手。

    return 0;
}

// 规定服务器自己在哪个范围、哪个端口等待别人的连接(被动监听)
int server_end_point() {
    // =========================================================================
    // 步骤 1. 明确服务器要占用的端口号。
    // 服务器必须在一个固定的、公开的端口上等待客户端,就像商铺必须有一个固定的门牌号。
    // 这里我们选择 3333 端口(注意:1024以下的端口通常需要系统管理员权限)。
    // =========================================================================
    unsigned short port_num = 3333;

    // =========================================================================
    // 步骤 2. 创建一个特殊的通配 IP 地址对象(来自本地主机的"广撒网"策略)。
    // 服务器通常有多个网络接口(比如:有线网卡、无线 Wi-Fi、本地环回接口 localhost 等)。
    // 如果指定死一个具体的 IP,服务器就只能接收从那一块网卡进来的数据。
    // =========================================================================
    
    // asio::ip::address_v6::any() 会返回一个特殊的 IPv6 通配地址(等同于 "::")。
    // 它的意思是:"我不关心数据是从哪块网卡、哪个本地 IP 进来的,只要是发往这台机器的,我全都要。"
    // 
    // 提示:大部分现代操作系统在监听到 IPv6 的 any() 地址时,默认也会同时兼容并监听 IPv4 的连接(双栈机制)。
    // 然后,我们将这个特殊的 IPv6 地址上转型并存储在通用的 asio::ip::address 对象中。
    asio::ip::address ip_address = asio::ip::address_v6::any();

    // =========================================================================
    // 步骤 3. 构造服务器端的终端节点(Endpoint)。
    // =========================================================================
    
    // 将"通配 IP 地址"与"指定的端口号"打包绑定,生成一个 TCP 终端节点。
    // 这个 ep(Endpoint)现在代表的意思是:"本机的任意 IP + 3333 端口"。
    // 到这里,服务器要开张营业的"地址凭证"就准备好了。
    asio::ip::tcp::endpoint ep(ip_address, port_num);

    // =========================================================================
    // 步骤 4. 终端节点创建完毕,后续使用说明。
    // =========================================================================
    
    // 此时的 ep 已经包含了完整的本地监听规则。在实际的服务器开发中,它通常被用于以下流程:
    // 1. 创建一个接收器:asio::ip::tcp::acceptor acceptor(io_context);
    // 2. 打开接收器:  acceptor.open(ep.protocol());
    // 3. 绑定该节点:  acceptor.bind(ep);  <-- 这里就会用到我们刚刚创建的 ep,告诉系统强占 3333 端口
    // 4. 开始监听:    acceptor.listen();
    
    // 打印验证:对于 IPv6 的 any() 地址,打印出来通常会显示为 "::"
    std::cout << "服务器监听 Endpoint 创建成功,准备监听地址: [" 
              << ep.address().to_string() << "]:" << ep.port() << std::endl;

    return 0;
}

int main() {
    client_end_point();
    server_end_point();
    return 0;
}
cmake 复制代码
cmake_minimum_required(VERSION 3.20)
project(MyAsioProject CXX)

find_package(Boost REQUIRED COMPONENTS asio)

add_executable(main main.cpp)

if(WIN32)
    # 如果是 Windows 平台,必须显式链接 ws2_32 (Winsock2 库) 
    # 还可以顺便补上 mswsock 库防止其他高级 Asio 操作报错
    target_link_libraries(main PRIVATE Boost::boost ws2_32 mswsock)
else()
    # Linux 或 macOS 平台
    target_link_libraries(main PRIVATE Boost::boost)
endif()

创建socket

创建socket分为4步,创建上下文iocontext,选择协议,生成socket,打开socket。

cpp 复制代码
/**
 * @brief 创建并初始化一个 TCP 套接字
 * @return int 返回 0 表示成功,返回非 0 值表示失败的错误码
 */
int create_tcp_socket() {
    // -------------------------------------------------------------------------
    // 步骤 1. 创建 I/O 上下文对象 (io_context)
    // -------------------------------------------------------------------------
    // io_context 是 Asio 库的核心核心。所有底层 I/O 服务的交互(如网络读写)
    // 都必须依赖它。套接字(Socket)的构造函数必须接收一个 io_context 的引用。
    // 注:在老版本 Asio 中它被称为 'io_service',新版本已更名为 'io_context'。
    asio::io_context ios;

    // -------------------------------------------------------------------------
    // 步骤 2. 指定协议类型
    // -------------------------------------------------------------------------
    // 创建一个 'tcp' 类的对象,并显式指定底层使用 IPv4 协议。
    // 如果需要支持 IPv6,可以使用 asio::ip::tcp::v6()。
    asio::ip::tcp protocol = asio::ip::tcp::v4();

    // -------------------------------------------------------------------------
    // 步骤 3. 实例化 TCP 套接字对象
    // -------------------------------------------------------------------------
    // 创建一个主动的(Active)TCP 套接字对象,并将上面创建的 io_context 传给它。
    // 此时套接字对象虽然被创建了,但底层操作系统还没有真正为它分配网络资源(即它还没被"打开")。
    asio::ip::tcp::socket sock(ios);

    // -------------------------------------------------------------------------
    // 步骤 4. 准备错误码接收器
    // -------------------------------------------------------------------------
    // 专门用来存储在打开套接字过程中可能发生的任何底层操作系统错误。
    // 使用这种重载方式可以避免 Asio 抛出 C++ 异常,让错误处理更符合可控的工程实践。
    boost::system::error_code ec;

    // -------------------------------------------------------------------------
    // 步骤 5. 真正打开套接字
    // -------------------------------------------------------------------------
    // 这一步会向操作系统申请真正的套接字资源,并将前面指定的 TCP IPv4 协议绑定到该套接字上。
    // 如果操作失败,错误信息会被写入到 ec 变量中。
    sock.open(protocol, ec);

    // -------------------------------------------------------------------------
    // 步骤 6. 错误检查与处理
    // -------------------------------------------------------------------------
    // ec.value() 返回 0 代表操作成功;返回非 0 意味着操作系统报错(例如:文件描述符耗尽等)。
    if (ec.value() != 0) {
        // 打印失败信息,包括错误码数值(ec.value())和人类可读的错误描述(ec.message())
        std::cout
            << "Failed to open the socket! Error code = "
            << ec.value() << ". Message: " << ec.message() << std::endl;
        
        // 返回具体的错误码给调用者
        return ec.value();
    }

    // 执行到这里说明套接字已经成功打开,可以进行接下来的 connect(连接)或 bind(绑定)操作了
    return 0;
}

上述socket只是通信的socket,如果是服务端,我们还需要生成一个acceptor的socket,用来接收新的连接。

cpp 复制代码
/**
 * @brief 创建并初始化一个用于监听的 TCP 接收器套接字(Acceptor)
 * @return int 返回 0 表示成功,返回非 0 值表示失败的错误码
 */
int create_acceptor_socket() {
    // -------------------------------------------------------------------------
    // 步骤 1. 创建 I/O 上下文对象 (io_context)
    // -------------------------------------------------------------------------
    // io_context 是整个 Asio 框架的核心引擎。所有的网络 I/O 操作、异步回调
    // 都必须依赖它。接收器(acceptor)在构造时必须传入一个 io_context 的引用。
    // 注:在老版本的 Asio 中它被称为 'io_service'。
    asio::io_context ios;

    // -------------------------------------------------------------------------
    // 步骤 2. 指定协议类型(此处为 IPv6)
    // -------------------------------------------------------------------------
    // 创建一个 'tcp' 类的对象,并显式指定底层使用 IPv6 协议。
    // 提示:在现代操作系统中,通常可以通过配置让 IPv6 的监听套接字同时兼听 IPv4 连接(双栈模式)。
    // 如果只需要 IPv4,可以使用 asio::ip::tcp::v4()。
    asio::ip::tcp protocol = asio::ip::tcp::v6();

    // -------------------------------------------------------------------------
    // 步骤 3. 实例化 TCP 接收器(Acceptor)对象
    // -------------------------------------------------------------------------
    // 实例化一个用于"接受连接"的套接字对象。
    // 此时它只是一个 C++ 对象,在操作系统底层还没有分配真正的网络监听资源。
    asio::ip::tcp::acceptor acceptor(ios);

    // -------------------------------------------------------------------------
    // 步骤 4. 准备错误码接收器
    // -------------------------------------------------------------------------
    // 用于捕获并存储在打开监听套接字过程中,底层操作系统可能返回的任何错误。
    // 传入此变量可以阻止 Asio 抛出 C++ 异常,使错误处理逻辑更平滑。
    boost::system::error_code ec;

    // -------------------------------------------------------------------------
    // 步骤 5. 真正打开接收器套接字
    // -------------------------------------------------------------------------
    // 这一步会向操作系统申请真正的套接字资源,并为其指定协议(这里是 TCP IPv6)。
    // 此时套接字被创建,但尚未绑定(bind)到具体的端口,也尚未开始监听(listen)。
    acceptor.open(protocol, ec);

    // -------------------------------------------------------------------------
    // 步骤 6. 错误检查与处理
    // -------------------------------------------------------------------------
    // ec.value() 返回 0 代表打开成功;返回非 0 意味着操作系统报错(例如权限不足、资源耗尽等)。
    if (ec.value() != 0) {
        // 打印错误信息,包括错误码(value)和具体的人类可读错误描述(message)
        std::cout
            << "Failed to open the acceptor socket! "
            << "Error code = "
            << ec.value() << ". Message: " << ec.message() << std::endl;
        
        // 将错误码返回给调用者
        return ec.value();
    }

    // 执行到这里,说明监听套接字已经成功打开。
    // 后续的标准服务器流程通常是:
    // 1. acceptor.set_option(...) -> 配置套接字(如允许端口复用 SO_REUSEADDR)
    // 2. acceptor.bind(...)       -> 绑定到指定的 IP 和端口
    // 3. acceptor.listen(...)     -> 开始监听排队的连接请求
    // 4. acceptor.accept(...)     -> 阻塞或异步等待并接受客户端连接
    return 0;
}

绑定acceptor

对于acceptor类型的socket,服务器要将其绑定到指定的端点,所有连接这个端点的连接都可以被接收到。

cpp 复制代码
/**
 * @brief 创建并绑定监听套接字到指定的 IP 和端口
 * @return int 返回 0 表示成功,返回非 0 值表示失败的错误码
 */
int bind_acceptor_socket() {

    // -------------------------------------------------------------------------
    // 步骤 1. 指定服务器要监听的端口号
    // -------------------------------------------------------------------------
    // 这里硬编码为 3333。实际开发中,这个值通常来自配置文件或命令行参数。
    // 注意:1024 以下的端口通常需要系统管理员(root/admin)权限才能绑定。
    unsigned short port_num = 3333;

    // -------------------------------------------------------------------------
    // 步骤 2. 创建网络端点 (Endpoint)
    // -------------------------------------------------------------------------
    // 端点(Endpoint)是"IP地址 + 端口号"的组合。
    // asio::ip::address_v4::any() 是一个特殊地址(相当于 0.0.0.0)。
    // 意思是:让服务器监听本地这台机器上"所有可用的 IPv4 网卡接口"(无论是局域网IP、公网IP还是环回IP 127.0.0.1)。
    asio::ip::tcp::endpoint ep(asio::ip::address_v4::any(), port_num);

    // -------------------------------------------------------------------------
    // 步骤 3. 创建 I/O 上下文对象
    // -------------------------------------------------------------------------
    // Asio 的核心引擎,负责底层事件循环。
    asio::io_context ios;

    // -------------------------------------------------------------------------
    // 步骤 4. 创建并自动打开接收器套接字
    // -------------------------------------------------------------------------
    // 这里使用了一个便捷的构造函数:传入 ios 和协议类型(通过 ep.protocol() 自动获取,这里是 IPv4 TCP)。
    // 它等同于先创建对象,再调用 open():
    //   asio::ip::tcp::acceptor acceptor(ios);
    //   acceptor.open(ep.protocol());
    asio::ip::tcp::acceptor acceptor(ios, ep.protocol());

    // -------------------------------------------------------------------------
    // 步骤 5. 准备错误码接收器
    // -------------------------------------------------------------------------
    // 用于捕获绑定操作中可能发生的底层系统错误(例如:端口已被其他程序占用)。
    boost::system::error_code ec;

    // -------------------------------------------------------------------------
    // 步骤 6. 绑定套接字到端点
    // -------------------------------------------------------------------------
    // 这一步告诉操作系统:"请把这个 acceptor 套接字和前面指定的 IP 与端口(0.0.0.0:3333)关联起来"。
    // 之后所有发往这台机器 3333 端口的 TCP 连接请求,都将由这个 acceptor 负责处理。
    acceptor.bind(ep, ec);

    // -------------------------------------------------------------------------
    // 步骤 7. 错误检查与处理
    // -------------------------------------------------------------------------
    // 如果端口已经被其他程序占用了(比如你启动了两个相同的服务器),或者没有权限,ec.value() 将不为 0。
    if (ec.value() != 0) {
        // 打印失败信息,方便排查问题
        std::cout << "Failed to bind the acceptor socket."
            << "Error code = " << ec.value() << ". Message: "
            << ec.message() << std::endl;

        // 返回具体的错误码给调用者
        return ec.value();
    }

    // 绑定成功后,接下来通常会调用 acceptor.listen() 让套接字进入监听状态
    return 0;
}

连接指定的端点

作为客户端可以连接服务器指定的端点进行连接

cpp 复制代码
/**
 * @brief 客户端连接到指定的远程服务器端点
 * @return int 返回 0 表示连接成功,返回非 0 值表示失败的错误码
 */
int connect_to_end() {
    // -------------------------------------------------------------------------
    // 步骤 1. 准备目标服务器的 IP 地址和端口号
    // -------------------------------------------------------------------------
    // "127.0.0.1" 是本地环回地址(Loopback Address),代表当前运行代码的这台电脑本身。
    // 端口号 3333 必须与服务器端监听的端口号完全一致。
    std::string raw_ip_address = "127.0.0.1";
    unsigned short port_num = 3333;

    // 开始异常捕获块。
    // 因为这里没有向 Asio 函数传入 boost::system::error_code 变量,
    // 所以一旦底层的网络操作(如解析IP、建立连接)失败,Asio 会直接抛出 C++ 异常。
    try {
        // -------------------------------------------------------------------------
        // 步骤 2. 创建表示目标服务器的端点对象 (Endpoint)
        // -------------------------------------------------------------------------
        // asio::ip::make_address() 将字符串形式的 IP(如 "127.0.0.1")
        // 转换成 Asio 内部高效的二进制格式 IP 地址。如果字符串格式不合法(如 "999.9.9.9"),此处会抛出异常。
        asio::ip::tcp::endpoint ep(
            asio::ip::make_address(raw_ip_address),
            port_num
        );

        // 创建 I/O 上下文,Asio 的核心引擎。
        asio::io_context ios;

        // -------------------------------------------------------------------------
        // 步骤 3. 创建并自动打开客户端套接字
        // -------------------------------------------------------------------------
        // 使用带有协议参数的构造函数。它会利用 ep.protocol()(此处由端点自动推导为 IPv4 TCP 协议)
        // 在底层操作系统中自动创建并打开(open)一个套接字。
        asio::ip::tcp::socket sock(ios, ep.protocol());

        // -------------------------------------------------------------------------
        // 步骤 4. 发起连接请求(经典的 TCP 三次握手)
        // -------------------------------------------------------------------------
        // 这一步是阻塞(Blocking)操作。代码会在这里"停住",向目标端点(ep)发起 TCP 连接请求。
        // 1. 如果连接成功:函数顺利执行完毕,程序继续往下走。
        // 2. 如果连接失败(如服务器未启动、网络超时、拒绝连接):它会立刻抛出 boost::system::system_error 异常,
        //    中断当前 try 块内部后续代码的执行,直接跳转到 catch 块。
        sock.connect(ep);

        // -------------------------------------------------------------------------
        // 连接成功后的处理
        // -------------------------------------------------------------------------
        // 运行到这里说明 TCP 三次握手成功!
        // 此时,套接字 `sock` 已经和服务器建立了稳定的双向通道,
        // 接下来你可以使用 sock.write_some() 发送数据,或者使用 sock.read_some() 接收数据。
        std::cout << "Successfully connected to the server!" << std::endl;

    }
    // -------------------------------------------------------------------------
    // 步骤 5. 异常处理块
    // -------------------------------------------------------------------------
    // 用于捕获来自 from_string 或 connect 的网络、系统级异常。
    // 注:在独立 Asio(非 Boost 版)中,通常捕获的是 std::system_error。
    catch (boost::system::system_error& e) {
        // e.code():获取具体的错误状态码(如连接超时、拒绝连接等)。
        // e.what():获取由系统提供的、人类可读的英文错误错误描述。
        std::cout << "Error occured! Error code = " << e.code()
            << ". Message: " << e.what() << std::endl;

        // 发生异常时,返回具体的错误码数值(非0)
        return e.code().value();
    }

    // 整个 try 块顺利执行完毕且没有抛出异常,说明连接大获成功,返回 0
    return 0;
}
bash 复制代码
Error occured! Error code = system:10061. Message: connect: 由于目标计算机积极拒绝,无法连接。 [system:10061 at C:/vcpkg/installed/x64-windows/include/boost/asio/detail/win_iocp_socket_service.hpp:629 in function 'connect']
bash 复制代码
Successfully connected to the server!

服务器接收连接

当有客户端连接时,服务器需要接收连接

cpp 复制代码
/**
 * @brief 初始化服务器并阻塞等待、接受一个客户端连接
 * @return int 返回 0 表示成功,返回非 0 值表示失败的错误码
 */
int accept_new_connection() {
    // -------------------------------------------------------------------------
    // 核心参数:连接请求队列的最大长度 (Backlog)
    // -------------------------------------------------------------------------
    // 当服务器正在处理当前客户端时,如果有其他客户端也连进来,操作系统会将它们放入一个排队队列中。
    // BACKLOG_SIZE = 30 意味着操作系统底层允许同时排队等待被服务器处理的"未决连接(Pending connections)"的最大数量为 30 个。
    // 如果队列满了,后续进来的客户端连接将会被直接拒绝(触发 Connection Refused 错误)。
    const int BACKLOG_SIZE = 30;

    // 步骤 1. 指定服务器要监听的本地端口号(此处为 3333)
    unsigned short port_num = 3333;

    // 步骤 2. 创建网络端点 (Endpoint),指定监听本地所有可用的 IPv4 网卡接口 (0.0.0.0:3333)
    asio::ip::tcp::endpoint ep(asio::ip::address_v4::any(), port_num);

    // 创建 Asio 的核心引擎 I/O 上下文对象
    asio::io_context ios;

    // 采用异常捕获风格。以下任何一步(如端口被占用、网络异常)报错,都会直接跳入 catch 块。
    try {
        // -------------------------------------------------------------------------
        // 步骤 3. 创建并自动打开接收器套接字 (Acceptor Socket)
        // -------------------------------------------------------------------------
        // 传入 ep.protocol(),自动推导并利用 TCP IPv4 协议在系统底层开辟套接字资源。
        asio::ip::tcp::acceptor acceptor(ios, ep.protocol());

        // -------------------------------------------------------------------------
        // 步骤 4. 绑定套接字到端点
        // -------------------------------------------------------------------------
        // 将这个监听套接字绑定到前面定义的本地 3333 端口。
        acceptor.bind(ep);

        // -------------------------------------------------------------------------
        // 步骤 5. 开始监听进入的连接请求
        // -------------------------------------------------------------------------
        // 这一步改变了套接字的状态,将其转为"监听(Passive/Listening)"状态。
        // 并告诉操作系统内核,开启一个最大容量为 BACKLOG_SIZE (30) 的等待队列来缓存客户端的握手请求。
        acceptor.listen(BACKLOG_SIZE);

        // -------------------------------------------------------------------------
        // 步骤 6. 创建一个空的主动套接字 (Active Socket)
        // -------------------------------------------------------------------------
        // 注意:这个 `sock` 此时是空的,内部没有分配真正的底层网络资源,也没有绑定任何协议。
        // 它是专门准备用来"接待"即将连入的那个客户端的。
        asio::ip::tcp::socket sock(ios);

        // -------------------------------------------------------------------------
        // 步骤 7. 阻塞并处理下一个连接请求
        // -------------------------------------------------------------------------
        // 这是一个经典的【阻塞(Blocking)】操作!
        // 程序运行到这里会完全"停住",直到底层等待队列中出现了一个完成 TCP 三次握手的客户端。
        // 
        // 一旦有客户端连接进来:
        // 1. `acceptor` 会将这个连接从队列里取出来。
        // 2. 将该连接的所有底层网络通道、身份信息全部注入并"激活"刚刚创建的空套接字 `sock`。
        // 3. `accept` 函数返回,代码继续向下执行。
        acceptor.accept(sock);

        // -------------------------------------------------------------------------
        // 连接成功后的处理
        // -------------------------------------------------------------------------
        // 运行到这里,说明已经成功捕获并连接了一个客户端!
        // 此时,`acceptor` 功成身退(继续在 3333 端口等待下一个新人),
        // 而当前的这个客户端已经和 `sock` 绑定。你可以通过 `sock` 读写数据了。
        std::cout << "Successfully accepted a client connection!" << std::endl;
    }
    // -------------------------------------------------------------------------
    // 步骤 8. 异常处理块
    // -------------------------------------------------------------------------
    catch (boost::system::system_error& e) {
        // 如果端口被占用,或者监听失败,会在这里捕获并打印具体的错误码与英文描述
        std::cout << "Error occured! Error code = " << e.code()
            << ". Message: " << e.what() << std::endl;

        return e.code().value();
    }

    // 成功执行完整个流程返回 0
    return 0;
}
bash 复制代码
Successfully accepted a client connection!

关于buffer

任何网络库都有提供 Buffer 的数据结构。所谓 Buffer,就是接收和发送数据时用作暂存数据的结构

基础 Buffer 结构

Boost.Asio 提供了两个最基础的缓冲区结构:

  1. asio::mutable_buffer:可变缓冲区,用于接收服务(Read/Receive)。
  2. asio::const_buffer:只读缓冲区,用于发送服务(Write/Send)。

核心原理: > 这两个结构本身并不实际拥有 连续的空间,它们内部只是存放了一个指针(指向后续数据的首字节地址) 以及一个长度(表示后续数据的长度)。因此,这两个结构通常不直接被 Boost.Asio 的 API 直接使用,而是通过更高级的序列概念进行包装。

BufferSequence(缓冲区序列)

为了节约空间并提高网络 I/O 效率(例如实现散布/聚集 I/O,即 Scatter/Gather I/O),对于 API 的 Buffer 参数,Asio 提出了 MutableBufferSequenceConstBufferSequence 的概念。

  • 定义 它们是由多个 asio::mutable_buffer或asio::const_buffer 组合而成的序列。
  • 作用: 它可以将一部分不连续的空间组合起来,作为统一的参数交给 API 使用。

内存结构原理图解

我们可以通过一个 std::vector<asio::mutable_buffer> 结构的数组来理解其底层的内存分布模型:

plain 复制代码
std::vector<asio::mutable_buffer>
+---------+---------+---------+---------+
| 0xabcd  | 0xabce  | 0xabcf  | 0xabd0  |  <-- vector 中存储的是各个 mutable_buffer 的地址
+----+----+----+----+----+----+----+----+
     |         |         |         |
     v         v         v         v
   +---+     +---+     +---+     +---+
   | 2 |     | 3 |     | 1 |     | 2 |   <-- 每个 mutable_buffer 的第一个字节表示长度 (Length)
   +---+     +---+     +---+     +---+
   |'m'|     |'a'|     |'b'|     |'f'|   <-- 后面紧跟着实际的数据内容 (Data)
   +---+     +---+     +---+     +---+
   |'n'|     |'b'|               |'g'|
   +---+     +---+               +---+
             |'c'|
             +---+
  • std::vector 存储的是每个 mutable_buffer 的地址。
  • 每个 mutable_buffer 的**第一个字节表示长度**,**后面跟着实际的数据内容**。

asio::buffer() 适配器函数

因为手动构建复杂的序列结构交给用户使用非常不便,所以 Asio 提供了 asio::buffer() 适配器函数。该函数接收多种形式的字节流,并适配返回 asio::mutable_buffers_1asio::const_buffers_1 结构的对象

  • 返回值逻辑:
    • 如果传给 asio::buffer() 的参数是一个只读类型 ,则返回 asio::const_buffers_1 类型对象。
    • 如果传给 asio::buffer() 的参数是一个可写类型 ,则返回 asio::mutable_buffers_1 类型对象。
  • 适配器作用: asio::const_buffers_1asio::mutable_buffers_1asio::mutable_bufferasio::const_buffer 的适配器,它们提供了符合 MutableBufferSequenceConstBufferSequence 概念的接口。因此,它们可以直接作为 Boost.Asio API 函数的参数使用

🔊总结: 我们可以直接利用 asio::buffer() 函数来生成我们需要的缓存数据。

代码实战与演练

Asio 的发送接口定义

比如 Boost 的发送接口 send 要求的参数就是 ConstBufferSequence 类型:

cpp 复制代码
template<typename ConstBufferSequence>
std::size_t send(const ConstBufferSequence& buffers);

传统/繁琐方式:手动将 "Hello World" 转化为取样类型

如果不使用方便的快捷函数,手动构建序列会非常冗长:

cpp 复制代码
void use_const_buffer() {
    std::string buf = "hello world!";
    // 手动创建 const_buffer,指定首地址和长度
    asio::const_buffer asio_buf(buf.c_str(), buf.length());
    
    // 构建一个 vector 容器来满足 BufferSequence 概念
    std::vector<asio::const_buffer> buffers_sequence;
    buffers_sequence.push_back(asio_buf);
    
    // 最终的 buffers_sequence 就可以传递给发送接口 send() 的类似接口。
    // 但是这太复杂了,所以可以直接调用 buffer() 函数将其转化为 send 需要的参数类型。
}

推荐方式一:使用 asio::buffer() 转换字符串

通过快捷函数,一行代码即可搞定:

cpp 复制代码
void use_buffer_str() {
    // output_buf 可以直接传递给 send 接口,我们也可以将其转化为 send 要求的类型
    asio::const_buffers_1 output_buf = asio::buffer("hello world");
}

推荐方式二:使用 asio::buffer() 包装原生数组/内存块

对于流式操作或原始内存,同样可以使用 asio::buffer 进行包裹:

cpp 复制代码
void use_buffer_array() {
    // 1. 定义缓冲区的大小(字节数),这里指定为 20 字节
    const size_t BUF_SIZE_BYTES = 20;
    
    // 2. 动态分配一块大小为 20 字节的 char 类型数组内存
    //    使用 std::unique_ptr 智能指针进行包裹,确保在函数退出、发生异常或生命周期结束时,
    //    这块内存会被自动释放(调用 delete[]),从而有效防止内存泄漏(Memory Leak)。
    std::unique_ptr<char[]> buf(new char[BUF_SIZE_BYTES]);
    
    // 3. 将原生内存指针和大小包装为 Boost.Asio 的标准缓冲区对象
    //    - buf.get(): 获取 unique_ptr 内部管理的原生 char* 指针。
    //    - static_cast<void*>(): 将 char* 显式转换为无类型指针 void*。因为 asio::buffer 
    //      接收通用内存地址,非 const 的 void* 会让其识别并返回一个"可变(可写)"的缓冲区。
    //    - BUF_SIZE_BYTES: 显式指定这块缓冲区的边界大小,防止 Asio 在读写时发生内存越界。
    //    - input_buf: 最终推导出的类型是 asio::mutable_buffers_1(或新版本中的 mutable_buffer),
    //      它符合 MutableBufferSequence 概念,可以直接作为 async_read、async_read_some 等网络接收接口的参数。
    auto input_buf = asio::buffer(static_cast<void*>(buf.get()), BUF_SIZE_BYTES);
    
    // 注意:如果在实际异步 I/O 中使用 input_buf,必须确保智能指针 buf 的生命周期
    // 一直延续到异步操作真正完成(Callback 回调函数被触发)之后。否则异步操作在后台向这块
    // 已经随函数结束而销毁的内存写入数据,会导致严重的"悬空指针"错误(产生段错误或内存损坏)。
}
cpp 复制代码
#include <memory>
#include <boost/asio.hpp>

void use_buffer_array_advanced() {
    // 1. 定义缓冲区的大小(字节数)
    const size_t BUF_SIZE_BYTES = 20;

    // 2. 安全内存分配
    //    使用 C++14 的 std::make_unique 来创建动态数组。
    //    相比于传统的 `new char[]`,它具有以下优势:
    //    - 完美符合"异常安全(Exception Safety)",避免了 new 成功但赋值给智能指针前发生异常导致的内存泄漏。
    //    - 语法更现代,且强制对分配的内存进行了值初始化(每个字节都会自动清零 '\0')。
    auto buf = std::make_unique<char[]>(BUF_SIZE_BYTES);
    
    // 3. 地道的 Asio 缓冲区包装
    //    直接将原生 char* 指针丢给 asio::buffer(),无需任何 static_cast<void*> 强转!
    //    - buf.get(): 直接获取内部管理的原生非 const 类型的 char* 指针。
    //    - Boost.Asio 内部拥有非常强大的函数重载机制,它能自动识别出传入的是"非 const 的指针",
    //      因此会自动将其推导并包装为可读写的 mutable_buffer(可变缓冲区)。
    //    - 这种写法不仅消除了丑陋的类型转换,而且保证了代码的类型安全。
    auto input_buf = boost::asio::buffer(buf.get(), BUF_SIZE_BYTES);
    
    // 此时 input_buf 已经准备就绪,可以直接作为参数传递给:
    // boost::asio::read()、boost::asio::async_read_some() 等任何需要接收数据的网络接口。
}

流式缓冲区 asio::streambuf

对于流式操作,我们可以使用 asio::streambuf。将输入输出流与 streambuf 绑定,可以非常方便地实现数据的输入输入与隔离输出。

streambuf 非常适合处理动态长度的数据,或者处理像 HTTP 这种以特定分隔符(如 \n 或 \r\n)结尾的协议数据。

cpp 复制代码
#include <iostream>
#include <string>
#include <boost/asio.hpp>

void use_stream_buffer() {
    // 1. 实例化一个 Asio 流式缓冲区对象
    //    与固定大小的数组缓冲区不同,streambuf 会根据写入的数据量自动动态增长(自动扩容),
    //    它内部维护了"输入序列(读取端)"和"输出序列(写入端)"两个指针。
    boost::asio::streambuf buf;
    
    // 2. 绑定输出流(std::ostream)到这个缓冲区
    //    通过将 buf 的地址传给 std::ostream,我们可以像使用 std::cout 一样,
    //    使用标准的流插入运算符(<<)向这个缓冲区里"写"数据。
    std::ostream output(&buf);
    
    // 3. 将消息写入到基于 stream 的缓冲区(stream-based buffer)中
    //    此时,字符串 "Message1\nMessage2" 被源源不断地推入到 buf 的内部存储空间中。
    //    注意:此时缓冲区的写入指针(put area)会向后移动。
    output << "Message1\nMessage2";
    
    // 4. 绑定输入流(std::istream)到同一个缓冲区
    //    现在我们想要从 streambuf 中读取数据。通过将同一个 buf 的地址传给 std::istream,
    //    我们就可以使用标准流提取操作(如 std::getline、>>)从缓冲区里"读"数据。
    std::istream input(&buf);
    
    // 5. 定义一个字符串变量,用于接收接下来从缓冲区读取出来的文本
    std::string message1;
    
    // 6. 使用 std::getline 从输入流中读取数据,直到遇到 '\n' 分隔符
    //    - getline 会从 input(即 buf)中读取字符,直到碰见第一个换行符 '\n'。
    //    - 它会将 '\n' 之前的内容提取出来赋值给 message1,并自动消费(消耗)掉缓冲区中的那个 '\n'。
    //    - 此时缓冲区的读取指针(get area)会向后移动,越过 "Message1\n"。
    std::getline(input, message1);
    
    // 7. 此时 message1 字符串中包含 "Message1"
    //    而缓冲区 `buf` 中由于只被 getline 消费了前半部分,目前里面还残留着 "Message2" 没有被读取。
    //    这非常适合网络通信中"先读出第一行报头,再读取后续内容"的场景。
}

:::info

当把 boost::asio::streambuf 配合 std::ostreamstd::istream 使用时,它在底层其实是一个双向队列/环形缓冲的概念:

  • output << ... 在向缓冲区的 "写空间(Put Area)" 存入数据。
  • `std::getline(input, ...) 从缓冲区的 "读空间(Get Area)" 拿走数据。

在网络编程中,可以直接把 buf 丢给 boost::asio::async_read_until(socket, buf, "\n", handler)。Asio 会自动把网络上收到的字节灌进这个 buf,直到撞见 \n。随后,就可以用 std::istream 极其优雅地把解析好的字符串整行提取出来。

:::

相关推荐
故事和你917 小时前
洛谷-【图论2-2】最短路3
开发语言·数据结构·c++·算法·动态规划·图论
yong99907 小时前
基于VC++的图像匹配金字塔算法
c++·算法·计算机视觉
剑神一笑7 小时前
Linux tar 归档命令深度解析:从文件打包到压缩算法的完整实现
linux·运维·服务器
coolwaterld7 小时前
Linux 移动硬盘挂载不上 wrong fs type, bad option, bad superblock
linux·服务器
J2虾虾7 小时前
Linux tar 命令详解
linux·运维·服务器
多敲代码防脱发8 小时前
Spring进阶(Bean的生命周期与Bean的后处理器)
java·服务器·开发语言·spring boot·spring·servlet
阳光九叶草LXGZXJ8 小时前
达梦数据库-学习-52-DmDrs参数介绍(Manager模块)
linux·运维·数据库·sql·学习
corpse20108 小时前
CentOS Linux release 8.5.2111下的CVE-2026-31431 Linux内核提权漏洞处置 过程问题记录
linux·运维·centos
此生决int8 小时前
算法从入门到精通——滑动窗口
c++·算法·蓝桥杯