目录
一、粘包/拆包问题产生的原因
1、接收端和发送端收发数据的过程
(1)TCP三次握手过程
(2)TCP四次挥手过程
2、一帧数据的结构
3、Nagle算法
4、粘包/拆包问题产生的原因
二、解决粘包问题的方式
在了解粘包/拆包问题产生的原因之前,我们需要对TCP/IP数据传输的原理有一定的了解。
三次握手是发生在建立连接的过程中的,是由客户端主动发起,发生在服务器accept(listen)函数和客户端的connect函数之间
三次握手是为了保证通信的双方都知道对方收发数据的能力没问题,同时三次握手也是同步序列号的过程。
四次挥手发生在连接断开的过程中,是由主动关闭方发起(服务器和客户端都可以发起)
以客户端主动发起为例
MTU(Maximum Transmission Unit)最大传输单元,是链路层单次传输数据大小的最大限制。
MSS(Maximum Segment Size)最大报文段长度,是网络层封装最大tcp数据包的最大限制,是用来限制应用层单次发送的最大字节数。
Nagle算法(Nagle algorithm)是拥塞控制领域的一个算法。根据上述对于数据传输和数据结构的描述中我们可以了解到,在TCP/IP协议中,每次发送数据的时候都要在原有数据的基础上加上协议头,同时双方接收数据都需要向对方发送ACK表示确认。为了充分利用网络带宽,TCP总是希望每次发送足够大的数据包。所以在网络层设置了MSS参数,TCP/IP希望每次都能发送MSS大小的数据包。Nagle算法就是为了尽可能发送大块数据,避免网络中频繁传输小数据块,带来资源的不必要开销。
TCP协议是面向连接的传输层协议,TCP协议数据传输是基于 "字节流" 的,它不包含消息、数据包等概念,需要应用层协议自己设计消息边界。日常网络应用开发大都在传输层进行,因此粘包拆包问题大都只发生在TCP协议中,当然链路层网络层也会发生粘包/拆包问题。
通俗就是由于Nagle算法主动影响了每次发送数据的大小,当我们在多次发送小片数据的时候,Nagle算法只会在第一次发送小片数据,收到ACK之后会尽量将剩余的小片数据整合为一个接近MSS大小的数据包再发送出去,除非下一个小片数据是紧急数据,这样在缓冲区保存小片数据的现象就是粘包。还有一种情况就是当发送端一次发送的数据的大小超过了MSS的大小,由于Nagle算法的存在,会将这次数据拆成两部分,将需要发送的那一部分控制在<=MSS的大小,剩下的部分会被放在缓冲区,这样的现象就叫做拆包。
方式1:
发送端可以指定每个数据包的开头的数据表示整个包的大小,这样接收端就可以根据每个包开头的数据来判定包的实际长度。
方式2:
发送端可以在数据的边缘设置一个特殊的标识,接收方可以通过判断收到数据包中是否有标识来判断数据的边界。
方式3:
发送端将每个数据包封装为固定长度(空闲的空间用0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就可以实现区分数据段。
下面是我写的一个小例子,用来解释这种方法。
(这个例子可以实现客户端下载服务器端所在文件夹的文件)
服务器端代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//打印错误码信息、打印出错行数
#define PRINT_ERR(msg) \
do { \
printf("%s %s:%d\n", __FILE__, __func__, __LINE__); \
perror(msg); \
return -1; \
} while (0)
//发送消息的数据包
typedef struct MSG {
int count; //发送端每次发出数据包中用户数据的大小
char txt[128]; //保存从文件中读到的内容
} msg_t;
int main(int argc, const char* argv[])
{
// 入参合理性检查
if (3 != argc) {
printf("Usage : %s \n", argv[0]);
exit(-1);
}
// 1.创建套接字
// 遵循ipv4协议(AF_INET) 流式套接字(SOCK_STREAM) 无附加协议 0
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd) {
PRINT_ERR("socket error");
}
// 2.填充服务器网络信息结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
//AF_INET 表示ipv4网络协议
serveraddr.sin_family = AF_INET;
//填充端口号 将命令行传参的字符串转化为整形 然后再将主机字节序转换为网络字节序
serveraddr.sin_port = htons(atoi(argv[2]));
//填充ip地址 通过inet_addr函数将字符串转换为点分十进制四字节整形的ip地址
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
//为了匹配下一步bind函数最后一个参数
//需要定义一个变量来表示网络信息结构体的大小
//防止编译警告
socklen_t serveraddr_len = sizeof(serveraddr);
// 3.将套接字与服务器的网络信息结构体绑定
//(struct sockaddr*)是用来强转对应bind函数的参数格式 防止编译警告
if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len)) {
PRINT_ERR("bind error");
}
// 4.将套接字设置成被动监听状态
//第二个参数表示半连接队列
//同时监听客户端的数目为5(非零即可)
if (-1 == listen(sockfd, 5)) {
PRINT_ERR("listen error");
}
// 定义结构体保存客户端信息
struct sockaddr_in clientaddr;
memset(&clientaddr, 0, sizeof(clientaddr));
socklen_t clientaddr_len = sizeof(clientaddr);
int nbytes = 0; //记录客户端发来的数据的字节数
msg_t buff; //与客户端数据传输使用的数据包
int acceptfd = 0; //一个专门用于服务器与客户端通信的文件描述符
int fd = 0; //一个使用文件io的方式打开文件所需的文件描述符
char file[128] = { 0 }; //用来保存客户端发来的文件名
//服务器循环等待客户端连接
while (1) {
printf("正在等待客户端连接..\n");
// 5.阻塞等待客户端连接
if (-1 == (acceptfd = accept(sockfd, (struct sockaddr*)&clientaddr, &clientaddr_len))) {
PRINT_ERR("accept error");
}
printf("客户端[%s:%d]连接到服务器..\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
// 收发数据 循环获取文件内容通过数据包发给客户端
while (1) {
// 接收客户端数据
memset(&buff, 0, sizeof(buff));
if (-1 == (nbytes = recv(acceptfd, &buff, sizeof(buff), 0))) {
PRINT_ERR("recv error");
} else if (0 == nbytes) { //处理客户端异常中断连接 (客户端断电或信号中断等)
printf("客户端[%s:%d]断开了连接..\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
break;
}
if (!strcmp(buff.txt, "quit")) { //处理客户端主动中断连接 (当客户端发来一个quit的包表明客户端主动退出)
printf("客户端[%s:%d]退出了..\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
break;
}
strcpy(file, buff.txt); // 保存接收文件名字
printf("收到的文件名为[%s]\n", buff.txt);
memset(&buff, 0, sizeof(buff));
if (-1 == (fd = open(file, O_RDONLY))) { //以只读的方式打开当前路径下存在的文件
strcpy(buff.txt, "***NOT_EXIST***"); //如果当前目录下文件不存在 则返回一个校验用的数据包
if (-1 == send(acceptfd, &buff, sizeof(buff), 0))
PRINT_ERR("send error");
} else {
strcpy(buff.txt, "***EXIST***");
if (-1 == send(acceptfd, &buff, sizeof(buff), 0))
PRINT_ERR("send error");
memset(&buff, 0, sizeof(buff));
//循环从文件中读取内容发送给客户端
while (0 < (nbytes = read(fd, buff.txt, 128))) {
buff.count = nbytes; //将从文件中读到的字符的个数赋值给buff.count
//通过发送固定大小的数据包解决粘包问题
if (-1 == send(acceptfd, &buff, sizeof(buff), 0)) //!!!!!!!!!!!!!
PRINT_ERR("send error");
memset(&buff, 0, sizeof(buff));
}
// 发送一个buff.count为0的数据包表示文件传输完毕
buff.count = 0;
if (-1 == send(acceptfd, &buff, sizeof(buff), 0))
PRINT_ERR("send error");
close(fd);
}
}
// 关闭套接字
close(acceptfd);
}
close(sockfd);
return 0;
}
客户端代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// 打印错误码信息、打印出错行数
#define PRINT_ERR(msg) \
do { \
printf("%s %s:%d\n", __FILE__, __func__, __LINE__); \
perror(msg); \
return -1; \
} while (0)
// 发送消息的载体
typedef struct MSG {
int count; // 用来记录发送
char txt[128];
} msg_t;
int main(int argc, const char* argv[])
{
// 入参合理性检查
if (3 != argc) {
printf("Usage : %s \n", argv[0]);
exit(-1);
}
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd) {
PRINT_ERR("socket error");
}
// 2.填充服务器网络信息结构体
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t serveraddr_len = sizeof(serveraddr);
// 3.与服务器建立连接
if (-1 == connect(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len)) {
PRINT_ERR("connect error");
}
printf("与服务器连接成功..\n");
// 收发数据
msg_t buff;
char file[128] = { 0 };
int fd, ret = 0;
while (1) {
memset(&buff, 0, sizeof(buff));
memset(file, 0, sizeof(file));
printf("请输入文件名:");
fgets(buff.txt, 128, stdin); // 从终端获取文件名字
buff.txt[strlen(buff.txt) - 1] = '\0'; // 清除终端输入文件名后最后敲的回车键
if (!strcmp(buff.txt, "quit")) { // 如果终端输入quit表示主动结束进程
if (-1 == send(sockfd, &buff, sizeof(buff), 0)) // 发送客户端断开的校验包
PRINT_ERR("send error");
break; //跳出循环 结束进程
}
// 发送数据
if (-1 == send(sockfd, &buff, sizeof(buff), 0)) // 发送文件名
PRINT_ERR("send error");
strcpy(file, buff.txt);
printf("[%s]文件已被接收\n", buff.txt);
// 接收应答消息
memset(&buff, 0, sizeof(buff));
if (-1 == recv(sockfd, &buff, sizeof(buff), 0))
PRINT_ERR("recv error");
if (!strcmp(buff.txt, "***EXIST***")) {
if (-1 == (fd = open(file, O_WRONLY | O_CREAT | O_TRUNC, 0666)))
PRINT_ERR("open error");
while (1) {
memset(&buff, 0, sizeof(buff));
//循环接收服务器发来的文件的内容的数据包
if (-1 == (ret = recv(sockfd, &buff, sizeof(buff), 0)))
PRINT_ERR("recv error");
if (0 == buff.count) { //判断是否收到文件传输完成的校验包
printf("文件传输完成\n");
break; //跳出循环结束进程
}
if (-1 == write(fd, buff.txt, strlen(buff.txt)))
PRINT_ERR("write error");
}
close(fd);
} else if (!strcmp(buff.txt, "***NOT_EXIST***")) { //收到文件不存在的校验包 重新循环输入文件名
printf("文件不存在请 重新输入\n");
}
}
// 关闭套接字
close(sockfd);
return 0;
}
注意:这种解决粘包的方式在处理高并发、大流量的需求时会造成资源的不必要开销。
方式4:
关闭Nagle算法,使用setsockopt函数可以关闭TCP级别的Nagle算法。
int flag =1;
if(-1 == setsockopt(sockfd,IPPROTO_TCP,TCP_NODELAY,flag,sizeof(flag))){
printf("setsockopt error");
}