Linux服务器开发:C/S文件传输,整包,拆包,粘包问题

发布于:2024-05-06 ⋅ 阅读:(33) ⋅ 点赞:(0)

C/S文件传输

本文教你如何使用C语言在Linux服务器上教你传输文件。

三个概念

首先理解三个概念:

  • 整包
  • 拆包
  • 粘包
  1. 整包

    • 整包是指一个完整的数据包,它在传输过程中没有被拆分或合并。
    • 在网络通信中,我们希望数据按照预定的格式被发送和接收,以确保正确性和完整性。
  2. 粘包

    • 粘包是指连续发送的数据包在接收端被合并成一个或多个包。
    • 例如,如果发送端连续发送了两个数据包(Packet1和Packet2),接收端可能一次性收到它们的组合,而不是分开的两个包。
    • 粘包的根本原因是TCP协议是流式协议,没有明确的分界标志,需要我们人为地划分边界。
  3. 拆包

    • 拆包是指一个数据包被拆分成多个小包进行发送。
    • 例如,如果一个包的大小超过了缓冲区的限制,发送端会将其拆分成多个小包,接收端需要重新组装这些小包以还原原始数据。

如何解决粘包问题

  • 固定包长的数据包:规定每个协议包的长度是固定的,接收端按照固定长度来解析数据。
  • 以指定字符为包的结束标志:例如,使用“\r\n”作为包的结束标志,接收端根据这个标志来划分数据包。
  • 包头 + 包体格式:包含固定大小的包头和一个字段来说明包体的大小,接收端根据包头中的信息来解析数据。

看完上面这些相信小白还是很懵,没关系,今天我的文章会配合代码为你详细讲解,另外,我还会提出一种新的解决粘包问题的方式。

数据包的发送与接收

如果我们要从网络上发送一个数据给对方,要考虑下面几点:

  • 发给谁?
  • 如何发?怎么发?(发送协议)
  • 数据包的组成形式?
  • 如何收?
  • 如何解析数据包?

大概就是上面的五点内容。

如何发送数据包?

肯定是利用套接字来收发数据,接下来就是思考采用的协议。

如何发?怎么发?(发送协议)

这里选择使用TCP传输协议,因为对于文件传输,我们需要保证文件内容的可靠性和传输的稳定性,而TCP协议是稳定可靠的,因此需要选择这个协议。

数据包的组成形式?

这是一个重点。假设我们不知道数据包的形式,我们可以先假设数据就是一个字符串数组,现在知道数组的长度。我们在发送端的套接字的写缓冲区中写入这样的一个数组,然后在读端的读缓冲区读取这样的一个数组。

然后来思考其中是否会有什么问题。

首先,写缓冲区有没有问题?对于写缓冲区的流程,似乎就是:
写缓冲区流程
数据的写入并不会有什么同步、顺序,数据丢失的可能,看起来没什么问题。

然后是读取端的读缓冲区:

写过程
总体过程如下:
总体过程
思考以下读取端有没有什么问题:

首先,读取端的socket会受到发送端送来的数据,如果发送端没有发送数据,接收端就会进入一个阻塞的状态,因此可以发现一个同步条件,但导致阻塞并不会导致数据损坏,所以没有问题。

然后思考每次读数据的过程,因为socket中的数据被读取时并没有详细规定一次读取的值,也就是说,对面发送过来的每一个包到了socket中时,会被粘住,无法分辨出每次能取出多少数据包。

这个就需要我们进行拆包粘包的处理了。

数据处理程序

这里介绍一种相对于大多数处理粘包的方法更加规范的方法,在设计程序的过程中会慢慢看到。

首先,对于数据包,我们不能单单定义出一个数组,我们需要以下的一个数据包结构:

;	struct file_msg {
;		char name[512] 	// 文件名
;		char buff[4096] // 实际数据包内容
;		long size 		// 数据包长度
;		int msgsize 	// 收数据时的数据包长度
;	}

首先先来写发包程序:

