如何理解TCP协议是无边界的,以及粘包?

更新记录

时间 版本修改
2020年4月2日 初稿
  • 我们从经典的计算机科学丛书上阅到的知识,都说:TCP协议是没有消息边界的。但是这个要怎么理解呢?在我没有接触底层的套接字相关逻辑时。我对此也没有特别的了解。直到阅读了套接字的相关逻辑源码,才对此有了一定的了解
  • TCP的发包和我们业务层所发出的协议数据是不一定吻合的。也就是说,我们发的数据库可能会被分拆成不同的包。然后再和别的协议(这里当然是只发往同一个端口)的数据封装同一个TCP包体。
  • 因此。对于我们业务网络层而言,我们需要在一个TCP包体里面区分出不同的实际业务包。
  • 目前业界常用的做法有三个,可参考TCP消息边界处理
  • 但是最常使用的是第二种做法。在我们发送的协议数据里面。协议头带上协议包体的长度,特定的协议号。以及特殊的用于标。协议头的数据。这些协议数据都会统一。作为TCP协议的包体数据。在网络上进行传输
  • 第一种方案和第三种方案的缺点都比较大。
  • 第二种方案,有一些好处。
    • 我们的应用程序中,app和server的数据交互是非常多,往往需要不同的协议号(也叫做命令字)去区分不同的业务场景,比如某几条协议负责登录,某条协议负责用户的个人资料等等。通过在业务协议头上,填充协议号,那么在客户端收到TCP回包,解析了协议头时,就可以往不同的业务上层抛出通知,处理起来就非常方便,水到渠成。
    • 其次,协议头里面,还可以塞入其他有需要塞入的数据。比如,客户端版本,登录的用户ID,客户端使用的语言类型等,总之,使得我们自定义的协议头的长度是固定的即可。
  • 一个典型的协议头设计如下:
字段 意义
包头标识_uint8[2] 为固定字符“XX”(可用于识别是否是本app的包头)
协议版本号_uint8 当前版本号
ClientType_uint8 客户端类型(PC,安卓等)
ClientVersion_uint16 Client版本
VersionType_uint8 Client版本类型(区分简繁体等)
UserID_uint32 登录用户ID(用户ID)
包类型标志_uint8 (应答or推送)
SerialNo_uint32 命令序列号,每发送一个命令后加1
CMD_uint16 协议号(区分上层业务)
BodyLength_uint32 协议包体长度(本文重点)
Reserved 任意Byte保留字节(保留当然不能太长罗)

粘包

  • 介绍上了上述方案的选择,就要面对这个方案面临的一个问题
  • 我们之前说的,我们发送的数据有三个包: [1,2,3] [4,5,6] [7,8,9,10],但是底层的TCP协议发出去的时候不一定是 [1,2,3] [4,5,6] [7,8,9,10]。有可能是 [1,2,3,4] [,5,6,7] [8,9,10]等,随机的一种组合。
  • 因此,我们的app应用层就需要去识别这些数据,正确地解成我们自定义的协议数据。
  • 下面结合实际代码,来演示从套接字中接收数据的整个过程。
