上一篇博文实现Linux下的shell后,我们进一步利用网络编程和系统编程的知识实现Linux下的FTPserver。我们以vsftpd为原型并实现了其大部分的功能。因为篇幅和时间的关系,这里不再一一赘述详细的实现过程,而是简要概述功能实现思想和部分核心代码。

(一)基本框架和流程

先解决两个疑问:

(1)为什么要使用nobody进程和服务进程两个进程?

在PORT模式下,server会主动建立数据通道连接client,server可能就没有权限做这样的事情,就须要nobody进程来帮忙。

Nobody进程会通过unix域协议(本机通信效率高)  将套接字传递给服务进程。

普通用户没有权限绑定20port,须要nobody进程的协助,所以须要nobody进程作为控制进程。

(2)为什么使用多进程而不是多线程?

原因是在多线程或IO复用的情况下。当前文件夹是共享的。无法依据每个连接来拥有自己的当前文件夹。也就是说当前用户文件夹的切换会影响到其它的用户。

(二)主被动模式的实现

主被动是相对于server来说的:

主动模式:server向client敲门,然后client开门

被动模式:client向server敲门,然后server开门

被动模式的出现主要是为了解决 防火墙或者NAT造成的问题。当通过NAT转换之后。server仅仅能得知NAT的地址而不能得知client的IP地址,因此server以20port主动向NAT的PORTport发动请求,可是NAT并没有启用PORTport,所以连接会被拒绝。

int get_transfer_fd(session_t *sess)
{
// 检測是否收到PORT或者PASV命令
if (!port_active(sess) && !pasv_active(sess))
{
ftp_reply(sess, FTP_BADSENDCONN, "Use PORT or PASV first.");
return 0;
} int ret = 1;
// 假设是主动模式
if (port_active(sess))
{ if (get_port_fd(sess) == 0)
{
ret = 0;
}
} if (pasv_active(sess))
{
if (get_pasv_fd(sess) == 0)
{
ret = 0;
} } if (sess->port_addr)
{
free(sess->port_addr);
sess->port_addr = NULL;
} if (ret)
{
// 又一次安装SIGALRM信号。并启动闹钟
start_data_alarm();
} return ret;
}

(三)基本命令的实现

參照RFC规范和vsftpd的演示结果,依次仿真实现下面命令:

static void do_user(session_t *sess);
static void do_pass(session_t *sess);
static void do_cwd(session_t *sess);
static void do_cdup(session_t *sess);
static void do_quit(session_t *sess);
static void do_port(session_t *sess);
static void do_pasv(session_t *sess);
static void do_type(session_t *sess);
static void do_retr(session_t *sess);
static void do_stor(session_t *sess);
static void do_appe(session_t *sess);
static void do_list(session_t *sess);
static void do_nlst(session_t *sess);
static void do_rest(session_t *sess);
static void do_abor(session_t *sess);
static void do_pwd(session_t *sess);
static void do_mkd(session_t *sess);
static void do_rmd(session_t *sess);
static void do_dele(session_t *sess);
static void do_rnfr(session_t *sess);
static void do_rnto(session_t *sess);
static void do_site(session_t *sess);
static void do_syst(session_t *sess);
static void do_feat(session_t *sess);
static void do_size(session_t *sess);
static void do_stat(session_t *sess);
static void do_noop(session_t *sess);
static void do_help(session_t *sess);

注:使用static是为了仅仅在一个模块中应用。



(四)上传/下载中断点续传的实现

断点续传的思想很easy,仅仅须要使用一个全局变量记录文件里的偏移量就可以。

下次从偏移量继续上传/下载。

static void do_retr(session_t *sess)
{
// 下载文件
// 断点续载 // 创建数据连接
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_reply(sess, FTP_FILEFAIL, "Failed to open file.");
return;
} int ret;
// 加读锁
ret = lock_file_read(fd);
if (ret == -1)
{
ftp_reply(sess, FTP_FILEFAIL, "Failed to open file.");
return;
} // 推断是否是普通文件
struct stat sbuf;
ret = fstat(fd, &sbuf);
if (!S_ISREG(sbuf.st_mode))
{
ftp_reply(sess, FTP_FILEFAIL, "Failed to open file.");
return;
} if (offset != 0)
{
ret = lseek(fd, offset, SEEK_SET);
if (ret == -1)
{
ftp_reply(sess, FTP_FILEFAIL, "Failed to open file.");
return;
}
} //150 Opening BINARY mode data connection for /home/jjl/tmp/echocli.c (1085 bytes). // 150
char text[1024] = {0};
if (sess->is_ascii)
{
sprintf(text, "Opening ASCII mode data connection for %s (%lld bytes).",
sess->arg, (long long)sbuf.st_size);
}
else
{
sprintf(text, "Opening BINARY mode data connection for %s (%lld bytes).",
sess->arg, (long long)sbuf.st_size);
} ftp_reply(sess, FTP_DATACONN, text); int flag = 0; // ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); 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, ret, 0);
if (sess->abor_received)
{
flag = 2;
break;
} bytes_to_send -= ret;
} if (bytes_to_send == 0)
{
flag = 0;
} // 关闭数据套接字
close(sess->data_fd);
sess->data_fd = -1; close(fd); if (flag == 0 && !sess->abor_received)
{
// 226
ftp_reply(sess, FTP_TRANSFEROK, "Transfer complete.");
}
else if (flag == 1)
{
// 451
ftp_reply(sess, FTP_BADSENDFILE, "Failure reading from local file.");
}
else if (flag == 2)
{
// 426
ftp_reply(sess, FTP_BADSENDNET, "Failure writting to network stream.");
} check_abor(sess);
// 又一次开启控制连接通道闹钟
start_cmdio_alarm(); }

