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

miniFTP项目实战一
miniFTP项目实战二
miniFTP项目实战三
miniFTP项目实战四
miniFTP项目实战五
miniFTP项目实战六

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(); //数据传输完毕之后 重新启动控制通道时钟
}

miniFTP项目实战五的更多相关文章

  1. miniFTP项目实战六

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  2. miniFTP项目实战三

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  3. miniFTP项目实战四

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  4. miniFTP项目实战二

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  5. React-Native 之 项目实战(五)

    前言 本文 有配套视频,可以酌情观看. 文中内容因各人理解不同,可能会有所偏差,欢迎朋友们联系我讨论. 文中所有内容仅供学习交流之用,不可用于商业用途,如因此引起的相关法律法规责任,与我无关,如文中内 ...

  6. miniFTP项目集合

    项目简介 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进 ...

  7. miniFTP项目实战一

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  8. 【.NET Core项目实战-统一认证平台】第十五章 网关篇-使用二级缓存提升性能

    [.NET Core项目实战-统一认证平台]开篇及目录索引 一.背景 首先说声抱歉,可能是因为假期综合症(其实就是因为懒哈)的原因,已经很长时间没更新博客了,现在也调整的差不多了,准备还是以每周1-2 ...

  9. 【.NET Core项目实战-统一认证平台】第五章 网关篇-自定义缓存Redis

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我们介绍了2种网关配置信息更新的方法和扩展Mysql存储,本篇我们将介绍如何使用Redis来实现网关的所有缓存功能,用到的文档及源码 ...

随机推荐

  1. Pytest学习笔记11-重复执行用例插件pytest-repeat

    前言 我们在平时做测试的时候,经常会遇到一些偶现的bug,通常我们会多次执行来复现此类bug,那么在自动化测试的时候,如何多次运行某个或某些用例呢,我们可以使用pytest-repeat这个插件来帮助 ...

  2. .NET 6 Preview 6 正式发布: 关注网络开发

    微软.NET 团队的项目经理在博客上发布了.NET 6 Preview 6,  在候选发布阶段之前的倒数第二个预览版,也就是8月份还会发布一个Preview 7,9月份开始进入RC,两个候选版本将专注 ...

  3. Codeforces Round #139 (Div. 2) 题解

    vp上古场次ing CF225A Dice Tower 1.题目简述: 有 \(n\) 个骰子被叠在了一起.对于每个骰子上的一个数,与它对面的数的和始终为 \(7\) . 你是小明,你只能从正面看这个 ...

  4. L inux系统安全及应用---暴力破解密码

    系统安全及应用一.开关机安全控制① 调整BIOS引导设置② GRUB限制二.终端登录安全控制① 限制root只在安全终端登录② 禁止普通用户登录举例三.系统弱口令检测① Joth the Ripper ...

  5. adb bat

    @REM 生成随机数@echo off@REM 设置延迟变量setlocal enabledelayedexpansionset min=9set max=21set /a mod=!max!-!mi ...

  6. python 读写sql2008 类

    import pymssql class MSSQL: def __init__(self,host,user,pwd,db): self.host = host self.user = user s ...

  7. 刚刚进公司不会SVN 菜鸟感觉好蛋疼-----------SVN学习记

    这篇文章源于6月份给公司新人作的关于SVN使用的培训,转眼已经过了几个月的时间,丢了也怪可惜的,于是整理出来希望能够帮助后来人快速入门. 转载:https://blog.csdn.net/maplej ...

  8. Day3 变量 运算符 及运算符的优先级

    变量 什么是变量: 可以变化的量 Java 是一种强类型语言,每个变量都必须声明其类型. Java变量是程序中最基本的存储单位,其要素包括变量名,变量类型,作用域. 注意事项: 每个变量都有类型, 类 ...

  9. Leetcode4. 寻找两个正序数组的中位数

    > 简洁易懂讲清原理,讲不清你来打我~ 输入两个递增数组,输出中位数![在这里插入图片描述](https://img-blog.csdnimg.cn/25550994642144228e9862 ...

  10. mybatis-5-关联查询

    外键查询 1.回忆外键约束 注意要在tbl_dept中添加外键 #添加外键约束 # 此处Employee为外键表,dept为主键表 # 删除Employee的数据不会影响dapt,而删除dept一定会 ...