void CMyWinTCPSocket::OnReceive(int nErrorCode)
{
    static unsigned int nHeaderLen = sizeof(PROTOCOL_HEADER); //自定义协议,固定长度的头部
    m_nLastErrorCode = nErrorCode;  //记录错误码
    if (nErrorCode != 0)
    {
        //错误码不为0,此处需要打印日志记录
    }

    //记录本次套接字被激活的时间
    m_uSocketActiveTime = ::GetTickCount();

    DWORD nBytes = 0;
    if (!IOCtl(FIONREAD, &nBytes) || nBytes == 0) //FIONREAD返回套接字上排队的第一个数据报大小
    {
	m_nLastErrorCode = WSAGetLastError();
        //打印该错误码,由于读取套接字上的数据失败,直接返回。
	return;
    }
    //此时套接字中可获取的数据有nBytes个字节

    //开始读取数据
    char *pReceiveBuffer = new char[nBytes]; //有多少读多少,一次性读完
    int nRead = CAsyncSocketEx::Receive(pReceiveBuffer, nBytes);    //nRead是实际读取到的数据
    if (nRead <= 0) //出现异常,需要退出
    {
        //释放new出来的char数组
        delete_array(pReceiveBuffer);
        return;
    }

    int nCurrentOffset = 0;     //记录读取本次的套接字数据的offset(偏移量)
    int nLeftSize = nRead;      //本次套接字返回的数据,剩下的未读取的字节数
    char *pOffsetBuffer = pReceiveBuffer;
    std::vector<tagRecvPack> vecRecvPacks;  //tagRecvPack表示一个收到的应用层的包,从成员变量offset来判断当前获取的字节数

    do
    {
        //注意:如果上次调用onReceive时,仍然存有数据(即不完整的包),此时就不会重新解析头部。(也就是,所谓的粘包操作)
	if (m_RecvPack.pHeadBuffer == NULL) //从头开始读取头部(一个新的包)
	{
	    m_RecvPack.pHeadBuffer = new char[nHeaderLen];  //记录自定义协议头的数据
	    ZeroMemory(m_RecvPack.pHeadBuffer, nHeaderLen);
	    m_RecvPack.uHeadOffset = 0;
	    m_RecvPack.uHeadTotal = nHeaderLen;     //记录包头的长度

	    //保护逻辑
	    MF_Delete1D(m_RecvPack.pBodyBuffer);    //记录包体的实际数据
	    m_RecvPack.pBodyBuffer = NULL;
	    m_RecvPack.uBodyOffset = 0;
	}

        //判断上次调用onReceive的数据是否已经读取完头部,
	if (m_RecvPack.uHeadOffset < nHeaderLen)
	{
            //读取头部,有两种情况:
            //a. 上次的onReceive没有读取完的(也就是要把上次onReceive收到的数据,和这次收到的数据粘起来,搞成一个新的包传给上层)
            //b. 本次读取套接字buffer时,新的一个包,重新解析头部的情况。

            //---------- 1. 读数据,把这个包的头部读取完---------------------//
	    char *p = m_RecvPack.pHeadBuffer + m_RecvPack.uHeadOffset;
	    int len = std::min<int>((nHeaderLen - m_RecvPack.uHeadOffset), nLeftSize);
	    memcpy(p, pOffsetBuffer, len);
	    nCurrentOffset += len;
	    pOffsetBuffer = pReceiveBuffer + nCurrentOffset;
	    nLeftSize -= len;
	    m_RecvPack.uHeadOffset += len;
            //---------- 1. 读数据,把这个包的头部读取完---------------------//
            if (m_RecvPack.uHeadOffset == nHeaderLen)
	    {
                //-----------2. 头部读完,开始做准备或者容错之类的工作-----------//
                assert(m_RecvPack.pBodyBuffer != NULL);
                PROTOCOL_HEADER *pHeader = (PROTOCOL_HEADER *)m_RecvPack.pHeadBuffer;
	        int nBodyLength = ntohl(pHeader->dwBodyLength);     //这就是传说中的,协议头上带上包体数据的长度

                //版本号,协议号等其他字段(可根据业务自行扩展,但后续不允许改动,否则老版本不兼容)
	        BYTE chVersion = pHeader->chVersion;
	        WORD wClientVersion = ntohs(pHeader->wClientVersion);
		WORD wCmdID = ntohs(pHeader->wCmdID);
		DWORD dwSerialNO = ntohl(pHeader->dwSerialNO);

		//简单的包校验
		if (chVersion != TCP_PROTOCOL_VERSION ||
			wClientVersion != m_wClientVersion ||
			pHeader->chMagicCode[0] != TCP_PROTOCOL_MAGIC_CODE1 ||
			pHeader->chMagicCode[1] != TCP_PROTOCOL_MAGIC_CODE2)
		{
		    //处理包校验错误的情况
		    Close(false); //出错,直接关闭socket
		    break;
		}

		m_RecvPack.uBodyTotal = nBodyLength;

		if (m_RecvPack.uBodyTotal == 0) //一个空包,以前的逻辑是直接抛弃的,现在空包也要
		{
                    //处理空包的情况
		}
		//包体过大,应该是数据错乱了,剩下的包已经不知道怎么解析了,只能断开
		else if (m_RecvPack.uBodyTotal > TCP_PROTOCOL_PACKET_MAX_LENGTH)
		{
		    //处理出错的情况
		    m_RecvPack.reset();
		    Close(false); //直接关闭socket算了,要不后面包也是乱的了
		    break;
		}
                //-----------2. 头部读完,开始做准备或者容错之类的工作-----------//

                //-----------3. 创建好干净的包体,用以存储包体数据-----------//
		if (m_RecvPack.uBodyTotal > 0)
		{
		    MF_Delete1D(m_RecvPack.pBodyBuffer);
		    m_RecvPack.pBodyBuffer = new char[m_RecvPack.uBodyTotal];
		    ZeroMemory(m_RecvPack.pBodyBuffer, m_RecvPack.uBodyTotal);
		    m_RecvPack.uBodyOffset = 0;
		}
                //-----------3. 创建好干净的包体,用以存储包体数据-----------//
	    }
        }

	//开始读取包体
	else if (m_RecvPack.pHeadBuffer && m_RecvPack.uHeadOffset == nHeaderLen)
	{
             //---4.根据协议头带上的包体长度,直接解析包体,如果套接字buffer不够长,要先存起来,等待下次onReceive调用时再粘包---//
	     if (m_RecvPack.uBodyTotal > 0 && m_RecvPack.uBodyOffset < m_RecvPack.uBodyTotal)
	     {
		char *p = m_RecvPack.pBodyBuffer + m_RecvPack.uBodyOffset;
		int len = std::max<int>(0, (std::min<int>((m_RecvPack.uBodyTotal - m_RecvPack.uBodyOffset), nLeftSize)));
		assert(len != 0);
		memcpy(p, pOffsetBuffer, len);
		nCurrentOffset += len;
		pOffsetBuffer = pReceiveBuffer + nCurrentOffset;
		nLeftSize -= len;
		m_RecvPack.uBodyOffset += len;
	    }
	    if (m_RecvPack.uBodyOffset == m_RecvPack.uBodyTotal && m_RecvPack.pBodyBuffer != NULL) //数据读取完成
	    {
                //本次套接字buffer解析出了一个包,用容器记录下来,后续一起抛给上层
		vecRecvPacks.push_back(m_RecvPack);
                //这个包解析晚了,清空这个成员变量,用以解析下一个包
		m_RecvPack.reset();
	    }
            //---4.根据协议头带上的包体长度,直接解析包体,如果套接字buffer不够长,要先存起来,等待下次onReceive调用时再粘包---//
        }
	else
	{
	    assert;
	}
    } while (nLeftSize > 0);

    //还回数据
    MF_Delete1D(pReceiveBuffer);

    //本次解析出来的包,每一个依次往上层抛出回调
    auto uConnectOrderSession = m_uConnectOrderSession;
    for (auto it : vecRecvPacks)
    {
	if (it.pHeadBuffer == NULL)
	{
	    continue;
	}

	if (uConnectOrderSession != m_uConnectOrderSession)
	{
            //出错了
	    OnErrorPack(it);
            assert;
	    break;
	}

	if (it.uBodyTotal == 0)
	{
            //收到了空包
	    OnRecvEmptyPack(it);
	}
	else
	{
            //收到了一个完整的包,通知对应的业务上层
	    OnRecvPack(it);
	}
    }
}

