实现轻量化FTP后台服务器

Posted _Camille

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现轻量化FTP后台服务器相关的知识,希望对你有一定的参考价值。

前情回顾:

我们在上次的代码初设里完成了一部分内容具体为:
可以通过leapftp连接我们的服务器,鉴权登录,建立数据连接,显示当前路径文件列表,但是点击这些文件是没有任何反馈的(因为我们还没有实现)。


本次目标:

在之前的基础上继续增加命令映射和其他关键功能,如
1.切换目录;
2.创建文件;
3.删除文件;
4.文件重命名;
5.文件大小;
6.文件上传/下载;
7.配置文件;
8.断点续传;
9.限速传输;
10.最大连接数。


命令映射的添加

//切换目录
static void do_cwd(session_t *sess)
{
	if(chdir(sess->arg) < 0)
		ftp_reply(sess, FTP_NOPERM, "Failed to change directory.");
	else
		ftp_reply(sess, FTP_CWDOK, "Directory successfully changed.");
}
//创建文件
static void do_mkd(session_t *sess)
{
	if(mkdir(sess->arg, 0755) < 0)
		ftp_reply(sess, FTP_NOPERM, "Create directory operation failed.");
	else
	{
		char text[MAX_BUFFER_SIZE] = {0};
		sprintf(text, "\\"%s\\" created", sess->arg);
		ftp_reply(sess, FTP_MKDIROK, text);
	}
}
//删除目录
static void do_rmd(session_t *sess)
{
	if(rmdir(sess->arg) < 0)
		ftp_reply(sess, FTP_FILEFAIL, "Remove directory operation failed.");
	else
	{
		ftp_reply(sess, FTP_RMDIROK, "Remove directory operation successful.");
	}
}
//删除文件
static void do_dele(session_t *sess)
{
	if(unlink(sess->arg) < 0)
		ftp_reply(sess, FTP_NOPERM, "Delete operation failed.");
	else
		ftp_reply(sess, FTP_DELEOK, "Delete operation successful.");
}
//计算大小
static void do_size(session_t *sess)
{
	struct stat sbuf;
	if(stat(sess->arg, &sbuf) < 0)
		ftp_reply(sess, FTP_FILEFAIL, "Could not get file size.");
	else
	{
		char text[MAX_BUFFER_SIZE] = {0};
		sprintf(text, "%d", (int)sbuf.st_size);
		ftp_reply(sess, FTP_SIZEOK, text);
	}
}
}

文件重命名:
首先在session会话结构体中添加
char *rnfr_name;//改名前的文件名

static void do_rnfr(session_t *sess) //文件重命名参数为曾用名路径
{
	unsigned int len = strlen(sess->arg);
	sess->rnfr_name = (char*)malloc(len + 1);
	memset(sess->rnfr_name, 0, len+1);
	strcpy(sess->rnfr_name, sess->arg);
	ftp_reply(sess, FTP_RNFROK, "Ready for RNTO.");//350
}
static void do_rnto(session_t *sess) //文件重命名参数为现用名路径
{
	if(sess->rnfr_name == NULL)
	{
		ftp_reply(sess, FTP_NEEDRNFR, "RNFR required first.");
		return;
	}
	if(rename(sess->rnfr_name, sess->arg) < 0)
	{
		ftp_reply(sess, FTP_NOPERM, "Rename failed.");
	}
	else
	{
		free(sess->rnfr_name);
		sess->rnfr_name = NULL;
		ftp_reply(sess, FTP_RENAMEOK, "Rename successful.");
	}
}

nobody进程

nobody进程是进程组中的父进程,那么其作用是什么呢?
我们在之前编写时将主动连接的套接字创建以及被动模式监听套接字都在ftpproto服务进程中编写,接下来我们将其放入nobody进程,在PORT模式下,服务器会主动建立数据通道连接客户端,服务器可能就没有权限做这种事情,就需要nobody进程来帮忙。 Nobody进程会通过unix域协议(本机通信效率高) 将套接字传递给服务进程。普通用户没有权限绑定20端口,需要nobody进程的协助,所以需要nobody进程作为控制进程。

