前言
文件传送是聊天室的最后一个功能了,这篇博客依然建立在聊天室的项目背景之上。
在我们可以使用Socket套接字在两个客户端进行通信的基础上,传送文件的难点在于如何接收以及文件的离线发送。
文件发送主要是以下两种模式:
- 客户端—>客户端(实时文件发送)
- 客户端—>服务器—>客户端(离线文件发送)
首先我们先确定发送文件所用的函数:sendfile()
在搜索sendfile函数的时候,好像大部分博客都在介绍sendfile零拷贝的优点,但却没有找到我最需要的东西:它该如何使用?
SYNOPSIS
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
讲得再多也不如一段例程来的直接。下面是第一版的代码,由于服务器与客户端之间的连接步骤比较繁琐,所以代码放在文末,如果有需要可以直接拉到下面去copy,这里只展示传送文件部分的代码。
客户端—>服务器
客户端(发送端):
#define BUFSIZE 1024
char *file_name;
char buf[BUFSIZE];
char file_path[BUFSIZE];
printf("请输入完整的路径名:");
scanf("%s", file_path);
//判断路径名是否正确
if(stat(file_path, &buffer) == -1)
{
printf("---非法的路径名---\n");
continue;
}
//获取文件名
file_name = basename(file_path);
//把文件名发送给接收端
strcpy(buf, file_name);
write(cfd, buf, BUFSIZE); //这里采用定长协议来发送
//打开文件
int fp = open(file_path, O_CREAT|O_RDONLY, S_IRUSR|S_IWUSR);
//开始发送文件
printf("---开始传送文件:%s---\n", buf);
sendfile(cfd, fp, 0, buffer.st_size);
printf("---文件<%s>传送成功---\n", buf);
//关闭文件描述符与套接字
close(fp);
close(cfd);
服务器端(接收端)
#define BUFSIZE 1024
char file_name[BUFSIZE];
char file_path[BUFSIZE];
char buf[BUFSIZE];
//接收文件名
read(cfd, file_name, BUFSIZE);
//如果想把文件接收到一个特定的目录下的话,可以多加这样一个步骤
//默认是保存到当前文件夹
//现在是保存到当前文件夹下的file_buf/目录里
sprintf(file_path, "./file_buf/%s", file_name);
//创建文件
FILE *fp = fopen(file_path, "wb");
if (fp == NULL)
{
perror("Can't open file");
exit(1);
}
//把数据写入文件
while((n = read(cfd, buf, BUFSIZE)) > 0)
{
fwrite(buf, sizeof(char), n, fp);
}
//关闭文件描述符与套接字
fclose(fp);
close(cfd);
到此为止,我们初步传送文件的任务已经完成。
但是,当接收端的read返回值为0时,循环while才会停止。
注意,并不是将套接字中的数据读完之后返回0,而是阻塞等待数据。
直到发送端的套接字被关闭之后发送来FIN包时,read才会返回0,终止循环。
这里的内容详细可以参考:
https://zfl9.github.io/c-socket.html
但是这样显然是不足以完成我们的需求的。作为一个聊天室,如果套接字断开的话,还要如何通信呢。所以我们需要设计一个判断点来判断文件是否已经传送完毕,而不是让read()去阻塞等待FIN包。
在发送端把文件的长度提前发送至接收端
很简单,我们把文件长度与文件名存入一个结构体中
struct len_name
{
unsigned int len;
char name[NAME_MAX];
};
但是write()、read()只能发送和接收字符串类型的包,所以我们需要在这里做一些类型转换
客户端(发送端)
struct len_name ln;
ln.len = buffer.buffer.st_size;
strcpy(ln.name, file_name);
//类型转换
//将结构体的内存逐字节地copy到buf内
char buf[BUFSIZE];
memcpy(buf, &ln, sizeof(ln));
//发送
write(cfd, buf, BUFSIZE);
服务器端(接收端)
struct len_name ln;
char buf[BUFSIZE];
//接收
read(cfd, buf, BUFSIZE);
//类型转换
memcpy(&ln, buf, sizeof(ln));
接下来,只需要把接收端的接收文件部分稍作修改,就可以解决上面出现的问题了。
服务器端(接收端)
char buf[BUFSIZE];
int len;
unsigned int sum = 0;
FILE *fp = fopen(temp, "wb");
while((n = read(cfd, buf, BUFSIZE)) > 0)
{
fwrite(buf, sizeof(char), n, fp);
sum += n;
if(sum >= ln.len) //当接收到足够的长度的时候,就说明文件已读取完毕
{
break;
}
}
完整的代码在这里参考:
https://github.com/hiyoyolumi/ChatRoom/tree/master/file_test
我们可以把发送端写到服务器里,接收端写到客户端里,这样就可以实现 服务器------>客户端
关于 客户端—>客户端 的实时传输文件模式,在掌握了上述的文件传送技术之后,也可以很轻松的实现出来,本文没有对此编写相关代码。
基本思路与 客户端—>服务器 基本一致。只要想办法让两个客户端分别得到对方客户端的套接字,一端发送一段接收,直接进行文件传送即可。