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,用来确定我们当前字节在读取字节的位置。

流程图

https://cdn.llfc.club/1683373951566.jpg

代码

// 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函数打印原始字节流,我们可以清楚的看到我们成功的解决了粘包问题。

posted @ 2025-12-24 23:13  大胖熊哈  阅读(17)  评论(0)    收藏  举报