IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> c项目- 实现轻量化FTP服务器I:项目框架及数据连接的建立 -> 正文阅读

[系统运维]c项目- 实现轻量化FTP服务器I:项目框架及数据连接的建立

FTP协议的介绍

FTP协议又称文件传输协议,它的组成包括两个部分:FTP服务端和FTP客户端,FTP服务器用来存储文件,其中20端口是传输数据的端口,21端口是传输控制信息的端口。

项目简介

本项目主要的功能是实现一个FTP服务器,客户端使用的是leapftp,是一个交互开发的过程,项目具体功能如下

  1. 可以实现FTP内部标准命令:UESR,PASS,PORT,PASV,LIST,STOR等。
  2. 实现配置文件的解析和用户的鉴权登录功能
  3. 实现FTP主动(port)模式和被动(pasv)模式的数据连接的建立
  4. 实现文件的上传下载,续传续载以及限速功能
  5. 实现系统的空闲断开(数据连接断开和控制连接断开)以及系统的连接数限制(客户连接数限制和每IP数限制)

FTP工作原理

在这里插入图片描述
我们将FTP客户端发起的一次请求称为一次会话。
1.1 启动FTP
启动FTP客户端,在客户端通过交互的用户页面,客户输入启动FTP的交互式指令。
1.2 建立控制连接
客户端根据用户命令给出的服务端的IP地址,向服务端的21端口发出主动连接的请求,服务器收到请求连接之后,通过TCP三次握手协议,在客户端的控制通道和服务端的控制通道建立了一条连接。
控制连接通道主要用来传送FTP的内部命令以及命令的响应等控制信息,它并不用来传输数据。
1.3建立数据连接
控制连接建立之后,进行数据传输的时候需要开辟数据连接通道。
FTP有两种建立数据连接的方式:分别是PORT模式和PASV模式。将分别在下面进行赘述。
1.4 关闭连接
数据传输完成之后,数据连接被关闭,重新回到FTP会话状态,直到控制连接被关闭,FTP服务结束。

FTP主动模式和被动模式连接

注意PORT模式和PASV模式都是相对于服务端来说的。
1.主动模式:
客户端向服务端发送PORT命令,客户端会创建临时套接字,绑定一个临时端口,并且在套接字上监听。
FTP服务器将通过20端口主动的连接客户端的临时端口,通过TCP三次握手建立数据连接通道。
2.被动模式:
客户端向服务端发送PASV命令,此时服务端会创建监听套接字,绑定一个端口,服务器在套接字上监听。
FTP服务端将通过控制通道告知自己的端口号,被动的等待客户端的连接。
客户端绑定临时端口和服务端创建的端口进行连接,通过TCP三次握手协议建立数据连接通道。

项目系统的逻辑架构

在这里插入图片描述
如上图所示:每来一个客户端。主进程就会创建一个新的进程组,该进程组专门为该客户端进行服务。其中nobody进程主要负责的是内部协议的解析,而FTP服务进程则负责数据传输和FTP协议的解析。两个进程之间的数据是通过进程通信模块来完成的。

项目实现

一、项目框架搭建

项目文件包含的文件如下图所示,文件主要可以分为以下3个部分

  1. 蓝色部分为配置文件加载模块,主要是仿照vsftpd中的配置文件,直接在miniftp.conf中修改配置项即可更改FTP相关的参数配置,方便用户操作。

  2. 橙色部分为系统公共模块,其中common.h:包含系统头文件,sysutil.c包含项目所需要的公有函数,str.c模块主要是用来解析命令行参数。

  3. 紫色部分为项目的重点模块,ftpproto.c模块用来实现ftp常用命令的解析,主被动模式的建立,文件的上传下载等功能。其中ftpcodes.h模块用来存储FTP的应答方式。服务器通过控制连接发送给客户端FTP应答,用户根据收到的应答信息来对服务器做出相关响应。
    privparent.c主要负责的是创建主动模式和被动模式下的数据连接套接字,它其实就是nobody进程(FTP服务进程的父进程),通过privsock.c模块中提供的套接字发送接收函数将nobody进程创建的套接字发送给其子进程ftpproto.c模块。

上文提到过,FTP的一次连接其实就是一次会话。故session.c用来建立一次会话,通过fork函数会创建子进程ftp服务进程(ftpproto.c)以及自身进程(nobody进程)。之后在下文中将依次介绍每个模块是如何实现的。
在这里插入图片描述

二、系统各个模块的实现

配置文件解析模块

配置解析模块主要用来将miniftp.conf中对应的配置项的值加载到tunable.c文件中对应的配置变量中去。
miniftp.conf的内容如下所示:

#服务器IP //服务器的ip地址
listen_address=192.168.138.8

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

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

#FTP服务器端口
listen_port=9100

#最大连接数
max_clients=2000

#每ip最大连接数
max_per_ip=50

#Accept超时间
accept_timeout=60

#Connect超时间
connect_timeout=60 

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

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

#掩码
local_umask = 077 

#最大上传速度
upload_max_rate=102400

