本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
[迁移博客]
粘包问题的准确描述
- 本篇博客主要描述的是发生在客户端与服务端基于 TCP 的套接字通信时产生的粘包问题
- 通信中数据的传输基于流,发送端与接收端对数据的处理数量与频率可以是不对等的,可以根据自身需求做决策
- 也就是说在实际的传输中包长是不确定的,若想取得指定长度的包就需要程序员自己努力
- TCP 是 面向连接的,安全的,流式
传输层协议
, 粘包问题的发生是由于程序的设计存在特殊的需求(比如每次处理指定数量的数据),才需要将包分开,才会存在粘包问题 - 所以粘包问题并不是 TCP 的设计存在缺陷,解决 TCP 粘包问题其实是解决
设计应用层协议
- 将粘包问题怪罪于 TCP 协议是对双方的侮辱
如何解决粘包问题
- 解决粘包问题就是
在服务端处理好客户端发来的不定长度数据包
解决方案:
-
使用标准的
应用层协议(http / https)
封装要传输的不定长的数据包 -
在每条数据的尾部添加特殊字符,遇到特殊字符则代表数据接收完毕
- 需逐字节处理,效率低
-
在发送数据块之前在数据块最前面添加一个固定大小的
数据头
,新数据包
由数据头
+数据块
组成 - 数据头:存储数据块
的总字节数,接收端先接受数据头
,然后再根据数据头接受相应大小的字节 - 数据块:原始内容 - HTTP 中的Content-Length
就起了类似的作用 -
数据块可能包含多种格式内容,此时可以用
JSON
来序列化
内容,我后面大概率会发一篇关于 cJson 的博客(挖坑)
新增数据头
法
发送端解决方案
- 根据待发送的数据长度
N
来动态申请
一块固定空间,空间大小为:N+4
(4 是包头占用的字节数) - 将待发送数据长度写入前四个字节,将其转换为
网络字节序(大端)
- 将待发送的数据拷贝到包头后面的空间中,将数据包
完整
发出(字符串不需要考虑字节序问题,但要考虑部分I/O
问题)- 部分 I/O 问题参考我的这篇博客
- 释放动态申请到的堆内存
SHOW ME THE CODE
-
发送端需实现一个
解决部分I/O的函数
与一个加包头的函数
-
发送指定字节数
的函数
/*
writen() 的参数与write()相同
循环使用了write()系统调用
确保请求的字节数总是能够得到全部传输
ssize_t writen(int fd, void *buffer, size_t count);
return number of bytes written, -1 on failure
*/
#include <unistd.h>
#include <sys/socket.h>
ssize_t writen(int fd, const char *msg, size_t size)
{
// 只读,所以const
const char *buf = msg;
int count = size;
// 循环处理, 直至全部发送
while(count > 0) {
// 专用于套接字的 I/O 系统调用
int len = send(fd, buf, count, 0);
if(len == -1) {
close(fd);
return -1;
}else if (len == 0){
continue;
}
// offset
buf += len;
// 计算剩余待发送量
count -= len;
}
return size;
}
加包头
函数
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
int sendMsg(int cfd, 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);
memcpy(data+4, msg, len);
// 发送数据
int ret = writen(cfd, data, len+4);
// 释放内存
free(data);
return ret;
}
接收端解决方案
- 接收数据头 4 字节数据,将其从网络字节序转换为
主机字节序
,得到待接收数据长度
- 根据得到的长度申请固定大小的堆内存,用来存储数据
- 接受固定数目的数据保存到申请到的内存之中
- 处理数据
- 释放堆内存
SHOW ME THE CODE
接收指定字节
函数
/*
readn() 的参数与read()相同
循环使用了read()系统调用
确保请求的字节数总是能够得到全部传输
ssize_t readn(int fd, void *buffer, size_t count);
return number of bytes read, 0 on EOF, -1 on failure
*/
#include <unistd.h>
#include <sys/socket.h>
ssize_t readn(int fd, char *buf, size_t 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 size-count;
}
p += len;
count -= len;
}
return size;
}
解包头函数
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
int recvMsg(int cfd, char **msg) {
// 首先接受报头
int len = 0;
readn(cfd, (char*)&len, 4);
len = ntohl(len);
printf("需接收的数据包大小: %d\n", len);
// 根据大小分配内存
char *buf = (char *)malloc(len + 1);
int ret = readn(cfd, buf, len);
if(ret != len) {
close(cfd);
free(buf);
return -1;
}
buf[len] = '\0';
*msg = buf;
return ret;
}
参考
- 《TLPI》
- TCP 数据粘包的处理