歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

Linux下FTP服務器的實現(仿vsftpd)

上一篇博文實現Linux下的shell後,我們進一步利用網絡編程和系統編程的知識實現Linux下的FTP服務器。我們以vsftpd為原型並實現了其大部分的功能。由於篇幅和時間的關系,這裡不再一一贅述具體的實現過程,而是簡要概述功能實現思想和部分核心代碼。

(一)基本框架和流程

\

 

\

先解決兩個疑問:

(1)為什麼要使用nobody進程和服務進程兩個進程?

在PORT模式下,服務器會主動建立數據通道連接客戶端,服務器可能就沒有權限做這種事情,就需要nobody進程來幫忙。Nobody進程會通過unix域協議(本機通信效率高) 將套接字傳遞給服務進程。普通用戶沒有權限綁定20端口,需要nobody進程的協助,所以需要nobody進程作為控制進程。

(2)為什麼使用多進程而不是多線程?

 

原因是在多線程或IO復用的情況下,當前目錄是共享的,無法根據每一個連接來擁有自己的當前目錄,也就是說當前用戶目錄的切換會影響到其他的用戶。

(二)主被動模式的實現

主被動是相對於服務器來說的:

主動模式:服務器向客戶端敲門,然後客戶端開門
被動模式:客戶端向服務器敲門,然後服務器開門

被動模式的出現主要是為了解決 防火牆或者NAT造成的問題。當通過NAT轉換之後,服務器只能得知NAT的地址而不能得知客戶端的IP地址,因此服務器以20端口主動向NAT的PORT端口發動請求,但是NAT並沒有啟用PORT端口,所以連接會被拒絕。

 

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是為了只在一個模塊中應用。

(四)上傳/下載中斷點續傳的實現

 

斷點續傳的思想非常簡單,只需要使用一個全局變量記錄文件中的偏移量即可。下次從偏移量繼續上傳/下載。

 

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》

Copyright © Linux教程網 All Rights Reserved