Asio08-PacketStickingProblem
Asio8-PacketStickingProblem
粘包问题
分析
粘包问题是指在使用TCP协议进行网络通信时,发送方发送的多个数据包在接收方接收时被"粘"在一起,形成一个大的数据包的现象。这不是TCP协议的bug,而是TCP作为流式协议的固有特性。
-
发送方: [包1][包2][包3]
接收方: [包1包2包3] // 多个包被合并接收 -
发送方: [大包]
接收方: [部分数据][剩余数据] // 一个包被分多次接收
TCP是面向字节流的,只保证字节发送正常可保不丢失,至于组织形式,是我们应用层的问题。也就是说,粘包是逻辑层面的问题而非物理层面。
解决办法就是定义收发规则,服务器和客户端按照这个规则进行发送解析,就可以避免粘包问题。
常用的规则:
tlv:- id-长度-消息体
我们这里简化为了:长度(node)-消息体(node)。
简化而言:
- 先看长度够不够填充长度node
- 不够:先填充一部分,
async_read_some继续读取 - 够:解析头部,得到消息体的长度,看剩下的字节长度是否够填充消息体
- 不够:填充一部分,然后继续
async_read_some读取 - 够:将数据直接全部塞入消息体内,一个node完毕。之后还要看剩下的字节:
- 重复上面的步骤,进行解析。
- 不够:填充一部分,然后继续
- 不够:先填充一部分,
因为我们分析解析头部和解析消息体两部分,因此我们定义pending,标志是否在处理头部。同时因为消息可能要多次循环处理,我们进入函数的时候加一个变量copy_len,用来确定我们当前字节在读取字节的位置。
流程图