#最大下载速度
download_max_rate=102400

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; // FTP监听的地址

我们需要实现的功能是使得配置文件miniftp.conf中配置的参数最终可以保存在tunable.c中对应的变量中。因此,需要建立两者的映射关系。parseconf.c文件就是用来将对应的配置项参数加载到配置变量中的。
首先需要定义对应的查询数组,如下图所示:
parseconf_str_setting用来解析配置项的参数值为字符串的情况,该结构体中定义两个字符指针,分别指向了配置项参数名和配置变量。同理parseconf_int_setting结构体是用来解析配置项的参数值为整数的情况的。
需要注意的是结构体数组中包含两个NULL指针,用来代表配置项结束。
在这里插入图片描述
其次它的内部逻辑如下所示:
将miniftp.conf中的数据解析为参数名和对应的参数值,分别用key和value作为表示。
循环遍历各自的结构体数组,将key和结构体成员p_setting_name作比较,两者相等的时候,将value值赋值给相对应的配置变量即可。
在这里插入图片描述
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_max_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)
{
	// 打开对应的miniftp.conf解析文件
	FILE *fp = fopen(path, "r");
	if(NULL == fp)
		ERR_EXIT("parseconf_load_file");

	char setting_line[MAX_SETTING_LINE_SIZE] = {0};
	// fgets函数,用来读取文件中的一行元素,fp是对应的文件流指针,读取到的元素存储到setting_line字符数组中。
	while(fgets(setting_line, MAX_SETTING_LINE_SIZE, fp) != NULL)
	{
		// 跳过注释行
		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};
	// 将miniftp.conf文件中的每一行数据分割为key和value.
	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)
	{
	   // key值和参数名称相等的时候,将value值保存在tunable中的配置变量中。
		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);
			// 注意这里使用的函数为strdup,它用来拷贝字符串,并且在底层会分配空间,调用malloc函数。
			*p_cur_setting = strdup(value);//malloc
			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)
		{
		   //转换为大写,value为YES或者NO,如果给定的是yes或者no的时候。
			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;
			//char value[MAX_VALUE_SIZE] = {0};
			// 这里特别需要注意的是value值是字符类型,所以必须要转化为整型,否则就会报错。
			*p_cur_setting = atoi(value);
			return;
		}
		p_uint_setting++;
	}
}

主函数模块

该文件主要是用来控制FTP服务器的启动,这里的主进程模拟的是root进程,为了防止主进程退出,服务端和客户端一旦建立连接之后,通过while循环来不断的等待新的连接请求到来。
它的主要流程如下所示:

  1. 加载配置文件miniftp.conf到主进程中。
  2. 检测是否是root启动,普通用户没有权限启动ftp服务端。
  3. 主进程完成和客户端的连接,产生控制连接的套接字sockconn.
  4. 创建子进程,子进程用来开启会话,通过session结构体中的ctrl_fd来获得连接套接字。
  5. 父进程关闭sockconn.

代码如下所示:

//全局会话结构指针
session_t *p_sess;
int main(int argc,char *argv[])
{
    //加载配置文件
    parseconf_load_file("miniftp.conf");
	//判断是否为root用户启动
	if(getuid()!=0)
	{
		printf("miniftp : must be started as root.\n");
		exit(EXIT_FAILURE);
	}
	//初始化会话结构体
	session_t sess=
	{
		-1,-1,-1,-1,-1,-1,NULL,"","","",1,NULL,0,0,0
	};
	p_sess = &sess;
	int listenfd=tcp_server(tunable_listen_address,tunable_listen_port);
	printf("listenfd=%d\n",listenfd);
	int sockConn;
	struct sockaddr_in addrCli;
	socklen_t addrlen;
    while(1)
    {
	 
      sockConn=accept(listenfd,(struct sockaddr*)&addrCli,&addrlen);
	  printf("sockConn=%d\n",sockConn);
      if(sockConn<0)
      {
        perror("accept");
        continue;
      }
      pid_t pid =fork();
      if(pid==0)
      {
        //子进程关闭监听套接字
        close(listenfd);
        // 将创建好的控制连接套接字赋值给会话结构体中的ctrl_fd
		sess.ctrl_fd=sockConn;
		begin_session(&sess);
		exit(EXIT_SUCCESS);
      }
      else{
        //父进程关闭控制连接的套接字
        close(sockConn);
      }
    }
}

会话体模块

session.h模块中定义了FTP会话所需要的数据成员,包含控制连接的套接字的文件描述符,父子进程通道描述符,数据连接描述符等信息。

#ifndef _SESSION_H_
#define _SESSION_H_
#include "common.h"
typedef struct session{
	uid_t uid;
//	控制连接
	int ctrl_fd;
//	父子进程
	int parent_fd;
    int child_fd;
//	传输数据时服务端的socket
	int data_fd;
//	被动模式下的监听套接字
	int pasv_listen_fd;
// PORT模式下用来获取客户端的IP地址和端口号
	struct sockaddr_in *port_addr;
// 命令行参数字符数组
	char cmdline[MAX_COMMOND_LINE_SIZE];
// 对应的命令
	char cmd[MAX_CMD_SIZE];
// 对应的参数
	char arg[MAX_ARG_SIZE];
//	ftp协议状态
	int is_ascii;
	char *rnfr_name;
}session_t;
void begin_session(session_t *sess);
#endif