提升权限绑定20端口:

static void minimize_privilege()
{
	//更改nobody进程
	struct passwd *pwd = getpwnam("nobody"); //lyx
	if(pwd == NULL)
		ERR_EXIT("getpwnam");
	if(setegid(pwd->pw_gid) < 0)
		ERR_EXIT("setegid");
	if(seteuid(pwd->pw_uid) < 0)
		ERR_EXIT("seteuid");

	struct __user_cap_header_struct cap_header;
	struct __user_cap_data_struct   cap_data;
	memset(&cap_header, 0, sizeof(cap_header));
	memset(&cap_data,   0, sizeof(cap_data));

	//设置头结构
	cap_header.version = _LINUX_CAPABILITY_VERSION_2;
	cap_header.pid = 0; //提升为root用户
	//设置数据结构
	unsigned int cap_mask = 0;
	cap_mask |= (1<<CAP_NET_BIND_SERVICE); // 0000 0000 0000 0000 1000 0000 0000 0000 
	cap_data.effective = cap_data.permitted = cap_mask;
	cap_data.inheritable = 0;
	//设置特殊能力
	capset(&cap_header, &cap_data);
}

并且由于父子进程属于两个进程,我们必须要添加两进程之间的通讯函数privsock。


添加privsock通讯模块

负责父子进程之间的内部通讯,例如服务进程需要创建主动连接套接字,服务进程需要发送特定的命令,然后nobody进程回复特定的命令。


添加配置文件及配置解析文件


配置文件

myftp.conf:

#为什么修改配置文件要重启,不重启就无法重新解析配置文件
#服务器IP
listen_address=172.17.0.5

#是否开启被动模式
pasv_enable=YES

#是否开启主动模式
port_enable=NO

#FTP服务器端口
listen_port=9100

#最大连接数
max_clients=3

#每ip最大连接数
max_per_ip=3

#Accept超时间
accept_timeout=60

#Connect超时间
connect_timeout=60 

#控制连接超时时间
idle_session_timeout=100

#数据连接超时时间
data_connection_timeout=15

#掩码
local_umask = 077 

#最大上传速度
upload_max_rate=1024

#最大下载速度
download_mas_rate=0

配置解析文件parseconf:

parseconf.h:

#ifndef _PARSE_CONF_H_
#define _PARSE_CONF_H_

//配置文件
#include"common.h"

void parseconf_load_file(const char *path);
void parseconf_load_setting(const char*setting);

#endif /* _PARSE_CONF_H_ */

parseconf.c:

#include"parseconf.h"
#include"tunable.h"
#include"str.h"
///解析配置文件


//bool型配置项
static struct parseconf_bool_setting
{
	const char *p_setting_name; //配置项的名字
	int        *p_variable;     //配置项的值
}
parseconf_bool_array[] = 
{
	{"pasv_enable", &tunable_pasv_enable},//是否开启主被动
	{"port_enable", &tunable_port_enable},
	{NULL, NULL}
};

//int配置项
static struct parseconf_uint_setting
{
	const char   *p_setting_name;
	unsigned int *p_variable;
}
parseconf_uint_array[] = 
{
	{"listen_port", &tunable_listen_port},
	{"max_clients", &tunable_max_clients},
	{"max_per_ip" , &tunable_max_per_ip},
	{"accept_timeout", &tunable_accept_timeout},
	{"connect_timeout", &tunable_connect_timeout},
	{"idle_session_timeout", &tunable_idle_session_timeout},
	{"data_connection_timeout", &tunable_data_connection_timeout},
	{"local_umask", &tunable_local_umask},
	{"upload_max_rate", &tunable_upload_max_rate},
	{"download_mas_rate", &tunable_download_max_rate},
	{NULL, NULL}//两个空作为配置项结束的标记
};

