首先需要明白,“TCP 粘包”这个称呼本身是有问题的,因为 TCP 是面向字节流的协议 ,不存在数据边界,所以 TCP 本身就不会有什么“粘包”的问题。要说“粘包”,这个词更适合形容 UDP 这类 面向数据报 的协议。所以说,“粘包”并不是 TCP 的范畴,而是程序员基于错误的理解来实现上层逻辑,从而导致的问题,和 TCP 本身无关。再具体而言,“TCP粘包问题”应该阐述为——在TCP传输协议下,应用层数据拼装发送和接收解析的问题 。不过为了方便描述,下文就采用“TCP粘包”一词。

粘包如何产生?

“粘包”的首要原因就是 基于字节流 这个特点。字节流可以理解为一个双向的通道里流淌的数据,这个数据其实就是我们常说的二进制数据,简单来说就是一大堆 01 串,而这些 01 串之间 没有任何边界 。应用层传到 TCP 协议的数据以字节流的方式发送到下游,这些数据可能被切割(过大)和组装(过小)成段,接收端收到这些段后没有正确还原原来的消息,因此出现粘包现象。粘包可以发生在发送端和接收端。

发送端:

在 Nagle 算法(参考此处)开启的状态下,数据包在以下两个情况会被发送:

  • 如果包长度达到 MSS (或含有 Fin 包),立刻发送,否则等待下一个包到来;如果下一包到来后两个包的总长度超过MSS的话,就会进行拆分发送;
  • 等待超时(一般为200ms),第一个包没到 MSS 长度,但是又迟迟等不到第二个包的到来,则立即发送。
negle引起发送端粘包

如果处理开发者把第一个收到的 msg1 + msg2(1) 就当做是一个完整消息进行处理,就会看上去就像是两个包粘在一起,就会导致粘包问题。

接收端:

如果接收端一直较忙,没来得及取出缓冲区的数据,导致缓冲区挤压,最后一次性取出大量数据,也可能产生粘包。

关闭Negle也可能粘包

如何解决粘包?

粘包出现的根本原因是不确定 消息的边界 。接收端在面对"无边无际"的二进制流的时候,根本不知道收了多少 01 才算一个消息 。一不小心拿多了就说是粘包。所以说粘包根本不是 TCP 的问题,是使用者对于 TCP 的理解有误导致的一个问题。

应用层的数据包称为“消息”,TCP 的数据包称为“段”,UDP 的数据包称为“数据报”,链路层则称为“帧”。所以,“粘包”也许应该叫“粘消息”,哈哈哈。

只要在发送端每次发送消息的时候给消息带上识别消息边界的信息 ,接收端就可以根据这些信息识别出消息的边界,从而区分出每个消息。一般有两种分包方法:

  1. 以指定字符(串)为包的结束标志 :这种协议包比较常见,即字节流中遇到特殊的符号值时就认为到一个包的末尾了。例如, FTP协议, SMTP 协议,一个命令或者一段数据后面加上"\r\n"(即所谓的 CRLF )表示一个包的结束。对端收到后,每遇到一个”\r\n“就把之前的数据当做一个数据包。这种协议一般用于一些包含各种命令控制的应用中。如果协议数据包内容部分需要使用包结束标志字符,就需要对这些字符做转码或者转义操作,以免被接收方错误地当成包结束标志而误解析。

    消息边界头尾标志
  2. 包头 + 包体格式 :这种格式的包一般分为两部分,即包头和包体,包头是固定大小的 ,且包头中必须含有一个字段来说明接下来的包体有多大。当收到数据时,首先提取包头大小的数据量,解析包头得到包体长度,根据此长度继续提取后面的包体即可。可见,此方法要求接收方从一开始就必须采用约定方式提取数据,而不能中途使用。

下面给出 包体+包头 方式的代码示范:

//强制1字节对齐
#pragma pack(push, 1)
//协议头
struct msg_header
{   
    int32_t  bodysize;         //包体大小  
};
#pragma pack(pop)

//包最大字节数限制为10M
#define MAX_PACKAGE_SIZE    10 * 1024 * 1024

void ChatSession::OnRead(const std::shared_ptr<TcpConnection>& conn, Buffer* pBuffer, Timestamp receivTime)
{
    while (true) //while连续读取数据
    {
        //不够一个包头大小
        if (pBuffer->readableBytes() < (size_t)sizeof(msg_header))
        {
            //LOGI << "buffer is not enough for a package header, pBuffer->readableBytes()=" << pBuffer->readableBytes() << ", sizeof(msg_header)=" << sizeof(msg_header);
            return;
        }

        //取包头信息
        msg_header header;
        memcpy(&header, pBuffer->peek(), sizeof(msg_header));//注意只能peek,不能提取,因为倘若接下来根据包头中的字段得到包体大小时,如果剩余数据不够一个包体大小,你又得把这个包头数据放回缓冲区。
        //包头有错误,立即关闭连接
        if (header.bodysize <= 0 || header.bodysize > MAX_PACKAGE_SIZE)
        {
            //务必判断bodysize的合法性,有可能是非法客户端发来攻击,也可能数据错误。
            LOGE("Illegal package, bodysize: %lld, close TcpConnection, client: %s", header.bodysize, conn->peerAddress().toIpPort().c_str());
            conn->forceClose();
            return;
        }

        //收到的数据不够一个完整的包
        if (pBuffer->readableBytes() < (size_t)header.bodysize + sizeof(msg_header))
            return;

        pBuffer->retrieve(sizeof(msg_header));
        //inbuf用来存放当前要处理的包
        std::string inbuf;
        inbuf.append(pBuffer->peek(), header.bodysize);
        pBuffer->retrieve(header.bodysize);
        //解包和业务处理
        if (!Process(conn, inbuf.c_str(), inbuf.length()))
        {
            //客户端发非法数据包,服务器主动关闭之
            LOGE("Process package error, close TcpConnection, client: %s", conn->peerAddress().toIpPort().c_str());
            conn->forceClose();
            return;
        }
    }// end while-loop
}

UDP会粘包吗?

不会,UDP 面向报文,其报头中有本报文的长度信息,可以区分数据包,所以不会粘包。

参考文章:详解粘包知乎

文章作者: 极简
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 后端技术分享
网络理论
喜欢就支持一下吧