第三部分:FTP服务器最大连接数的限制

服务端的最大连接数

将当前的连接数保存在会话结构体成员中的max_clients,当有一个客户登录的时候对应的max_clients的值会加1。客户退出的时候,max_client的值会减少1个。
需要注意的是,当前的max_clients的值如果大于配置文件中的tunable_max_clients,那么客户端就无法登录。
连接的限制的函数是位于主函数miniftp.c中。
代码如下所示:

while(1)
{
	sockConn = accept(listenfd, (struct sockaddr*)&addrCli, &addrlen);
	if(sockConn < 0)
	{
		perror("accept");
		continue;
	}
	//最大连接数
	s_children_nums++;
	sess.max_clients = s_children_nums;
	pid_t pid = fork();
	// 进程创建失败的话,此时的连接数需要减少1.
	if(pid == -1)
	{
		s_children_nums--;
		ERR_EXIT("fork");
	}

	if(pid == 0)
	{
		//Child Process
		close(listenfd);
		sess.ctrl_fd = sockConn;
		//连接限制
		check_limit(&sess);
		begin_session(&sess);
		exit(EXIT_SUCCESS);
	}
	else
	{
		close(sockConn);
	}
}

handle_limit函数如下所示:

static void check_limit(session_t *sess)
{
	if(tunable_max_clients!=0 && sess->max_clients>tunable_max_clients)
	{
		//421 There are too many connected users, please try later.
		ftp_reply(sess, FTP_TOO_MANY_USERS, "There are too many connected users, please try later.");
		exit(EXIT_FAILURE);
	}
}

客户端退出时,对应的max_client需要减少。要实现这个功能就需要对子进程进行回收,即nobody进程退出之后,会向主进程发送SIGCHID信号,为该信号定义自定义处理函数sig_child,在该函数内部将client的数值减去1即可。

服务端的每ip连接数

服务器的每ip连接数是使用hash表来实现的。hash表的具体实现详见下面这篇博客。本文不再赘述原理部分,直接使用函数接口开发相应的功能。

ftp 有连接数限制嘛java windows ftp连接数限制_hash表


这里需要注意的是:

  1. 必须使用两个hash表来维护,因为进程退出之后,对应的count值需要减一,我们可以很容易的建立ip到count的正向关系。一旦子进程退出之后,父进程只能通过waitpid函数获取到子进程对应的pid号,我们可以建立子进程的PID和IP的对应关系,继而再通过ip和count的关系实现对count的减少。
  2. 在获取客户端的ip地址时,需要对addrlen进行初始化,因为自己写代码的时候没有对socklen_t addrlen进行初始化,导致一开始的客户端ip地址获取错误。
  3. 添加pid和ip的映射关系是在父进程中完成滴。
  4. 增加ip和count的映射关系定义了handle_ip_count函数。
  5. 删除ip和count的映射关系定义了drop_ip_count函数

代码如下所示:

//申请hash表
s_ip_count_hash = hash_alloc(MAX_BUCKET_SIZE, hash_func);
s_pid_ip_hash = hash_alloc(MAX_BUCKET_SIZE, hash_func);
while(1)
	{
		sockConn = accept(listenfd, (struct sockaddr*)&addrCli, &addrlen);
		if(sockConn < 0)
		{
			perror("accept");
			continue;
		}
		//最大连接数
		s_children_nums++;
		sess.max_clients = s_children_nums;
		
		//每IP连接数
		unsigned int ip = addrCli.sin_addr.s_addr;
		sess.max_per_ip = handle_ip_count(&ip);
		pid_t pid = fork();
		// 进程创建失败的话,此时的连接数需要减少1.
		if(pid == -1)
		{
			s_children_nums--;
			ERR_EXIT("fork");
		}
		if(pid == 0)
		{
			//Child Process
			close(listenfd);
			sess.ctrl_fd = sockConn;

			//连接限制
			check_limit(&sess);

			begin_session(&sess);
			exit(EXIT_SUCCESS);
		}
		//父进程
		else
		{
			//Parent Process
			//增加pid跟ip的映射
			hash_add_entry(s_pid_ip_hash, &pid, sizeof(pid), &ip, sizeof(ip));

			close(sockConn);
		}

信号处理函数如下所示:

static void handle_sigchld(int sig)
{
	//减少最大连接数
	s_children_nums--;
	//减少每ip的连接数
	pid_t pid;
	while((pid = waitpid(-1, NULL, WNOHANG)) > 0)
	{
		unsigned int *ip = (unsigned int *)hash_lookup_entry(s_pid_ip_hash, &pid, sizeof(pid));
		if(ip == NULL)
			continue;
		drop_ip_count(ip);
		// 由于当前的子进程已经退出咯,因此删除对应的pid和ip的哈希结点。
		hash_free_entry(s_pid_ip_hash, &pid, sizeof(pid));
	}
}
unsigned int hash_func(unsigned int buket_size, void *key)
{
	return (*(unsigned int*)key) % buket_size;
}

unsigned int handle_ip_count(void *ip)
{
	unsigned int *p_count = (unsigned int *)hash_lookup_entry(s_ip_count_hash, ip, sizeof(unsigned int));
	if(p_count == NULL)
	{
		unsigned int count = 1;
		hash_add_entry(s_ip_count_hash, ip, sizeof(unsigned int), &count, sizeof(unsigned int));
		return count;
	}

	(*p_count)++;
	return *p_count;
}

void drop_ip_count(unsigned int *ip)
{
	unsigned int *p_count = (unsigned int*)hash_lookup_entry(s_ip_count_hash, ip, sizeof(unsigned int));
	if(p_count == NULL)
		return;
	//查找到对应的ip地址,对应的count要减一。
	(*p_count)--;
	// 当前ip用户减少为0,所以删除对应的hash表。
	if(*p_count == 0)
		hash_free_entry(s_ip_count_hash, ip, sizeof(unsigned int));
}

绑定20端口

需要注意的是,nobody进程没有权限去绑定20端口的,需要提升权限到root之后,才可以绑定20端口。
使用capset这个函数,对进程进行能力的限制。

int capset(cap_user_header_t hdrp, const cap_user_data_t datap)
{	
	//通过系统调用函数
	return syscall(__NR_capset, hdrp, datap);
}
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);
}