博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
miniFTP项目实战四
阅读量:3951 次
发布时间:2019-05-24

本文共 17522 字,大约阅读时间需要 58 分钟。

项目简介:

在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务进程来为客户提供服务。同时每个ftp服务进程配套了nobody进程(内部私有进程),主要是为了做权限提升和控制。
实现功能:
除了基本的文件上传和下载功能,还实现模式选择、断点续传、限制连接数、空闲断开、限速等功能。
用到的技术:
socket、I/O复用、进程间通信、HashTable
欢迎技术交流q:2723808286
项目开源!!!

在这里插入图片描述

文章目录

传输目录列表

在用户登录之后,客户端会与服务器协商,传输的文件类型以及传输的类型,之后再LIST申请目录列表:

在这里插入图片描述

文件的传输类型一般都是ASCII,传输模式需要根据有无NAT防火墙来选择,具体的确保在之前以及介绍过了。

4.1 PASV模式

服务器被动连接,由nobody进程创建监听socket,将创建好的监听socket传递给服务进程,服务进程返回服务器的IP地址及端口号,其处理逻辑如下:

static void do_pasv(session_t *sess){
//由nobody进程创建监听套接字 ,并返回端口 服务进程中通过getlocalip获取IP地址 priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_LISTEN); unsigned short port = (int)priv_sock_get_int(sess->child_fd); //获取端口号 char ip[16] = {
0}; int ret = getlocalip(ip); if (ret == -1) printf("getlocalip filed\n"); char tmp[1024] = {
0}; unsigned int v[4]; sscanf(ip, "%u.%u.%u.%u", &v[0], &v[1], &v[2], &v[3]); sprintf(tmp, "Entering Passive Mode (%u,%u,%u,%u,%u,%u)", v[0], v[1], v[2], v[3], port>>8, port&0xff); ftp_relply(sess, FTP_PASVOK, tmp);}

由于getlocalip(ip)获取的是点分形式的IP地址,所以通过sscanf格式化获取IP地址,然后格式化到tmp字符串中返回给客户端。

4.2 PORT模式

PORT模式下,服务器主动连接客户端,客户端的命令参数中说明了客户端的IP地址以及端口号,服务进程解析IP地址及端口号,保存在sess中,后续nobody进程根据sess中的IP地址进行连接,处理逻辑如下:

