Linux下FTPserver的实现(仿vsftpd)
继上一篇博文实现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)的更多相关文章
- Linux下使用docker 拉取 vsftpd 镜像搭建 Ftp 服务器,连接 Ftp 时遇到的错误(425 Failed to establish connection)
Ftp踩坑系列: Linux上的ftp服务器 vsftpd 之配置满天飞--设置匿名用户访问(不弹出用户名密码框)以及其他用户可正常上传 ftp服务器Serv-U 设置允许自动创建不存在的目录 FTP ...
- linux下vsftpd的安装及配置使用详细步骤(推荐)
vsftpd 是“very secure FTP daemon”的缩写,安全性是它的一个最大的特点. vsftpd 是一个 UNIX 类操作系统上运行的服务器的名字,它可以运行在诸如 Linux.BS ...
- Linux下用ftp更新web内容!
使用ftp更新web!让网页更新一次OK! 配置如下: 1.在Linux下安装ftp服务器! yum -y install vsftpd #ftp由vsftpd提供! 2.配置主配置文件/etc/vs ...
- Linux下部署FTP服务器
Linux下部署FTP服务器 下载安装包 在这里介绍的是离线部署FTP,首先下载对应的rpm包,下载链接为: 下载vsftpd服务 下载FTP客户端 安装ftp服务器 关闭防火墙 service ip ...
- linux下部署项目问题
1. 今天linux下部署thinkphp项目,数据库用的mysql. 页面其他都是正常的,但是从数据库中取出的数据都是乱码.最后查了资料 解决方案: 在ThinkPHP里面 Library\Thin ...
- [转] Linux下防火墙iptables用法规则详及其防火墙配置
from: http://www.cnblogs.com/yi-meng/p/3213925.html 备注: 排版还不错,建议看以上的链接. iptables规则 规则--顾名思义就是规矩和原则,和 ...
- 操作笔记:linux下安装ftp
1,安装ftp [root@iZ945sgm0ugZ ~]# yum install vsftpd 安装成功的信息: [root@iZ945sgm0ugZ ~]# yum install vsftpd ...
- Linux下vsftp服务器—上传、下载
一. FTP 说明 Linux下常用的FTP Server是vsftp(Very Security File Transfer Protocol),及profpt(Professtional ftp ...
- Linux 下搭建ftp服务器 指定用户指定目录及其他操作
搭建 Linux下 rpm -qa |grep vsftpd查看是否安装 没安装yum安装 /etc/vsftpd/目录下有vsftpd.conf配置文件 根据需求 进行配置 是否使用匿名用户以及文 ...
随机推荐
- codeforces 493 D Vasya and Chess【 博弈 】
题意:给出n*n的棋盘,白方在(1,1),黑方在(1,n)处,每一步可以上下左右对角线走,哪个先抓到另一个,则它获胜 可以画一下,发现n是奇数的时候,白方先走,无论它怎么走,黑方和它走对称的,黑方都一 ...
- POJ 2155 Matrix【 二维树状数组 】
题意:给出两种操作,C是给出一个矩形的左上角和左下角的下标,把这个矩形里面的0变成1,1变成0,Q是询问某个点的值 看这篇论文讲得很清楚 http://wenku.baidu.com/view/1e5 ...
- LeetCode(10)Regular Expression Matching
题目如下: Python代码: # -*- coding:utf-8 -*- def ismatch(s,p): #先将dp[s+1][p+1]二维数组全置为False dp = [[False] * ...
- ActiveMQ学习笔记(6)----ActiveMQ整合Spring开发
1. 添加依赖 spring 提供了对JMS的支持,需要添加Spring支持jms的包和Spring的核心包,如下: <dependency> <groupId>org.apa ...
- SpringCloud学习笔记(15)----Spring Cloud Netflix之Hystrix Dashboard的使用
1. 引入依赖 在前面几节中的消费者中添加pom依赖. <dependency> <groupId>org.springframework.cloud</groupId& ...
- 紫书 例题 11-3 UVa 1151 (有边集的最小生成树+二进制枚举子集)
标题指的边集是说这道题的套餐, 是由几条边构成的. 思路是先做一遍最小生成树排除边, 因为如果第一次做没有加入的边, 到后来新加入了很多权值为0的边,这些边肯定排在最前面,然后这条边的前面的那些边肯定 ...
- @SpringBootApplication cannot be resolved to a type In STS
@SpringBootApplication cannot be resolved to a type In STS 学习了:https://stackoverflow.com/questions/4 ...
- 【Oracle】Exadata虚拟机配置(一)
Exadata是Oracle软硬结合的一体机.通过自己的机子不可能全然模拟出来,这里搭建的虚拟机仅仅是一个简单的.能够供我们学习Exadata的环境. 最简单能够模拟的Exadata虚拟机须要两个节点 ...
- lenovo G系列重装系统
lenovo G41 的笔记本默认安装的是win8 中文版 的操作系统,使用非常不方便,用U盘重装成WIN7的系统. 1.用启动工具软件制作U盘启动盘. 详细能够參照 http://www.uqi ...
- angularjs 模块化
<!DOCTYPE HTML> <html ng-app="myApp"> <head> <meta http-equiv="C ...