简介:本资源是《Boost.Asio C++ Network Programming》配套源代码实践包,面向C++中高级开发者及网络编程学习者,聚焦异步I/O、高并发服务构建与底层通信机制理解。压缩包含45个文件,以23个cpp核心示例为主(涵盖asyserver/asyclient、synserver/synclient、tcpacserver等典型服务端/客户端实现),辅以4个说明文本、3个Shell脚本(如push.sh、x.sh)、2个Makefile构建文件及.gitignore等工程配置,总大小923KB,结构清晰,便于按通信模型(同步/异步)、协议类型(TCP/UDP)和功能模块(定时器、strand、buffer管理)分层研读。已有446人学习下载,代码覆盖deadline_timer使用、async_read/write回调调度、IO对象生命周期控制、多线程执行器协同等关键实践点,结合注释与可运行实例,帮助读者深入掌握Asio事件驱动模型、避免常见竞态与资源泄漏问题,为开发高性能Web服务器、实时通信中间件或分布式节点打下扎实基础。
1. 一个不依赖 Qt、不靠 MFC、连 Winsock API 都没碰的 C++ 网络服务,是怎么用 300 行代码跑通 TCP 回显 + SSL 加密 + 异步定时器的?
你手头有个嵌入式设备要上报状态,或者在写一个跨平台的后台 agent,又或者只是单纯厌倦了每次改个 socket 超时就得重编译整个模块——这时候翻 Boost.Asio 的文档,大概率会卡在io_context::run()那一页:它不像 Python 的 asyncio 那样有async/await语法糖兜底,也不像 Java Netty 那样自带 EventLoopGroup 和 ChannelPipeline 的可视化分层。但正因如此,Asio 是少数几个能让你在 C++ 里亲手捏出事件循环骨架、把每个async_read_some的回调地址都印在脑子里、甚至在 release 模式下用perf抓到 epoll_wait 调用栈的网络库。这份源代码包不是 Boost 官方发行版的压缩包,而是某开发者从 Boost 1.82 中完整剥离出 Asio 子系统后,剔除所有测试和文档、仅保留include/boost/asio/下全部头文件 + 5 个最小可运行 demo(含tcp_echo_server.cpp、ssl_chat_client.cpp、steady_timer_demo.cpp)的精简工程。它不编译 Boost.System 或 Boost.Thread,所有同步/异步 I/O、SSL 封装、定时器、信号处理全靠模板元编程 +std::function+std::shared_ptr实现。新手照着CMakeLists.txt三行命令就能跑通;老手能直接进impl/目录看epoll_reactor.ipp里怎么把epoll_ctl封装成register_descriptor。这不是教学玩具——它是某跨平台系统中实际部署的通信底座原型,所有#ifdef _WIN32分支都经过真实 Windows Server 2022 + VS2022 编译验证。
2. 构建与运行:从零启动 TCP 回显服务,看清 Asio 的“无栈协程”如何落地
2.1 环境准备:只依赖标准库 + OpenSSL(可选),拒绝 Boost.Build 黑盒
Asio 是纯头文件库(header-only),但它的 SSL 支持需要链接 OpenSSL,异步 DNS 解析需要getaddrinfo_a(Linux)或GetAddrInfoW(Windows)。本项目已预置两套构建路径:
- Linux/macOS(推荐 clang++ 15+ / g++ 12+):
# 确保已安装 openssl-dev(Ubuntu/Debian)或 openssl-devel(CentOS/RHEL) sudo apt install libssl-dev # Ubuntu # 或 sudo yum install openssl-devel # CentOS mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release -DASIO_USE_OPENSSL=ON .. make -j4 - Windows(MSVC 2022):
使用 CMake GUI 或命令行指定工具链:mkdir build && cd build cmake -G "Visual Studio 17 2022" -A x64 -DASIO_USE_OPENSSL=ON -DOPENSSL_ROOT_DIR="C:/OpenSSL-Win64" .. cmake --build . --config Release
提示:
-DASIO_USE_OPENSSL=ON是开关,关掉则ssl_chat_client.cpp不参与编译;若只测 TCP,可安全关闭以跳过 OpenSSL 依赖。
2.2 核心 demo 解析:tcp_echo_server.cpp—— 300 行里的异步生命周期管理
这是最精简的 Asio 服务端实现,没有线程池、没有连接池、没有日志框架,但完整展示了io_context、ip::tcp::acceptor、ip::tcp::socket三者如何协作完成“监听 → 接收 → 异步读 → 异步写 → 关闭”的闭环:
// tcp_echo_server.cpp(节选关键逻辑) #include <boost/asio.hpp> #include <iostream> #include <memory> using boost::asio::ip::tcp; class session : public std::enable_shared_from_this<session> { public: explicit session(tcp::socket socket) : socket_(std::move(socket)) {} void start() { do_read(); } private: void do_read() { socket_.async_read_some( boost::asio::buffer(data_, max_length), [self = shared_from_this()](boost::system::error_code ec, std::size_t length) { if (!ec) { self->do_write(length); } else if (ec != boost::asio::error::operation_aborted) { std::cerr << "Read error: " << ec.message() << "\n"; } }); } void do_write(std::size_t length) { boost::asio::async_write( socket_, boost::asio::buffer(data_, length), [self = shared_from_this()](boost::system::error_code ec, std::size_t /*length*/) { if (!ec) { self->do_read(); // 循环读取 } else if (ec != boost::asio::error::operation_aborted) { std::cerr << "Write error: " << ec.message() << "\n"; } }); } tcp::socket socket_; enum { max_length = 1024 }; char data_[max_length]; }; class server { public: server(boost::asio::io_context& io_context, short port) : acceptor_(io_context, tcp::endpoint(tcp::v4(), port)), socket_(io_context) { do_accept(); } private: void do_accept() { acceptor_.async_accept( socket_, [this](boost::system::error_code ec) { if (!ec) { std::make_shared<session>(std::move(socket_))->start(); socket_ = tcp::socket(acceptor_.get_executor()); // 重置 socket 供下次 accept do_accept(); // 继续监听 } else { std::cerr << "Accept error: " << ec.message() << "\n"; } }); } tcp::acceptor acceptor_; tcp::socket socket_; }; int main(int argc, char* argv[]) { try { if (argc != 2) { std::cerr << "Usage: " << argv[0] << " <port>\n"; return 1; } boost::asio::io_context io_context; server s(io_context, std::atoi(argv[1])); io_context.run(); // 启动事件循环 } catch (std::exception& e) { std::cerr << "Exception: " << e.what() << "\n"; } return 0; }逐段说明:
session类继承std::enable_shared_from_this:这是 Asio 异步模型的基石。因为async_read_some的回调是延迟执行的,当回调触发时,原始session对象可能已被析构(比如客户端断开后未及时清理)。shared_from_this()确保回调内持有的self是强引用,防止悬空指针。do_read()中async_read_some的 lambda 捕获[self = shared_from_this()]:注意不是[this]!this在异步回调时早已失效,必须用shared_ptr延长生命周期。server::do_accept()中socket_ = tcp::socket(...):每次accept后必须重置socket_,否则下次async_accept会失败(socket已处于 connected 状态)。这是新手高频翻车点。io_context.run():这是真正的事件循环入口。它内部调用epoll_wait(Linux)或kevent(macOS)或GetQueuedCompletionStatus(Windows),阻塞等待 I/O 事件。它不会返回,除非io_context.stop()被调用,或所有异步操作完成且无新任务入队。
2.3 运行验证:用 netcat 测试,观察连接建立与数据回显
编译完成后,在终端启动服务:
./tcp_echo_server 8080另开终端,用nc连接并发送数据:
nc 127.0.0.1 8080 Hello Asio! # 服务端立即回显: Hello Asio! # 再发一行: Boost is awesome # 回显: Boost is awesome此时查看服务端控制台,应无错误输出。若看到Read error: Connection reset by peer,是客户端主动断开的正常日志(nc退出时发送 RST);若出现Accept error: Too many open files,说明系统文件描述符限制过低,需调大ulimit -n。
3. SSL 加密通信实战:ssl_chat_client.cpp如何绕过证书校验实现快速联调
3.1 SSL 模块结构:ssl::context+ssl::stream的双层封装
Asio 的 SSL 支持基于 OpenSSL,但完全隐藏了SSL_CTX*和SSL*的裸指针操作。核心抽象是:
boost::asio::ssl::context:对应 OpenSSL 的SSL_CTX,负责加载 CA 证书、私钥、配置加密套件;boost::asio::ssl::stream<tcp::socket>:包装一个已连接的tcp::socket,提供async_handshake、async_read_some、async_write_some等 SSL 特有接口。
本 demo 使用自签名证书(server.pem和server.key已内置在certs/目录),客户端默认启用证书校验。但开发联调时,常需跳过校验以快速验证业务逻辑——这正是 Asio 提供verify_mode::none的意义。
3.2 关键代码:客户端握手与双向加密读写
// ssl_chat_client.cpp(简化版) #include <boost/asio.hpp> #include <boost/asio/ssl.hpp> #include <iostream> #include <string> using boost::asio::ip::tcp; namespace ssl = boost::asio::ssl; int main(int argc, char* argv[]) { try { if (argc != 3) { std::cerr << "Usage: " << argv[0] << " <host> <port>\n"; return 1; } boost::asio::io_context io_context; ssl::context ctx(ssl::context::sslv23); // 关键:跳过证书校验(仅用于开发!) ctx.set_verify_mode(ssl::verify_none); // 若需校验,改为: // ctx.load_verify_file("ca.pem"); // 加载 CA 证书 // ctx.set_verify_mode(ssl::verify_peer); tcp::resolver resolver(io_context); ssl::stream<tcp::socket> stream(io_context, ctx); // 1. 同步解析域名(避免 async_resolve 的复杂性) auto endpoints = resolver.resolve(argv[1], argv[2]); boost::asio::connect(stream.next_layer(), endpoints); // 2. SSL 握手 stream.handshake(ssl::stream_base::client); // 3. 发送消息 std::string msg = "Hello over TLS!\n"; boost::asio::write(stream, boost::asio::buffer(msg)); // 4. 接收响应 char reply[1024]; size_t len = stream.read_some(boost::asio::buffer(reply)); std::cout << "Reply: " << std::string(reply, len); } catch (std::exception& e) { std::cerr << "Exception: " << e.what() << "\n"; } return 0; }参数说明:
ssl::context::sslv23:兼容性最强的协议版本(实际协商为 TLSv1.2/TLSv1.3),比tlsv12更稳妥;ctx.set_verify_mode(ssl::verify_none):开发阶段救命设置,生产环境必须替换为verify_peer+load_verify_file;stream.next_layer():获取底层tcp::socket,用于connect;SSL 握手前必须先建立 TCP 连接;stream.handshake(ssl::stream_base::client):执行 TLS Client Hello 流程,阻塞直到完成。
注意:服务端需先运行
ssl_echo_server(本包含),它使用server.pem/key启动 HTTPS 风格回显服务。ssl_chat_client仅作客户端验证,不实现服务端逻辑。
3.3 证书生成:三行命令搞定自签名 PEM 文件(附certs/目录结构)
项目已内置certs/server.pem和certs/server.key,但你可能需要更新或生成自己的证书。以下是 Linux/macOS 下标准流程:
# 1. 生成私钥(2048位,AES256加密,密码设为 'password') openssl genrsa -aes256 -out certs/server.key 2048 # 2. 生成证书签名请求(CSR),Common Name 填 'localhost' openssl req -new -key certs/server.key -out certs/server.csr # 3. 自签名生成证书(有效期365天) openssl x509 -req -days 365 -in certs/server.csr -signkey certs/server.key -out certs/server.pem生成后,server.pem包含证书,server.key是私钥。服务端代码中通过ctx.use_certificate_chain_file("certs/server.pem")和ctx.use_private_key_file("certs/server.key", ssl::context::pem)加载。
4. 避坑指南:5 条血泪经验总结,覆盖编译、运行、调试全链路
4.1 现象:undefined reference to 'SSL_library_init'
原因:链接时未指定-lssl -lcrypto,或 OpenSSL 版本太旧(< 1.1.0)导致符号名变更。
解决:
- Linux:在
CMakeLists.txt的target_link_libraries中追加ssl crypto; - macOS:用
brew install openssl后,添加-I/opt/homebrew/include -L/opt/homebrew/lib; - Windows:确保
OPENSSL_ROOT_DIR指向包含libssl.lib和libcrypto.lib的目录。
4.2 现象:async_read_some回调从未触发,程序卡死在io_context.run()
原因:io_context构造后未启动任何异步操作,或async_accept/async_connect调用失败但未检查error_code。
解决:
- 在
main()中io_context.run()前,确认server或client对象已构造(触发do_accept/do_connect); - 所有
async_*调用后,必须在 lambda 中检查ec,打印错误而非静默忽略; - 添加
std::cout << "Starting io_context...\n";在run()前,确认执行流到达此处。
4.3 现象:ssl::stream握手失败,报错handshake_failed (system:104)
原因:system:104是 Linux 的ECONNRESET,常见于服务端未启动、防火墙拦截、或服务端证书私钥不匹配。
解决:
- 先用
openssl s_client -connect localhost:8080测试服务端是否响应; - 检查服务端
server.pem和server.key是否为同一对密钥(openssl rsa -noout -modulus -in server.key | openssl md5与openssl x509 -noout -modulus -in server.pem | openssl md5应一致); - 确认服务端
ssl::context设置了ctx.use_certificate_chain_file和ctx.use_private_key_file。
4.4 现象:多线程调用io_context::run()时崩溃,报double free or corruption
原因:io_context不是线程安全的,多个线程同时调用run()会竞争内部队列。
解决:
- 单线程模型:只在一个线程调用
run(),其他线程通过post()或dispatch()投递任务; - 多线程模型:创建多个
io_context实例,或使用io_context::strand串行化回调(本包 demo 未启用,需自行添加); - 最佳实践:
io_context实例与线程一一绑定,用thread_pool管理 N 个io_context。
4.5 现象:steady_timer定时器只触发一次,无法循环
原因:async_wait的回调中未重新调用async_wait,或timer对象被提前析构。
解决:
- 参考
steady_timer_demo.cpp中的写法:在回调 lambda 内再次调用timer.async_wait(...); - 将
steady_timer成员变量置于session或server类中,确保其生命周期长于定时任务; - 避免在回调中
reset()timer,应使用expires_after()+async_wait()组合实现周期调度。
5. 核心模块拆解:impl/目录下的epoll_reactor.ipp如何把 Linux epoll 封装成统一接口
5.1 Asio 的 reactor 模式:epoll_reactor是 Linux 的心脏
Asio 在不同平台使用不同 I/O 多路复用机制:Linux 用epoll,macOS 用kqueue,Windows 用IOCP。所有平台-specific 实现在boost/asio/detail/impl/下,而epoll_reactor.ipp是 Linux 的核心。它不暴露epoll_fd,而是通过reactor::schedule_timer()、reactor::start_op()等接口,将用户注册的定时器、socket 读写事件,统一转为epoll_ctl调用。
关键数据结构:
struct epoll_reactor::descriptor_state:每个 socket 对应一个状态对象,记录epoll_event.events(EPOLLIN/EPOLLOUT)、就绪标志、回调函数指针;epoll_reactor::op_queue<operation>:存放待执行的异步操作(如reactor_op),由do_run_one()从队列取出并执行。
5.2start_op()源码片段:一次async_read_some如何落到epoll_ctl
// boost/asio/detail/impl/epoll_reactor.ipp(简化) void epoll_reactor::start_op(int op_type, descriptor_state* descriptor_data, reactor_op* op, bool is_continuation, bool is_non_blocking) { // 1. 获取 epoll fd int epoll_fd = epoll_fd_; // 2. 构造 epoll_event struct epoll_event ev = {}; ev.data.ptr = op; // 关键:将用户回调 op 存入 data.ptr switch (op_type) { case read_op: ev.events = EPOLLIN | EPOLLET; // 边沿触发 break; case write_op: ev.events = EPOLLOUT | EPOLLET; break; } // 3. 注册到 epoll if (::epoll_ctl(epoll_fd, EPOLL_CTL_ADD, descriptor_data->descriptor_, &ev) == 0) { // 注册成功,等待事件 } else if (errno == EEXIST) { // 已存在,改用 MOD ::epoll_ctl(epoll_fd, EPOLL_CTL_MOD, descriptor_data->descriptor_, &ev); } }逻辑说明:
ev.data.ptr = op是 Asio 的玄学设计:epoll_wait返回时,ev.data.ptr就是当初注册的reactor_op*,无需哈希表查找,O(1) 定位回调;EPOLLET(边沿触发)是 Asio 默认模式,要求应用层一次性读完所有可用数据(否则会漏事件),这也是async_read_some需配合循环do_read()的原因;epoll_ctl(..., EPOLL_CTL_ADD)失败时捕获EEXIST并降级为MOD,避免重复注册错误。
5.3do_run_one():事件循环主干,epoll_wait的封装体
// boost/asio/detail/impl/epoll_reactor.ipp int epoll_reactor::do_run_one(mutex::scoped_lock& lock, const boost::asio::detail::chrono_time_traits< boost::asio::chrono::steady_clock>::time_point& timeout, boost::system::error_code& ec) { // 1. 计算 epoll_wait 超时(毫秒) int msec = timeout.is_max() ? -1 : static_cast<int>( (timeout - boost::asio::chrono::steady_clock::now()).count() / 1000000); // 2. 等待事件 epoll_event events[128]; int num_events = epoll_wait(epoll_fd_, events, 128, msec); // 3. 遍历就绪事件,执行回调 for (int i = 0; i < num_events; ++i) { reactor_op* op = static_cast<reactor_op*>(events[i].data.ptr); op->do_perform(); // 调用用户注册的 on_read/on_write } return num_events; }参数说明:
msec:-1表示永久阻塞,0表示非阻塞轮询,正数为超时毫秒数;Asio 的steady_timer就是靠这个msec控制精度;events[128]:单次最多处理 128 个事件,避免epoll_wait返回过多导致延迟;op->do_perform():最终调用用户传入的 lambda,完成“从内核事件到业务逻辑”的最后一跳。
6. 二次开发实战:如何给tcp_echo_server添加连接数统计与优雅关闭功能
6.1 连接数统计:全局计数器 + RAII 析构自动减量
目标:实时统计当前活跃连接数,并通过 HTTP 端点/status暴露(用最简方式,不引入第三方 Web 框架)。
改造步骤:
- 在
server类中添加静态成员connection_count_; - 在
session构造时原子增,析构时原子减; - 新增
status_handler函数,响应GET /status请求。
// 修改 server.hpp(新增) #include <atomic> #include <mutex> #include <map> class server { public: static std::atomic_int connection_count_; static std::mutex status_mutex_; static std::map<std::string, std::string> status_info_; // ...原有代码 }; std::atomic_int server::connection_count_{0}; std::mutex server::status_mutex_; std::map<std::string, std::string> server::status_info_{ {"version", "Asio Echo v1.0"}, {"uptime_sec", "0"} }; // 修改 session 构造与析构 class session : public std::enable_shared_from_this<session> { public: explicit session(tcp::socket socket) : socket_(std::move(socket)) { ++server::connection_count_; // 连接建立 } ~session() { --server::connection_count_; // 连接关闭 } // ...其余不变 };新增status_handler(嵌入server::do_accept的 accept 回调中):
// 在 server::do_accept 的 lambda 内添加: if (request_line.find("GET /status ") == 0) { std::string response = "HTTP/1.1 200 OK\r\n" "Content-Type: application/json\r\n\r\n" "{" "\"connections\":" + std::to_string(server::connection_count_.load()) + "," "\"version\":\"" + server::status_info_["version"] + "\"" "}"; boost::asio::async_write( socket_, boost::asio::buffer(response), [](boost::system::error_code ec, std::size_t) { if (ec) std::cerr << "Status write error: " << ec.message() << "\n"; }); return; // 不再进入 do_read }注意:此处为演示简化,真实项目应解析 HTTP 请求头,区分方法与路径。
6.2 优雅关闭:捕获 SIGINT,等待所有连接自然结束
目标:按Ctrl+C时,不再接受新连接,但允许已有连接处理完再退出。
改造main()函数:
// main() 中添加信号处理 boost::asio::signal_set signals(io_context, SIGINT, SIGTERM); signals.async_wait([&](const boost::system::error_code&, int) { std::cout << "\nStopping server gracefully...\n"; // 1. 关闭 acceptor,拒绝新连接 acceptor_.close(); // 2. 等待所有 session 结束(connection_count_ 归零) auto check_done = [&]() { if (server::connection_count_.load() == 0) { io_context.stop(); // 退出 run() return; } // 100ms 后再检查 boost::asio::steady_timer timer(io_context, std::chrono::milliseconds(100)); timer.async_wait([check_done](const boost::system::error_code&) { check_done(); }); }; check_done(); });关键点:
acceptor_.close()是关键一步,它让async_accept的后续回调立即失败,不再创建新session;connection_count_是唯一可信指标,不能依赖io_context.stopped(),因为stopped()只表示run()退出,不保证业务逻辑结束;- 使用
steady_timer轮询检查,避免while(connection_count_ > 0)阻塞主线程。
6.3 验证与压测:用ab工具验证优雅关闭行为
启动服务后,用 Apache Bench 发起 100 个并发连接,每连接发送 10 次请求:
ab -n 1000 -c 100 http://127.0.0.1:8080/此时按Ctrl+C,观察输出:
Stopping server gracefully... # 等待约 1~2 秒后 # 程序退出,且 ab 报告无连接超时(Connection refused)若未加优雅关闭,ab会大量报apr_socket_recv: Connection reset by peer;加上后,所有请求均成功返回。
从那以后我每次给 Asio 服务加 shutdown 逻辑,都强制走一遍acceptor.close()→wait count==0→io_context.stop()三步,哪怕只是 demo。因为线上环境不会给你第二次 Ctrl+C 的机会——连接数统计是监控基线,优雅关闭是 SLA 底线。希望帮到你。
本文还有配套的精品资源,点击获取