static void do_port(session_t *sess){
unsigned int v[6] = {
0}; //直接使用sscanf格式化输入,提取相关数据 sscanf(sess->arg, "%u,%u,%u,%u,%u,%u", &v[2], &v[3], &v[4], &v[5], &v[0], &v[1]); sess->port_addr = (struct sockaddr_in*)malloc(sizeof(struct sockaddr_in)); memset(sess->port_addr, 0, sizeof(struct sockaddr_in)); sess->port_addr->sin_family = AF_INET; unsigned char *p = (unsigned char*)&sess->port_addr->sin_port; p[0] = v[0]; p[1] = v[1]; p = (unsigned char*)&sess->port_addr->sin_addr; p[0] = v[2]; p[1] = v[3]; p[2] = v[4]; p[3] = v[5]; //收到PORT后要回复 ftp_relply(sess, FTP_PORTOK, "PORT command sucessful. COnsider using PASV."); //接下来客户端会发送LIST命令}

4.3 LIST 命令中创建数据连接

PASV和PORT已经确定了双方的连接方式,LIST命令是传输当前目录的文件列表,首先应该创建数据传输通道!

数据传输通道创建完成之后,读取目录列表信息,经过提取信息后传输目录列表,最后关闭数据传输通道,回应226

在数据传输完成之后要及时关闭数据传输通道,即socket,因为客户端处于一直接收的状态,只有服务器关闭socket客户端才会停止接收,作出反应,逻辑如下:

static void do_list(session_t *sess){
//获取数据传输通道的fd if (get_transfer_fd(sess) == 0) {
return ; } //回应150 ftp_relply(sess, FTP_DATACONN, "Here comes the directory listing."); //传输列表 list_common(sess, 1); //全部 //关闭数据通道 如果不及时关闭通道 客户端是不会接收停止的,即关闭之后客户端才会作出反应 close(sess->data_fd); //回应226 ftp_relply(sess, FTP_TRANSFEROK, "Directory send ok.");}

其中最重要的有两部分:

  • 数据传输通道的创建,即nobody通过socket与客户端建立连接
  • 传输目录列表,要读取当前目录的信息,并且获取文件的信息发送

4.4 数据传输通道的建立

会根据PORT还是PASV来创建数据传输通道,所以在连接之前首先判断是PORT模式还是PASV模式,处理逻辑如下:

/*  * 根据模式的不同建立数据连接通道 * PORT:主动连接客户端 * PASV:被动接受客户端连接 *  */int get_transfer_fd(session_t *sess){
int ret = 1; //检测 PORT or PASV 是否都没有激活 if (!port_active(sess) && !pasv_active(sess)) {
ftp_relply(sess, FTP_BADSENDCONN, "Use PORT or PASV first."); return 0; } //主动模式服务器绑定20端口 创建socket主动connect客户端,调用sysutil.c中实现的tcp_client if (port_active(sess)) {
if (get_port_fd(sess) == 0) {
//失败 ret = 0; } } if (pasv_active(sess)) {
if (get_pasv_fd(sess) == 0) {
//获取到之后就保存在sess->data_fd ret = 0; } //监听socket作用就是 为数据连接通道做准备,每次数据连接完成之后都会断开,下一次重新连接 close(sess->pasv_listen_fd); } //malloc的地址已经没有利用价值了,以及绑定好啦 if (sess->port_addr != NULL) {
free(sess->port_addr); sess->port_addr = NULL; } if (ret) start_data_alarm(); //在数据传输之前重新安装信号 并启动闹钟 return ret;}

4.5 PORT模式下数据传输通道的创建

//获取PORT模式下数据传输通道的fdint get_port_fd(session_t *sess){
//由nobody进程创建数据连接通道,服务进程向nobody发起一个PRIV_SOCK_GET_DATA_SOCK创建数据通道请求 priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_GET_DATA_SOCK); //然后向nobody发送一个int port unsigned short port = ntohs(sess->port_addr->sin_port); priv_sock_send_int(sess->child_fd, (int)port); //发送实际是short 强转为int //然后向nobody发送IP地址 字符串 char *ip = inet_ntoa(sess->port_addr->sin_addr); priv_sock_send_buf(sess->child_fd, ip, strlen(ip)); //接收应答判断 int res = priv_sock_get_result(sess->child_fd); if (res == PRIV_SOCK_RESULT_BAD) {
printf("create data filed\n"); return 0; } else if (res == PRIV_SOCK_RESULT_OK) {
sess->data_fd = priv_sock_recv_fd(sess->child_fd); //接收数据传输通道sock fd } return 1;}

服务进程向nobody进程发送PRIV_SOCK_GET_DATA_SOCK,请求nobody进程建立数据传输通道,接着向nobody进程发送客户端的IP地址与端口号:

void privop_pasv_get_data_sock(session_t *sess){
//接收IP地址与端口 unsigned short port = priv_sock_get_int(sess->parent_fd); char ip[16] = {
0}; priv_sock_recv_buf(sess->parent_fd, ip, sizeof(ip)); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); //转换为网络字节序 addr.sin_addr.s_addr = inet_addr(ip); int fd = tcp_client(20); //绑定20端口 if (fd == -1) {
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD); return ; } //建立数据连接 if (connect_timeout(fd, &addr, tunable_connect_timeout) < 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, fd); close(fd);}

4.6PASV模式下数据传输通道的创建

int get_pasv_fd(session_t *sess) {
//请求PASV 连接socket fd priv_sock_send_cmd(sess->child_fd, PRIV_SOCK_PASV_ACCEPT); char ret = priv_sock_get_result(sess->child_fd); if (ret == PRIV_SOCK_RESULT_BAD) {
return 0; } else if (ret == PRIV_SOCK_RESULT_OK) {
sess->data_fd = priv_sock_recv_fd(sess->child_fd); } return 1;}

