关于TCP
首先TCP和UDP一样属于传输层的协议,特点是面向连接、传输安全、基于流式传输协议。由于传输是基于流,所以发送方和接收方每次处理的数据量可以不一样,处理的频率也可以不一样,这不会影响数据的传输。
粘包问题
考虑一个需求如下:
有一个客户端和一个服务端,它们通过TCP套接字进行连接,客户端将数据发送给服务端,服务端来接收数据并进行解析。由前面所述,由于tcp是基于流的协议,因此,对于服务端在接收数据的时候,可能会存在以下几种情况:
- 最理想情况,Server一次收到一个完整的数据包
- 一次收到多个数据包
- 一次收到多个数据包 + 下个包的部分数据
- 一次收到半个数据包
上述现象就是TCP粘包问题,它并不是TCP的问题,而是我们程序员的问题,需要我们在使用的方式上进行一些约束定义,保证数据能够正确解析
- 使用标准的应用层协议(如http,https,websocket)来封装不定长的数据包,来避免粘包
- 使用特殊字符,如包头包尾使用特殊字符(效率低,需要对每个字节进行判断)
- 约定数据发送格式:数据头(存放数据体的大小,int类型,固定4个字节) + 数据体
处理粘包(方法3)
发送端处理方式
- 待发送数据长度N,动态申请一块内存,大小尾N+4(4是包头大小)
- 前4个字节写入数据的总长度(本地:小端序)(需要转换为网络字节序-大端序)
- 待发送数据使用memcpy到包头后边的地址空间中(不需要考虑字节序)
- 发送完整数据包,释放内存
客户端发送数据包,需要将一个数据包完全发送出去,因此在发送端需要一个发送一个完整数据包的函数,如下:
// 发送指定字节数
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){
close(fd);
return -1;
}
else if(len == 0)
{
continue;
}
buf += len;
count -= len;
}
return size;
}
// 发送数据包
// msg待发送数据,len待发送数据长度
int sendMessag(int fd, char* msg, int len){
if(!msg || len <= 0 || fc <= 0) return -1;
char* data = (char*)malloc(len + 4);
int bigEndianLen = htonl(len);
memcpy(data, &bigEndiaLen, 4);
memcpy(data+4, msg, len);
int ret = wrinten(fd, data, len + 4);
free(data);
return ret;
}
服务端处理方式
对应于客户端的处理方式,服务端也有类似的处理操作:
- 首先接收4个字节,获取数据体长度(考虑转字节序)
- 申请堆内存,存储接收数据
- 保存数据,释放内存
上述操作需要保证能接收到一个完整的数据包
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) return -1;
if(len == 0)
return size - count; // socket close,return number bytes of read
pt += len;
count -=len;
}
return size;
}
// 接收带数据头的数据包
int recvPacket(int fd, char** msg)
{
int len = 0;
readn(fd, (char*)&len, 4);
len = ntohl(len);
// 这个地方特殊,+1 是为了在最后面存放 '\0'
char* buf = (char*)malloc(len + 1);
int ret = readn(fd, buf, len);
if(ret != len){
close(fd);
free(buf);
return -1;
}
buf[len] = '\0';
*msg = buf;
return ret;
}