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

devtools/2024/10/22 18:30:50/

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 拜拜~~~


http://www.ppmy.cn/devtools/31633.html

相关文章

Git-flow分支管理与Aone-flow分支管理对比

Git-flow分支管理与Aone-flow分支管理对比 git-flow分支管理&#xff1a; master: 主分支&#xff0c;主要用来版本发布。 hotfix&#xff1a;线上 bug 紧急修复用到的临时分支。这个分支用来修复主线master的BUG release&#xff08;预发布分支&#xff09;&#xff1a;rel…

引领农业新质生产力,鸿道(Intewell®)操作系统助力农业机器人创新发展

4月27日至29日&#xff0c;2024耒耜国际会议在江苏大学召开。科东软件作为特邀嘉宾出席此次盛会&#xff0c;并为江苏大学-科东软件“农业机器人操作系统”联合实验室揭牌。 校企联合实验室揭牌 在开幕式上&#xff0c;江苏大学、科东软件、上交碳中和动力研究院、遨博智能研究…

代码随想录算法训练营Day29 | 491.递增子序列、46.全排列、47.全排列 II | Python | 个人记录向

注&#xff1a;5.1—5.3放假。 本文目录 491.递增子序列做题看文章 46.全排列做题看文章 47.全排列 II做题看文章 以往忽略的知识点小结个人体会 491.递增子序列 代码随想录&#xff1a;491.递增子序列 Leetcode&#xff1a;491.递增子序列 做题 写了一会&#xff0c;但捋不…

为什么 IP 地址通常以 192.168 开头?(精简版)

网络通讯的本质就是收发数据包。如果说收发数据包就跟收发快递一样。IP地址就类似于快递上填的收件地址和发件地址一样&#xff0c;路由器就充当快递员的角色&#xff0c;在这个纷繁复杂的网络世界里找到该由谁来接收这个数据包&#xff0c;所以说&#xff1a;IP地址就像快递里…

链表的回文结构(详解)

链表的回文结构&#xff08;详解&#xff09; 题目&#xff1a; 链表的回文结构 对于一个链表&#xff0c;请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法&#xff0c;判断其是否为回文结构。 给定一个链表的头指针A&#xff0c;请返回一个bool值&#xff0c;代表…

第十三届蓝桥杯国赛真题 Java C 组【原卷】

文章目录 发现宝藏试题 A: 斐波那契与 7试题 B: 小蓝做实验试题 C: 取模试题 D: 内存空间试题 E \mathrm{E} E : 斐波那契数组试题 F: 最大公约数试题 G: 交通信号试题 I: 打折试题 J: 宝石收集 发现宝藏 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#x…

基于Python的LSTM网络实现单特征预测回归任务(TensorFlow)

单特征&#xff1a;数据集中只包含2列&#xff0c;时间列价格列&#xff0c;仅利用价格来预测价格 目录 一、数据集 二、任务目标 三、代码实现 1、从本地路径中读取数据文件 2、数据归一化 3、创建配置类&#xff0c;将LSTM的各个超参数声明为变量&#xff0c;便于后续…

Docker-Compose编排lnmp(dockerfile) 完成Wordpress

目录 一、创建nginx镜像 二、创建mysql镜像 三、创建php镜像 四、启动wordpress 五、安装Compose 六、准备环境 ​编辑 七、编写docker-compose.yml 八、启动并运行 九、浏览器访问 一、创建nginx镜像 #基于基础镜像 FROM centos:7 #用户信息 MAINTAINER this is ngi…