session.c代码如下:

#include"session.h"
#include"ftpproto.h"
#include "privsock.h"
#include"privparent.h"
void begin_session(session_t *sess)
{
   // 初始化父子进程对应的套接字
	priv_sock_init(sess);
	pid_t pid = fork();
	if(pid == -1)
		ERR_EXIT("session fork");

	if(pid == 0)
	{
		//ftp 服务进程
		priv_sock_set_child_context(sess);
		handle_child(sess);
	}
	else
	{
		priv_sock_set_parent_context(sess);
		//nobody 进程
		handle_parent(sess);
	}
}

下面我们先看一下FTP服务进程的流程,这也是本次项目的最重要的模块,它完成了对FTP命令行参数的解析,鉴权登录,文件传输(上传下载),空闲断开(控制空闲断开和数据空闲断开)等一系列功能。

FTP服务进程模块

对应的步骤如下所示:

  1. 接收客户端发送过来的命令行,利用str.c里面的函数将命令行进行分割:分割为命令+参数,命令和参数以空格作为分割符,在解析命令行之前需要删除\r和\n,相关代码如下所示
    例如:cmdline=USER mnitjh,cmd=USER,arg=mnitjh;cmdline=PASS 123456,cmd=PASS,arg=123456
void str_trim_crlf(char *str)
{
	char *p = str + (strlen(str)-1);
	while(*p=='\r' || *p=='\n')
		*p-- = '\0';
}
void str_split(const char *str, char *left, char *right, char token)
{
   // 用来确定字串的位置,pos指向的是token所在的位置。
	char *pos = strchr(str, token);
   // pos为空,即右侧没有参数。
	if(pos == NULL)
		strcpy(left, str);
   // pos不为空的,将pos左侧的字串拷贝到left中,pos右侧的字串拷贝到right中。
	else
	{
		strncpy(left, str, pos-str);
		strcpy(right, pos+1);
	}
}
  1. 建立命令映射关系:定义一个结构体变量:用来保存命令和对应处理函数的地址,如下所示:
typedef struct ftpcmd
{
	const char *cmd; // 命令
	void(*cmd_handler)(session_t *sess); //命令处理方法,函数指针,指向cmd_handler函数。
}ftpcmd_t;
// 常见的命令和对应的处理函数
ftpcmd_t ctrl_cmds[] = 
{
	{"USER", do_user},
	{"PASS", do_pass},
	{"PWD",  do_pwd},
	{"TYPE", do_type},
	{"PORT", do_port},
	{"PASV", do_pasv},
	{"LIST", do_list},
	{"CWD",  do_cwd},
	{"RMD" , do_rmd },
	{"MKD" , do_mkd },
	{"DELE", do_dele},
	{"SIZE", do_size},
	{"RNFR", do_rnfr},
	{"RNTO", do_rnto},
	{"RETR", do_retr},
	{"STOR", do_stor},
	{"REST", do_rest},
};

  1. 遍历所有的命令:
    查找服务器接收到的参数是否存储在该结构体内,若存在,则响应相关的命令处理函数。处理完相关的命令之后,服务器向客户端回应响应代码。查找命令响应函数的代码如下所示:
while(1)
	{
		// 每次接收新的命令参数之前,需要将之前的字符数组清空。
		memset(sess->cmdline, 0, MAX_COMMOND_LINE_SIZE);
		memset(sess->cmd, 0, MAX_CMD_SIZE);
		memset(sess->arg, 0, MAX_ARG_SIZE);
		// 利用控制连接通道接收客户端发送过来的命令行参数。
		int ret =recv(sess->ctrl_fd,sess->cmdline,MAX_CMD_SIZE,0);
		if(ret==0)
			exit(EXIT_SUCCESS);
		if(ret<0)
			ERR_EXIT("recv");
		//对命令行的处理:剔除回车和换行,分割字符串。
		str_trim_crlf(sess->cmdline);
		str_split(sess->cmdline, sess->cmd, sess->arg, ' ');
		//命令映射表部分
		int table_size = sizeof(ctrl_cmds) / sizeof(ctrl_cmds[0]);
		int i;
		for(i=0; i<table_size; ++i)
		{
		// 循环遍历命令映射表,接收到的命令行参数和表中的匹配,那么执行相关的处理函数。
			if(strcmp(sess->cmd, ctrl_cmds[i].cmd) == 0)
			{
				if(ctrl_cmds[i].cmd_handler)
					ctrl_cmds[i].cmd_handler(sess);
				//命令存在,对应的响应函数没有实现,回复502代码。
				else
					ftp_reply(sess, FTP_COMMANDNOTIMPL, "Unimplement command.");
				break;
			}
		}
		//命令不存在,回复500代码
		if(i >= table_size)
			ftp_reply(sess, FTP_BADCMD, "Unknown command.");
	}