//str配置项
static struct parseconf_str_setting
{
	const char *p_setting_name;
	const char **p_variable;//指向字符串所在空间指针的指针
}
parseconf_str_array[] = 
{
	{"listen_address", &tunable_listen_address},
	{NULL, NULL}
};



void parseconf_load_file(const char *path)
{
	FILE *fp = fopen(path, "r");//以只读方式打开
	if(NULL == fp)
		ERR_EXIT("parseconf_load_file");

//解析配置文件
	char setting_line[MAX_SETTING_LINE_SIZE] = {0};//设置行
	while(fgets(setting_line, MAX_SETTING_LINE_SIZE, fp) != NULL)//读取fp内容到setting_line
	{
		if(setting_line[0]=='\\0' || setting_line[0]=='#')
			continue;
		str_trim_crlf(setting_line);//去掉回车和换行

		//解析配置行
		parseconf_load_setting(setting_line);

		memset(setting_line, 0, MAX_SETTING_LINE_SIZE);
	}

	fclose(fp);
}

//listen_port=9100
void parseconf_load_setting(const char *setting)
{
	char key[MAX_KEY_SIZE] = {0};
	char value[MAX_VALUE_SIZE] = {0};
	str_split(setting, key, value, '=');

	//查询str配置项(从小到大查询)
	const struct parseconf_str_setting *p_str_setting = parseconf_str_array;
	while(p_str_setting->p_setting_name != NULL)
	{
		if(strcmp(key, p_str_setting->p_setting_name) == 0)
		{
			const char **p_cur_setting = p_str_setting->p_variable;
			if(*p_cur_setting)
				free((char *)*p_cur_setting);
			*p_cur_setting = strdup(value);//malloc,strdup会在底层开辟空间
			return;
		}
		p_str_setting++;//找下一个数组元素
	}

	//查询bool配置项
	const struct parseconf_bool_setting *p_bool_setting = parseconf_bool_array;
	while(p_bool_setting->p_setting_name != NULL)
	{
		if(strcmp(key, p_bool_setting->p_setting_name) == 0)
		{
			str_upper(value);//转化为大写
			int *p_cur_setting = p_bool_setting->p_variable;
			if(strcmp(value, "YES") == 0)
				*p_cur_setting = 1;
			else if(strcmp(value, "NO") == 0)
				*p_cur_setting = 0; 
			else
				ERR_EXIT("parseconf_load_setting");
			return;
		}
		p_bool_setting++;
	}

	//查询int配置项
	const struct parseconf_uint_setting *p_uint_setting = parseconf_uint_array;
	while(p_uint_setting->p_setting_name != NULL)
	{
		if(strcmp(key, p_uint_setting->p_setting_name) == 0)
		{
			unsigned int *p_cur_setting = p_uint_setting->p_variable;
			*p_cur_setting = atoi(value);
			return;
		}
		p_uint_setting++;
	}
}

配置解析文件解析(两个函数1.加载配置文件2.解析配置文件)出来的变量保存在系统变量文件中,解析出相应的配置项,就将其添加到系统配置变量中.

系统变量文件:

tunable.h:

#ifndef _TUNABLE_H_
#define _TUNABLE_H_

//保存配置变量
extern int tunable_pasv_enable; //是否开启被动模式
extern int tunable_port_enable;//是否开启主动模式
extern unsigned int tunable_listen_port;//FTP服务器端口
extern unsigned int tunable_max_clients; //最大连接数
extern unsigned int tunable_max_per_ip; //每ip最大连接数
extern unsigned int tunable_accept_timeout; //Accept超时间
extern unsigned int tunable_connect_timeout; //Connect超时间
extern unsigned int tunable_idle_session_timeout; //控制连接超时时间
extern unsigned int tunable_data_connection_timeout; //数据连接超时时间
extern unsigned int tunable_local_umask; //掩码
extern unsigned int tunable_upload_max_rate; //最大上传速度
extern unsigned int tunable_download_max_rate; // 最大下载速度
extern const char *tunable_listen_address;