// 传入两个参数,一个是用于通信的套接字,一个是文件的路径名称
void send_file(int sockfd, const char *filename) {
        FILE *fp = NULL; // 用于打开本地的文件
        // 包结构的定义
        struct file_msg filemsg;
        size_t size = 1;
        char *p;
        bzero(&filemsg, sizeof(filemsg));
        // 以二进制形式读取文件
        if ((fp = fopen(filename, "rb")) == NULL) {
                perror("fopen");
                return ;
        }

        // 将文件指针移动到末尾
        fseek(fp, 0, SEEK_END);
        // 返回指针的位数,得到了文件长度
        filemsg.size = ftell(fp);
        // 将指针重新设置回开头 
        fseek(fp, 0, SEEK_SET);
        
        // 从文件的路径获取文件名称
        strcpy(filemsg.name, (p = strrchr(filename, '/')) ? p + 1 : filename);

        // 从fp中读取文件到buff中,一次性读取最大值,直到读到文件结尾
        int loc = 0; // 偏移量,记录上一次读数据的指针位置
        int read_size;
        while (size) {
        		// 读一次,一次读buff大小的数据
                size = fread(filemsg.buff, sizeof(filemsg.buff), 1, fp);
                read_size = ftell(fp) - loc;  // 这次读完后的指针减去上一次读的末尾,就是长度,这样做的目的是可靠稳定的
                filemsg.msgsize = read_size; // 存到结构体中
                loc = ftell(fp); // 更新这次的指针位置
                send(sockfd, (void *)&filemsg, sizeof(filemsg), 0); // 发送
                bzero(filemsg.buff, sizeof(filemsg.buff)); // 初始化
        }
        return ;
}

根据注释就可以理解代码了。

对于收数据的一方,有一些细节需要介绍,对于粘包的处理方式,我们需要介绍一下:

我们创建三个与数据包相同的空间:

数据1
offset是一个偏移量,除此之外,还有一个recv_size

这三个数据包的作用以及offset的作用等会再说。

首先,我们将所有新收到的数据全部放入packet_t中,受到的数据长度记录在recv_size中。
接下来分析一下收到数据之后的情况:

  1. recv_size + offset == packet_size,也就是收到的数据正好能够满足packet的长度。如下:
    1
  2. recv_size + offset < packet_size, 也就是收到的数据小于packet的长度。我们将收到的数据放进packet,更新offset的值,然后收下一个包,如图:
    2
    3
  3. recv_size + offset > packet_size,这个时候需要进行拆包,从新收到的包中取出能够补全packet大小的数据放入packet,剩下的包放入packet_pre,如图:
    4
    5
    下一次收包之前先将packet_pre中的数据放到packet中,然后继续第二步的操作。
    6
    如上。

接下来看看收包程序。

收包程序:

// 传入接收数据的套接字
void recv_file(int sockfd) {
		// 三个结构体
        struct file_msg packet, packet_t, packet_pre;
        int packet_size = sizeof(packet); // 包长
        int offset = 0, recv_size = 0, cnt = 0; // 偏移量,cnt指的是收到的包的数量
        FILE *fp = NULL;
        
        // 初始化
        bzero(&packet, sizeof(packet));
        bzero(&packet_t, sizeof(packet_t));
        bzero(&packet_pre, sizeof(packet_pre));

        // 收所有整包
        while (1) {
                bzero(&packet_t.buff, sizeof(packet_t.buff));
                // 在开始收一个新的包之前,先检查packet_pre中是否有数据,如果有之间拷贝到packet中
                if (offset) {
                        memcpy(&packet, &packet_pre, offset);
                }

                // 收一个整包
                while ((recv_size = recv(sockfd, (void *)&packet_t, packet_size, 0)) > 0) {
                        if (offset + recv_size == packet_size) {
                                // 整包
                                memcpy((char *)&packet + offset, (char *)&packet_t, recv_size);
                                offset = 0;
                                break;
                        } else if (offset + recv_size < packet_size) {
                                // 拆包
                                memcpy((char *)&packet + offset, (char *)&packet_t, recv_size);
                                offset += recv_size;
                        } else if (offset + recv_size > packet_size) {
                                // 粘包
                                int wait = packet_size - offset;
                                memcpy((char *)&packet + offset, &packet_t, wait);
                                offset = recv_size - wait;
                                memcpy((char *)&packet_pre, (char *)&packet_t + wait, offset);
                                break;
                        }
                }
                // 判断是否收到了包,如果没有直接退出
                if (recv_size <= 0) break;
                // 到这里已经收到一个整包了
                // 判断是不是第一个包,先检查有没有文件,如果没有直接创建。
                if (!cnt) {
                        char name[1024] = {0};
                        sprintf(name, "./data/%s", packet.name);
                        if ((fp = fopen(name, "wb")) == NULL) {
                                perror("fopen");
                                return ;
                        }

                }
                ++cnt;
                // 数据写入
                int wsize = fwrite(packet.buff, 1, packet.msgsize, fp);
                bzero(&packet.buff, sizeof(packet.buff));
        }
        fclose(fp);
        return ;
}

:wq 拜拜~~~