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来标识。当进程在访问系统资源的时候,必须要有一定的权限,这个权限就是进程所在用户给予的,也就是说进程必须携带发起这个进程的用户的身份信息才能够进行合法的操作。

参考:linux 用户身份与进程权限

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 */
};

服务进程的用户切换在验证客户端登录后进行。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值