#endif /* _TUNABLE_H_ */

tunable.c:

#include"tunable.h"

int tunable_pasv_enable = 1; //是否开启被动模式
int tunable_port_enable = 1; //是否开启主动模式
unsigned int tunable_listen_port = 21; //FTP服务器端口
unsigned int tunable_max_clients = 2000; //最大连接数
unsigned int tunable_max_per_ip = 50; //每ip最大连接数
unsigned int tunable_accept_timeout = 60; //Accept超时间
unsigned int tunable_connect_timeout = 60; //Connect超时间
unsigned int tunable_idle_session_timeout=300; //控制连接超时时间
unsigned int tunable_data_connection_timeout=300; //数据连接超时时间
unsigned int tunable_local_umask = 077; //掩码
unsigned int tunable_upload_max_rate = 0; //最大上传速度
unsigned int tunable_download_max_rate=0; // 最大下载速度
const char *tunable_listen_address;

文件上传下载(搭配限速以及断点续传)

上传和下载的前提条件是保持通畅数据连接,如果想要断点续传只需要在session会话结构体中定义重传点,然后在通过调整文件描述符偏移量即可完成。限速相当于现实中的高速路上的区间限速,如果速度大于配置文件中的限速,则相应的拖时间从而降低速度。

空闲断开

1.1 控制连接空闲断开

  1. 首先是安装信号SIGALRM,并启动定时闹钟
  2. 如果在闹钟到来之前没有收到任何命令,则在SIGALRM信号处理程序中关闭控制连接,
    并给客户421Timeout的响应,并且退出会话。

1.2 数据连接空闲断开

  1. 如果当前处于数据传输的状态,那么即使控制连接通道空闲(在空闲时间内没有收到任
    何客户端的命令),也不应该退出会话。实现方法,只需要将先前设定的闹钟关闭即可
  2. 数据连接通道建立了,但是在一定的时间没有传输数据,那么应该将整个会话断开
  3. 在传输数据之前安装信号SIGALRM,并启动闹钟
  4. 在传输数据过程中,如果收到SIGALRM信号
    4.1 如果sess->data_process = 0,则给客户端超时的响应421Data timeout. Reconnect
    Sorry, 并且退出会话。
    4.2 如果sess->data_proces=1,将sess->data_process=0,重新安装信号SIGALRM,并启
    动闹钟。

以下是文件上传下载(限速及断点续传和空闲断开)源码:

void limit_rate(session_t *sess, unsigned long bytes_transfer, int is_upload)
{
	//登记结束时间
	unsigned long long cur_sec = get_time_sec();
	unsigned long long cur_usec = get_time_usec();

	double pass_time = (double)(cur_sec - sess->transfer_start_sec);//时间差
	pass_time += ((double)(cur_usec - sess->transfer_start_usec) / 1000000);

	//当前的传输速度
	unsigned long cur_rate = (unsigned long)(bytes_transfer / pass_time);
	double rate_ratio; //速率

	if(is_upload)
	{
		//上传
		if(tunable_upload_max_rate==0 || cur_rate<=tunable_upload_max_rate)
		{
			//不限速
			sess->transfer_start_sec = get_time_sec();
			sess->transfer_start_usec = get_time_usec();
			return;
		}
		rate_ratio = cur_rate / tunable_upload_max_rate;
	}
	else
	{
		//下载
		if(tunable_download_max_rate==0 || cur_rate <= tunable_download_max_rate)
		{
			//不限速
			sess->以上是关于实现轻量化FTP后台服务器的主要内容,如果未能解决你的问题,请参考以下文章

实现轻量化FTP后台服务器

实现轻量化FTP后台服务器

实现轻量化FTP后台服务器

实现轻量化FTP后台服务器

个人项目:实现轻量化FTP服务器

个人项目:实现轻量化FTP服务器