原文地址:https://www.cnblogs.com/HelloGreen/p/12617014.html

时间: 2024-10-10 06:10:45

如何理解TCP协议是无边界的,以及粘包?的相关文章

python 基于tcp协议的文件传输3_解决粘包问题

server import jsonimport structimport socket# 接收sk = socket.socket()sk.bind(('127.0.0.1',9001))sk.listen() conn,_ =sk.accept()msg_len = conn.recv(4)dic_len = struct.unpack('i',msg_len)[0]msg = conn.recv(dic_len).decode('utf-8')msg = json.loads(msg) w

【转】TCP协议的无消息边界问题

http://www.cnblogs.com/eping/archive/2009/12/12/1622579.html 使用TCP协议编写应用程序时,需要考虑一个问题:TCP协议是无消息边界的,即不能保证来自单个Send方法的数据能被单个Receive方法读取. eg: 第一次发送:abcdefg   第二次发送:123456         接收方接收数据时,可能会出现以下情况: 第一次接收:abcdefg123456   也可能出现:第一次接收:abc 第二次接收:efg12 第三次接收:

深入理解TCP协议及其源代码

深入理解TCP协议及其源代码 前言 在前面实验我们分别实现了Socket 通信工具,探讨了Socket API.Socket 调用原理等.但是还没有针对某一实例进行讲解,在本实验我们将针对TCP协议进行详细分析,期待在Linux内核进行分析TCP原理. 1.Tcp基本原理 TCP是一种面向连接.可靠.基于字节流的传输协议,位于TCP/IP模型的传输层. 面向连接:不同于UDP,TCP协议需要通信双方确定彼此已经建立连接后才可以进行数据传输: 可靠:连接建立的双方在进行通信时,TCP保证了不会存在

