博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
miniFTP项目实战五
阅读量:3951 次
发布时间:2019-05-24

本文共 10084 字,大约阅读时间需要 33 分钟。

项目简介:

在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进程来为客户提供服务。同时每个ftp服务进程配套了nobody进程(内部私有进程),主要是为了做权限提升和控制。
实现功能:
除了基本的文件上传和下载功能,还实现模式选择、断点续传、限制连接数、空闲断开、限速等功能。
用到的技术:
socket、I/O复用、进程间通信、HashTable
欢迎技术交流q:2723808286
项目开源!!!

在这里插入图片描述

文章目录

5.1 下载文件 断点续载

RETR命令是从服务器下载文件,命令流程如下:

在这里插入图片描述

通过RETR 命令来指定下载文件,当传输中断的时候客户端保存已下载文件的偏移量,后面续传的时候从REST的位置继续传输,从而达到断点续载的效果。

加锁读取文件

在获取数据传输通道之后,服务器要打开文件,以只读的方式打开文件,并加读锁:

//打开文件 只读int  fd = open(sess->arg, O_RDONLY);if (fd == -1) {
ftp_relply(sess, FTP_FILEFAIL, "Failed to open file.");}//加读锁int ret = lock_file_read(fd);if (ret == -1) {
ftp_relply(sess, FTP_FILEFAIL, "Failed to open file."); return ;}

加锁的具体操作如下:

int lock_file_read(int fd){
int ret; struct flock the_lock; memset(&the_lock, 0, sizeof(the_lock)); the_lock.l_type = F_RDLCK; //加锁类型为:读锁 the_lock.l_whence = SEEK_SET; //文件头部开始加锁 the_lock.l_start = 0; //文件头开始的偏移地址开始加锁 the_lock.l_len = 0; //加锁的范围 0表示将整个文件加锁 do {
ret = fcntl(fd, F_SETLKW, &the_lock); //文件描述符 加锁 锁相关的结构体 } while (ret < 0 && errno == EINTR); //排除信号中断 return ret;}

定位到断点

如果是之前传输中断之后的,会通过REST来设置断点,在do_rest函数中,保存断点位置到sess中:

static void do_rest(session_t *sess){
//字符串转换为long long sess->restart_pos = str_to_longlong(sess->arg); char text[1024] = {
0}; sprintf(text, "Restart position accepted (%lld)", sess->restart_pos); ftp_relply(sess, FTP_RESTOK, text);}

然后通过lseek函数定位到断点处:

//定位到断点if (offset != 0) {
ret = lseek(fd, offset, SEEK_SET); //从头开始 if (ret == -1) {
ftp_relply(sess, FTP_FILEFAIL, "Failed to open file."); return ; }}

传输文件

文件本身是放在磁盘中存放的,从磁盘到内核是通过DMA取得,这里我们从内核再读取文件,通过read系统调用来读取文件,然后通过writen写入内核,通过socket发送出去:

char buf[4096];int flag = 0;while (1) {
ret = read(fd, buf, sizeof(buf)); if (ret == -1) {
//被中断打断 继续执行 其他情况退出 if (errno == EINTR) continue; else {
flag = 1; break; } } else if (ret == 0) {
//成功读完 flag = 0; break; } if (writen(sess->data_fd, buf, sizeof(buf)) != ret) {
//写入失败 flag = 2; break; }}

但是这样传输文件得话,从内核到用户空间,再从用户空间到内核,开销太大了。

下面采用直接在内核中完成拷贝得方式:

long long bytes_to_send = sbuf.st_size;    if (offset > bytes_to_send) {
bytes_to_send = 0; } else {
bytes_to_send -= offset; } sess->bw_transfer_start_sec = get_time_sec(); sess->bw_transfer_start_usec = get_time_usec(); while (bytes_to_send) {
int num_this_time = bytes_to_send > 4096 ? 4096 : bytes_to_send; //决定当此发送的数据字节数 ret = sendfile(sess->data_fd, fd, NULL, num_this_time); if (ret == -1) {
flag = 2; break; } limit_rate(sess, bytes_to_send, 0); //限速 if (sess->abor_received) {
flag = 2; break; } bytes_to_send -= ret; //更新要发送的Byte } if (bytes_to_send == 0) {
flag = 0; } else {
flag = 2; }

主要是通过sendfile函数实现,其函数原型如下:

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

sendfile 函数在两个文件描写叙述符之间直接传递数据(全然在内核中操作,传送),从而避免了内核缓冲区数据和用户缓冲区数据之间的拷贝,操作效率非常高,被称之为零拷贝。

