zoukankan      html  css  js  c++  java
  • 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》





  • 相关阅读:
    BZOJ2002 [HNOI2010] 弹飞绵羊
    BZOJ1030 [JSOI2007] 文本生成器
    BZOJ3233 [AHOI2013] 找硬币
    BZOJ4269 再见xor
    BZOJ5297 CQOI2018 社交网络
    LOJ149 0/1分数规划
    BZOJ2132 圈地计划
    UOJ131 [NOI2015] 品酒大会
    composer速度慢(composer更换国内镜像)
    thinkphp6安装报错,composer install tp6 报错 Parse error: syntax error
  • 原文地址:https://www.cnblogs.com/mfmdaoyou/p/7049864.html
Copyright © 2011-2022 走看看