常用命令的解析
1.鉴权登录:需要使用到两个函数分别是getpwnam(passwordname)和getspname(shadow password)。其中getpwnam返回的是一个结构体指针,它的类型是passwd,结构体包含的内容如下所示。函数的返回值为NULL,如果当前匹配的用户不存在的话。
其中name是用户输入的名称。

struct passwd *getpwnam(const char *name)

passwd结构体中包含的内容如下所示,参数解释如下所示

struct passwd {
               char   *pw_name;       /* username */
               char   *pw_passwd;     /* user password */
               uid_t   pw_uid;        /* user ID */
               gid_t   pw_gid;        /* group ID */
               char   *pw_gecos;      /* user information */
               char   *pw_dir;        /* home directory */
               char   *pw_shell;      /* shell program */
           };

USER响应函数的步骤:只需要将解析到的name作为参数传递给getpwnam即可,返回值若为空,代表客户端输入的用户名称不存在。之后回应相关的FTP响应代码即可,代码如下所示,这里需要注意一点:假设用户名称和系统中保存的用户名一致,我们需要存储当前用户ID的uid,为了验证密码所需要的。

static void do_user(session_t *sess)
{
	struct passwd *pwd = getpwnam(sess->arg);
	if(pwd != NULL)
		sess->uid = pwd->pw_uid; //保存用户ID即uid
	ftp_reply(sess, FTP_GIVEPWORD, "Please specify the password");
}

验证密码的步骤如下所示:这里读者可能会有困惑,为什么不直接获取输入的用户名称,而需要通过uid来获取结构体从而获得用户名,这不是多此一举嘛。其实是因为在USER命令向响应结束,那么下一次等待客户端发来PASS命令的时候,之前存储的命令行参数都会被清空,所以我们是没有办法直接获取到用户的名称的,所以需要定义用户的uid通过uid间接的访问到用户名。

其实也可以尝试一下,直接在session结构体中保存它的用户名,会觉得这样更方便一点。

此外,有读者可能困惑,为什么不直接使用明文进行对比呢,因为linux系统中,为了防止密码被泄漏,会使用加密算法对明文加密,加密之后的密码保存在shadow.h文件中。
我们没有办法从加密之后的密码推断之前的明文是什么,只能将客户端用户输入的密码利用crypt函数进行加密之后,和正确的明文加密密码进行对比,从而判断密码是否正确。

在这里插入图片描述
对应代码如下所示,需要注意的是需要更改当前用户的uid和当前用户的groupid,否则对应的用户名是root用户。

static void do_pass(session_t *sess)
{
	//鉴权登录
	struct passwd *pwd = getpwuid(sess->uid);
	if(pwd == NULL)
	{
		//用户不存在
		ftp_reply(sess, FTP_LOGINERR, "Login incorrect.");
		return;
	}
	struct spwd *spd = getspnam(pwd->pw_name);
	if(spd == NULL)
	{
		//用户不存在
		ftp_reply(sess, FTP_LOGINERR, "Login incorrect.");
		return;
	}
	//spd->sp_pwdp:encrypted password:加密之后的密码
	char *encrypted_pw = crypt(sess->arg, spd->sp_pwdp);
//	printf("the user password is %s\n",sess->arg);
	// 将用户输入的加密密码和正确的加密密码进行比较,不相等的话,回复错误代码
	if(strcmp(encrypted_pw, spd->sp_pwdp) != 0)
	{
//密码错误
//		printf("the user encrpted_true_password is %s\n",spd->sp_pwdp);
//		printf("the user encrpted_password is %s\n",encrypted_pw );
		ftp_reply(sess, FTP_LOGINERR, "Login incorrect.");
		return;
	}
//	 更改ftp服务进程的名称为当前用户,如果不更改的话,利用ps -ef查看当前进程会发现nobody进程和用户进程都是root进程。
	setegid(pwd->pw_gid);
    seteuid(pwd->pw_uid);
	chdir(pwd->pw_dir);
	ftp_reply(sess, FTP_LOGINOK, "Login successful.");
}

后续再看:

void handle_child(session_t *sess)
{
//	测试fork之后:父子进程之间的信息就不共享了嗷,要是子进程改变了也会影响父进程,那就了不得了呀!
//	memset(sess->test,49,5);
//	printf("child_sess->test=%s\n",sess->test);
	send(sess->ctrl_fd,"220 (miniftp1.0.1)\r\n",strlen("220 (miniftp1.0.1)\r\n"),0);
	while(1)
	{
		memset(sess->cmdline, 0, MAX_COMMOND_LINE_SIZE);
		memset(sess->cmd, 0, MAX_CMD_SIZE);
		memset(sess->arg, 0, MAX_ARG_SIZE);
		//设置对应的函数,这里要放在recv函数之前,recv是阻塞等待,接收数据的。如果客户端没有发送数据,一旦定时的时间到达,CPU的控制权就会到signal(SIGALRM,signal_handler)中的signal_handler中。
		handle_idel_connection();
		int ret =recv(sess->ctrl_fd,sess->cmdline,MAX_CMD_SIZE,0);
		if(ret==0)
			exit(EXIT_SUCCESS);
		if(ret<0)
			ERR_EXIT("recv");
		str_trim_crlf(sess->cmdline);
		str_split(sess->cmdline, sess->cmd, sess->arg, ' ');
//		printf("cmdline=%s\n",sess->cmdline);
		//命令映射
		int table_size = sizeof(ctrl_cmds) / sizeof(ctrl_cmds[0]);
		int i;
		for(i=0; i<table_size; ++i)
		{
			if(strcmp(sess->cmd, ctrl_cmds[i].cmd) == 0)
			{
				if(ctrl_cmds[i].cmd_handler)
					ctrl_cmds[i].cmd_handler(sess);
				else
					ftp_reply(sess, FTP_COMMANDNOTIMPL, "Unimplement command.");
				break;
			}
		}
		if(i >= table_size)
			ftp_reply(sess, FTP_BADCMD, "Unknown command.");
	}
}

2.数据连接的建立过程:PORT模式和PASV模式的建立。
这里我们需要知道FTP服务进程中需要的数据连接的套接字并不是由FTP服务进程本身创建的,而是通过nobody进程创建数据连接套接字,两个进程通过套接字通信的方式,将创建的套接字发送给FTP服务进程。

那么为什么要使用nobody进程呢?
PORT模式下:服务器需要connect客户端,服务器可能没有权限做这种事情,需要nobody进程来帮忙,此外普通用户没有权限绑定20端口。
PASV模式下:创建套接字以及对套接字的监听涉及到内核的相关操作,因此直接由ftp服务进程去做,是不安全的。
nobody进程和FTP服务进程的通信协议规定如下:

  1. 由于父子进程之间双方都需要发送和读取数据,所以使用socket_pair函数创建一对无名的,相互连接的套接字。socketpair创建的套接字是全双工通信,每一个套接字既可以读数据也可以写数据,例如:可以往sv[0]中写数据,从sv[1]中读数据;也可以往sv[1]中写数据,从sv[0]中读数据。
    函数声明如下,其中domain为作用域type为套接字类型,protocol设置为0即可,sv代表创建的套接字。由于nobody进程和ftp服务进程用于本地通信,因此domain选用的是AF_UNIX,注意这里不再是我们经常使用的AF_INET(用于网络通信的)。
int socketpair(int domain, int type, int protocol, int sv[2])
  1. 完成父子进程上下文的设置,由于文件描述符是父子进程共享的,因此需要关闭对方的描述符。
void priv_sock_set_parent_context(session_t *sess)
{
//	父进程需要关闭子进程的套接字
	if(sess->child_fd != -1)
	{
		close(sess->child_fd );
		sess->child_fd = -1;
	}
}
//子进程需要关闭父进程的套接字
void priv_sock_set_child_context(session_t *sess)
{
	if(sess->parent_fd != -1)
	{
		close(sess->parent_fd);
		sess->parent_fd = -1;
	}
}
  1. 内部通讯机制还定义了一系列的函数如下所示,主要关注的是最后一组函数,发送文件描述符和接收文件描述符。由于之前创建的控制连接的文件描述符是在fork之前创建的,因此子进程是可以共享父进程的信息的。而此时,我们需要在nobody进程中创建数据连接的套接字,发送给FTP服务进程,两个进程是独立的,互不影响的。因此不能直接传递4个字节的整型变量,而需要传递一个打开的文件描述符。
//FTP服务进程向nobody进程请求的命令
#define PRIV_SOCK_GET_DATA_SOCK 1
#define PRIV_SOCK_PASV_ACTIVE 2
#define PRIV_SOCK_PASV_LISTEN 3
#define PRIV_SOCK_PASV_ACCEPT 4

//nobody 进程对FTP服务进程的应答
#define PRIV_SOCK_RESULT_OK 1
#define PRIV_SOCK_RESULT_BAD 2

void priv_sock_send_cmd(int fd, char cmd); //发送命令(子进程向父进程发送请求)
char priv_sock_get_cmd(int fd); //接收命令(父进程接收子进程命令)
void priv_sock_send_result(int fd, char res); //发送结果(父进程向子进程发送结果) 
char priv_sock_get_result(int fd); //接收结果(子进程接收父进程发送的结果)
void priv_sock_send_int(int fd, int the_int); //发送一个整数
int priv_sock_get_int(int fd);//接收一个整数
void priv_sock_send_buf(int fd, const char *buf, unsigned int len); //发送一个字符串
void priv_sock_recv_buf(int fd, char *buf, unsigned int len); //接收一个字符串
void priv_sock_send_fd(int sock_fd, int fd);//发送套接字
int priv_sock_recv_fd(int sock_fd); //接收套接字

关于发送套接字和接收套接字的详细情况:可以看这篇博文:

传递文件描述符
关于msghdr的详细讲解

实现的内部使用到的函数是sendmsg和recvmsg函数。

ssize_t sendmsg(int sockfd,const msghdr *msg,int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);  