代码
// CSession.h
#pragma once
#include <boost/asio.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <iostream>
#include <memory>
#include <mutex>
#include <queue>
using namespace std;
#define MAX_LENGTH 1024 * 2
using boost::asio::ip::tcp;
class CServer;
const int HEAD_LENGTH = 2;
static int as = 0;
class MsgNode {
friend class CSession;
public:
// 这里面多加了HEAD_LENGTH字节,用于存放长度
// 这里是发送的时候需要的结构
MsgNode(char* msg, unsigned short max_len)
: _cur_len(0)
, _total_len(max_len + HEAD_LENGTH)
, _data(new char[_total_len + 1])
{
memcpy(_data, &max_len, HEAD_LENGTH);
memcpy(_data + HEAD_LENGTH, msg, max_len);
_data[_total_len] = '\0';
}
// 我们进行接受的时候,使用这个结构
MsgNode(unsigned short max_len)
: _total_len(max_len)
, _cur_len(0)
, _data(new char[_total_len + 1])
{
}
void Clear()
{
::memset(_data, 0, _total_len);
_cur_len = 0;
}
~MsgNode()
{
delete[] _data;
}
private:
int _cur_len;
int _total_len;
char* _data;
};
class CSession : public std::enable_shared_from_this<CSession> {
public:
CSession(boost::asio::io_context& io_context, CServer* server);
~CSession()
{
std::cout << "Ssession destruct" << endl;
}
tcp::socket& GetSocket();
std::string& GetUuid();
void Start();
void Send(char* msg, int max_length);
void PrintRecvData(char* data, int length);
private:
void HandleRead(const boost::system::error_code& error, size_t bytes_transferred, shared_ptr<CSession> _self_shared);
void HandleWrite(const boost::system::error_code& error, shared_ptr<CSession> _self_shared);
tcp::socket _socket;
std::string _uuid;
CServer* _server;
std::queue<shared_ptr<MsgNode>> _send_que;
std::mutex _send_lock;
char _data[MAX_LENGTH];
bool _head_parsed; // 是否在处理头部
std::shared_ptr<MsgNode> _recv_msg_node; // 消息体
std::shared_ptr<MsgNode> _recv_head_node;// 头部
};
// CSession.cpp
#include "CSession.h"
#include "CServer.h"
#include <chrono>
#include <iomanip>
#include <iostream>
#include <thread>
CSession::CSession(boost::asio::io_context& io_context, CServer* server)
: _socket(io_context)
, _server(server)
, _head_parsed(false)
{
boost::uuids::uuid a_uuid = boost::uuids::random_generator()();
_uuid = boost::uuids::to_string(a_uuid);
_recv_head_node = make_shared<MsgNode>(HEAD_LENGTH);
}
tcp::socket& CSession::GetSocket()
{
return _socket;
}
std::string& CSession::GetUuid()
{
return _uuid;
}
void CSession::Start()
{
memset(_data, 0, MAX_LENGTH);
_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_from_this()));
}
void CSession::Send(char* msg, int max_length)
{
bool pending = false;
std::lock_guard<std::mutex> lock(_send_lock);
if (_send_que.size() > 0) {
pending = true;
}
_send_que.push(make_shared<MsgNode>(msg, max_length));
if (pending) {
return;
}
boost::asio::async_write(_socket, boost::asio::buffer(msg, max_length),
std::bind(&CSession::HandleWrite, this, std::placeholders::_1, shared_from_this()));
}
void CSession::HandleWrite(const boost::system::error_code& error, shared_ptr<CSession> _self_shared)
{
if (!error) {
std::lock_guard<std::mutex> lock(_send_lock);
_send_que.pop();
if (!_send_que.empty()) {
auto& msgnode = _send_que.front();
boost::asio::async_write(_socket, boost::asio::buffer(msgnode->_data, msgnode->_total_len),
std::bind(&CSession::HandleWrite, this, std::placeholders::_1, _self_shared));
}
} else {
std::cout << "handle write failed, error is " << error.what() << endl;
_server->ClearSession(_uuid);
}
}
void CSession::HandleRead(const boost::system::error_code& error, size_t bytes_transferred, shared_ptr<CSession> _self_shared)
{
if (!error) {
PrintRecvData(_data, bytes_transferred);
std::chrono::milliseconds dura(2000);
std::this_thread::sleep_for(dura);
// 已经移动的字节数:在消息体中
int copy_len = 0;
while (bytes_transferred > 0) {
// 如果还未解析头部
if (!_head_parsed) {
// 确保头部信息体+待处理的也不会溢出:收到的比头部小
if (bytes_transferred + _recv_head_node->_cur_len < HEAD_LENGTH) {
memcpy(_recv_head_node->_data + _recv_head_node->_cur_len, _data + copy_len, bytes_transferred);
_recv_head_node->_cur_len += bytes_transferred;
::memset(_data, 0, MAX_LENGTH);
_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, _self_shared));
return;
}
// 收到的比头部大,解析头部
int head_remain = HEAD_LENGTH - _recv_head_node->_cur_len;
memcpy(_recv_head_node->_data + _recv_head_node->_cur_len, _data + copy_len, head_remain);
copy_len += head_remain;
bytes_transferred -= head_remain;
short data_len = 0;
memcpy(&data_len, _recv_head_node->_data, HEAD_LENGTH);
// 网络字节序转成本地字节序
data_len = boost::asio::detail::socket_ops::network_to_host_short(data_len);
// data_len = ntohs(data_len);
if (data_len > MAX_LENGTH) {
std::cout << "data_len" << data_len << std::endl;
std::cout << "Data length is too long" << std::endl;
_server->ClearSession(_uuid);
return;
}
_recv_msg_node = make_shared<MsgNode>(data_len);
// 这时候发现消息长度小于规定长度,数据未收全,先存放到接受节点中
if (bytes_transferred < data_len) {
memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred);
_recv_msg_node->_cur_len += bytes_transferred;
::memset(_data, 0, MAX_LENGTH);
// 头部处理完成
_head_parsed = true;
_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, _self_shared));
return;
}
// 这时候消息长度大于规定长度,数据收全,直接处理
memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, data_len);
_recv_msg_node->_cur_len += data_len;
copy_len += data_len;
bytes_transferred -= data_len;
_recv_msg_node->_data[_recv_msg_node->_total_len] = '\0';
std::cout << "Received message: " << _recv_msg_node->_data << std::endl;
Send(_recv_msg_node->_data, _recv_msg_node->_total_len);
_head_parsed = false;
_recv_head_node->Clear();
if (bytes_transferred <= 0) {
::memset(_data, 0, MAX_LENGTH);
_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, _self_shared));
return;
}
continue;
}
std::cout << "--------------------" << std::endl;
// 已经处理完头部,继续上次未处理完的消息
int remain_msg = _recv_msg_node->_total_len - _recv_msg_node->_cur_len;
if (bytes_transferred < remain_msg) {
memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred);
_recv_msg_node->_cur_len += bytes_transferred;
::memset(_data, 0, MAX_LENGTH);
_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, _self_shared));
return;
}
memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, remain_msg);
_recv_msg_node->_cur_len += remain_msg;
bytes_transferred -= remain_msg;
copy_len += remain_msg;
_recv_msg_node->_data[_recv_msg_node->_total_len - 1] = '\0';
std::cout << "Received message2: " << _recv_msg_node->_data << std::endl;
Send(_recv_msg_node->_data, _recv_msg_node->_total_len);
_head_parsed = false;
_recv_head_node->Clear();
if (bytes_transferred <= 0) {
::memset(_data, 0, MAX_LENGTH);
_socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, _self_shared));
return;
}
continue;
}
} else {
std::cout << "handle read failed, error is " << error.what() << endl;
_server->ClearSession(_uuid);
}
}
void CSession::PrintRecvData(char* data, int length)
{
stringstream ss;
string result = "0x";
for (int i = 0; i < length; i++) {
string hexstr;
ss << hex << std::setw(2) << std::setfill('0') << int(data[i]) << endl;
ss >> hexstr;
result += hexstr;
}
std::cout << "receive raw data is : " << result << endl;
}
// 客户端:Client.cpp
#include <iostream>
#include <boost/asio.hpp>
#include <thread>
using namespace std;
using namespace boost::asio::ip;
const int MAX_LENGTH = 1024 * 2;
const int HEAD_LENGTH = 2;
int main()
{
try {
//创建上下文服务
boost::asio::io_context ioc;
//构造endpoint
tcp::endpoint remote_ep(boost::asio::ip::make_address("127.0.0.1"), 9999);
tcp::socket sock(ioc);
boost::system::error_code error = boost::asio::error::host_not_found; ;
sock.connect(remote_ep, error);
if (error) {
cout << "connect failed, code is " << error.value() << " error msg is " << error.message();
return 0;
}
thread send_thread([&sock] {
for (;;) {
this_thread::sleep_for(std::chrono::milliseconds(20));
const char* request = "hello world!";
short request_length = strlen(request);
char send_data[MAX_LENGTH] = { 0 };
//转为网络字节序
short request_host_length = boost::asio::detail::socket_ops::host_to_network_short(request_length);
memcpy(send_data, &request_host_length, 2);
memcpy(send_data + 2, request, request_length);
boost::asio::write(sock, boost::asio::buffer(send_data, request_length + 2));
}
});
thread recv_thread([&sock] {
for (;;) {
this_thread::sleep_for(std::chrono::milliseconds(2));
cout << "begin to receive..." << endl;
char reply_head[HEAD_LENGTH];
size_t reply_length = boost::asio::read(sock, boost::asio::buffer(reply_head, HEAD_LENGTH));
short msglen = 0;
memcpy(&msglen, reply_head, HEAD_LENGTH);
//转为本地字节序
msglen = boost::asio::detail::socket_ops::network_to_host_short(msglen);
char msg[MAX_LENGTH] = { 0 };
size_t msg_length = boost::asio::read(sock, boost::asio::buffer(msg, msglen));
std::cout << "Reply is: ";
std::cout.write(msg, msglen) << endl;
std::cout << msg << std::endl;
std::cout << "Reply len is " << msglen;
std::cout << "\n";
}
});
send_thread.join();
recv_thread.join();
}
catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << endl;
}
return 0;
}
在客户端代码中,我们使用两个线程分别一直发送信息和接受信息,而在服务器中,我们接收到信息之后。调用PrintRecvData函数打印原始字节流,我们可以清楚的看到我们成功的解决了粘包问题。

浙公网安备 33010602011771号