Tcp粘包问题分析与对策

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

出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。

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

如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议)。关闭连接主要是要双方都发送close连接(参考tcp关闭协议)。

如:A需要发送一段字符串给B,那么A与B建立连接,然后发送双方都默认好的协议字符如"hello give me sth abour yourself",然后B收到报文后,就将缓冲区数据接收,然后关闭连接,这样粘包问题不用考虑到,因为大家都知道是发送一段字符。

如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包。

如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:

1)“hellogive me sth abour yourself”

2)“Don’tgive me sth abour yourself”

那这样的话,如果发送方连续发送这个两个包出去,接收方一次接收可能会是"hellogive me sth abour yourselfDon’t give me sth abour yourself"这样接收方就傻了,到底是要干嘛?不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在头加一个数据长度之类的包,以确保接收。

粘包出现原因

简单得说,在流传输中出现,UDP不会出现粘包,因为它有消息边界(参考Windows网络编程)

发送端需要等缓冲区满才发送出去,造成粘包

接收方不及时接收缓冲区的包,造成多个包接收

具体点:

发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。

接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

解决方案

粘包问题在实际的网络编程中是常见的,需要采取一些策略来解决或者减少其影响:

消息边界标记:在发送的消息中加入特定的消息边界标记(如换行符 \n),接收端根据消息边界标记来分割接收到的数据,从而识别出完整的消息。有缺陷: 效率低, 需要一个字节一个字节接收, 接收一个字节判断一次, 判断是不是那个特殊字符串

消息长度固定:发送端将每个消息的长度固定,接收端根据固定长度来分割接收到的数据,从而确保每个接收到的数据包含完整的消息。缺点:容易造成空间浪费

消息头部长度字段:发送端在每个消息前加入一个固定长度的消息头部,包含消息的长度信息,接收端根据头部长度字段来读取对应长度的消息数据。这时候数据由两部分组成:数据头+数据块,数据头:存储当前数据包的总字节数,接收端先接收数据头,然后在根据数据头接收对应大小的字节,数据块:当前数据包的内容

使用标准的应用层协议(比如:http、https)来封装要传输的不定长的数据包

解决方案具体实现

这里我们使用消息头+数据块的解决方案,如果使用TCP进行套接字通信,如果发送的数据包粘连到一起导致接收端无法解析,我们通常使用添加包头的方式轻松地解决掉这个问题。关于数据包的包头大小可以根据自己的实际需求进行设定,这里没有啥特殊需求,因此规定包头的固定大小为4个字节,用于存储当前数据块的总字节数。

发送端设计

对于发送端来说,数据的发送分为以下四步:

动态申请内存: 根据待发送的数据长度 N申请一块大小为 N+4 的内存,其中4个字节用于存储包头信息。

写入包头: 将待发送数据的总长度(N)写入申请的内存的前四个字节中,并将其转换为网络字节序(大端序)。

拷贝数据并发送: 将待发送的数据拷贝到包头后面的地址空间中,然后将整个数据包发送出去。这里需要确保数据包能够完整发送,因此可以设计一个发送函数,确保当前数据包中的数据全部发送完毕。

释放内存: 发送完毕后,释放申请的堆内存。

示例代码:

/*

函数描述: 发送指定的字节数

函数参数:

    - fd: 通信的文件描述符(套接字)

    - msg: 待发送的原始数据

    - size: 待发送的原始数据的总字节数

函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1

*/

int writen(int fd, const char* msg, int size) {

    const char* buf = msg; // 指向待发送数据的指针

    int count = size; // 记录剩余待发送的数据字节数

 

    while (count > 0) {

        // 尝试发送剩余数据

        int len = send(fd, buf, count, 0);

        if (len == -1) {

            perror("send");

            close(fd);

            return -1; // 发送失败

        } else if (len == 0) {

            continue; // 发送未成功,继续尝试

        }

        buf += len; // 更新待发送数据的起始地址

        count -= len; // 更新剩余待发送的数据字节数

    }

    return size; // 全部数据发送完毕,返回发送的总字节数

}

 