需要对msghdr结构体内部的成员进行初始化,内部的结构比较复杂。有兴趣的同学可以配合上面的博文看一下具体实现的细节。
最终发送套接字实际上将套接字作为辅助数据进行发送的。

  1. 需要注意的是:将需要发送的文件描述符send_fd的长度作为参数传送给CMSG_SPACE,就可以得到cmsghdr整个结构体的大小。
    之后填充msg_control和msg_controllen即可。
  2. 填充cmsghdr结构体的步骤:
    利用宏函数CMSG_FIRSTHDR,将msg的地址传入,既可以得到msghdr指向的第一个cmsghdr结构体的地址,对内部变量cmsg_len,cmsg_level,cmsg_type填充即可。
  3. 使用CMSG_DATA宏,将填充好的cmsghdr结构体地址传入,即可以得到对应的辅助数据地址。将需要传输的文件描述符放入该地址即可。
    在这里插入图片描述

此外我们可以看到,在内部通讯模块中,还定义了2组宏定义,其中A组宏定义用来表述FTP服务进程向nobody进程请求获取数据连接套接字。B组宏定义,它用来代表nobody进程队FTP服务进程的应答。

nobody进程中定义了4个函数分别用来对应A组宏定义,如下所示:

//获取主动连接套接字
static void privop_port_get_data_sock(session_t *sess);
//判定被动模式是否被激活
static void privop_pasv_active(session_t *sess);
//创建被动模式的监听套接字
static void privop_pasv_listen(session_t *sess);
//被动模式下接收连接
static void privop_pasv_accept(session_t *sess);

响应PORT命令时:
会话结构体中增加成员变量:port_addr:类型是struct sockaddr_in,用来保存客户端的IP地址和端口号。
因为后面在建立数据连接的时候,服务器20端口需要主动connect客户端,由于每次响应命令的时候,之前保存的参数是会被清空,因此需要保存记录客户端的ip地址和端口号。
响应PASV命令时:
1.从配置文件中获取对应的服务端的IP地址。
2.其次产生数据连接的监听套接字,会话结构体中增加成员变量pasv_listenfd。由于监听套接字也是由nobody进程产生的,故FTP服务进程会向nobody进程发送获取监听字的命令请求。
3.将获取到的服务端的ip地址和端口号格式化写入到text数组中,通过ftp_reply函数向客户端回复相应代码。
需要注意的是,这里响应成功并不能代表就可以成功建立数据连接,PORT命令中只是获取到了客户端的ip和port号,同理PASV命令中只是产生了监听套接字,等待客户端来连接。

对应的代码如下所示:

static void do_port(session_t *sess)
{
	unsigned int v[6];
	// 注意这里是将参数格式化输出到给定的v数组中,因为是无符号的整数,因此使用u来保存。
	sscanf(sess->arg,"%u,%u,%u,%u,%u,%u,",&v[0],&v[1],&v[2],&v[3],&v[4],&v[5]);
	sess->port_addr=(struct sockaddr_in*)malloc(sizeof(struct sockaddr_in));
	unsigned char *p=(unsigned char*)&sess->port_addr->sin_port;
	// 指针p是端口号因此指向的是v[4]和v[5]。
	p[0]=v[4];
	p[1]=v[5];
	sess->port_addr->sin_family = AF_INET;
	p = (unsigned char *)&(sess->port_addr->sin_addr);
	// p指向的是结构体struct sockaddr_in中的port_addr成员中的ip地址。
	// p是一个字符指针,可以获取v[0]的低8位,即192。p是字符型指针
	p[0] = v[0];
	p[1] = v[1];
	p[2] = v[2];
	p[3] = v[3];
	ftp_reply(sess, FTP_PROTOK, "PORT command successful. Consider using PASV.");
}
static void do_pasv(session_t *sess)
{
//	ftp服务进程向nobody进程请求需要监听套接字
	priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_LISTEN);
//	获取被动模式下的监听字
	sess->pasv_listen_fd =priv_sock_get_int(sess->child_fd);
	char ip[16] = {0};
//	接收IP
	int len=priv_sock_get_int(sess->child_fd);
	priv_sock_recv_buf(sess->child_fd,ip,(unsigned int)len);
//  接收PORT
	unsigned short port = (unsigned short)priv_sock_get_int(sess->child_fd);