(五)限速的实现

限速是通过使进程睡眠实现的。设置一个定时器计算当前的速度,假设发现大于限定的速度。那么就通过   睡眠时间 = (当前传输速度 / 最大传输速度 – 1) * 当前传输时间来计算。

void limit_rate(session_t *sess, int bytes_transfered, int is_upload)
{
sess->data_process = 1; // 睡眠时间 = (当前传输速度 / 最大传输速度 – 1) * 当前传输时间;
long curr_sec = get_time_sec();
long curr_usec = get_time_usec(); double elapsed;
elapsed = (double)(curr_sec - sess->bw_transfer_start_sec);
elapsed += (double)(curr_usec - sess->bw_transfer_start_usec) / (double)1000000;
if (elapsed <= (double)0)
{
elapsed = (double)0.01;
} // 计算当前传输速度
unsigned int bw_rate = (unsigned int)((double)bytes_transfered / elapsed); double rate_ratio;
if (is_upload)
{
if (bw_rate <= sess->bw_upload_rate_max)
{
// 不须要限速
sess->bw_transfer_start_sec = curr_sec;
sess->bw_transfer_start_usec = curr_usec;
return;
} rate_ratio = bw_rate / sess->bw_upload_rate_max;
}
else
{
if (bw_rate <= sess->bw_download_rate_max)
{
// 不须要限速
sess->bw_transfer_start_sec = curr_sec;
sess->bw_transfer_start_usec = curr_usec;
return;
} rate_ratio = bw_rate / sess->bw_download_rate_max;
} // 睡眠时间 = (当前传输速度 / 最大传输速度 – 1) * 当前传输时间;
double pause_time;
pause_time = (rate_ratio - (double)1) * elapsed; nano_sleep(pause_time); sess->bw_transfer_start_sec = get_time_sec();
sess->bw_transfer_start_usec = get_time_usec(); }

(六)单IP最大连接数的限制

使用哈希表实现。

映射之后假设发现某个IP的连接数超过规定的数字。不同意连接就可以。这里须要注意的是要建立两个哈希表。分别记录 IP&进程之间的映射和 IP&连接数之间的映射。由于当用户断开连接时我们必须知道进程和IP之间的关系。

请參考我的 博客中介绍哈希表的博文:http://blog.csdn.net/nk_test/article/details/50526184

s_ip_count_hash = hash_alloc(256, hash_func);
s_pid_ip_hash = hash_alloc(256, hash_func);

void check_limits(session_t *sess)
{
if (tunable_max_clients > 0 && sess->num_clients > tunable_max_clients)
{
ftp_reply(sess, FTP_TOO_MANY_USERS,
"There are too many connected users, please try later."); exit(EXIT_FAILURE);
} if (tunable_max_per_ip > 0 && sess->num_this_ip > tunable_max_per_ip)
{
ftp_reply(sess, FTP_IP_LIMIT,
"There are too many connections from your internet address."); exit(EXIT_FAILURE);
}
}

关于项目的具体实现 请到我的 Github下载源代码。

參考:

FTP协议的官方规范:RFC 959

M.J 《动手实现FTP》

