如何使用Boost ASIO实现TCP IPC协议并解决连接被重置问题
核心错误点
空指针访问直接触发服务端崩溃
ServerConnection类中定义的properties_是propertiesPacket*类型,初始化值为nullptr,调用async_read_some时直接把空指针作为缓冲区传入,收到客户端数据后会直接写入非法内存,触发服务端进程崩溃,自然会向客户端发送RST包,你看到的connection was reset by the peer就是这么来的。同理filePacket_初始化也为nullptr,如果processData中没有正确分配内存,写入时也会触发同样的问题。async_read_some使用不符合需求
TCP是流式协议,async_read_some只保证至少读1字节就返回,不能保证一次读完你需要的完整2字节propertiesPacket结构,大概率会出现收到半包就进入处理逻辑的情况。要固定读取指定长度的数据,应该用boost::asio::async_read接口。连续调用异步写入属于未定义行为
Boost.ASIO要求同一个socket上不能同时存在多个未完成的异步写操作,你在doRead回调里连续调用两次doWrite,前一个异步写还没完成就发起第二个,会导致数据错乱甚至程序崩溃。正确的做法是第一个写操作完成后,在它的回调里再发起第二个写操作。写入完成后的逻辑错误
你的会话流程写完两段数据后就应该断开连接,但是你在doWrite的回调里又调用了doRead,这时候客户端已经完成数据读取会主动关闭连接,服务端此时发起读操作会报错,而且也不符合你的会话设计。结构体写入长度错误
你发送processedData时用了sizeof(*filePacket_)作为长度,这是编译期计算的固定值,但processedData是可变大小结构体,实际长度应该用你提前计算好的filePacketSize_,会导致少发或者多发数据。缺少字节序兼容处理(可选但建议修复)
2字节的长度字段在网络传输时应该统一转成大端(网络字节序),客户端收到后再转成主机字节序,避免不同CPU架构的设备通信时出现长度解析错误。
修复后的服务端核心代码示例
// 修改后的ServerConnection类 class ServerConnection : public std::enable_shared_from_this<ServerConnection> { public: using TCPSocket = boost::asio::ip::tcp::socket; ServerConnection(TCPSocket socket) : socket_(std::move(socket)) { // 给properties分配实际内存,不再用空指针 properties_ = std::make_unique<propertiesPacket>(); } void start() { doReadProperties(); } private: void doReadProperties() { auto self(shared_from_this()); // 用async_read保证读满整个propertiesPacket的大小 boost::asio::async_read(socket_, boost::asio::buffer(properties_.get(), sizeof(propertiesPacket)), [this, self](boost::system::error_code ec, std::size_t /*length*/) { if (!ec) { processData(); // 先写长度字段 doWriteSize(); } }); } void doWriteSize() { auto self(shared_from_this()); // 转网络字节序 unsigned short netSize = htons(filePacketSize_); boost::asio::async_write(socket_, boost::asio::buffer(&netSize, sizeof(unsigned short)), [this, self](boost::system::error_code ec, std::size_t /*length*/) { if (!ec) { // 长度写完再写数据体 doWriteData(); } }); } void doWriteData() { auto self(shared_from_this()); boost::asio::async_write(socket_, boost::asio::buffer(filePacket_.get(), filePacketSize_), [this, self](boost::system::error_code ec, std::size_t /*length*/) { if (!ec) { // 所有数据写完,主动关闭连接即可,不需要再读 boost::system::error_code ignore_ec; socket_.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ignore_ec); socket_.close(ignore_ec); } }); } void processData() { // 这里处理properties_的数据,给filePacket_分配内存,计算filePacketSize_ // 示例:filePacket_ = std::unique_ptr<processedData>(reinterpret_cast<processedData*>(malloc(filePacketSize_))); } TCPSocket socket_; // 改用智能指针管理内存,避免野指针和内存泄漏 std::unique_ptr<propertiesPacket> properties_; std::unique_ptr<processedData> filePacket_; unsigned short filePacketSize_ = 0; };
客户端补充建议
客户端读取长度的时候也需要转成主机字节序:
unsigned short responseSize {}; boost::asio::read(socket, boost::asio::buffer(&responseSize, sizeof(unsigned short))); // 加一行转主机字节序 responseSize = ntohs(responseSize);
内容的提问来源于stack exchange,提问作者Spiwocoal