//
    unsigned v[4] = {0};
	sscanf(ip, "%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]);
	char text[MAX_BUFFER_SIZE] = {0};
	// 这里需要注意:ip地址为32位,端口号为16位二进制组成的,port>>8:获取高8位的端口号
	// port&0x00ff:获取低8位的端口号。
	
	sprintf(text, "Entering Passive Mode (%u,%u,%u,%u,%u,%u).",
		v[0],v[1],v[2],v[3], port>>8, port&0x00ff);
	//227 Entering Passive Mode (192,168,232,10,248,159).
	ftp_reply(sess, FTP_PASVOK, text);
}
static void privop_pasv_listen(session_t *sess)
{
	char ip[16]="192.168.138.8";
    unsigned int v[4]={0};
//	sscanf:把从ip中获取的内容,按照格式化输出到v中。
	sscanf(ip,"%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]);
//	创建监听套接字
	int sockfd=tcp_server(ip,0);
	sess->pasv_listen_fd = sockfd;
	struct sockaddr_in addr;
	socklen_t addrlen=sizeof(struct sockaddr);
	if(getsockname(sess->pasv_listen_fd,(struct sockaddr*)&addr,&addrlen)<0)
		ERR_EXIT("getsockname");
//	获取端口号和IP
// 需要注意的是:需要将网络字节序转换为host字节序哦。
	unsigned short port = ntohs(addr.sin_port);
//	发送监听套接字
	priv_sock_send_int(sess->parent_fd,sess->pasv_listen_fd);
	//发送ip
	priv_sock_send_int(sess->parent_fd, strlen(ip));
	priv_sock_send_buf(sess->parent_fd, ip, strlen(ip));
	//发送port
	priv_sock_send_int(sess->parent_fd, (int)port);
}

建立数据连接的过程:
以list命令为例子:

  1. 主动连接被激活,使用主动模式进行通信
  2. 被动连接被激活,使用被动模式进行通信
  3. 服务器响应150代码
  4. 列表的传输
  5. 传输结束之后,服务器响应226代码
  6. 关闭数据连接

定义了get_transfer_fd函数,分为主动模式下的连接和被动模式下的连接。get_transfer_fd函数包含以下三种情况:

  1. 被动模式和主动模式均没有激活
  2. 只有主动模式被激活
  3. 只有被动模式被激活。
  4. 其他情况(两种模式均被激活)都是错误的。

情况1:获取主动模式下的数据连接套接字:get_port_fd函数:成功建立连接:函数返回0,否则返回1。
1.FTP服务进程向nobody进程发送1个字符的大小:PORT_GET_DATA_SOCK
2.通过之前的内部通讯模块中的send_buf函数,send_int函数来发送客户端ip地址和端口号。
3.nobody服务进程在while循环中不停的等待FTP服务进程发送过来的消息,recv函数会阻塞等待,接收到1个字符的:PROT_GET_DATA_SOCK,运行相关的处理函数
4.nobody进程接收相关的ip地址和端口号。
5.nobody进程创建服务端的数据连接套接字,通过connect函数来主动连接客户端。连接成功或者失败,均发送相关的应答字符。成功的时候,将服务端产生的套接字通过前文中提到的套接字发送函数发送给FTP服务进程即可。
6.FTP服务进程接收到套接字之后,将其存储在会话结构体中的data_fd数据成员中。
7.是否成功建立通过之前定义的nobody进程向FTP服务进程的应答请求来判断。

static void privop_port_get_data_sock(session_t *sess)
{
	//ip
	char ip[16] = {0};
	int len = priv_sock_get_int(sess->parent_fd);
	priv_sock_recv_buf(sess->parent_fd, ip, len);

	//port
	unsigned short port = (unsigned short)priv_sock_get_int(sess->parent_fd);

	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr(ip);

	int sock = tcp_client();  
	socklen_t addrlen = sizeof(struct sockaddr);
	if(connect(sock, (struct sockaddr*)&addr, addrlen) < 0)
	{
		priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD);
		return;
	}
	priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_OK);
	priv_sock_send_fd(sess->parent_fd, sock);
	close(sock);
}

情况2:获取被动模式下的数据连接套接字:
和主动模式不同,被动模式下客户端connect服务端,因此只需要发送PORT_SOCK_PASV_ACCEPT请求即可。
对应的代码如下所示:

static void privop_pasv_accept(session_t *sess)
{
	int sockConn;
	struct sockaddr_in addr;
	socklen_t addrlen;
	// accept函数会返回一个新的套接字,该套接字是用来进行数据连接的。accept函数的第一个参数是监听套接字。
	if((sockConn = accept(sess->pasv_listen_fd, (struct sockaddr*)&addr, &addrlen)) < 0)
	{
		priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD);
		return;
	}
	// 发送应答请求。
	priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_OK);
	// 发送数据连接套接字
	priv_sock_send_fd(sess->parent_fd, sockConn);
	//关闭监听套接字
	close(sess->pasv_listen_fd);
	sess->pasv_listen_fd = -1;
	close(sockConn);
}

对应的获取被动模式的套接字的代码如下所示:

int get_pasv_fd(session_t *sess)
{
	priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_ACCEPT);
	char res = priv_sock_get_result(sess->child_fd);
	if(res == PRIV_SOCK_RESULT_BAD)
		return -1;

	sess->data_fd = priv_sock_recv_fd(sess->child_fd);
	return 0;
}

判断被动模式被激活有两种方法:
方法1:FTP服务进程向nobody进程发送PRIV_SOCK_PASV_ACTIVE请求,父进程产生监听套接字发送给子进程,通过判断套接字对应的文件描述符是否为-1,来判断被动模式是否被激活。
代码如下所示:

ftpproto.c
int pasv_active(session_t *sess)
{
	priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_ACTIVE);
	int active = priv_sock_get_int(sess->child_fd);
	if(active != -1)
	{
		if(port_active(sess))
			ERR_EXIT("both port an pasv are active");
		return 1;
	}
	return 0;
}
privparent.c
static void privop_pasv_active(session_t *sess)
{
	int active = -1; //未激活
	if(sess->pasv_listen_fd != -1)
		active = 1; //激活
	priv_sock_send_int(sess->parent_fd, active);
}

方法2:需要注意的是fork之后的两个进程是读时共享,写时复制的原理。如果子进程中的成员变量不进行修改的话,拷贝后的数据指向内存中的同一块物理地址空间,虚拟地址空间是不一样的。如果子进程对父进程的资源进行修改的话,操作系统将为修改之后的资源重新分配物理空间,这样做的好处时可以节省内存,真的是太赞了!因此可以不定义privop_pasv_active这个函数,我们直接在PASV请求中,获取到对应的监听套接字,注意父子进程之间的数据只要发生改变了,它们之间是不会互相影响的,各自有自己的虚拟地址空间,是独立自主的。代码如下所示:获取到了被动模式下的监听套接字。

static void do_pasv(session_t *sess)
{
//	ftp服务进程向nobody进程请求需要监听套接字
	priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_LISTEN);
//	获取被动模式下的监听字
	sess->pasv_listen_fd =priv_sock_get_int(sess->child_fd);
	char ip[16] = {0};
//	接收IP
	int len=priv_sock_get_int(sess->child_fd);
	priv_sock_recv_buf(sess->child_fd,ip,(unsigned int)len);
//  接收PORT
	unsigned short port = (unsigned short)priv_sock_get_int(sess->child_fd);
//
    unsigned v[4] = {0};
	sscanf(ip, "%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]);
	char text[MAX_BUFFER_SIZE] = {0};
	// 这里需要注意:ip地址为32位,端口号为16位二进制组成的,port>>8:获取高8位的端口号
	// port&0x00ff:获取低8位的端口号。
	
	sprintf(text, "Entering Passive Mode (%u,%u,%u,%u,%u,%u).",
		v[0],v[1],v[2],v[3], port>>8, port&0x00ff);
	//227 Entering Passive Mode (192,168,232,10,248,159).
	ftp_reply(sess, FTP_PASVOK, text);
}

pasv_active部分代码如下所示:

int pasv_active(session_t *sess)
{
//	监听套接字等于-1;
	printf("sess->pasv_listen_fd==%d\n",sess->pasv_listen_fd);
	if(sess->pasv_listen_fd != -1)
	{
		if(port_active(sess)==0)
		{
			printf("both mode can not be actived in the meantime");
			exit(EXIT_FAILURE);
		}
		return 0;
	}
	return -1;
}

此外,判断主动模式是否被激活的代码如下所示,思路就是判断会话结构体中的port_addr指针是否为空,如果为空的话,则说明我们没有使用到主动模式的建立,因为给port_addr指向的结构体成员赋值的过程是在PORT命令中完成的。此外,还需要注意主动模式被激活之后,被动模式也不可以被激活,因此这种情况程序也会退出,报错。

int port_active(session_t *sess)
{
	if(sess->port_addr)
	{
		if(pasv_active(sess)==0)
		{
			printf("both mode can not be actived in the meantime");
			exit(EXIT_FAILURE);
		}
		else
			return 0;
	}
	else
		return -1;
}

最终整体的代码如下所示:

static int get_transfer_fd(session_t *sess)
{
	//两种模式都没有被激活
	if(port_active(sess)== -1 &&pasv_active(sess)== -1)
	{
		//425 Use PORT or PASV first.
		ftp_reply(sess, FTP_BADSENDCONN, "Use PORT or PASV first.");
		return -1;
	}
	//	主动模式被激活之后,需要释放之前malloc的结构体成员。
	if(port_active(sess)==0&&get_port_fd(sess)==0)
	{
		if(sess->port_addr)
		{
			free(sess->port_addr);
			sess->port_addr = NULL;
		}
		return 0;
	}
	//被动模式被激活
	if(pasv_active(sess)==0&&get_pasv_fd(sess)==0)
	{
		return 0;
	}
	else
		return -1;

	
}

list命令对应的代码如下所示:

static void do_list(session_t *sess)
{
	//1 创建数据连接
	if(get_transfer_fd(sess) != 0)
	{
		printf("the connection is not succeed\n");
		ERR_EXIT("connect_faliure");
	}
	//	printf("the connection is  succeed\n");
	//2 150
	ftp_reply(sess, FTP_DATACONN, "Here comes the directory listing.");
	//3 传输列表
	list_common(sess);
	//4 226
	ftp_reply(sess, FTP_TRANSFEROK, "Directory send OK.");
	//关闭数据连接
	close(sess->data_fd);
	sess->data_fd = -1;
}

下一章节将讨论文件的上传,下载,限速等功能的实现,呜呼,写的太累了!

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-04-24 09:50:00  更:2022-04-24 09:52:06 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 19:26:04-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码