• 拆包粘包问题的解决方案


    拆包粘包处理

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

    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进制后才能进行相加。

  • 相关阅读:
    一个很香的python练习网站
    关于接口测试用例设计的一些思考
    pytest、tox、Jenkins实现python接口自动化持续集成
    django实战商城项目注册业务实现
    面试测试开发被问到数据库索引不知道怎么办?这篇文章告诉你
    python框架Django实战商城项目之用户模块创建
    python框架Django实战商城项目之工程搭建
    golang在gitlab中的工作流
    kubernetes extension point
    kubernetes controller 实现
  • 原文地址:https://www.cnblogs.com/seaman1900/p/15140527.html
Copyright © 2020-2023  润新知