上一篇博文实现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. HDU 1551 Cable master【二分答案】

    题意:给出n块木板,它们分别的高度,现在要把它们裁切成k块,问裁切成的最大的高度 二分答案,上限是这n块木板里面的最大值 然后每一个答案去判断一下是否满足能够裁切成k块 #include<ios ...

  2. HDU 1505 City Game【DP】

    题意:是二维的1506,即在1506的基础上,再加一个for循环,即从第一行到最后一行再扫一遍--- 自己写的时候,输入的方法不对---发现输不出结果,后来看了别人的----@_@发现是将字母和空格当 ...

  3. ActiveMQ学习笔记(19)----Consumer高级特性(一)

    1. Exclusive Consumer 独有消费者:Queue中的消息是按照顺序被分发到consumer的,然而,当你有多个consumers同时从相同的queue中提取消息时,你将失去这个保证. ...

  4. 3ds Max绘制青花瓷茶壶

    1.在桌面找到3DMAX软件,左键双击,启动程序: 2.在命令案板中,找到几何体,茶壶,在顶视图绘制一个茶壶: 3.在百度图片中搜索,查找“青花瓷”,找到一个自己喜欢的精美图案,截图保存备用: 4.在 ...

  5. ListNode的python 实现

    class Node(object): def __init__(self): self.val = None self.next = None class Node_handle(): def __ ...

  6. 路飞学城Python-Day15

    模拟实现一个ATM + 购物商城程序 额度 15000或自定义 实现购物商城,买东西加入 购物车,调用信用卡接口结账 可以提现,手续费5% 支持多账户登录 支持账户间转账 记录每月日常消费流水 提供还 ...

  7. python3安装xadmin失败

    环境win7 旗舰版.python3 使用pip install xadmin命令的时候出现了错误>:\ (⊙o⊙) 解决方法如下: 使用pip download xadmin 现将xadmin ...

  8. [agc004d]salvage robot

    题意: 别问我谁翻译的 虫合虫莫国的领土我们可以抽象为H*W的笼子,在这虫合土上,有若干个机器人和一个出口,其余都是空地,每次虫合虫莫会要求让所有的机器人向某个方向移动一步,当机器人移动到出口时会被虫 ...

  9. javascript 继承之拷贝,原型,类式

    // 拷贝继承,在子类内调用父类并修正this指向,再通过for in 拷贝父类的方法实现继承,具体实现如下代码 : function Tab(){//父类构造函数 this.name='aaa'; ...

  10. Springboot+swagger2的接口文档开发

    一.创建一个SpringBoot项目 1. 2. 3. 4. 把web里的web选中,SQL里选择自己需要的,点击next 二.创建各项所需的controller,configure等 1. 项目布局 ...