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》

时间: 2024-10-08 15:01:13

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

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

Ftp踩坑系列: Linux上的ftp服务器 vsftpd 之配置满天飞--设置匿名用户访问(不弹出用户名密码框)以及其他用户可正常上传 ftp服务器Serv-U 设置允许自动创建不存在的目录 FTP协议的粗浅学习--利用wireshark抓包分析相关tcp连接 一.前言 出现这个问题,在docker这类容器出现之前,原因可能是防火墙的问题: FTP服务器一般默认使用被动模式,即,客户端一般会和服务端的21端口建立连接,该连接用来传输命令.真正传输数据时,服务端会返回一个随机端口,告诉客户端新建

LINUX下搭建VSFTPD服务器

1.FTP服务器的简介 关于ftp的介绍,大家也一定不陌生了.我就直接把百度百科上的介绍拿过来,和大家一起温习一下概念. FTP 是File Transfer Protocol(文件传输协议)的英文简称,而中文简称为"文传协议".用于Internet上的控制文件的双向传输.同时,它也是一个应用程序(Application).基于不同的操作系统有不同的FTP应用程序,而所有这些应用程序都遵守同一种协议以传输文件.在FTP的使用当中,用户经常遇到两个概念:"下载"(Do

开发电子商城5(linux下安装tvsftpd)

1:先检查linux下是否安装了vsftpd 2:安装了的话就删除原来的       yum remove vsftpd 3::再到yum库中安装   yum -y install vsftpd 4:在根目录下创建一个文件夹.这是文件上传保存的路径 mkdir /ftpfile 或者 cd  / mkdir ftpfile 5:为上传添加一个用户 ,以i后上传都用这个用户       useradd ftpuser -d /ftpfile/ -s /sbin/nologin 这里意思是添加一个叫

linux下vsftp服务搭建

实验拓扑: Linux Client -----RHEL5.9(vmnet1)----------(vmnet1) Win7 Client 实验一:测试默认安装vsftpd的结果 匿名用户与本地用户都可以登录 匿名用户登录到/var/ftp,只能下载不能上传 本地用户登录到本地用户的家目录,可以上传和下载 [[email protected] ~]# rpm -q vsftpd  //检查软件包是否安装 package vsftpd is not installed [[email protec

Linux下FTP虚拟账户配置

参考模版/usr/share/doc/vsftpd-2.0.5/EXAMPLE/VIRTUAL_USERS) 1.创建虚拟账户 [[email protected] ~]#yum install db4-utils [[email protected] ~]#vim /etc/vsftpd/vlogin tomcat #账户名称 123456 #密码 jerry #账户名称 654321 #密码 [[email protected] ~]#db_load -T -t hash -f /etc/v

Linux下hosts、host.conf、resolv.conf的区别

/etc/resolv.conf 该文件是DNS域名解析的配置文件,它的格式很简单,每行以一个关键字开头,后接配置参数.resolv.conf的关键字主要有四个,分别是:nameserver   #定义DNS服务器的IP地址domain         #定义本地域名search          #定义域名的搜索列表sortlist         #对返回的域名进行排序 详细说明:nameserver 表明DNS服务器的IP地址.可以有很多行的nameserver,每一个带一个IP地址.在查

Linux下用ftp更新web内容!

使用ftp更新web!让网页更新一次OK! 配置如下: 1.在Linux下安装ftp服务器! yum -y install vsftpd #ftp由vsftpd提供! 2.配置主配置文件/etc/vsftpd/vsftpd.conf,修改如下: 1 # Example config file /etc/vsftpd/vsftpd.conf 2 # 3 # The default compiled in settings are fairly paranoid. This sample file

Linux下的配置iptables防火墙增强服务器安全

Linux下的配置iptables防火墙增强服务器安全 实验要求 iptables常见概念 iptables服务器安装及相关配置文件 实战:iptables使用方法 例1:使用iptables防火墙保护公司web服务器 例2:使用iptables搭建路由器,通过SNAT使用内网机器上网 例3:限制某些IP地址访问服务器 例4:使用DNAT功能把内网web服务器端口映射到路由器外网 实验环境 iptables服务端:xuegod-63   IP:192.168.1.63 iptables客户端:x

linux下mysql 安装

小菜鸟接触linux太晚, 装个mysql(免安装 mysql-5.6.22-linux-glibc2.5-x86_64版本,最简单的安装方法) 竞折腾了两个晚上… 网上到处有linux下mysql的安装,但我自己安装过程中总出现这样那样的问题,现将此次安装过程及错误记录,以供自己日后参考,也希望可以给后来人一些帮助… 1. 去Oracle下载mysql-5.6.22-linux-glibc*.tar.gz 2.解压 tar -zxvf mysql-5.6.22-linux-glibc*.tar