tcp 粘包问题及其解决
tcp 粘包问题及其解决
tcp及粘包介绍
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输协议,它保证了数据的可靠性和顺序性。
TCP粘包是指在TCP协议中发送方发送的若干个小数据包在接收方接收时被合并成一个大数据包的现象,或者多个大数据包在接收时被分割成若干个小数据包的现象。
这种现象的产生是因为TCP协议中的流控制和数据传输机制。TCP是面向流的传输协议,发送方将数据流划分为一系列的报文段进行传输,而接收方通过接收和缓存报文段来进行重组和还原数据流。当发送方连续发送多个小的数据包时,如果这些数据包在传输过程中没有被拆分或者合并,那么接收方在接收时就会将它们当作一个大的数据包来处理,从而导致粘包现象的产生。
TCP粘包可能会导致接收方在处理数据时出现错误,因为接收方可能会将多个数据包误认为是一个完整的数据包,或者将一个完整的数据包错误地拆分成多个小的数据包。[1]
粘包原因
因为TCP是基于字节流的协议,它只负责把接收到的字节流按照顺序交给应用层,根据TCP的发送和接收策略,它会尽可能多的将数据发送出去或者接收进来,因此多个发送的小数据包就有一定的可能被合并成一个大的数据包发送或者接收,或者一个超大的数据包被拆分成多个合适的小数据包发送或接收。
粘包造成的现象
-
服务器一次接收到 n 个完整的数据包(n>=1)的合并
-
服务器一次接收到 n 个完整的数据包 以及 前半个数据包 的合并
-
服务器一次接收到后半个数据包 以及 n 个完整的数据包 的合并
-
服务器一次接收到后半个数据包 以及 前半个数据包 的合并
粘包问题复现
1. 小数据包合并复现
复现描述:下面代码将复现小数据包合并成大数据包发送和接收
服务器端代码:
//server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>
int main(){
int sfd = socket(AF_INET, SOCK_STREAM, 0);
// 设置端口复用
int opt = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("192.168.3.88");
serv_addr.sin_port = htons(9999);
bind(sfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(sfd, 1024);
struct sockaddr_in clientsocket;
socklen_t peerAddrSize=sizeof(struct sockaddr);
int len = sizeof(struct sockaddr);
while (1) {
bzero(&clientsocket, len);
int cfd = accept(sfd,(struct sockaddr *)&clientsocket,&peerAddrSize);
char buff[10240];
int times = 0;
while (1)
{
int n = read(cfd,buff,sizeof(buff));
if (n<0)
{
perror("read error\n");
return -1;
}else if (n==0)
{
printf("client close\n");
break;
}
printf("第%d次 recv len: %d\n",++times,n);
}
}
return 0;
}
客户端代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>
int main(){
int sfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("192.168.3.88");
serv_addr.sin_port = htons(9999);
int ret = connect(sfd,(const struct sockaddr *)&serv_addr,sizeof(serv_addr));
if (ret<0)
{
perror("conn error");
return -1;
}
char buff[10];
memset(buff,'1',sizeof(buff));
while (1)
{
write(sfd,buff,sizeof(buff));
}
return 0;
}
复现结果
数据传输到后面就开始出现 tcp 粘包现象
2. 大数据包拆分复现
复现描述:下面代码将复现大数据包拆分成小数据包发送和接收
服务端代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>
int main(){
int sfd = socket(AF_INET, SOCK_STREAM, 0);
// 设置端口复用
int opt = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("192.168.3.88");
serv_addr.sin_port = htons(9999);
bind(sfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(sfd, 1024);
struct sockaddr_in clientsocket;
socklen_t peerAddrSize=sizeof(struct sockaddr);
int len = sizeof(struct sockaddr);
while (1) {
bzero(&clientsocket, len);
int cfd = accept(sfd,(struct sockaddr *)&clientsocket,&peerAddr //修改部分 //修改部分//修改部分//修改部分Size);
char buff[102400]; //修改部分
int times = 0;
while (1)
{
int n = read(cfd,buff,sizeof(buff));
if (n<0)
{
perror("read error\n");
return -1;
}else if (n==0)
{
printf("client close\n");
break;
}
printf("第%d次 recv len: %d\n",++times,n);
}
}
return 0;
}
客户端代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <strings.h>
int main(){
int sfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("192.168.3.88");
serv_addr.sin_port = htons(9999);
int ret = connect(sfd,(const struct sockaddr *)&serv_addr,sizeof(serv_addr));
if (ret<0)
{
perror("conn error");
return -1;
}
char buff[102400]; //修改部分
memset(buff,'1',sizeof(buff));
while (1)
{
write(sfd,buff,sizeof(buff));
}
return 0;
}
复现结果
数据包过大,传输过程中拆成若干小包
粘包解决
粘包的解决方式
-
标志位
设置一个标志位,当接收端看到标志位表示一个包结束。需要挨个字节判断,不好。 -
禁用Nagle算法
可以禁用Nagle算法,一有数据包就发送,不等待。但是这只是解决了发送端不粘包的问题,接收端还是会粘包,无法解决。 -
定长包
发送方在发送数据前将数据按照固定长度进行切分,接收方在接收到数据后按照固定长度进行处理,这样可以保证每个数据包的长度是固定的,从而避免了粘包现象的产生,但适用场景单一。 -
包头添加长度界定
可以重新封装数据包,在发送数据包的时候,在包头添加长度信息。然后在接收包信息的时候先获取长度信息,然后再读取数据包。下面根据这个写下面代码。
解决代码
读写部分封装
读写封装:
//base_server.h
#ifndef __BASE_SERVER_H___
#define __BASE_SERVER_H___
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
int writen(int fd, const char* msg, int size);
int sendMsg(int cfd, const char* msg, int len);
int readn(int fd, char* buf, int size);
int recvMsg(int cfd, char** msg);
#endif
读写封装实现:
//base_server.c
#include "base_server.h"
/*
函数描述: 发送指定的字节数
函数参数:
- 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; // 返回发送的字节数
}
/*
函数描述: 接收指定的字节数
函数参数:
- 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;
int n = readn(cfd, (char*)&len, 4);
if ( n == 0 ) {
return 0; // 客户端断开连接了
}else if (n != 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; // 返回接收的字节数
}
网络通信部分
服务器端
//server.c
#include <netinet/in.h>
#include <strings.h>
#include "base_server.h"
int main(){
int sfd = socket(AF_INET, SOCK_STREAM, 0);
// 设置端口复用
int opt = 1;
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("192.168.3.88");
serv_addr.sin_port = htons(9999);
bind(sfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(sfd, 1024);
struct sockaddr_in clientsocket;
socklen_t peerAddrSize=sizeof(struct sockaddr);
int len = sizeof(struct sockaddr);
while (1) {
bzero(&clientsocket, len);
int cfd = accept(sfd,(struct sockaddr *)&clientsocket,&peerAddrSize);
char *recvbuff;
char buff[10240];
int times = 0;
while (1)
{
int n = recvMsg(cfd,&recvbuff);
if (n<0)
{
perror("read error\n");
return -1;
}else if (n==0)
{
printf("client close\n");
break;
}
printf("第%d次 recv len: %d\n",++times,n);
}
}
return 0;
}
客户端
//client.c
#include "base_server.h"
#include <netinet/in.h>
#include <strings.h>
int main(){
int sfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("192.168.3.88");
serv_addr.sin_port = htons(9999);
int ret = connect(sfd,(const struct sockaddr *)&serv_addr,sizeof(serv_addr));
if (ret<0)
{
perror("conn error");
return -1;
}
char buff[10];
memset(buff,'1',sizeof(buff));
int num =0;
while (1)
{
int n = sendMsg(sfd,buff,sizeof(buff));
printf("第%d次发送 %d 个\n",++num,n-4);
// 403138这个数字随便写的,写大点就行
if (num==403138)
{
break;
}
}
return 0;
}
编译
gcc server.c base_server.c -o server -I./
gcc client.c base_server.c -o client -I./
运行结果
可以看到粘包问题解决
结尾
粘包问题解决。