C/S文件传输
本文教你如何使用C语言在Linux服务器上教你传输文件。
三个概念
首先理解三个概念:
- 整包
- 拆包
- 粘包
整包:
- 整包是指一个完整的数据包,它在传输过程中没有被拆分或合并。
- 在网络通信中,我们希望数据按照预定的格式被发送和接收,以确保正确性和完整性。
粘包:
- 粘包是指连续发送的数据包在接收端被合并成一个或多个包。
- 例如,如果发送端连续发送了两个数据包(Packet1和Packet2),接收端可能一次性收到它们的组合,而不是分开的两个包。
- 粘包的根本原因是TCP协议是流式协议,没有明确的分界标志,需要我们人为地划分边界。
拆包:
- 拆包是指一个数据包被拆分成多个小包进行发送。
- 例如,如果一个包的大小超过了缓冲区的限制,发送端会将其拆分成多个小包,接收端需要重新组装这些小包以还原原始数据。
如何解决粘包问题:
- 固定包长的数据包:规定每个协议包的长度是固定的,接收端按照固定长度来解析数据。
- 以指定字符为包的结束标志:例如,使用“\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 ;
}
根据注释就可以理解代码了。
对于收数据的一方,有一些细节需要介绍,对于粘包的处理方式,我们需要介绍一下:
我们创建三个与数据包相同的空间:
offset
是一个偏移量,除此之外,还有一个recv_size
这三个数据包的作用以及offset的作用等会再说。
首先,我们将所有新收到的数据全部放入packet_t
中,受到的数据长度记录在recv_size
中。
接下来分析一下收到数据之后的情况:
- recv_size + offset == packet_size,也就是收到的数据正好能够满足
packet
的长度。如下:
- recv_size + offset < packet_size, 也就是收到的数据小于
packet
的长度。我们将收到的数据放进packet,更新offset
的值,然后收下一个包,如图:
- recv_size + offset > packet_size,这个时候需要进行拆包,从新收到的包中取出能够补全
packet
大小的数据放入packet
,剩下的包放入packet_pre
,如图:
下一次收包之前先将packet_pre
中的数据放到packet
中,然后继续第二步的操作。
如上。
接下来看看收包程序。
收包程序:
// 传入接收数据的套接字
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 拜拜~~~