如何用boost::asio::read_some依次接收客户端发送的多段序列化大数据?
用boost::asio::read_some依次接收多段二进制数据的解决方案
首先得明确一个核心点:TCP是无边界的字节流协议,你直接发送两个大对象的序列化数据,服务器根本没法区分哪里是第一个对象的结尾、第二个对象的开头。所以第一步必须让客户端配合,给每个对象的二进制数据加上长度前缀——先发送对象的序列化长度,再发送实际数据。服务器端通过先读取长度,再按长度读取完整的对象数据,就能准确区分两个对象了。
下面是具体的实现步骤和代码示例:
一、先搞定客户端的发送逻辑(必须配合)
客户端在发送每个对象前,要先把对象的序列化长度转换成网络字节序(解决不同机器的大小端问题),然后用boost::asio::write依次发送长度前缀和对象数据:
#include <boost/asio.hpp> #include <vector> #include <cstdint> using boost::asio::ip::tcp; // 发送单个序列化对象的辅助函数 void send_serialized_object(tcp::socket& socket, const std::vector<char>& serialized_data) { // 把长度转成网络字节序(这里用4字节的uint32_t,适合最大4GB的对象) uint32_t data_len = htonl(static_cast<uint32_t>(serialized_data.size())); // 先发送长度前缀 boost::asio::write(socket, boost::asio::buffer(&data_len, sizeof(data_len))); // 再发送实际的序列化数据 boost::asio::write(socket, boost::asio::buffer(serialized_data)); } // 调用示例:发送两个对象 // int main() { // boost::asio::io_context io_context; // tcp::socket socket(io_context); // socket.connect(tcp::endpoint(boost::asio::ip::address::from_string("127.0.0.1"), 12345)); // // std::vector<char> obj1_data = ...; // 第一个对象的序列化结果 // std::vector<char> obj2_data = ...; // 第二个对象的序列化结果 // // send_serialized_object(socket, obj1_data); // send_serialized_object(socket, obj2_data); // // socket.close(); // return 0; // }
二、服务器端用read_some实现可靠接收
read_some的特点是只读取当前可用的数据,不会保证一次读完你需要的所有字节,所以我们需要封装一个read_exact辅助函数,循环调用read_some直到读取到指定长度的数据。然后按照「读长度→读对应数据」的流程依次接收两个对象:
1. 封装read_exact辅助函数
#include <boost/asio.hpp> #include <vector> #include <cstdint> #include <stdexcept> using boost::asio::ip::tcp; // 从socket读取指定长度的字节,直到读满为止 void read_exact(tcp::socket& socket, std::vector<char>& buffer, std::size_t required_bytes) { std::size_t total_read = 0; buffer.clear(); buffer.resize(required_bytes); boost::system::error_code ec; while (total_read < required_bytes && !ec) { // 每次读取剩余需要的字节数 std::size_t bytes_read = socket.read_some( boost::asio::buffer(buffer.data() + total_read, required_bytes - total_read), ec ); if (!ec) { total_read += bytes_read; } } if (ec) { throw std::runtime_error("Read failed: " + ec.message()); } }
2. 服务器接收两个对象的主逻辑
int main() { try { boost::asio::io_context io_context; // 监听12345端口 tcp::acceptor acceptor(io_context, tcp::endpoint(tcp::v4(), 12345)); std::cout << "Waiting for client connection..." << std::endl; tcp::socket socket(io_context); acceptor.accept(socket); // 等待客户端连接 std::cout << "Client connected." << std::endl; // ===== 接收第一个对象 ===== std::vector<char> len_buffer; // 先读4字节的长度前缀 read_exact(socket, len_buffer, sizeof(uint32_t)); // 转成主机字节序 uint32_t obj1_len = ntohl(*reinterpret_cast<const uint32_t*>(len_buffer.data())); // 读取第一个对象的完整数据 std::vector<char> obj1_data; read_exact(socket, obj1_data, obj1_len); std::cout << "Received first object, size: " << obj1_len << " bytes" << std::endl; // 这里可以把obj1_data反序列化成你的对象 // ===== 接收第二个对象 ===== read_exact(socket, len_buffer, sizeof(uint32_t)); uint32_t obj2_len = ntohl(*reinterpret_cast<const uint32_t*>(len_buffer.data())); std::vector<char> obj2_data; read_exact(socket, obj2_data, obj2_len); std::cout << "Received second object, size: " << obj2_len << " bytes" << std::endl; // 这里可以把obj2_data反序列化成你的对象 socket.close(); std::cout << "Connection closed." << std::endl; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; } return 0; }
三、关键注意事项
- 字节序处理:必须用
htonl/ntohl(或者htonll/ntohll处理超大对象)转换长度,否则不同大小端的机器之间通信会出错。如果需要更通用的处理,可以用Boost.Endian库。 - 超大对象支持:如果你的对象序列化后超过4GB,把长度类型换成
uint64_t即可,对应的字节序函数换成htonll/ntohll(注意有些平台可能需要手动实现,或者用Boost的字节序工具)。 - 异常处理:示例中用了try-catch捕获异常,实际项目中可以根据需求选择用error_code版本的接口替代异常抛出。
- 异步场景适配:如果你的服务器是异步架构,逻辑类似——在异步回调中循环读取直到满足长度要求,不过需要用
async_read_some并维护接收状态(已读字节数、目标长度等)。
内容的提问来源于stack exchange,提问作者user9414424
相关产品推荐
相关产品推荐

