Socket网络编程中的sendfile传送文件

6 篇文章 0 订阅
3 篇文章 0 订阅

前言

文件传送是聊天室的最后一个功能了,这篇博客依然建立在聊天室的项目背景之上。
在我们可以使用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

我们可以把发送端写到服务器里,接收端写到客户端里,这样就可以实现 服务器------>客户端


关于 客户端—>客户端 的实时传输文件模式,在掌握了上述的文件传送技术之后,也可以很轻松的实现出来,本文没有对此编写相关代码。
基本思路与 客户端—>服务器 基本一致。只要想办法让服务器得到两个客户端的套接字,一端发送一端接收,服务器作为一个中转即可。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值