TCP协议-TCP粘包问题

这篇具有很好参考价值的文章主要介绍了TCP协议-TCP粘包问题。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、前言

        我们知道,TCP是一个面向字节流的传输层协议。“流” 意味着 TCP 所传输的数据是没有边界的。这不同于 UDP 协议提供的是面向消息的传输服务,其传输的数据是有边界的。TCP 的发送方无法保证对方每次收到的都是一个完整的数据包。于是就有了粘包、拆包问题的出现。粘包、拆包问题只发生在TCP协议中。

二、什么是粘包、拆包?

假设客户端向服务器连续发送了两个数据包,用 packet1 和 packet2 来表示,那么服务端收到的数据可以分为下面三种情况:

  • 第一种情况,接收端正常收到两个数据包,即没有发生拆包和粘包的现象,此种情况不在本文的讨论范围内。

TCP协议-TCP粘包问题

  • 第二种情况,接收端只收到一个TCP报文段,去掉首部后,这一个报文段中包含了发送端发送来的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。

TCP协议-TCP粘包问题

  • 第三种情况,这种情况有两种表现形式,如下图。接收端收到了两个TCP报文段,但是去掉首部后,两个数据包要么是不完整的,要么就是多出来一部分,这种现象即为粘包、拆包问题。同第二种情况一样,由于不知道两个数据包的界限,对于接收端来说同样不好处理。

TCP协议-TCP粘包问题

综上所述,我们可以得出结论:

TCP粘包是指发送方发送的若干数据包到达接收方时粘成一个数据包,从接收缓存看,后一个数据包的头紧接着前一个数据包的尾。

TCP拆包是指应用程序的数据包被拆分成若干部分发送出去,从接收缓存看,接收方收到的只是数据包的一部分内容。

由此可见,粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的数据包

三、粘包产生原因及解决办法

3.1 TCP粘包产生原因

TCP 发生粘包、拆包问题,主要是由于以下几个原因:

(1)应用进程写入的数据量大于TCP发送缓冲区的大小,这将会发生拆包。应用进程调用write()系统调用,将应用层的缓冲区中的数据拷贝到TCP socket 的发送缓冲区。而TCP的发送缓冲区有一个 SO_SNDBUF 的大小限制,如果应用层的缓冲区数据大小大于TCP发送缓冲区的大小,则数据包需要进行拆包处理,分多次进行发送。

(2)应用进程写入的数据量小于TCP发送缓冲区的大小,这将会发生粘包。应用进程调用write()系统调用,将应用层的缓冲区中的数据拷贝到TCP socket 的发送缓冲区。由于TCP发送缓存空间足够,它会等到有多个数据包时,再组装成一个TCP报文段,然后通过网卡发送到网络中去。

TCP协议允许发送端将几次发送的数据包先缓存起来合成一个数据包发送到网络上去,因为这样可以获得更高的效率,这一行为通常是在操作系统提供的socket中实现,所以在应用层对此毫无所觉。所以我们在程序中调用socketsend()函数,发送了数据后操作系统有可能是先缓存了起来,等待后续的数据。当socket的发送缓存的数据足够多时,再一起发送出去,而不是立即发送出去。这就是缓存发送,它可以提高网络信道的利用率。

(3)当应用进程发送的数据包大于 MSS(最大报文段长度)时,将会发生拆包。TCP在传送数据时,是以报文段为单位发送数据的,而待发送的数据是以 MSS 为单位进行分割的。如果应用进程发送的数据包packet的长度大于 MSS,必然需要对该数据包进行拆包处理。

<注1> MSS(Maximum Segment Size,最大报文段长度)  这个名词的含义是:每一个 TCP 报文段中的数据部分的最大长度。它不是指整个TCP报文段的最大长度,而是 “TCP报文段的长度 - TCP首部长度”。TCP 在组装报文段时,是以 MSS 为单位对需要传送的数据进行分割的,然后加上TCP首部后,就组装成一个完整的TCP报文段了。MSS的默认值一般是 536 字节长。

补充说明】TCP 之所以有一个 MSS 的限定,是因为在数据链路层的数据传输单位—数据帧,它有一个最大传送长度的限制,即 MTU。要求发送的帧的数据部分长度不超过 MTU,如果超过了就需要进行分包发送处理。

MTU(Maximum Transfer Unit,最大传送单元) 这个名词的含义是:数据链路层协议中规定的所能传送的帧的数据部分长度上限

<注2> TCP报文段 = TCP首部 + TCP数据部分