/*

函数描述: 发送带有数据头的数据包

函数参数:

    - cfd: 通信的文件描述符(套接字)

    - msg: 待发送的原始数据

    - len: 待发送的原始数据的总字节数

函数返回值: 函数调用成功返回发送的字节数, 发送失败返回-1

*/

int sendMsg(int cfd, const char* msg, int len) {

    if (msg == NULL || len <= 0 || cfd <= 0) {

        return -1; // 参数无效

    }

    // 申请内存空间: 数据长度 + 包头4字节(存储数据长度)

    char* data = (char*)malloc(len + 4);

    if (data == NULL) {

        perror("malloc");

        return -1; // 内存申请失败

    }

    // 将数据长度转换为网络字节序(大端序)并存储在包头

    int bigLen = htonl(len);

    memcpy(data, &bigLen, 4);

    // 将待发送的数据拷贝到包头后面

    memcpy(data + 4, msg, len);

    // 发送带有包头的数据包

    int ret = writen(cfd, data, len + 4);

    // 释放申请的内存

    free(data);

    return ret; // 返回发送的字节数

}

接收端设计

在接收端,需要确保每次接收到的都是完整的数据包,避免粘包问题。以下是具体的步骤和代码实现:

接收4字节的包头,并将其从网络字节序转换为主机字节序,得到即将要接收的数据的总长度。

根据总长度申请固定大小的堆内存,用于存储待接收的数据。

根据数据块长度接收固定数量的数据并保存到申请的堆内存中。

处理接收的数据。

释放存储数据的堆内存。

示例代码:

/*

函数描述: 接收指定的字节数

函数参数:

    - fd: 通信的文件描述符(套接字)

    - buf: 存储待接收数据的内存的起始地址

    - size: 指定要接收的字节数

函数返回值: 函数调用成功返回接收的字节数, 接收失败返回-1

*/

int readn(int fd, char* buf, int size) {

    char* pt = buf; // 指向待接收数据的缓冲区

    int count = size; // 记录剩余需要接收的字节数

 

    while (count > 0) {

        // 尝试接收数据

        int len = recv(fd, pt, count, 0);

        if (len == -1) {

            perror("recv");

            return -1; // 接收失败

        } else if (len == 0) {

            return size - count; // 对方关闭连接,返回已接收的字节数

        }

        pt += len; // 更新缓冲区指针

        count -= len; // 更新剩余需要接收的字节数

    }

    return size; // 返回实际接收的字节数

}

 

/*

函数描述: 接收带数据头的数据包

函数参数:

    - cfd: 通信的文件描述符(套接字)

    - msg: 一级指针的地址,函数内部会给这个指针分配内存,用于存储待接收的数据,这块内存需要使用者释放

函数返回值: 函数调用成功返回接收的字节数, 接收失败返回-1

*/

int recvMsg(int cfd, char** msg) {

    // 接收数据头(4个字节)

    int len = 0;

    if (readn(cfd, (char*)&len, 4) != 4) {

        return -1; // 接收数据头失败

    }

    // 将数据头从网络字节序转换为主机字节序,得到数据长度

    len = ntohl(len);

    printf("数据块大小: %d\n", len);

    // 根据读出的长度分配内存,多分配1个字节用于存储字符串结束符'\0'

    char* buf = (char*)malloc(len + 1);

    if (buf == NULL) {

        perror("malloc");

        return -1; // 内存分配失败

    }

    // 接收数据

    int ret = readn(cfd, buf, len);

    if (ret != len) {

        close(cfd);

        free(buf);

        return -1; // 接收数据失败

    }

 

    buf[len] = '\0'; // 添加字符串结束符

    *msg = buf;

 

    return ret; // 返回接收的字节数

}

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦龙zmc

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值