miniFTP项目实战二
项目简介:
在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进程来为客户提供服务。同时每个ftp服务进程配套了nobody进程(内部私有进程),主要是为了做权限提升和控制。
实现功能:
除了基本的文件上传和下载功能,还实现模式选择、断点续传、限制连接数、空闲断开、限速等功能。
用到的技术:
socket、I/O复用、进程间通信、HashTable
欢迎技术交流q:2723808286
项目开源!!!
miniFTP项目实战一
miniFTP项目实战二
miniFTP项目实战三
miniFTP项目实战四
miniFTP项目实战五
miniFTP项目实战六
2.1总体框架
- 服务器启动之后,先判断是否以root用户启动,然后创建监听socket监听对应端口的连接请求。
- 从配置文件中读取配置选项,将配置文件的选项赋值给相应的全局变量。
- while循环中通过accept_taimeout接收客户端的连接,保存客户端的IP进行连接数的记录。然后fork一个子进程作为新会话!!!
- 子进程抽象为一个新的会话,子进程本身作为nobody进程为FTP提供更高权限的操作,nobody的子进程作为服务进程与客户端进行交互。还要创建nobody进程与服务进程之间的通信,通过socketpair创建一对已连接的域内socket,进行内部通信
- fork出的服务进程,通过mian中accept_taimeout返回的socket作为控制连接与客户端进行命令交互。在while循环中不断从控制连接通道中读取命令,进行解析,然后根据命令与处理函数的映射关系找到对应的命令操作函数执行。
2.2 多进程并发模型处理
- 基于多进程并发模型,要处理好僵尸进程的处理。
- 创建监听socket的时候,根据传入的参数进行绑定端口,且开启地址重用。
- 在accept_timeout接收客户端连接的时候,可以指定超时时间,避免一直阻塞。
- 处理父子进程的socket描述符,要及时关闭。
信号处理僵尸进程
在子进程结束后,如果父进程不及时回收子进程的资源,就会产生僵尸进程。在多进程模型中会产生大量的子进程,各个子进程之间可能在不同时候结束,所以要做好子进程回收机制以避免出现僵尸进程。
子进程结束后会给父进程发送一个SIGCHLD信号,默认是忽略的,我们在SIGCHLD信号的处理函数中进行子进程资源的回收,首先要注册信号处理函数:
signal(SIGCHLD, signal_handler);
//函数原型
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
要注意信号处理函数的格式,在信号处理函数中调用waitpid来回收子进程资源:
void signal_handler(int argc)
{
//等待任意,没等到立刻返回0
unsigned int pid;
while (pid = waitpid(-1, NULL, WNOHANG) > 0) { //WNOHANG如果没有,立刻返回
/* 连接数限制部分的代码
--s_children; //统计的进程数-1 改变的是父进程的变量
unsigned int *ip = hash_lookup_entry(s_pid_ip_hash, &pid, sizeof(pid));
if (ip == NULL) {
continue;
}
drop_ip_count(ip); //pid->ip->count--
hash_free_entry(s_pid_ip_hash, &pid, sizeof(pid)); //释放空间
*/
}
return ;
}
//waitpid函数原型
pid_t waitpid(pid_t pid, int *wstatus, int options);
The value of pid can be:
< -1 meaning wait for any child process whose process group ID is equal to the absolute value of pid.
-1 meaning wait for any child process.
0 meaning wait for any child process whose process group ID is equal to that of the calling process.
> 0 meaning wait for the child whose process ID is equal to the value of pid.
The value of options is an OR of zero or more of the following constants:
WNOHANG return immediately if no child has exited.
WUNTRACED also return if a child has stopped (but not traced via ptrace(2)). Status for traced chil‐dren which have stopped is provided even if this option is not specified.
WCONTINUED (since Linux 2.6.10)also return if a stopped child has been resumed by delivery of SIGCONT.
创建监听socket
要根据传入参数进行监听socket的创建,并返回监听socket_fd,传入的参数是服务器的IP地址或者主机名 和 端口号。
服务器端创建监听socket的流程如下:
- 创建流式socket
- 通过传入的参数bind绑定socket地址(要注意字节序的转换,要转换为网络字节序才可以绑定)
- 开启地址重用
- 通过listen将socket设置为监听socket
绑定socket地址
重点就是根据传入的参数绑定IP地址,允许传入参数为NULL,IP为NULL时默认监听主机所有IP地址,创建监听socket函数的声明如下:
int tcp_server(const char *host, unsigned short port);
//host为主机名或者IP地址,若host为NULL,监听所有IP地址
//port为端口号
进行绑定IP地址的时候首先要判断host是否为NULL,如果非NULL!!!还要判断host是点分形式的IP地址还是主机名!!!
host是点分形式IP地址
可以通过inet_pton的返回值来判断,inet_pton是用来转换IP地址的,将IP地址从点分形式转换为32位整数形式,并转换为网络字节序,其返回值描述如下:
inet_pton() returns 1 on success (network address was successfully converted). 0 is returned if src
does not contain a character string representing a valid network address in the specified address fam‐
ily. If af does not contain a valid address family, -1 is returned and errno is set to EAFNOSUPPORT.
可以看出,当返回1时,代表host是点分形式的IP地址,若返回0表示host是主机名,接下来就根据inet_pton的返回值做区分。
host是主机名
如果是点分形式,那之间将转换后的网络字节序的32位整数IP地址赋值给地址结构体即可;如果是主机名,还要通过主机名获取一个IP地址,通过struct hostent *gethostbyname(const char *name);函数实现,其函数原型如下:
struct hostent *gethostbyname(const char *name);
The hostent structure is defined in <netdb.h> as follows:
struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
}
#define h_addr h_addr_list[0] /* for backward compatibility */
根据传入的主机名获取主机的信息,根据返回结构体中的h_addr_list可以获取主机中IP地址的列表,然后根据h_addr取得列表中的第一个IP地址进行绑定。
host是NULL
为NULL在绑定的时候,通过宏定义INADDR_ANY来指定地址为任意地址:
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
开启地址重用
socket在关闭之后,系统不会立即回收对端口的操作权,而是要等待一段时间,如果在这段时间内对端口再次进行绑定,就会出错,所以我们要将监听socket的地址重用开启,通过setsockopt函数,其函数原型如下:
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
//sockfd: 套接字描述字
//level: 选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6.
//optname: 需要设置的选项
//optval: 指针,指向存放选项值的缓冲区
//optlen: optval缓冲区长度
SOL_SOCKET的设置项如下:
选项名称 | 说明 | 数据类型 |
---|---|---|
SO_BROADCAST | 允许发送广播数据 | int |
SO_DEBUG | 允许调试 | int |
SO_DONTROUTE | 不查找路由 | int |
SO_ERROR | 获得套接字错误 | int |
SO_KEEPALIVE | 保持连接 | int |
SO_LINGER | 延迟关闭连接 | struct linger |
SO_OOBINLINE | 带外数据放入正常数据流 | int |
SO_RCVBUF | 接收缓冲区大小 | int |
SO_SNDBUF | 发送缓冲区大小 | int |
SO_RCVLOWAT | 接收缓冲区下限 | int |
SO_SNDLOWAT | 发送缓冲区下限 | int |
SO_RCVTIMEO | 接收超时 | struct timeval |
SO_SNDTIMEO | 发送超时 | struct timeval |
SO_REUSERADDR | 允许重用本地地址和端口 | int |
SO_TYPE | 获得套接字类型 | int |
SO_BSDCOMPAT | 与BSD系统兼容 | int |
在设置监听socket地址重用的时候,操作如下:
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));
代码
int tcp_server(const char *host, unsigned short port)
{
int listenfd, addrlen;
struct sockaddr_in server_addr;
struct hostent *hp;
int ret;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
ERR_EXIT("server socket filed \n");
}
/* 绑定IP地址与端口,对host的不同情况做处理:主机名或者是IP地址 */
addrlen = sizeof(server_addr);
memset(&server_addr, 0, addrlen);
server_addr.sin_family = AF_INET;
//这里的gethostbyname没有经过验证 可能存在错误
if (host != NULL) { //host非空进行指定IP绑定
if (inet_pton(AF_INET, host, (void*)&server_addr.sin_addr.s_addr) == 0) { //给出主机名
if ((hp = gethostbyname(host)) == NULL) ERR_EXIT("gethostbyname"); //错误主机名
server_addr.sin_addr = *(struct in_addr*)hp->h_addr; //获取主机上第一个IP地址
}
} else { //绑定所有IP地址
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
}
server_addr.sin_port = htons(port);
int on = 1;
//int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on)) < 0) //允许地址重用
ERR_EXIT("server getsockopt");
ret = bind(listenfd, (struct sockaddr*)&server_addr, (socklen_t)addrlen);
if (ret == -1) {
ERR_EXIT("server bind filed \n");
}
/* 监听listenfd */
ret = listen(listenfd, 5);
if (ret == -1) {
ERR_EXIT("server listen filed \n");
}
return listenfd;
}
2.3 命令映处理框架
在控制连接通道中,客户端与服务进程进行FTP命令交互,传输的是FTP的命令,服务进程会根据解析出来的命令提供相应的服务。
要建立一个映射关系,使服务器解析出来的命令有对应的操作函数来执行命令。
通过结构体来绑定命令与操作函数,结构体成员就是命令(字符串形式)和函数指针(指向操作函数的指针),如下:
typedef struct ftpcmd {
const char *cmd;
void (*cmd_func)(session_t *sess);
} ftpcmd_t;
每一个命令与相应操作的函数都抽象在一个结构体中,由于FTP的命令很多,所以将这些命令抽象的结构体放在一个结构体数组中统一管理(结构体中如果实现了命令的操作就传递函数名即可),如下:
// 访问控制命令
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 ftpcmd_t ctrl_cmds_map[] =
{
// 访问控制映射
{"USER", do_user},
{"PASS", do_pass},
{"CWD", do_cwd},
{"XCWD", do_cwd},
{"CDUP", do_cdup},
{"XDUP", do_cdup},
{"QUIT", do_quit},
{"ACCT", NULL},
{"SMNT", NULL},
{"REIN", NULL},
// 传输参数命令
{"PORT", do_port},
{"PASV", do_pasv},
{"TYPE", do_type},
{"STRU", NULL},
{"MODE", NULL},
// 服务命令
{"RETR", do_retr},
{"STOR", do_stor},
{"APPE", do_appe},
{"LIST", do_list},
{"NLST", do_nlst},
{"REST", do_rest},
{"ABOR", do_abor},
{"\377\364\377\362ABOR", do_abor},
{"PWD", do_pwd},
{"XPWD", do_pwd},
{"MKD", do_mkd},
{"XMKD", do_mkd},
{"RMD", do_rmd},
{"XRMD", do_rmd},
{"DELE", do_dele},
{"RNFR", do_rnfr},
{"RNTO", do_rnto},
{"SITE", do_site},
{"SYST", do_syst},
{"FEAT", do_feat},
{"SIZE", do_size},
{"STAT", do_stat},
{"NOOP", do_noop},
{"HELP", do_help},
{"STOU", NULL},
{"ALLO", NULL}
};
向对应操作函数传递的参数,是一个结构体指针,这个结构体中包含了服务会话中的关键变量:
typedef struct session {
//控制连接
uid_t uid; //用户ID
int ctl_fd; //控制连接socket
char cmdline[MAX_COMMAND_LINE]; //命令行
char cmd[MAX_COMMAND]; //解析出的命令
char arg[MAX_ARG]; //命令的参数
//数据连接
struct sockaddr_in *port_addr;
int pasv_listen_fd;
int data_fd; //数据传输通道socket
int data_process; //数据连接通道建立与否
//限速
unsigned int bw_upload_rate_max;
unsigned int bw_download_rate_max;
long bw_transfer_start_sec; //s
long bw_transfer_start_usec; //us
//服务进程与nobody进程 通信的通道
int parent_fd;
int child_fd;
//FTP协议的状态
int is_ascii;
long long restart_pos;
char *rnfr_name;
int abor_received;
//连接数限制
unsigned int num_clients;
unsigned int num_this_ip; //当前IP的连接数
}session_t;
session_t中有当前会话所需的各种信息以及各种操作所需要的文件描述符。
2.4 双进程处理框架
每当服务进接收一个客户端的连接请求的时候,就会有主进程fork出一个会话,会话进程就是nobody进程,再由nobody进程fork服务进程。服务进程专门负责与客户端的命令交互以及相应的处理(比如上传、下载、登录……),nobody与服务进程之间通过域间socket来通信,nobody主要帮助服务进程执行权限更高的操作,因为服务进程是属于登录用户的,所以有一些操作时权限不够的,比如在PASV模式下绑定20端口。
服务进程建立数据连接通道的时候,也需要nobody进程帮助,nobody进程根据IP地址与端口创建连接通道,然后传给服务进程,由服务进程进行数据传输。
创建一个新会话的函数如下:
int begin_session(session_t *sess)
{
activate_oobinline(sess->ctl_fd);//开启接收带外数据功能 通过紧急模式来接收数据
pid_t pid;
priv_sock_init(sess); //初始化进程间通信 创建进程间通信通道
pid = fork();
if (pid == 0) { //FTP服务进程 还是属于root,因为在用户登录验证的时候nobody
priv_sock_set_child_context(sess); //绑定socketpair
handle_child(sess); //从客户端循环接收数据,处理FTP协议相关细节,处理控制连接和数据链接
} else if (pid > 0) { //nobody进程
priv_sock_set_parent_context(sess);//绑定socketpair
handle_parent(sess); //从服务进程接收信息,辅助服务器与客户端之间建立数据连接
} else ERR_EXIT("session fork");
return 0;
}
由于服务器是由root用户启动的,所以这里的nobody进程和服务进程还是属于root用户的。接下来会将nobody进程设置在nobody用户下,用户登录验证成功后也会将服务进程设置在登录用户下。
为什么要强调用户呢?因为对于支持多任务的Linux系统来说,用户身份就是获取资源的凭证,用户通常表现为一个唯一的数字标识以uid来标识。当进程在访问系统资源的时候,必须要有一定的权限,这个权限就是进程所在用户给予的,也就是说进程必须携带发起这个进程的用户的身份信息才能够进行合法的操作。
nobody切换用户,实际上就是重新设置组ID和用户ID,如下:
/* 以root用户启动的时候 gid、uid都是0,所以要获取用户登录相关信息 */
struct passwd *pw = getpwnam("nobody"); //获取用户登录相关信息
if (setegid(pw->pw_gid) < 0) ERR_EXIT("session setegid"); //先设置组ID,然后设置用户ID
if (seteuid(pw->pw_uid) < 0) ERR_EXIT("session seteuid");
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 */
};
服务进程的用户切换在验证客户端登录后进行。
miniFTP项目实战二的更多相关文章
- miniFTP项目实战五
项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...
- miniFTP项目实战六
项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...
- miniFTP项目实战三
项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...
- miniFTP项目实战四
项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...
- 【NFS项目实战二】NFS共享数据的时时同步推送备份
[NFS项目实战二]NFS共享数据的时时同步推送备份 标签(空格分隔): Linux服务搭建-陈思齐 ---本教学笔记是本人学习和工作生涯中的摘记整理而成,此为初稿(尚有诸多不完善之处),为原创作品, ...
- PHP之MVC项目实战(二)
本文主要包括以下内容 GD库图片操作 利用GD库实现验证码 文件上传 缩略图 水印 GD库图片操作 <?php $img = imagecreatetruecolor(500, 300); // ...
- React-Native 之 项目实战(二)
前言 本文有配套视频,可以酌情观看. 文中内容因各人理解不同,可能会有所偏差,欢迎朋友们联系我. 文中所有内容仅供学习交流之用,不可用于商业用途,如因此引起的相关法律法规责任,与我无关. 如文中内容对 ...
- appium+python自动化项目实战(二):项目工程结构
废话不多说,直接上图: nose.cfg配置文件里,可以指定执行的测试用例.生成测试报告等.以后将详细介绍.
- miniFTP项目集合
项目简介 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进 ...
随机推荐
- 6-x2 echo命令:将指定字符串输出到 STDOUT
echo 用法 常用转义符 echo 用法 echo 用来在终端输出字符串,并在最后默认加上换行符. echo 加上-n参数可以使数据字符串后不再换行 echo 加上-e参数可以解析转义字符 ...
- rename 批量修改文件名
1.rename的用法 rename与mv的区别就是mv只能对单个文件重命名,而rename可以批量修改文件名 linux中的rename有两种版本,一种是C语言版的,一种是Perl版的.早期的Lin ...
- Python单元测试框架unittest之批量用例管理(discover)
前言 我们在写用例的时候,单个脚本的用例好执行,那么多个脚本的时候,如何批量执行呢?这时候就需要用到unittet里面的discover方法来加载用例了.加载用例后,用unittest里面的TextT ...
- 以太网MAC地址组成与交换机基本知识
以太网MAC地址 MAC地址由48位二进制组成,通常分为六段,用十六进制表示,工作在数据链路层. 数据链路层功能: 链路的建立,维护与拆除 帧包装,帧传输,帧同步 帧的差错恢复 简单的流量控制 第八位 ...
- 从sql2008导入表结构及数据到mysql
打开navicat for mysql连接mysqlcreate database lianxi刷新,双击lianxi,双击"表"点击右边的"导入向导"选择&q ...
- OSPF的基本工作原理
OSPF的基本工作原理 1.定义 2.特点 3.基本概念 4.OSPF五种分组类型 5.DR/BDR 6.区域 1.定义 开放最短路径优先OSPF,是为了克服RIP的缺点在1989年开发出来的. &q ...
- SQL注入:sqli-labs lesson-1 小白详解
为什么是小白详解?因为我就是小白 SQL注入早有耳闻,今天算是真正打开这个门了,但是想要跨进去应该还是没有那么容易. 在B站上听了40分钟的网课,老实说,他讲的还不错,第一遍听不懂也正常 https: ...
- 微信小程序云开发-云存储-获取带图片的商品列表
一.将商品图片上传至云存储 如下图,已准备5张商品图片,并且已经将商品图片上传至云存储 二.数据库表添加图片字段 在数据库表goods添加字段image,该字段用来存储图片的url信息 image在 ...
- debian 9安装细节
1.安装KDE桌面 2.开机桌面正常启动,首先在grub启动界面,按"e"键,在linux......quiet后面加上nomodeset,然后进入桌面,在终端输入: su -vi ...
- 浏览器不支持promise的finally
IE浏览器以及edge浏览器的不支持es6里面promise的finally 解决方法: 1.npm install axios promise.prototype.finally --save 2. ...