PASV模式下,服务进程向nobody进程发送PRIV_SOCK_PASV_ACCEPT,nobody通过accept_timeout等待客户端的连接请求,当连接建立之后,nobody向服务进程传递数据传输通道的文件描述符,nobody进程的响应如下:

//服务进程请求数据连接socket的时候会向nobody发送accept请求void privop_pasv_accept(session_t *sess){
int data_fd = accept_timeout(sess->pasv_listen_fd, NULL, tunable_accept_timeout); close(sess->pasv_listen_fd); sess->pasv_listen_fd = -1; if (data_fd == -1) {
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_BAD); return ; } else {
priv_sock_send_result(sess->parent_fd, PRIV_SOCK_RESULT_OK); priv_sock_send_fd(sess->parent_fd, data_fd); close(data_fd); //nobody进程不进行数据传输 断开 }}

4.7目录的传输

无论PORT模式的数据传输,还是PASV模式的数据传输,对服务进程都一样!!!因为服务进程最终拿到的是数据传输通道的socket文件描述符,服务进程可以通过这个socket文件描述符向客户端发送数据。

在处理LIST命令的时候逻辑如下:

static void do_list(session_t *sess){
//获取数据传输通道的fd if (get_transfer_fd(sess) == 0) {
return ; } //回应150 ftp_relply(sess, FTP_DATACONN, "Here comes the directory listing."); //传输列表 list_common(sess, 1); //全部 //关闭数据通道 如果不及时关闭通道 客户端是不会接收停止的,即关闭之后客户端才会作出反应 close(sess->data_fd); //回应226 ftp_relply(sess, FTP_TRANSFEROK, "Directory send ok.");}

上面已经介绍了get_transfer_fd是如何创建数据传输通道的,下面就说一下list_common如何传输列表信息。

需要传输的列表信息有:

  • 文件类型以及权限
  • 文件连接数、uid、gid、大小
  • 文件日期,分为两种格式
  • 文件名,注意符号链接文件还要显式支持原文件名字

如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VkMvU1PV-1613482997348)(F:\destop\m笔记\图\image-20210216181605784.png)]

打开当前目录

首先要打开当前目录,通过opendir函数可以打开目录,其函数原型如下:

DIR *opendir(const char *name);//The  opendir()  function  opens  a  directory  stream corresponding to the directory name, and returns a//pointer to the directory stream.  The stream is positioned at the first entry in the directory.//即根据路径打开一个目录,返回一个目录流指针,指针指向目录流中的第一个项目

然后通过readdir返回目录流所指向文件的信息,readdir函数原型如下:

struct dirent *readdir(DIR *dirp);struct dirent {
ino_t d_ino; /* Inode number */ off_t d_off; /* Not an offset; see below */ unsigned short d_reclen; /* Length of this record */ unsigned char d_type; /* Type of file; not supported by all filesystem types */ char d_name[256]; /* Null-terminated filename */};

结构体中我们只需要关注d_name,即关注文件的名字,通过文件的名字可以获取文件的状态信息,stat函数原型如下:

int stat(const char *pathname, struct stat *statbuf);int fstat(int fd, struct stat *statbuf);int lstat(const char *pathname, struct stat *statbuf);struct stat {
dev_t st_dev; /* ID of device containing file */ ino_t st_ino; /* Inode number */ mode_t st_mode; /* File type and mode */ nlink_t st_nlink; /* Number of hard links */ uid_t st_uid; /* User ID of owner */ gid_t st_gid; /* Group ID of owner */ dev_t st_rdev; /* Device ID (if special file) */ off_t st_size; /* Total size, in bytes */ blksize_t st_blksize; /* Block size for filesystem I/O */ blkcnt_t st_blocks; /* Number of 512B blocks allocated */ /* Since Linux 2.6, the kernel supports nanosecond precision for the following timestamp fields. For the details before Linux 2.6, see NOTES. */ struct timespec st_atim; /* Time of last access */ struct timespec st_mtim; /* Time of last modification */ struct timespec st_ctim; /* Time of last status change */ #define st_atime st_atim.tv_sec /* Backward compatibility */ #define st_mtime st_mtim.tv_sec #define st_ctime st_ctim.tv_sec};

主要就是通过stat结构体来获取要发送的信息!!!

  • st_mode:保存文件类型及权限位,以及可以获取符号链接文件指向的源文件
  • st_size:文件大小
  • st_mtime:文件最后的修改时间

下面一一介绍:

获取文件类型及权限位

通过lstat函数获取文件的状态信息,然后根据statbuf.st_mode判断文件的类型与权限位,通过与宏定义相与的结果来判断。

得到的结果用一个数组来容纳,最后将每个部分的信息格式化到一个字符串中,发送字符串,获取文件类型与权限位的代码如下:

char perms[] = "----------"; //获取文件类型以及权限位 十个字符mode_t mode = statbuf.st_mode;  //statbuf.st_mode 中保存文件类型以及权限位switch (mode & S_IFMT) {
case S_IFREG:perms[0] = '-'; break; case S_IFDIR:perms[0] = 'd'; break; case S_IFBLK:perms[0] = 'b'; break; case S_IFLNK:perms[0] = 'l'; break; case S_IFCHR:perms[0] = 'c'; break; case S_IFSOCK:perms[0] = 's';break; case S_IFIFO:perms[0] = 'p'; break; default:break;}if (mode & S_IRUSR) perms[1] = 'r';if (mode & S_IWUSR) perms[2] = 'w';if (mode & S_IXUSR) perms[3] = 'x';if (mode & S_IRGRP) perms[4] = 'r';if (mode & S_IWGRP) perms[5] = 'w';if (mode & S_IXGRP) perms[6] = 'x';if (mode & S_IROTH) perms[7] = 'r';if (mode & S_IWOTH) perms[8] = 'w';if (mode & S_IXOTH) perms[9] = 'x';// special permsif (mode & S_ISUID) perms[3] = (perms[3] == 'x' ? 's' : 'S');if (mode & S_ISGID) perms[6] = (perms[6] == 'x' ? 's' : 'S');if (mode & S_ISVTX) perms[9] = (perms[9] == 'x' ? 's' : 'S');

获取连接数、uid、gid、文件大小

都是通过stat结构体直接获取:

char buf[1024] = {
0}; //每次都要重新初始化off = 0;off += sprintf(buf, "%s ", perms); //添加文件类型 权限位off += sprintf(buf + off, "%3ld %-8d %-8d ", statbuf.st_nlink, statbuf.st_uid, statbuf.st_gid); //连接数、uid、gidoff += sprintf(buf + off, "%8lu ", (unsigned long)statbuf.st_size); //添加文件大小

所有文件的信息都放在buf数组中,根据off来决定下一中属性存放的位置。

获取时间

先分析一下FTP中日期的格式:

drwxr-xr-x    3 1000     1000         4096 Feb 02 11:37 Desktopdrwxr-xr-x    2 1000     1000         4096 Mar 21  2020 Documentsdrwxr-xr-x    2 1000     1000         4096 Mar 21  2020 Downloadsdrwxr-xr-x    2 1000     1000         4096 Mar 21  2020 Musicdrwxr-xr-x    2 1000     1000         4096 Mar 21  2020 Picturesdrwxr-xr-x    2 1000     1000         4096 Mar 21  2020 Publicdrwxr-xr-x    2 1000     1000         4096 Mar 21  2020 Templatesdrwxr-xr-x    2 1000     1000         4096 Mar 21  2020 Videos-rw-r--r--    1 1000     1000         8980 Mar 21  2020 examples.desktopdrwxrwxr-x    9 1000     1000         4096 Feb 06 10:04 learn-rw-r--r--    1 1000     1000         2193 Mar 28  2020 vimrc

日期分为两种格式:

//如果文件时间新drwxr-xr-x    3 1000     1000         4096 Feb 02 11:37 Desktop//如果文件时间旧 或者是半年之前的文件drwxr-xr-x    2 1000     1000         4096 Mar 21  2020 Documents

首先要获取当前系统的时间,和文件最后一次修改时间进行比较,判断文件的格式

获取当前时间可以通过gettimeofday:

int gettimeofday(struct timeval *tv, struct timezone *tz); //tz为NULL表示当前系统时区The functions gettimeofday() and settimeofday() can get and set the time as well as a timezone.  The tv argument is a struct timeval (as specified in 
): struct timeval {
time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ };and gives the number of seconds and microseconds since the Epoch (see time(2)). The tz argument is a struct timezone: struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */ int tz_dsttime; /* type of DST correction */ };

然后和stat中的struct timespec st_atim; /* Time of last access */比较

如果文件时间比系统时间大,系统文件比文件时间早半年 表示文件是旧的,采用如下格式:

drwxr-xr-x    2 1000     1000         4096 Mar 21  2020 Documentsp_date_format = “%b %e   %Y”;

通过调用size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);格式化时间,但是需要一个struct tm *tm,需要将秒转换为结构体的形式,通过localtime:

size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);  //格式化时间struct tm *localtime(const time_t *timep);  //将秒 转换为struct tm

代码如下:

//获取时间char date_buf[64] = {
0};const char *p_date_format = "%b %e %H:%M";struct timeval tv;gettimeofday(&tv, NULL);time_t local_time = tv.tv_sec;if (statbuf.st_mtime > local_time || (local_time - statbuf.st_mtime) > 60*60*24*182) p_date_format = "%b %e %Y";struct tm *p_tm = localtime(&local_time);strftime(date_buf, sizeof(date_buf), p_date_format, p_tm); //将时间按照对应格式格式化为字符串off += sprintf(buf + off, "%s ", date_buf);

获取文件名

文件名的获取时要注意,符号链接文件要显式出与指向文件的关系,使用readlink函数获取实际指向的文件,将指向的文件保存在buf中。

所以符号链接文件和一般文件要分开获取文件名:

//获取文件名  符号链接文件要显式指向源文件if (S_ISLNK(statbuf.st_mode)) {
char tmp[1024] = {
0}; readlink(dt->d_name, tmp, sizeof(tmp)); sprintf(buf + off, "%s -> %s\r\n", dt->d_name, tmp);} else {
sprintf(buf + off, "%s\r\n", dt->d_name);}

整体融合

在获取到各个部分的信息之后,整合在buf中,通过writen发送给客户端,如下:

int list_common(session_t *sess, int detail){
DIR *dir = opendir("./"); //打开当前目录 struct dirent *dt; //从目录中获取文件 struct stat statbuf; //获取文件信息 int off = 0; //在整合的时候记录位置 if (dir == NULL) return 0; //根据readdir遍历目录 使用lstat获取文件状态信息 //这里使用lstat,就是在符号链接文件的情况 查看链接文件的状态,而不是产看源文件 if (detail == 1) {
while ((dt = readdir(dir)) != NULL) {
if (lstat(dt->d_name, &statbuf) < 0 || dt->d_name[0] == '.') {
//获取文件状态信息 continue; } char perms[] = "----------"; //获取文件类型以及权限位 十个字符 mode_t mode = statbuf.st_mode; //statbuf.st_mode 中保存文件类型以及权限位 switch (mode & S_IFMT) {
case S_IFREG:perms[0] = '-'; break; case S_IFDIR:perms[0] = 'd'; break; case S_IFBLK:perms[0] = 'b'; break; case S_IFLNK:perms[0] = 'l'; break; case S_IFCHR:perms[0] = 'c'; break; case S_IFSOCK:perms[0] = 's';break; case S_IFIFO:perms[0] = 'p'; break; default:break; } if (mode & S_IRUSR) perms[1] = 'r'; if (mode & S_IWUSR) perms[2] = 'w'; if (mode & S_IXUSR) perms[3] = 'x'; if (mode & S_IRGRP) perms[4] = 'r'; if (mode & S_IWGRP) perms[5] = 'w'; if (mode & S_IXGRP) perms[6] = 'x'; if (mode & S_IROTH) perms[7] = 'r'; if (mode & S_IWOTH) perms[8] = 'w'; if (mode & S_IXOTH) perms[9] = 'x'; // special perms if (mode & S_ISUID) perms[3] = (perms[3] == 'x' ? 's' : 'S'); if (mode & S_ISGID) perms[6] = (perms[6] == 'x' ? 's' : 'S'); if (mode & S_ISVTX) perms[9] = (perms[9] == 'x' ? 's' : 'S'); char buf[1024] = {
0}; //每次都要重新初始化 off = 0; off += sprintf(buf, "%s ", perms); //添加文件类型 权限位 off += sprintf(buf + off, "%3ld %-8d %-8d ", statbuf.st_nlink, statbuf.st_uid, statbuf.st_gid); //左对齐 添加连接数、uid、gid off += sprintf(buf + off, "%8lu ", (unsigned long)statbuf.st_size); //添加文件大小 //获取时间 char date_buf[64] = {
0}; const char *p_date_format = "%b %e %H:%M"; struct timeval tv; gettimeofday(&tv, NULL); time_t local_time = tv.tv_sec; if (statbuf.st_mtime > local_time || (local_time - statbuf.st_mtime) > 60*60*24*182) p_date_format = "%b %e %Y"; struct tm *p_tm = localtime(&local_time); strftime(date_buf, sizeof(date_buf), p_date_format, p_tm); //将时间按照对应格式格式化为字符串 off += sprintf(buf + off, "%s ", date_buf); //获取文件名 符号链接文件要显式指向源文件 if (S_ISLNK(statbuf.st_mode)) {
char tmp[1024] = {
0}; readlink(dt->d_name, tmp, sizeof(tmp)); sprintf(buf + off, "%s -> %s\r\n", dt->d_name, tmp); } else {
sprintf(buf + off, "%s\r\n", dt->d_name); } //通过sess中的数据通道socket发送 writen(sess->data_fd, buf, strlen(buf)); } } else {
while ((dt = readdir(dir)) != NULL) {
if (lstat(dt->d_name, &statbuf) < 0 || dt->d_name[0] == '.') {
//获取文件状态信息 continue; } char buf[1024] = {
0}; //每次都要重新初始化 //获取文件名 符号链接文件要显式指向源文件 if (S_ISLNK(statbuf.st_mode)) {
char tmp[1024] = {
0}; readlink(dt->d_name, tmp, sizeof(tmp)); sprintf(buf, "%s -> %s\r\n", dt->d_name, tmp); } else {
sprintf(buf, "%s\r\n", dt->d_name); } //通过sess中的数据通道socket发送 writen(sess->data_fd, buf, strlen(buf)); } } closedir(dir); return 1;}

其中detail参数表示是否发送文件的详细信息。

转载地址:http://vxwzi.baihongyu.com/

你可能感兴趣的文章
Android开发——架构组件LiveData源码解析
查看>>
IDEA常用快捷键整理
查看>>
【Vue】两个元素同一行显示
查看>>
XXL-Job使用
查看>>
如何在SwaggerAPI中添加统一授权认证
查看>>
多线程
查看>>
【Linux】Centos7 常用命令
查看>>
【Redis】Centos7下安装Redis
查看>>
【Redis】Centos7下搭建Redis集群
查看>>
【Redis】Centos7下搭建Redis集群——哨兵模式
查看>>
【Linux】本地ping不同VM虚拟机
查看>>
【SpringCloud】Hystrix
查看>>
乐观锁、悲观锁、公平锁、可重入锁
查看>>
快速阅读——《认知篇》
查看>>
【C#】返回值为DataTable的数据
查看>>
【Asp.net】基本概念
查看>>
【Asp.net】Web服务器控件
查看>>
【Asp.net】内置对象
查看>>
C语言数据类型笔记 by STP
查看>>
C语言指针笔记 by STP
查看>>