问题提出以及分析
基于TCP流式传输协议,服务器端总是可以接收到客户端发过来的数据包。但是有一个很严肃的问题:发送端和接收端处理数据的频率是不相等的(假如客户端每隔1s发送一次数据包,而服务端每隔300微秒接收一次数据包,这样数据包就会粘在一块,如何处理粘在一块的数据包就变成了一个很麻烦的事情。)
虽然把以上描述的现象很多时候我们将其称之为 TCP的粘包问题,是这种叫法不太对的,本身 TCP 就是面向连接的流式传输协议,特性如此,我们却说是 TCP 这个协议出了问题,这只能说是使用者的无知。多个数据包粘连到一起无法拆分是我们的需求过于复杂造成的,是程序猿的问题而不是协议的问题,TCP 协议表示这锅它不想背。
对于这个不定长度的数据包,我们有什么方法来解决呢?
- 使用标准的应用层协议(比如:http、https)来封装要传输的不定长的数据包
- 在每条数据的尾部添加特殊字符,如果遇到特殊字符,代表当条数据接收完毕了
有缺陷:效率低,需要一个字节一个字节接收,接收一个字节判断一次,判断是不是那个特殊字符串 - 在发送数据块之前,在数据块最前边添加一个固定大小的数据头,这时候数据由两部分组 成:数据头 + 数据块
数据头:存储当前数据包的总字节数,接收端先接收数据头,然后在根据数据头接收对应大小的字节
数据块:当前数据包的内容
感觉第三种方法挺人性化的,就来写写他吧,嘿嘿(当然不是懒得看前两种啦,手动狗头)
规定包头的固定大小为4个字节,用于存储当前数据块的总字节数。
下面的都是一些发送接收端的关键代码,如果有需要可以登陆我的Github上查看整个源码。(链接在最后)
发送端
发送端要考虑的问题:
- 发送的时候当指定字符串比写缓冲区要占的字节数多的时候采用什么方法。
- 循环发送数据,要记录并且减掉每次已经发送完成的数据。
- 同时还要记录还有多少数据没有发送完毕。
//发送指定的字节数
int writen (int fd, const char* msg, int size) {
const char* buf = msg;
int current = size;
while (current > 0) {
int len = send(fd, buf, count, 0);
//往对应的写缓冲区里面写内容,但是缓冲区内容有限
//返回实际发送的字节数
if (len == -1) {
close(fd);
return -1;
} else if (len == 0) {
continue; //重新发送
} else {
buf += len; //指针偏移
current -= len; //剩余的字节数量
}
}
return size;
}
//发送数据
int sendMsg(int cfd, const char* msg, int len) {
if (cfd < 0 || msg == NULL || len <= 0) {
return -1;
}
char* data = (char *)malloc(len + 4);
int biglen = htonl(len);//将长度转化为网络字节序
memcpy(data, &biglen, 4);//连续的4个字节数拷贝到内存中去
memcpy(data + 4, msg, len);//将数据包内容进行发送
int ret = writen(cfd, data, len + 4);
if (ret == -1) {
//perror("send\n");
close(cfd);
}
return ret;
}
接收端
接收端要考虑的问题:
- 接收的时候没有接收到完整的全部信息的时候,只收到其中几个包的时候如何处理?
- 读缓冲区里面的内容十分有限
- 不断接收数据而且要首先接收4字节数据,直到所有数据全部接收完成之后
- 记录总共需要接收的数据以及还有多少数据没有接收完成的
//接收指定的字节数
int readn(int fd, char* buf, int size) {
char *p = buf;
int count = size;
while (count > 0) {
int len = recv(fd, p, count, 0);
if (len == -1) {
return -1;
} else if (len == 0) {
return
} else {
pt += len; //有效内存处继续接收数据
count -= len; //还有多少没有接收
}
}
return size;
}
//单纯地接受数据
int recvMsg(int cfd, char** msg) {
//第二个参数是一个传出参数
//char head[4];
int len = 0;
readn(cfd, (char *)&len, 4);//先读取前面4个字节数
int count = ntohl(len);
printf("要接收的数据块的大小是:%d\n", count);
char* data = (char *)malloc(count + 1);
int length = readn(cfd, data, count);
if (length != count) {
printf("接收数据失败\n");
close(cfd);
free(data);
return -1;
}
data[count] = '\0';
*msg = data;
return length;
}
当然在实际处理过程中还可以对数据包里面的内容进行序列化处理,感兴趣的朋友可以尝试一下you。
Github:https://github.com/XiaoYaoheihei/Learning-and-Exercise/tree/main/Linux%20learning/socket/%E7%B2%98%E5%8C%85%E9%97%AE%E9%A2%98
参考内容:https://subingwen.cn/linux/tcp-data-package/#2-%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88