参考:https://blog.csdn.net/u010649766/article/details/80339988

几种零拷贝技术的对比:https://mp.weixin.qq.com/s/eHhhW8j3vs8puMkC5zoIpQ

综合起来看一下RETR的操作函数:

static void do_retr(session_t *sess){
//获取数据传输通道的fd if (get_transfer_fd(sess) == 0) {
return ; } long long offset = sess->restart_pos; sess->restart_pos = 0; //打开文件 只读 int fd = open(sess->arg, O_RDONLY); if (fd == -1) {
ftp_relply(sess, FTP_FILEFAIL, "Failed to open file."); } //加读锁 int ret = lock_file_read(fd); if (ret == -1) {
ftp_relply(sess, FTP_FILEFAIL, "Failed to open file."); return ; } //判断文件类型 是否是普通文件 struct stat sbuf; ret = fstat(fd, &sbuf); if (!S_ISREG(sbuf.st_mode)) {
//不是普通文件 ftp_relply(sess, FTP_FILEFAIL, "Failed to open file."); return ; } //定位到断点 if (offset != 0) {
ret = lseek(fd, offset, SEEK_SET); //从头开始 if (ret == -1) {
ftp_relply(sess, FTP_FILEFAIL, "Failed to open file."); return ; } } //回应150 ASCII与二进制传输唯一的区别就是 :是否对\r\n处理 char tmp[1024] = {
0}; if (sess->is_ascii) {
//ASCII模式 sprintf(tmp, "Opening ASCII mode data connection for %s (%lld bytes).", sess->arg, (long long)sbuf.st_size); } else {
//二进制模式 sprintf(tmp, "Opening BINARY mode data connection for %s (%lld bytes).", sess->arg, (long long)sbuf.st_size); } ftp_relply(sess, FTP_DATACONN, tmp); //下载文件 //从内核到用户空间 再到内核 char buf[4096]; int flag = 0; // while (1) {
// ret = read(fd, buf, sizeof(buf)); // if (ret == -1) { //被中断打断 继续执行 其他情况退出 // if (errno == EINTR) continue; // else {
// flag = 1; // break; // } // } else if (ret == 0) { //成功读完 // flag = 0; // break; // } // if (writen(sess->data_fd, buf, sizeof(buf)) != ret) { //写入失败 // flag = 2; // break; // } // } //直接在内核中完成拷贝 long long bytes_to_send = sbuf.st_size; if (offset > bytes_to_send) {
bytes_to_send = 0; } else {
bytes_to_send -= offset; } sess->bw_transfer_start_sec = get_time_sec(); sess->bw_transfer_start_usec = get_time_usec(); while (bytes_to_send) {
int num_this_time = bytes_to_send > 4096 ? 4096 : bytes_to_send; //决定当此发送的数据字节数 ret = sendfile(sess->data_fd, fd, NULL, num_this_time); if (ret == -1) {
flag = 2; break; } limit_rate(sess, bytes_to_send, 0); //限速 if (sess->abor_received) {
flag = 2; break; } bytes_to_send -= ret; //更新要发送的Byte } if (bytes_to_send == 0) {
flag = 0; } else {
flag = 2; } close(sess->data_fd); sess->data_fd = -1; close(fd); if (flag == 0) {
ftp_relply(sess, FTP_TRANSFEROK, "Transfer complete."); } else if (flag == 1) {
//读取失败 ftp_relply(sess, FTP_BADSENDFILE, "Failure reading from local file."); } else if (flag == 2) {
//发送失败 ftp_relply(sess, FTP_BADSENDNET, "Failure writting to networks stream."); } check_abor(sess); start_cmdio_alarm(); //数据传输完毕之后 重新启动控制通道时钟}

5.2 上传文件 断点续传

上传文件有三种方式:

  • STOR + REST:断点续传
  • APPE:追加
  • STOR:覆盖

直接通过upload_common来实现上传文件操作

/* 上传命令      下载命令 *  STOR          RETR *  * 断点续传      断点续载 *  REST          REST *  STOR          RETR *  *  APPE * 用upload_common() 来区别APPE断点续传与REST+STOR断点续传 *  */static void do_stor(session_t *sess){
upload_common(sess, 0);}static void do_appe(session_t *sess){
upload_common(sess, 1);}

具体操作如下:

void upload_common(session_t *sess, int is_append){
int flag = 0; //获取数据传输通道的fd if (get_transfer_fd(sess) == 0) {
return ; } //保存断点ian long long offset = sess->restart_pos; sess->restart_pos = 0; //以写入的方式创建文件 int fd = open(sess->arg, O_CREAT | O_WRONLY, 0666); if (fd == -1) {
ftp_relply(sess, FTP_UPLOADFAIL, "Could not create the file."); return ; } //加写锁 int ret = lock_file_write(fd); if (ret == -1) {
ftp_relply(sess, FTP_UPLOADFAIL, "Could not create the file."); return ; } //三种上传方式:STOR、STOR+REST、APPE if (!is_append && offset == 0) {
//覆盖文件 ftruncate(fd, 0); if (lseek(fd, 0, SEEK_SET) < 0) {
ftp_relply(sess, FTP_UPLOADFAIL, "Could not create the file."); return ; } } else if (!is_append && offset != 0) {
//REST+STOR 断点续传 if (lseek(fd, offset, SEEK_SET) < 0) {
ftp_relply(sess, FTP_UPLOADFAIL, "Could not create the file."); return ; } } else if (is_append) {
//APPE 追加到末尾 if (lseek(fd, 0, SEEK_END) < 0) {
ftp_relply(sess, FTP_UPLOADFAIL, "Could not create the file."); return ; } } //获取文件状态 struct stat sbuf; ret = fstat(fd, &sbuf); if (!S_ISREG(sbuf.st_mode)) {
//不是普通文件 ftp_relply(sess, FTP_UPLOADFAIL, "Could not create the file."); return ; } //回应150 ASCII与二进制传输唯一的区别就是 :是否对\r\n处理 char tmp[1024] = {
0}; if (sess->is_ascii) {
//ASCII模式 sprintf(tmp, "Opening ASCII mode data connection for %s (%lld bytes).", sess->arg, (long long)sbuf.st_size); } else {
//二进制模式 sprintf(tmp, "Opening BINARY mode data connection for %s (%lld bytes).", sess->arg, (long long)sbuf.st_size); } ftp_relply(sess, FTP_DATACONN, tmp); //接收文件 从data_fd中接收数据,放在buf中,然后写入文件 char buf[1024]; int data_fd = sess->data_fd; sess->bw_transfer_start_sec = get_time_sec(); sess->bw_transfer_start_usec = get_time_usec(); while (1) {
ret = read(data_fd, buf, sizeof(buf)); if (ret == -1) {
//被中断打断 继续执行 其他情况退出 if (errno == EINTR) continue; else {
flag = 2; //从数据socket读取失败 break; } } else if (ret == 0) {
//成功读完 flag = 0; break; } limit_rate(sess, ret, 1); //判断上传限速 if (sess->abor_received) {
//数据传输过程中的ABOR处理 给426回复 flag = 2; break; } if (writen(fd, buf, sizeof(buf)) != ret) {
//写入到本地文件失败 flag = 1; break; } } close(sess->data_fd); sess->data_fd = -1; close(fd); if (flag == 0) {
ftp_relply(sess, FTP_TRANSFEROK, "Transfer complete."); } else if (flag == 1) {
//写入本地失败 ftp_relply(sess, FTP_BADSENDFILE, "Failure writting to local file."); } else if (flag == 2) {
//读取网络失败 ftp_relply(sess, FTP_BADSENDNET, "Failure reading from networks stream."); } check_abor(sess); //传输完成后检查ABOR start_cmdio_alarm(); //数据传输完毕之后 重新启动控制通道时钟}
你可能感兴趣的文章
解决报错 “build.sh /bin/bash^M: 坏的解释器:没有那个文件或目录”
查看>>
linux学习之tr操作符用法
查看>>
shell的dirname $0和readlink用法
查看>>
设计模式——外观模式详解
查看>>
Android APP架构设计——MVC、MVP和MVVM介绍
查看>>
Android APP架构设计——MVP的使用示例
查看>>
Android开发——布局性能优化的一些技巧(一)
查看>>
NetWork——关于TCP协议的三次握手和四次挥手
查看>>
设计模式——观察者模式详解
查看>>
Java集合——LinkedList源码详解
查看>>
Android开发——LinearLayout和RelativeLayout的性能对比
查看>>
NetWork——关于HTTP、HTTPS的知识总结(以及Android网络优化建议)
查看>>
Android开发——支付宝和微信支付快速接入流程
查看>>
NetWork——TCP的流量控制和拥塞控制
查看>>
Android开发——解决方法数越界问题
查看>>
算法相关——Java排序算法之希尔排序(五)
查看>>
算法相关——Java排序算法之选择排序(六)
查看>>
Android开发—— 热修复Tinker源码浅析
查看>>
算法相关——Java排序算法之堆排序(七)
查看>>
Android开发——Volley的使用详解
查看>>