拆包粘包问题的解决方案

拆包粘包处理

在传输大文件的时候,很显然并不能一次性直接把大文件交给对方,只能一个一个分割开来上交。
收集了一下网友的回答,专业一点:

  1. 应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包

  2. 应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包

  3. 进行MSS(最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包

  4. 接收方法不及时读取套接字缓冲区数据,这将发生粘包

其实会发生这些问题都在于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进制后才能进行相加。

原文地址:https://www.cnblogs.com/seaman1900/p/15140527.html