粘包产生的原因 socket 基于tcp实现远程执行命令(解决粘包)low

# 粘包产生的原因 # 粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的. # 基于tcp协议的套接字会有粘包现象,而基于udp协议的套接字不会产生粘包现象 # tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住:而udp是基于数据报的,即使你输入的是空内容,那也不是空消息,udp协议会帮你封装上消息头(ip+端口的方式),这样就有了消息办界 # 两种情况下会发生粘包 # 1.发送端需要等缓冲区满才发送

通俗大白话来理解TCP协议的三次握手和四次分手

通俗理解: 但是为什么一定要进行三次握手来保证连接是双工的呢,一次不行么?两次不行么?我们举一个现实生活中两个人进行语言沟通的例子来模拟三次握手. 引用网上的一些通俗易懂的例子,虽然不太正确,后面会指出,但是不妨碍我们理解,大体就是这么个理解法. 第一次对话: 老婆让甲出去打酱油,半路碰到一个朋友乙,甲问了一句:哥们你吃饭了么? 结果乙带着耳机听歌呢,根本没听到,没反应.甲心里想:跟你说话也没个音,不跟你说了,沟通失败.说明乙接受不到甲传过来的信息的情况下沟通肯定是失败的. 如果乙听到了甲说的话

深入理解TCP协议及其源代码-send和recv背后数据的收发过程

send和recv背后数据的收发过程 send和recv是TCP常用的发送数据和接受数据函数,这两个函数具体在linux内核的代码实现上是如何实现的呢? ssize_t recv(int sockfd, void buf, size_t len, int flags) ssize_t send(int sockfd, const void buf, size_t len, int flags) 理论分析 对于send函数,比较容易理解,捋一下计算机网络的知识,可以大概的到实现的方法,首先TCP是

深入理解TCP协议的三次握手,分析源码并跟踪握手过程

1.TCP三次握手建立连接 在TCP中,面向连接的传输需要经过三个阶段:连接建立.数据传输和连接终止. 三次握手建立连接 在我们的例子中,一个称为客户的应用程序希望使用TCP作为运输层协议来和另一个称为服务器的应用程序建立连接. 这个过程从服务器开始.服务器程序告诉它的TCP自己已准备好接受连接.这个请求称为被动打开请求.虽然服务器的TCP已准备好接受来自世界上任何一个机器的连接,但是它自己并不能完成这个连接. 客户程序发出的请求称为主动打开.打算与某个开放的服务器进行连接的客户告诉它的TCP,

深入理解TCP协议及其源代码-拥塞控制算法分析

这是我的第五篇博客,鉴于前面已经有很多人对前四个题目如三次握手等做了很透彻的分析,本博客将对拥塞控制算法做一个介绍. 首先我会简要介绍下TCP协议,其次给出拥塞控制介绍和源代码分析,最后结合源代码具体分析拥塞控制算法. 一.TCP协议 关于TCP协议,其实在我的第二篇博客中:https://www.cnblogs.com/xiaofengustc/p/12012638.html 已有简要的介绍,并且在该博客中我还拿TCP协议与HTTP协议.UDP协议做了相关对比.有兴趣的同学可以参见我的第二篇博

深入理解TCP协议及其源代码——connect及bind、listen、accept背后的“三次握手”

一.TCP简介 TCP(Transmission Control Protocol,传输控制协议)是一个传输层(Transport Layer)协议,它在TCP/IP协议族中的位置如图1所示.它是专门为了在不可靠的互联网络上提供一个面向连接的且可靠的端到端(进程到进程)字节流而设计的.互联网络与单个网络不同,因为互联网络的不同部分可能有截然不同的拓扑.带宽.延迟.分组大小和其他参数.TCP的设计目标是能够动态地适应互联网络的这些特性,而且当面对多种失败的时候仍然足够健壮. 图1 TCP在TCP/