Linux下FTPserver的实现(仿vsftpd)的更多相关文章

  1. Linux下使用docker 拉取 vsftpd 镜像搭建 Ftp 服务器,连接 Ftp 时遇到的错误(425 Failed to establish connection)

    Ftp踩坑系列: Linux上的ftp服务器 vsftpd 之配置满天飞--设置匿名用户访问(不弹出用户名密码框)以及其他用户可正常上传 ftp服务器Serv-U 设置允许自动创建不存在的目录 FTP ...

  2. linux下vsftpd的安装及配置使用详细步骤(推荐)

    vsftpd 是“very secure FTP daemon”的缩写,安全性是它的一个最大的特点. vsftpd 是一个 UNIX 类操作系统上运行的服务器的名字,它可以运行在诸如 Linux.BS ...

  3. Linux下用ftp更新web内容!

    使用ftp更新web!让网页更新一次OK! 配置如下: 1.在Linux下安装ftp服务器! yum -y install vsftpd #ftp由vsftpd提供! 2.配置主配置文件/etc/vs ...

  4. Linux下部署FTP服务器

    Linux下部署FTP服务器 下载安装包 在这里介绍的是离线部署FTP,首先下载对应的rpm包,下载链接为: 下载vsftpd服务 下载FTP客户端 安装ftp服务器 关闭防火墙 service ip ...

  5. linux下部署项目问题

    1. 今天linux下部署thinkphp项目,数据库用的mysql. 页面其他都是正常的,但是从数据库中取出的数据都是乱码.最后查了资料 解决方案: 在ThinkPHP里面 Library\Thin ...

  6. [转] Linux下防火墙iptables用法规则详及其防火墙配置

    from: http://www.cnblogs.com/yi-meng/p/3213925.html 备注: 排版还不错,建议看以上的链接. iptables规则 规则--顾名思义就是规矩和原则,和 ...

  7. 操作笔记:linux下安装ftp

    1,安装ftp [root@iZ945sgm0ugZ ~]# yum install vsftpd 安装成功的信息: [root@iZ945sgm0ugZ ~]# yum install vsftpd ...

  8. Linux下vsftp服务器—上传、下载

    一.  FTP 说明 Linux下常用的FTP Server是vsftp(Very Security File Transfer Protocol),及profpt(Professtional ftp ...

  9. Linux 下搭建ftp服务器 指定用户指定目录及其他操作

    搭建 Linux下 rpm -qa |grep vsftpd查看是否安装 没安装yum安装 /etc/vsftpd/目录下有vsftpd.conf配置文件 根据需求 进行配置  是否使用匿名用户以及文 ...

随机推荐

  1. JWT的初步了解以及session、cookie机制

    1.什么是状态保持? 想要了解JWT,首先需要知道什么是状态保持,举一个例子来说:无论是在web上还是在手机app上,我们都可以以游客的身份访问,此时都会有登录/注册字眼,当我们登录之后,就会是我们的 ...

  2. 【BZOJ4826】【HNOI2017】影魔

    题意: Description 影魔,奈文摩尔,据说有着一个诗人的灵魂.事实上,他吞噬的诗人灵魂早已成千上万.千百年来,他收集了各式各样的灵魂,包括诗人.牧师.帝王.乞丐.奴隶.罪人,当然,还有英雄. ...

  3. BZOJ 2794 [Poi2012]Cloakroom(离线+背包)

    2794: [Poi2012]Cloakroom Time Limit: 20 Sec  Memory Limit: 128 MBSubmit: 406  Solved: 241[Submit][St ...

  4. wackoPicko 渗透平台的安装

    2016-05-17 wackoPicko 的介绍及下载地址        https://github.com/adamdoupe/WackoPicko#from=codefrom.com 首先我们 ...

  5. [luogu] P4514 上帝造题的七分钟 (树状数组,二维差分)

    P4514 上帝造题的七分钟 题目背景 裸体就意味着身体. 题目描述 "第一分钟,X说,要有矩阵,于是便有了一个里面写满了0的n×m矩阵. 第二分钟,L说,要能修改,于是便有了将左上角为(a ...

  6. 斗地主算法的设计与实现(一)--项目介绍&如何定义和构造一张牌

    大学期间,我在别人的基础上,写了一个简易的斗地主程序. 主要实现了面向对象设计,洗牌.发牌.判断牌型.比较牌的大小.游戏规则等算法. 通过这个斗地主小项目的练习,提高了我的面向对象设计能力,加深了对算 ...

  7. 安装了python之后Windows的cmd中cd指令无法转换路径怎么办?

    1首先我们看看盘符,我的电脑里有 C D E,F. G盘. 2按下WIN+R键 输入cmd,打开cmd窗口. 3默认路径为用户文档路径,如果想切换到D盘 ,输入cd d: 是不行的. 4:AppDat ...

  8. 【BZOJ 1218】 [HNOI2003]激光炸弹

    [链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] 一开始以为可以炸多次. 然后发现是一次. 那么久直接做个前缀和就好了 枚举正方形的左上角. 然后刚好和网格对齐的话. 肯定没有放在( ...

  9. Qt之自定义布局管理器(QFlowLayout)

    简述 QFlowLayout,顾名思义-流布局,实现了处理不同窗口大小的布局.根据应用窗口的宽度来进行控件放置的变化. 具体实现要求不再赘述,请参考前两节内容. 简述 实现 效果 源码 实现 QFlo ...

  10. A题之变态青蛙跳

    一仅仅青蛙一次能够跳上1级台阶,也能够跳上2级--它也能够跳上n级. 求该青蛙跳上一个n级的台阶总共同拥有多少种跳法. 分析: 这是一个斐波拉契数列的引申问题,先来看看斐波拉契数列: n<=1, ...