(4) 接收方不及时读取接收缓冲区中的数据,将会发生粘包。这是因为接收方先把接收到的数据存放在内核接收缓冲区中,用户进程从接收缓冲区读取数据,若下一个数据包到达时前一个数据包尚未被用户进程取走,则下一个数据包放到内核接收缓冲区时就和前一数据包粘在一起,而用户进程根据预先设定的缓冲区大小从内核接收缓冲区读取数据,这样就一次性读取到了多个数据包。

以上这几种情况,都会导致一个完整的应用层数据包被分割成多片或者是多个应用层数据包拼接成一个更大的传输层数据包,再发送出去,从而导致接收方不是按完整的应用层数据包方式来接收数据的。

3.2 什么时候需要考虑粘包问题?

TCP是长连接,并且传输的是结构化数据时,如:传送的是一个结构体类型的数据,由于不知道结构化数据的边界,容易导致粘包问题的出现。这时需要考虑粘包问题的影响。

什么时候不需要考虑粘包问题?

(1)如果 TCP 是短连接,即只进行一次数据通信过程,通信完成就关闭连接,这样就不会出现粘包问题。

(2)如果传输的是字符串、文件等无结构化数据时,也不会出现粘包问题。因为发送方只管发送,接收方只管接收存储就行了。

3.3 粘包问题的解决办法

粘包问题的最本质原因在于接收方无法分辨出消息与消息之间的边界在哪?我们通过使用某种方法给出边界,常用的方法有以下几种:

(1)发送定长包。即发送端将每个数据包封装为固定长度(长度不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。(适合定长结构的数据

(2)包头加上包体长度。发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便可以知道每一个数据包的实际长度了。(适合不定长结构的数据

(3)在包尾部设置边界标记。发送端在每个数据包尾部添加边界标记,可以使用特殊符号作为边界标记。如此,接收端通过这个边界标记就可以将不同的数据包拆分开来。但这可能会存在一个问题:如果数据包内容中也包含有边界标记,则会被误判为消息的边界,导致出错。这样方法要视具体情况而定。例如,FTP协议就是采用 "\r\n" 来识别一个消息的边界的。

四、粘包解决方案—代码实现

4.1 粘包解决方案1:使用定长包

这里需要实现对 read()、write() 系统调用的封装,函数声明如下:

ssize_t readn(int fd, void *buf, size_t count);

ssize_t writen(int fd, void *buf, size_t count);

这两个封装函数的参数列表和返回值与 read、write 一致。它们的作用是读取 / 写入 count 个字节后再返回。代码实现如下:

/*
readn 函数
读取count字节的数据
*/
ssize_t readn(int fd, void *buf, size_t count)
{
    int left = count;           //剩余的字节数
    char * ptr = (char*)buf ;
    while(left>0)
    {
        int readBytes = read(fd, ptr, left);
        if(readBytes < 0)//read函数小于0有两种情况:1中断;2出错
        {
            if(errno == EINTR) //读被中断
            {
                continue;
            }
            else
                return -1;
        }
        if(readBytes == 0) //读到了EOF
        {
            //对方关闭了呀
            printf("peer close\n");
            break;
        }
        left -= readBytes;
        ptr += readBytes;
    }
    return count - left;
}

/*
writen 函数
写入count字节的数据
*/
ssize_t writen(int fd, void *buf, size_t count)
{
    int left = count ;
    char * ptr = (char *)buf;
    while(left > 0)
    {
        int writeBytes = write(fd, ptr, left);
        if(writeBytes<0)
        {
            if(errno == EINTR)
                    continue;
            return -1;
        }
        else if(writeBytes == 0)
            continue;
        
        left -= writeBytes;
        ptr += writeBytes;
    }
    return count;
}

有了这两个封装函数,我们就可以使用定长包来发送/接收数据了。示例代码如下:

char readbuf[512] = {0};
readn(conn,readbuf,sizeof(readbuf));  //每次读取512个字节

//同理,写入的时候也写入512个字节
char writebuf[512] = {0};
fgets(writebuf, sizeof(writebuf), stdin);  //从终端输入数据
writen(conn, writebuf, sizeof(writebuf));

程序分析:每个消息体都是以固定的 512 字节长度来发送,以此来区分每一个消息,这就是用定长包的方法解决粘包的问题。

缺点:定长包解决方案的缺点在于会增加网络的负担,无论每次发送的有效数据是多大,都得按照固定的数据长度进行发送。

4.2 粘包解决方案2:使用结构体,显式说明数据部分的长度

在这个方案中,我们需要定义一个消息结构体,结构体中指明包体中数据部分的长度,用4个字节的无符号整数来表示。当接收端收到消息后,先读取前4个字节,获取消息的长度,然后根据消息长度进行数据的读取。定义的结构体如下:

typedef unsigned int uint32;

typedef struct packet
{
    uint32 msgLen;   //4个字节大小,原来描述数据部分的长度
    char data[512];  //数据部分
}PACKET_T;

读写过程如下所示,这里只给出关键代码进行说明:

//发送数据过程
PACKET_T writebuf;
memset(&writebuf,0,sizeof(writebuf));
while(fgets(writebuf.data, sizeof(writebuf.data), stdin) != NULL)
{
    int n = strlen(writebuf.data);     //计算要发送的数据的字节数
    writebuf.msgLen =htonl(n);         //将数据部分长度保存在msgLen字段中,注意字节序的转换
    writen(conn, &writebuf, 4+n);      //发送数据,数据长度为4个字节的msgLen 加上data长度
    memset(&writebuf,0,sizeof(writebuf)); 
}

下面是读取数据的过程,先读取4个字节长度,获取msgLen字段的值,该字段指示了有效数据的长度,然后依据该字段给出长度再读取data部分。

#define err_exit(m)\
do{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)

//读取数据过程
PACKET_T readbuf;
memset(&readbuf,0,sizeof(readbuf));
int ret = readn(conn, &readbuf.msgLen, 4); //先读取四个字节,确定后续数据部分的长度
if(ret == -1)
{
    err_exit("readn");
}
else if(ret == 0)
{
   printf("peer close\n");
   break;
}
int dataBytes = ntohl(readbuf.msgLen); //字节序的转换
int readBytes = readn(conn, readbuf.data, dataBytes); //读取出后续的数据
if(readBytes == 0)
{
    printf("peer close\n");
    break;
}
if(readBytes<0)
{
    err_exit("read");
}

4.3 粘包解决方案3:按行读取

FTP 协议就是采用 "\r\n" 标记来识别一个消息的边界的。我们这里实现一个按行读取的函数,该函数能够按 '\n' 来识别消息的边界。这里先介绍一个函数:

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

与 read 函数相比,recv 函数的区别在于两点:

1. recv 函数只能用于用于socket IO。

2. recv 函数含义 flags 参数,可以指定一些选项。

recv 函数的 flags 参数常用的选项有:

  • MSG_OOB:接收带外数据,即通过紧急指针发送的数据。
  • MSG_PEEK:从缓冲区中读取数据,但并不从缓冲区中清除所读数据。

        为了实现按行读取,我们需要使用 recv 函数的 MSG_PEEK 选项。PEEK 本意是“偷看、窥视”,我们可以理解为“窥视数据”,看看 socket 的缓冲区是否有某种数据,但不清除缓冲区中的内容。

/**
封装了recv函数
函数功能:从缓冲区中读取指定长度的数据,但不清除缓冲区内容。
返回值说明:-1 读取出错
*/
ssize_t read_peek(int sockfd, void *buf, size_t len )
{
    while(1)
    {
        int ret = recv (sockfd , buf ,len ,MSG_PEEK);
        if(ret  == -1)
        {
            if(errno ==EINTR)  //出现中断
                continue ;
        }
        return ret ;
    }
}

下面的函数实现了按行读取的功能,代码如下:

/**
按行读取数据
参数说明:
    sockfd:  套接字描述符
    buf:    应用层缓冲区,保存读取到的数据
    maxline: 所规定的一行的长度
返回值说明:
    == 0 : 对端关闭
    == -1 :读取错误
    其他:  一行的字节数,包含\n
*/
ssize_t read_line (int sockfd , void *buf ,size_t maxline)
{
    int ret;
    int nRead = 0;
    int left = maxline ;         //剩下的字节数
    char *pbuf = (char *) buf ;
    int count = 0;
    while(1)
    {
        ret = read_peek ( sockfd, pbuf, left);  //从socket缓冲区中读取指定长度的内容,但并不删除
        if(ret <0)
        {
            return ret;
        }
        nRead = ret;
        for(int i = 0; i<nRead; ++i)//看看读取出来的数据中是否有换行符\n
        {
            if(pbuf[i]=='\n')//如果有换行符
            {
                ret = readn(sockfd , pbuf , i+1);//读取一行
                if(ret != i+1)  //一定会读到i+1个字符,否则是读取出错
                {
                    exit(EXIT_FAILURE);
                }
                return ret + count ;
            }
        }
        //如果窥探的数据中并没有换行符
        //把这段没有换行符\n的内容读取出来
        ret = readn(sockfd , pbuf, nRead);
        if(ret != nRead )
        {
            exit(EXIT_FAILURE);
        }
        pbuf += nRead;
        left -= nRead;
        count += nRead;
    }

    return -1;
}

参考

TCP粘包,拆包及解决方法

Socket编程(4)TCP粘包问题及解决方案

TCP新手误区--粘包的处理

TCP粘包问题分析和解决(全)文章来源地址https://www.toymoban.com/news/detail-404285.html

到了这里,关于TCP协议-TCP粘包问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 你真的知道TCP协议中的序列号确认、上层协议及记录标识问题吗?

    在前面的内容中,我们已经详细讲解了一系列与TCP相关的面试问题。然而,这些问题都是基于个别知识点进行扩展的。今天,我们将重点讨论一些场景问题,并探讨如何解决这些问题。 当A主机与B主机建立了TCP连接后,A主机发送了两个TCP报文,分别大小为500和300字节。第一个

    2024年01月18日
    浏览(27)
  • 聊聊TCP协议的粘包、拆包以及http是如何解决的?

    目录 一、粘包与拆包是什么? 二、粘包与拆包为什么发生? 三、遇到粘包、拆包怎么办? 解决方案1:固定数据大小 解决方案2:自定义请求协议 解决方案3:特殊字符结尾  四、HTTP如何解决粘包问题的? 4.1、读取请求行/请求头、响应行/响应头 4.2、 怎么读取body数据呢?

    2024年02月11日
    浏览(31)
  • TCP粘包问题

    TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾。 TCP粘包现象的根本原因是 TCP采用了面向字节流的传输,面向字节流的传输不认为消息是一条一条的,它是无保护消息边界的 。 具体来说,出现粘包

    2024年02月15日
    浏览(33)
  • Linux tcp/ip 网路协议栈学习-00 前言

    Linux tcp/ip 网路协议栈学习-00 前言 目录 Linux  tcp/ip 网路协议栈学习-00 前言 (1)预备知识  (2)前置知识 (3)学习目标 (4)总结     (1)预备知识  好工具事半功倍,做任何事情都需要有方法和工具,同样,阅读 Linux 内核源码也是如此。由于当前内核源码非常庞大,学习上,不能一

    2024年04月26日
    浏览(28)
  • 解决TCP粘包/拆包问题的方法及示例

    TCP粘包和拆包是网络编程中常见的问题,特别是在数据传输的过程中,可能会发生将多个数据包粘在一起或将一个数据包拆成多个数据包的情况,这可能会导致应用程序无法正确解析数据,从而造成数据错误或系统故障。本文将介绍TCP粘包和拆包的原因、解决方案以及两个示

    2024年02月10日
    浏览(36)
  • TCP粘包和拆包问题及其解决方法

    含义: TCP 传输协议是面向流的,没有数据包界限,也就是说消息无边界。客户端向服务端发送数据时,可能将一个完整的报文拆分成多个小报文进行发送,也可能将多个报文合并成一个大的报文进行发送。(TCP协议的底层,并不了解上层业务的具体定义,它会根据TCP缓冲区

    2023年04月21日
    浏览(30)
  • 【计网】一起聊聊TCP的粘包拆包问题吧

    在TCP中,粘包和拆包问题是十分常见的,如 基于TCP协议 的RPC框架、Netty等。 粘包(Packet Stickiness) 指的是在网络通信中,发送方连续发送的多个小数据包被接收方一次性接收的现象。这可能是因为底层传输层协议(如TCP)会将 多个小数据包合并成一个大的数据块 进行传输,导

    2024年04月12日
    浏览(26)
  • 「 计算机网络 」TCP的粘包拆包问题

    参考鸣谢 大病初愈,一分钟看懂TCP粘包拆包 雷小帅 TCP 的粘包拆包以及解决方案 一乐说 当我们在进行网络传输时,由于各种原因,数据包的发送和接收可能会出现粘包和拆包的问题。粘包和拆包都是数据分组错误的情况,其中粘包指的是多个数据包被合并成一个,而拆包则

    2024年02月01日
    浏览(27)
  • Tcp的粘包和半包问题及解决方案

    目录 粘包: 半包: 应用进程如何解读字节流?如何解决粘包和半包问题? ①:固定长度 ②:分隔符 ③:固定长度字段存储内容的长度信息 一次接收到多个消息,粘包 应用进程无法从一个粘包中解析出数据 出现粘包的原因: ①:发送方每次写入数据内核缓冲区大小;导致

    2024年02月11日
    浏览(28)
  • 粘包/拆包问题一直都存在,只是到TCP就拆不动了。

    OSI open-system-Interconnection TCP/IP 5层协议栈 应用层和操作系统的边界是 系统调用 ,对应到网络编程是socket api TCP/UDP 概况 TCP粘包问题 TCP/IP报头深思 定义了网络框架,以层为单位实现协议,同时控制权逐层传递。 OSI实际并没有落地,TCP/IP 5层协议栈是目前主流的落地实现 。 TC

    2024年02月03日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包