为解决TCP协议中的粘包问题,提供以下实例说明:
TCP 协议不允许传送空数据包
通信协议(Communication Protocol):为了实现正常通信,参与通信的双方或多方必须共同遵守的规则,比如通信的流程、数据格式等。
世界知名的文件传输协议就是 FTP 协议,它也是基于 TCP 协议的应用层协议
TCP 协议内部实现了很多机制以保障传输可靠:
-
不会发生乱序问题,因为 TCP 协议内部会根据发送方的发送顺序在接收方缓冲区中进行重新排序。(按发送顺序来)。
-
不会发生丢包问题,内部具有拥塞控制和流量控制机制。
-
差错校验、出错重传。
粘包问题
TCP 协议为了提升传输效率,内部实现了一些优化算法,发送方调用send/write 发送一份数据时,TCP 协议并不会立即将这份数据打包送走,而是将这份数据存放在发送缓冲区中,并等待一小段时间,如果等待期间发送方又调用了 send/write 发送数据,TCP 协议会进行同样的处理。如果等待期间发送方没有再发送数据或者发送缓冲区满了,那么TCP协议会将发送缓冲区中现有数据直接打包送走,这样就导致了”粘包“问题发生。
当数据包到达接收方后,系统会解包后将数据按发送顺序存放在接收缓冲区中,如果接收方没有及时调用 recv/read 从接收缓冲区中收取数据,那么多个数据包可能都在接收缓冲区中连在一起,这样也会导致粘包。(两个缓冲区面向当前套接字)
解决粘包问题的方法:
- 给要发送的数添加定长的包头(比如一个按 1 字节对齐的结构体数据,用于描述后续数据的特性)和特殊的包尾。
- 采用应答式通信。
- 增加连续两次发送数据操作的时间间隔(无法根本解决问题,不太建议使用!)。
- 设置套接字属性,禁用 TCP 协议内部 Nagle算法(还是无法完全解决粘包问题,很无脑,不太建议使用!)。
开发经验:不要通过 send 函数返回值轻易判断对方成功接收到数据,因为 send 函数只是将要发送的数据拷贝到网络发送缓冲区中,它就成功返回。最可靠的做法就是通过接收对方的回复消息来判断是否成功。
客户端
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#pragma pack(1)
struct str_info
{
int size;
char type;
};
#pragma pack()
int main(int argc, char** argv)
{
//socket 函数:创建一个新套接字
// 参数意义:
// 第一个参数:地址家族,通常是 AF_INET
// 第二个参数:套接字类型,通常有两种:SOCK_STREAM(流套接字,用于 TCP 协议通信) 和 SOCK_DGRAM(数据报式套接字,用于 UDP 协议通信)
// 第三个参数:通常为 0, 表示使用默认协议。
// 返回值为新套接字的文件描述符,如果失败则为 -1
// 第一步:创建一个新的套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sock)
{
perror("socket");
return 1;
}
// // 第二步:绑定地址
// // 指定地址信息
// struct sockaddr_in myaddr;
// myaddr.sin_family = AF_INET; // 指定地址家族为 Internet 地址家族
// myaddr.sin_addr.s_addr = INADDR_ANY; // 指定 IP 地址为本机任意地址
// //myaddr.sin_addr.s_addr = inet_addr("192.168.0.56"); // 指定 IP 地址为本机某个确定的 IP 地址
// myaddr.sin_port = htons(8888); // 指定端口号为 8888
// // htons:将一个短整型数据从主机字节序(通常是小端)转换为网络字节序(通常是大端)
// // inet_addr:将字符串形式的 IP 地址转换为无符号 32 位整数,并且是网络字节序
// // 将上面指定的地址和套接字绑定
// if(-1 == bind(sock, (struct sockaddr*)&myaddr, sizeof(myaddr)))
// {
// perror("bind");
// return 1;
// }
// 第三步:连接服务器
// 指定服务器的地址
struct sockaddr_in srv_addr;
srv_addr.sin_family = AF_INET;
srv_addr.sin_addr.s_addr = inet_addr(argv[1]);
srv_addr.sin_port = htons(atoi(argv[2]));
if(-1 == connect(sock, (struct sockaddr*)&srv_addr, sizeof(srv_addr)))
{
perror("connect");
return 1;
}
// 第四步:收发数据
// 发送数据
char msg[100];
int ret;
struct str_info si;
strcpy(msg, "你是傻子!");
si.size = strlen(msg);
si.type = 'a';
ret = send(sock, &si, sizeof(si), 0);
ret = send(sock, msg, strlen(msg), 0);
// 接收对方回复
// ret = recv(sock, msg, sizeof(msg), 0);
// msg[ret] = '\0';
// if(strcmp(msg, "ok") != 0)
// {
// goto end;
// }
strcpy(msg, "hello,world...");
si.size = strlen(msg);
si.type = 'b';
ret = send(sock, &si, sizeof(si), 0);
ret = send(sock, msg, strlen(msg), 0);
//usleep(10000);
// 接收对方回复
// ret = recv(sock, msg, sizeof(msg), 0);
// msg[ret] = '\0';
// if(strcmp(msg, "ok") != 0)
// {
// goto end;
// }
strcpy(msg, "你好,中国!");
si.size = strlen(msg);
si.type = 'a';
ret = send(sock, &si, sizeof(si), 0);
ret = send(sock, msg, strlen(msg), 0);
end:
// 第五步:端开连接
close(sock);
return 0;
}
服务器端
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#pragma pack(1)
struct str_info
{
int size;
char type;
};
#pragma pack()
int main()
{
//socket 函数:创建一个新套接字
// 参数意义:
// 第一个参数:地址家族,通常是 AF_INET
// 第二个参数:套接字类型,通常有两种:SOCK_STREAM(流套接字,用于 TCP 协议通信) 和 SOCK_DGRAM(数据报式套接字,用于 UDP 协议通信)
// 第三个参数:通常为 0, 表示使用默认协议。
// 返回值为新套接字的文件描述符,如果失败则为 -1
// 第一步:创建一个新的监听套接字
int sock_listen = socket(AF_INET, SOCK_STREAM, 0);
if(-1 == sock_listen)
{
perror("socket");
return 1;
}
// 第二步:绑定地址
// 指定地址信息
struct sockaddr_in myaddr;
myaddr.sin_family = AF_INET; // 指定地址家族为 Internet 地址家族
myaddr.sin_addr.s_addr = INADDR_ANY; // 指定 IP 地址为本机任意地址
//myaddr.sin_addr.s_addr = inet_addr("192.168.0.56"); // 指定 IP 地址为本机某个确定的 IP 地址
myaddr.sin_port = htons(8888); // 指定端口号为 8888
// htons:将一个短整型数据从主机字节序(通常是小端)转换为网络字节序(通常是大端)
// inet_addr:将字符串形式的 IP 地址转换为无符号 32 位整数,并且是网络字节序
// 将上面指定的地址和套接字绑定
if(-1 == bind(sock_listen, (struct sockaddr*)&myaddr, sizeof(myaddr)))
{
perror("bind");
return 1;
}
// 第三步:监听
// listen 函数的第二个参数为监听等待队列的长度
if(-1 == listen(sock_listen, 5))
{
perror("listen");
return 1;
}
while(1)
{
// 第四步:接受客户端连接请求
//accept(sock_listen, NULL, NULL); // 如果对客户端地址信息不感兴趣
// 如果想获取当前客户端的地址信息,就使用下面的方式
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
// 调用 accept 函数接收一个客户端连接请求(队头请求)
// 如果成功,返回值为一个套接字描述符,这个套接字是和该客户端一一对应的
// 这个套接字专门用于和对应的客户端通信,所以通常称它为连接套接字。
// 如果当前没有任何客户端连接请求到来,accept 函数将会阻塞当前线程,直到成功接收到一个连接请求或出错
int sock_conn = accept(sock_listen, (struct sockaddr*)&client_addr, &len);
if(-1 == sock_conn)
{
perror("accept");
}
// 第五步:收发数据
// 发送数据
char msg[101] = "";
char buf[101] = "";
int ret, cnt;
struct str_info si;
recv(sock_conn, &si, sizeof(si), 0);
// 最稳妥做法
cnt = 0;
// 接收数据,如果当前没有任何数据,recv 函数会阻塞当前线程,直到成功接收到数据或对方断开连接(返回 0)或出错(返回 -1)。
while(cnt < si.size && (ret = recv(sock_conn, buf, si.size - cnt, 0)) > 0)
{
memcpy(msg + cnt, buf, ret);
cnt += ret;
}
msg[cnt] = '\0';
printf("%s\n", msg);
// send(sock_conn, "ok", 2, 0);
// 简单做法,不一定可靠!
recv(sock_conn, &si, sizeof(si), 0);
ret = recv(sock_conn, msg, si.size, 0);
msg[ret] = '\0';
printf("%s\n", msg);
// send(sock_conn, "ok", 2, 0);
// 简单做法,不一定可靠!
recv(sock_conn, &si, sizeof(si), 0);
ret = recv(sock_conn, msg, si.size, 0);
msg[ret] = '\0';
printf("%s\n", msg);
// send(sock_conn, "ok", 2, 0);
// 第六步:断开客户端连接
close(sock_conn);
}
// 第七步:关闭监听套接字
close(sock_listen);
return 0;
}