拆包粘包处理
在传输大文件的时候,很显然并不能一次性直接把大文件交给对方,只能一个一个分割开来上交。
收集了一下网友的回答,专业一点:
-
应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包
-
应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包
-
进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包
-
接收方法不及时读取套接字缓冲区数据,这将发生粘包
其实会发生这些问题都在于TCP是一个流传输协议,一个字节一个字节这样子给你,它只保证了流的有序性,至于你的数据的结构,他根本不关心,数据的分割,边界划分的主动权就落在了我们程序员的手里,我下面的解决方案核心思想就是人为地给数据定义一个包(结构体),给定大小,最后拼在一块
但是,但是,但是,你如果整个程序只传输一个文件,是不可能发生这种问题的。
所以问题的根本在于对缓冲区的理解和TCP协议的理解,对概念本身不停争议是没有意义的,有这时间不如多看看源码,多看几本计算机原理的书。。。
情况1.传来的数据刚好是一个整包
此时的数据刚好能够传递给上层,于是直接给packet,而offset(即还差多少)设置为0
情况2.拆包
情况3.粘包
这样处理过后又回到了拆包的情况
代码复现
准备工作
头文件的编写,调试文件的编写
head.h
包含必要的系统头文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
color.h
调试信息带颜色便于分辨,看起来也赏心悦目一点
#ifndef _COLOR_H
#define _COLOR_H
#define NONE "e[0m" //清除颜色,即之后的打印为正常输出,之前的不受影响
#define BLACK "e[0;30m" //深黑
#define L_BLACK "e[1;30m" //亮黑,偏灰褐
#define RED "e[0;31m" //深红,暗红
#define L_RED "e[1;31m" //鲜红
#define GREEN "e[0;32m" //深绿,暗绿
#define L_GREEN "e[1;32m"//鲜绿
#define BROWN "e[0;33m" //深黄,暗黄
#define YELLOW "e[1;33m" //鲜黄
#define BLUE "e[0;34m" //深蓝,暗蓝
#define L_BLUE "e[1;34m" //亮蓝,偏白灰
#define PINK "e[0;35m" //深粉,暗粉,偏暗紫
#define L_PINK "e[1;35m" //亮粉,偏白灰
#define CYAN "e[0;36m" //暗青色
#define L_CYAN "e[1;36m" //鲜亮青色
#define GRAY "e[0;37m" //灰色
#define WHITE "e[1;37m" //白色,字体粗一点,比正常大,比bold小
#define BOLD "e[1m" //白色,粗体
#define UNDERLINE "e[4m" //下划线,白色,正常大小
#define BLINK "e[5m" //闪烁,白色,正常大小
#define REVERSE "e[7m" //反转,即字体背景为白色,字体为黑色
#define HIDE "e[8m" //隐藏
#define CLEAR "e[2J" //清除
#define CLRLINE "
e[K" //清除行
#endif
debug.h
调试的时候加入-D DBG选项即可显示调试信息
#ifdef DBG
#define DEBUG(fmt,args...) printf(fmt,##args)
#else
#define DEBUG(fmt,args...)
#endif
datatype.h
用于定义接收文件的数据类型,结构体
struct filePacket{
char name[50];//文件名
uint64_t size;//文件大小
char buff[4096];//文件块,故意设置成超过1460字节,这样就会被拆包
};//记得结构体后面一定要加 ' ; ' 不然编译会报错说哪里哪里缺少一个 ' ; '
函数编写
m_socket.c
套接字的创建和客户端的连接
#include "head.h"
#include "datatype.h"
#include "m_socket.h"
int socket_create(int port){
int sockfd;
if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0){
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port=htons(port);
addr.sin_addr.s_addr=inet_addr("0.0.0.0");
if(bind(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0){
return -1;
}
if(listen(sockfd,8)<0)return -1;
return sockfd;
}
int socket_connect(const char* ip,int port){
int sockfd;
if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0){
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port=htons(port);
addr.sin_addr.s_addr=inet_addr(ip);
if(connect(sockfd,(struct sockaddr*)&addr,sizeof(addr))<0){
return -1;
}
return sockfd;
}
filetransfer.c
文件的接收和发送函数
#include "head.h"
#include "datatype.h"
#include "color.h"
#include "debug.h"
int send_file(int sockfd,const char * name){
//调试输出文件名,看是否正常传参,避免传进来一个空字符
DEBUG(BLUE"<debug>: "NONE"file name:%s
",name);
FILE* fp;
if((fp=fopen(name,"rb"))==NULL){
DEBUG(RED"<error>: "NONE"fopen failed
");
return -1;
}
//初始化结构体
struct filePacket packet;
size_t spacket = sizeof(packet);
size_t sbuff = sizeof(packet.buff);
bzero(&packet,spacket);
//start 获取文件长度
fseek(fp,0L,SEEK_END);
packet.size = ftell(fp);
fseek(fp,0L,SEEK_SET);
//end 获取文件长度
//start 获取文件名
char filename[50];
char * p ;
p = strrchr(name,'/');
if(p==NULL){
strcpy(filename,name);
printf("file name :%s
",filename);
}else{
strcpy(filename,p+1);
}
//end 获取文件名
strcpy(packet.name,filename);
DEBUG(YELLOW"<debug>: "NONE"file name = %s,file size = %ld
",packet.name,packet.size);
//由于fread无法判断错误和文件末尾,需要额外的辅助feof和ferror来判断
while(!feof(fp)){
size_t rsize = fread(packet.buff,1,sbuff,fp);
if(ferror(fp)){
DEBUG(RED"<error>: "NONE"read occurs error
");
return -1;
}
ssize_t ssize = send(sockfd,(void*)&packet,spacket,0);
memset(packet.buff,0,sbuff);
DEBUG(YELLOW"<debug>: "NONE"ssize = %ld,rsize=%ld
",ssize,rsize);
}
return 0;
}
int recv_file(int sockfd){
//对应上面三张图的三个结构体
struct filePacket packet_pre,packet_temp,packet;
size_t spacket = sizeof(struct filePacket);
bzero(&packet_pre,spacket);
bzero(&packet_temp,spacket);
bzero(&packet,spacket);
int offset=0;
int count=0;
uint64_t file_size,total_size=0;
size_t buff_size;
size_t wsize;
FILE *fp;
while(1){
if(offset){
//如果offset不为零,说明上一次是粘包,拷贝到packet就行
memcpy((void*)(&packet),&packet_pre,offset);
}
memset(packet_pre.buff,0,sizeof(packet_pre.buff));
memset(packet_temp.buff,0,sizeof(packet_temp.buff));
//完成一个整包的接收
while(1){
ssize_t rsize = recv(sockfd,(void*)&packet_temp,spacket,0);
printf("rsize=%ld
",rsize);
if(rsize<=0)break;
if((offset+rsize)==spacket){
DEBUG(BLUE"<debug>: "NONE"收到一个整包
");
memcpy((char*)&packet+offset,&packet_temp,rsize);
offset=0;
break;
}else if((offset+rsize)<spacket){
DEBUG(YELLOW"<debug>: "NONE"发生了拆包
");
memcpy((char*)&packet+offset,&packet_temp,rsize);
offset+=rsize;
}else if((offset+rsize)>spacket){
DEBUG(L_PINK"<debug>: "NONE"发生了粘包
");
int need = spacket-offset;
memcpy((char*)(&packet+offset),(&packet_temp),need);
memcpy((char*)(&packet_pre),&packet_temp+need,rsize-need);
offset = rsize - need;
break;
}
}
//收到第一个包的时候,读取文件的基本信息:文件名,大小
if(count==0){
char path [512]={0};
sprintf(path,"%s/%s","./data",packet.name);
DEBUG(YELLOW"<debug>: "NONE"packet.name = %s,packet.size = %ld
",packet.name,packet.size);
file_size = packet.size;
if((fp=fopen(path,"wb"))==NULL){
DEBUG(RED"<error>: "NONE"fopen error occurs
");
return -1;
}
}
count++;
buff_size = sizeof(packet.buff);
if(file_size-total_size>buff_size){
wsize = fwrite(packet.buff,1,buff_size,fp);
}else{
wsize = fwrite(packet.buff,1,file_size-total_size,fp);
}
memset(packet.buff,0,buff_size);
memset(packet_temp.buff,0,sizeof(packet_temp.buff));
total_size += wsize;
DEBUG(L_PINK"<debug>: "NONE"total_size = %ld,wsize = %ld,file_size=%ld
",total_size,wsize,file_size);
if(total_size>=file_size){
DEBUG(YELLOW"<debug>: "NONE"文件传输完成
");
break;
}
}
fclose(fp);
return 0;
}
客户端和服务端的编写
这个很简单,服务端加一个fork,然后recv_file,客户端直接send_file就行了
服务端
#include "head.h"
#include "m_socket.h"
#include "filetransfer.h"
void check(int argc, int correctValue,char * proname);
int main(int argc,char **argv){
check(argc,2,argv[0]);
int server_listen,sockfd;
if((server_listen=socket_create(atoi(argv[1])))<0){
perror("socket_create");
exit(1);
}
printf("socket listening on port : %d
",server_listen);
while(1){
if((sockfd=accept(server_listen,NULL,NULL))<0){
perror("accept");
exit(1);
}
pid_t pid;
if((pid=fork())<0){
perror("fork");
exit(1);
}
if(pid){
close(sockfd);
continue;
}
close(server_listen);
int ret = recv_file(sockfd);
if(ret<0){
perror("recv_file");
}
break;
}
return 0;
}
void check(int argc, int correctValue,char * proname){
if(argc!=correctValue){
fprintf(stderr,"USAGE:%s is not correct!
",proname);
exit(1);
}
}
客户端
#include "head.h"
#include "m_socket.h"
#include "filetransfer.h"
int main(int argc , char ** argv){
int sockfd;
if((sockfd=socket_connect(argv[1],atoi(argv[2])))<0){
perror("connect");
exit(1);
}
send_file(sockfd,argv[3]);
return 0;
}
编写脚本直接编译
gcc server.c m_socket.c filepackage.c -o server -D DBG
gcc client.c m_socket.c filepackage.c -o client -D DBG
写在后面
当时为了验证我传输的文件是否是完整的,和原来的文件一模一样的,去搞了一个sshfs,结果浪费了一下午的时间没能够完成。(我之前的环境是WSL,不想在Windows上再装sshfs,我的小电脑要尽量保持精简哈哈哈)后来我换成了虚拟机本地跑sshfs,没想到,成了!浪费我一天时间!!!
还有一个更加可恶的,很不起眼的错误导致程序跑一半就挂了
错误示例
在filetransfer.c中的recv_file函数中处理整包时
memcpy((char*)(&packet+offset),&packet_temp,rsize);
正确示例
memcpy((char*)&packet+offset,&packet_temp,rsize);
当时为了好看易懂随手加的括号竟然成了隐患。。。
解释
说明一下,(个人理解,还未实验)就是c语言中的加号比较灵性,后面跟着的offset会自动乘上packet的大小(有点像数组那样)
摘自其他博客:
一般情况下声明一个数组之后,比如int array[5],数组名array就是数组首元素的首地址,而且是一个地址常量。但是,在函数声明的形参列表中除外。
-
在C中, 在几乎所有使用数组的表达式中,数组名的值是个指针常量,也就是数组第一个元素的地址。 它的类型取决于数组元素的类型: 如果它们是int类型,那么数组名的类型就是“指向int的常量指针“。——《C和指针》
-
在以下两中场合下,数组名并不是用指针常量来表示,就是当数组名作为sizeof操作符和单目操作符&的操作数时。 sizeof返回整个数组的长度,而不是指向数组的指针的长度。 取一个数组名的地址所产生的是一个指向数组的指针,而不是一个指向某个指针常量的指针。所以&a后返回的指针便是指向数组的指针,跟a(一个指向a[0]的指针)在指针的类型上是有区别的。——《C和指针》
-
“+1”就是偏移量问题:一个类型为T的指针的移动,是以sizeof(T)为移动单位。
即array+1:在数组首元素的首地址的基础上,偏移一个sizeof(array[0])单位。此处的类型T就是数组中的一个int型的首元素。由于程序是以16进制表示地址结果,array+1的结果为:0012FF34+1sizeof(array[0])=0012FF34+1sizeof(int)=0012FF38。即&array+1:在数组的首地址的基础上,偏移一个sizeof(array)单位。此处的类型T就是数组中的一个含有5个int型元素的数组。由于程序是以16进制表示地址结果,&array+1的结果为:0012FF34+1sizeof(array)=0012FF34+1sizeof(int)5=0012FF48。注意1sizeof(int)*5(等于00000014)要转换成16进制后才能进行相加。