FTP简介

文件传输协议FTP(File Transfer Protocol,由RFC 959描述)。

FTP工作在TCP/IP协议族的应用层,其传输层使用的是TCP协议,它是基于客户/服务器模式工作的。

FTP支持的文件类型

①、ASCII码文件,这是FTP默认的文本格式【常用】

②、EBCDIC码文件,它也是一种文本类型文件,用8位代码表示一个字符,该文本文件在传输时要求两端都使用EBCDIC码【不常用】

③、图象(Image)文件,也称二进制文件类型,发送的数据为连续的比特流,通常用于传输二进制文件【常用】

④、本地文件,字节的大小有本地主机定义,也就是说每一字节的比特数由发送方规定【不常用】

所以我们要实现的FTP也只支持ASCII码文件和图像文件类型,对于这两种文件类型,到底有什么区别呢?下面做一个简要的介绍:

对于文本文件和二进制文件,最直观的区别就是文本文件是可以查看,而二进制打开看到的是乱码,实际上,这两者在物理结构(或存储结构)上都是由一系列的比特位构成的,它们之间的区别仅仅是在逻辑上:ASCII码文件是由7个比特位构成,最高位总是0(因为一个字节=8位),所以ASCII码文件最多能表示的字符数为2^7=128个,通过man帮助也能看到:

而如果最高位为1则打开就会是乱码,也就是二进制文件最高位应该就是1,这是一个区别。

另外一个区别就是\r\n换行符,在不同平台上是不一样的:windows上换行是用\r\n表示;linux上换行是用\n表示;mac上换行是用\r表示。如果在传输文件的时候,以这两种文件类型传输实际上对\r\n的解析方式是不同的,至于有什么不同,这里通过FTP客户端连接FTP服务端来做一个演示,首先启动FTP服务器,这里用vsftpd服务器:

接下来进行ftp文件配置:

配置好之后接下来重新启动vsftpd服务:

接下来用一个ftp客户端来进行连接,连接ftp服务器的客户端有很多工具,这里用“LeapFtp”:

接下来新建一个文件进行上传:

可以用十六进制的文本编辑器来查看一下内容:

接下来开始上传它至FTP服务器:

上传之后的大小也是8个字节,来用命令查看一下:

那如果是用二进制文件上传又会是怎么样呢?

那这两种类型难道没有差别么,实际上在我的机器上是没差别,在有些机器上是有区别的,区别如下:

如果以ASCII方式来传输文件,并且从windows->linux会将\r\n转换成\n,而从linux->windows会将\n转换成\r\n;而如果以二进制文件来传输文件,那么不做任何转换。

在C语言阶段其实我们也学过了打开文件可以以ASCII和二进制两种方式打开,这两者的区别也就只是换行符的不同,跟上面一样。

FTP文件的数据结构【仅做了解】

文件结构,这是FTP默认的方式,文件被认为是一个连续的字节流,文件内部没有表示结构的信息。

记录结构,该结构只适用于文本文件(ASCII码或EBCDIC码文件)。记录结构文件是由连续的记录构成的。

页结构,在FTP中,文件的一个部分被称为页。当文件是由非连续的多个部分组成时,使用页结构,这种文件称为随机访问文件。每页都带有页号发送,以便收方能随机地存储各页。

文件的传输方式【文件的数据结构会影响传输方式】

流方式,这是支持文件传输的默认方式,文件以字节流的形式传输。【主流FTP也仅仅实现了这种方式】

块方式,文件以一系列块来传输,每块前面都带有自己的头部。头部包含描述子代码域(8位)和计数域(16位),描述子代码域定义数据块的结束标志登内容,计数域说明了数据块的字节数。

压缩方式,用来对连续出现的相同字节进行压缩,现在已很少使用。

FTP工作原理

启动FTP

在客户端,通过交互式的用户界面,客户从终端上输入启动FTP的用户交互式命令

建立控制连接

客户端TCP协议层根据用户命令给出的服务器IP地址,向服务器提供FTP服务的21端口(该端口是TCP协议层用来传输FTP命令的端口)发出主动建立连接的请求。服务器收到请求后,通过3次握手,就在进行FTP命令处理的用户协议解释器进程和服务器协议解释器进程之间建立了一条TCP连接。

以后所有用户输入的FTP命令和服务器的应答都由该连接进行传输,因此把它叫做控制连接。

建立数据连接

当客户通过交互式的用户界面,向FTP服务器发出要下载服务器上某一文件的命令时,该命令被送到用户协议解释器。

其中用户的动作会解析成相对应的一些FTP命令,如看到的:

其实也可以用windows的命令来进行FTP连接,也能很清晰地看出用户的每个动作都会解析成对应的FTP命令:

FTP命令【先列出来众览下,之后会一一实现】

FTP应答

FTP应答格式

服务器通过控制连接发送给客户的FTP应答,由ASCII码形式的3位数字和一行文本提示信息组成,它们之间用一个空格分割。应答信息的每行文本以回车<CR>和换行<LF>对结尾。

如果需要产生一条多行的应答,第一行在3位数字应答代码之后包含一个连字符“-”,而不是空格符;最后一行包含相同的3位数字应答代码,后跟一个空格符,关于这个可以实际查看下:

FTP应答作用

确保在文件传输过程中的请求和正在执行的动作保持一致

保证用户程序总是可以得到服务器的状态信息,用户可以根据收到的状态信息对服务器是否正常执行了有关操作进行判定。

FTP应答数字含义【做了解,不需要记,想知道什么含义到时对照查看既可】

第一位数字标识了响应是好,坏或者未完成

第二位数响应大概是发生了什么错误(比如,文件系统错误,语法错误)

第三位为第二位数字更详细的说明

如:

500 Syntax error, command unrecognized. (语法错误,命令不能被识别)可能包含因为命令行太长的错误。

501 Syntax error in parameters or arguments. (参数语法错误)

502 Command not implemented. (命令没有实现)

503 Bad sequence of commands. (命令顺序错误)

504 Command not implemented for that parameter. (没有实现这个命令参数)

FTP应答示例【定义的宏,之后程序会用到,先列出来】

#define FTP_DATACONN 150

#define FTP_NOOPOK 200

#define FTP_TYPEOK 200

#define FTP_PORTOK 200

#define FTP_EPRTOK 200

#define FTP_UMASKOK 200

#define FTP_CHMODOK 200

#define FTP_EPSVALLOK 200

#define FTP_STRUOK 200

#define FTP_MODEOK 200

#define FTP_PBSZOK 200

#define FTP_PROTOK 200

#define FTP_OPTSOK 200

#define FTP_ALLOOK 202

#define FTP_FEAT 211

#define FTP_STATOK 211

#define FTP_SIZEOK 213

#define FTP_MDTMOK 213

#define FTP_STATFILE_OK 213

#define FTP_SITEHELP 214

#define FTP_HELP 214

#define FTP_SYSTOK 215

#define FTP_GREET 220

#define FTP_GOODBYE 221

#define FTP_ABOR_NOCONN 225

#define FTP_TRANSFEROK 226

#define FTP_ABOROK 226

#define FTP_PASVOK 227

#define FTP_EPSVOK 229

#define FTP_LOGINOK 230

#define FTP_AUTHOK 234

#define FTP_CWDOK 250

#define FTP_RMDIROK 250

#define FTP_DELEOK 250

#define FTP_RENAMEOK 250

#define FTP_PWDOK 257

#define FTP_MKDIROK 257

#define FTP_GIVEPWORD 331

#define FTP_RESTOK 350

#define FTP_RNFROK 350

#define FTP_IDLE_TIMEOUT 421

#define FTP_DATA_TIMEOUT 421

#define FTP_TOO_MANY_USERS 421

#define FTP_IP_LIMIT 421

#define FTP_IP_DENY 421

#define FTP_TLS_FAIL 421

#define FTP_BADSENDCONN 425

#define FTP_BADSENDNET 426

#define FTP_BADSENDFILE 451

#define FTP_BADCMD 500

#define FTP_BADOPTS 501

#define FTP_COMMANDNOTIMPL 502

#define FTP_NEEDUSER 503

#define FTP_NEEDRNFR 503

#define FTP_BADPBSZ 503

#define FTP_BADPROT 503

#define FTP_BADSTRU 504

#define FTP_BADMODE 504

#define FTP_BADAUTH 504

#define FTP_NOSUCHPROT 504

#define FTP_NEEDENCRYPT 522

#define FTP_EPSVBAD 522

#define FTP_DATATLSBAD 522

#define FTP_LOGINERR 530

#define FTP_NOHANDLEPROT 536

#define FTP_FILEFAIL 550

#define FTP_NOPERM 550

#define FTP_UPLOADFAIL 553

FTP两种工作模式

上次我们说过,FTP是由两种类型的连接构成的,一种是控制连接【主要是接收FTP客户端发来的命令请求,并且对这些命令进行应答】,一种是数据连接【双方之间进行数据的传输,包括目录列表的传输以及文件的传输】,其中控制连接总是由客户端向服务器发起,而数据连接则不同了,它有两种工作模式:主动模式【由服务器向客户端发起连接而建立数据连接通道】和被动模式【由客户端向服务器发起连接而建立数据连接通道】。下面来看一下这两个工作模式的工作过程:

主动模式

FTP客户端首先向服务器端的21端口发起连接,经过三次握手建设立控制连接通道,客户端本地也会选择一个动态的端口号AA,一旦控制连接通道建立之后,双方就可以交换信息了:客户端可以通过控制连接通道发起命令请求,服务器也可以通过它向客户端对这些命令请求进行应答。

接下来,如果要涉及到数据的传输,势必要创建一个数据连接:

在创建数据连接之前,要选择工作模式,如果是PORT模式,客户端会上服务器端发送PORT命令,这也是通过控制连接通道完成的,向服务器的21端口传送一个PORT命令,并且告知客户端的一个端口号BB,因为这个信息服务器端才知道要连接客户端的哪个端口号,服务器端得到了这个信息,最后就向BB端口号发起了一个请求,建立了一个数据连接通道,数据连接通道一旦建立完毕,就可以进行数据的传输了,包含目录列表、文件的传输,一旦数据传输完毕,数据连接通道就会关闭掉,它是临时的。

这里需要注意一点:

接下来用实验来说明一下双方建立的详细命令,这边通过登录一个客户端来看一下双方之间所交换的命令:

接下来进行数据传输,假设要传输一个列表,刷新一下。在获得列表之前需要创建一个数据连接,而在创建数据连接时需要根据模式来创建数据连接,这里面采用的是PORT模式:

其整个的工作过程如下:

被动模式

在了解了主动模式之后,被动模式就比较好理解了,如下:

从中可以发现,主被动模式只是连接建立的方向不同而已,同样的,也通过实验来查看一下PASV模式所要交换的FTP命令:

这时同样请求列表:

其整个的工作过程如下:

以上就是FTP的两种工作模式,那为什么要有这两种模式呢?这实际上是跟NAT或防火墙对主被动模式有关系,下面就来了解下:

NAT或防火墙对主被动模式的影响

什么是NAT

NAT的全称是(Network Address Translation),通过NAT可以将内网私有IP地址转换为公网IP地址。一定程度上解决了公网地址不足的问题。

其地址映射关系可以如下:

192.168.1.100:5678【内网IP】 -> 120.35.3.193:5678【NAT转换IP】 -> 50.118.99.200:80【外网IP】

从而就建立了一个连接,而连接的建立是通过NAT服务器进行地址转换完成的。

FTP客户端处于NAT或防火墙之后的主动模式

建立控制连接通道

因为NAT会主动记录由内部发送外部[相反则无法记录]的连接信息,而控制连接通道的建立是由客户向服务器端连接的,因此这一条接可以顺利地建立起来。
复制代码

客户端与服务器端数据连接建立时的通知

客户端先启用PORT BB端口,并通过命令通道告知FTP服务器,且等待服务器端的主动连接。
复制代码

服务器主动连接客户端

由于通过NAT转换之后,服务器只能得知NAT的地址并不知道客户端的IP地址,因此FTP服务器会以20端口主动地向NAT的PORT BB端口发送主动连接请求,但NAT并没有启用PORT BB端口,因而连接被拒绝。
复制代码

FTP客户端处于NAT或防火墙之后的被动模式

FTP服务器处于NAT或防火墙之后的被动模式

FTP服务器处于NAT或防火墙之后的主动模式

参数配置

我们要将程序中的开关做成可配置的,这里可以看一下VSFTP的配置文件:

空闲断开

保存并重启VSFTP服务:

可见过了5秒空闲连接就断开了,这时进程也结束了:

限速

也就是上传跟下载文件的限速功能,下面也来演示一下,默认情况下是没有限速的:

其速度传输过程序中会慢慢降到100K的样子。

连接数限制

这里包含两个方面的限制:总连接数的限制,针对所有IP来说的、同一个IP连接数的限制,下面来进行配置:

接下来配置同一个IP的连接数的限制:

断点续载与断点续传

当成功连接一个客户端时,这时可以看到创建了两个进程:

可见该FTP服务器是采用多进程的方式来实现的,为什么不用多线程的方式呢?

对于FTP服务器来讲,多线程的方式是绝对不可取的,因为:

那为什么连接一个客户端要创建两个进程呢?先看一下系统逻辑结构:

从中可以发现,服务进程是直接跟客户端进行通讯,而nobody进程并没有,它仅仅是跟服务进程通信,来协助服务进程来建立数据连接通道,以及需要一些特珠权限的控制,比如服务进程建立了连接之后,假设是PORT模式,由于是服务器端主动连接客户端,服务器端需要绑定20端口来连接客户端,而服务进程是没有权限来绑定20端口的,也就意味着没办法正常建立数据连接通道,所以需要加入nobody进程。而nobody和服务进程是采用内部通信的协议,这个协议对外是不可见的,完全可以由我们自己来定义,所以可以用UNIX域协议来进行通讯,而不用TCP/IP协议了。

功能实现

#ifndef LINUX_FTP_COMMON_H
#define LINUX_FTP_COMMON_H#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>#include <stdlib.h>
#include <stdio.h>
#include <string.h>#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} \while (0)#endif //LINUX_FTP_COMMON_H
复制代码
#ifndef LINUX_FTP_SYSUTIL_H
#define LINUX_FTP_SYSUTIL_H#include "common.h"int tcp_server(const char *host, unsigned short port);int getlocalip(char *ip);void activate_nonblock(int fd);
void deactivate_nonblock(int fd);int read_timeout(int fd, unsigned int wait_seconds);
int write_timeout(int fd, unsigned int wait_seconds);
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds);
int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds);ssize_t readn(int fd, void *buf, size_t count);
ssize_t writen(int fd, const void *buf, size_t count);
ssize_t recv_peek(int sockfd, void *buf, size_t len);
ssize_t readline(int sockfd, void *buf, size_t maxline);void send_fd(int sock_fd, int fd);
int recv_fd(const int sock_fd);#endif //LINUX_FTP_SYSUTIL_H
复制代码
//
// Created by zpw on 2019-06-08.
//#include "sysutil.h"/*** tcp_server - 启动TCP服务器* @param host 服务器IP地址或者服务器主机名* @param port 服务器端口* @return 成功返回监听套接字*/
int tcp_server(const char *host, unsigned short port) {//创建套接字int listenfd;if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0) {ERR_EXIT("socket");}struct sockaddr_in servaddr;memset(&servaddr, 0, sizeof(servaddr));servaddr.sin_family = AF_INET;if (host != NULL) {if (inet_aton(host, &servaddr.sin_addr) == 0) {//证明传过来的是主机名而不是点分十进制的IP地址,接下来要进行转换struct hostent *hp;hp = gethostbyname(host);if (hp == NULL) {ERR_EXIT("gethostbyname");}servaddr.sin_addr = *(struct in_addr *) hp->h_addr;}} else {servaddr.sin_addr.s_addr = htonl(INADDR_ANY);}servaddr.sin_port = htons(port);//端口号//设置地址重复利用int on = 1;if ((setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const char *) &on, sizeof(on))) < 0) {ERR_EXIT("gethostbyname");}//绑定if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {ERR_EXIT("bind");}//监听if (listen(listenfd, SOMAXCONN) < 0) {ERR_EXIT("listen");}return listenfd;
}int getlocalip(char *ip) {char host[100] = {0};if (gethostname(host, sizeof(host)) < 0) {return -1;}struct hostent *hp;if ((hp = gethostbyname(host)) == NULL) {return -1;}strcpy(ip, inet_ntoa(*(struct in_addr *) hp->h_addr));return 0;
}/*** activate_noblock - 设置I/O为非阻塞模式* @fd: 文件描符符*/
void activate_nonblock(int fd) {int ret;int flags = fcntl(fd, F_GETFL);if (flags == -1) {ERR_EXIT("fcntl");}flags |= O_NONBLOCK;ret = fcntl(fd, F_SETFL, flags);if (ret == -1) {ERR_EXIT("fcntl");}
}/*** deactivate_nonblock - 设置I/O为阻塞模式* @fd: 文件描符符*/
void deactivate_nonblock(int fd) {int ret;int flags = fcntl(fd, F_GETFL);if (flags == -1) {ERR_EXIT("fcntl");}flags &= ~O_NONBLOCK;ret = fcntl(fd, F_SETFL, flags);if (ret == -1) {ERR_EXIT("fcntl");}
}/*** read_timeout - 读超时检测函数,不含读操作* @fd: 文件描述符* @wait_seconds: 等待超时秒数,如果为0表示不检测超时* 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT*/
int read_timeout(int fd, unsigned int wait_seconds) {int ret;if (wait_seconds > 0) {fd_set read_fdset;struct timeval timeout;FD_ZERO(&read_fdset);FD_SET(fd, &read_fdset);timeout.tv_sec = wait_seconds;timeout.tv_usec = 0;do {ret = select(fd + 1, &read_fdset, NULL, NULL, &timeout);} while (ret < 0 && errno == EINTR);if (ret == 0) {ret = -1;errno = ETIMEDOUT;} else if (ret == 1) {ret = 0;}}return ret;
}/*** write_timeout - 读超时检测函数,不含写操作* @fd: 文件描述符* @wait_seconds: 等待超时秒数,如果为0表示不检测超时* 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT*/
int write_timeout(int fd, unsigned int wait_seconds) {int ret;if (wait_seconds > 0) {fd_set write_fdset;struct timeval timeout;FD_ZERO(&write_fdset);FD_SET(fd, &write_fdset);timeout.tv_sec = wait_seconds;timeout.tv_usec = 0;do {ret = select(fd + 1, NULL, NULL, &write_fdset, &timeout);} while (ret < 0 && errno == EINTR);if (ret == 0) {ret = -1;errno = ETIMEDOUT;} else if (ret == 1) {ret = 0;}}return ret;
}/*** accept_timeout - 带超时的accept* @fd: 套接字* @addr: 输出参数,返回对方地址* @wait_seconds: 等待超时秒数,如果为0表示正常模式* 成功(未超时)返回已连接套接字,超时返回-1并且errno = ETIMEDOUT*/
int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) {int ret;socklen_t addrlen = sizeof(struct sockaddr_in);if (wait_seconds > 0) {fd_set accept_fdset;struct timeval timeout;FD_ZERO(&accept_fdset);FD_SET(fd, &accept_fdset);timeout.tv_sec = wait_seconds;timeout.tv_usec = 0;do {ret = select(fd + 1, &accept_fdset, NULL, NULL, &timeout);} while (ret < 0 && errno == EINTR);if (ret == -1) {return -1;} else if (ret == 0) {errno = ETIMEDOUT;return -1;}}if (addr != NULL) {ret = accept(fd, (struct sockaddr *) addr, &addrlen);} else {ret = accept(fd, NULL, NULL);}return ret;
}/*** connect_timeout - connect* @fd: 套接字* @addr: 要连接的对方地址* @wait_seconds: 等待超时秒数,如果为0表示正常模式* 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT*/
int connect_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds) {int ret;socklen_t addrlen = sizeof(struct sockaddr_in);if (wait_seconds > 0) {activate_nonblock(fd);}ret = connect(fd, (struct sockaddr *) addr, addrlen);if (ret < 0 && errno == EINPROGRESS) {fd_set connect_fdset;struct timeval timeout;FD_ZERO(&connect_fdset);FD_SET(fd, &connect_fdset);timeout.tv_sec = wait_seconds;timeout.tv_usec = 0;do {ret = select(fd + 1, NULL, &connect_fdset, NULL, &timeout);} while (ret < 0 && errno == EINTR);if (ret == 0) {ret = -1;errno = ETIMEDOUT;} else if (ret < 0) {return -1;} else if (ret == 1) {/* ret返回为1,可能有两种情况,一种是连接建立成功,一种是套接字产生错误,*//* 此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取。 */int err;socklen_t socklen = sizeof(err);int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen);if (sockoptret == -1) {return -1;}if (err == 0) {ret = 0;} else {errno = err;ret = -1;}}}if (wait_seconds > 0) {deactivate_nonblock(fd);}return ret;
}/*** readn - 读取固定字节数* @fd: 文件描述符* @buf: 接收缓冲区* @count: 要读取的字节数* 成功返回count,失败返回-1,读到EOF返回<count*/
ssize_t readn(int fd, void *buf, size_t count) {size_t nleft = count;ssize_t nread;char *bufp = (char *) buf;while (nleft > 0) {if ((nread = read(fd, bufp, nleft)) < 0) {if (errno == EINTR)continue;return -1;} else if (nread == 0)return count - nleft;bufp += nread;nleft -= nread;}return count;
}/*** writen - 发送固定字节数* @fd: 文件描述符* @buf: 发送缓冲区* @count: 要读取的字节数* 成功返回count,失败返回-1*/
ssize_t writen(int fd, const void *buf, size_t count) {size_t nleft = count;ssize_t nwritten;char *bufp = (char *) buf;while (nleft > 0) {if ((nwritten = write(fd, bufp, nleft)) < 0) {if (errno == EINTR)continue;return -1;} else if (nwritten == 0)continue;bufp += nwritten;nleft -= nwritten;}return count;
}/*** recv_peek - 仅仅查看套接字缓冲区数据,但不移除数据* @sockfd: 套接字* @buf: 接收缓冲区* @len: 长度* 成功返回>=0,失败返回-1*/
ssize_t recv_peek(int sockfd, void *buf, size_t len) {while (1) {int ret = recv(sockfd, buf, len, MSG_PEEK);if (ret == -1 && errno == EINTR)continue;return ret;}
}/*** readline - 按行读取数据* @sockfd: 套接字* @buf: 接收缓冲区* @maxline: 每行最大长度* 成功返回>=0,失败返回-1*/
ssize_t readline(int sockfd, void *buf, size_t maxline) {int ret;int nread;char *bufp = buf;int nleft = maxline;while (1) {ret = recv_peek(sockfd, bufp, nleft);if (ret < 0) {return ret;} else if (ret == 0) {return ret;}nread = ret;int i;for (i = 0; i < nread; i++) {if (bufp[i] == '\n') {ret = readn(sockfd, bufp, i + 1);if (ret != i + 1)exit(EXIT_FAILURE);return ret;}}if (nread > nleft) {exit(EXIT_FAILURE);}nleft -= nread;ret = readn(sockfd, bufp, nread);if (ret != nread) {exit(EXIT_FAILURE);}bufp += nread;}return -1;
}void send_fd(int sock_fd, int fd) {int ret;struct msghdr msg;struct cmsghdr *p_cmsg;struct iovec vec;char cmsgbuf[CMSG_SPACE(sizeof(fd))];int *p_fds;char sendchar = 0;msg.msg_control = cmsgbuf;msg.msg_controllen = sizeof(cmsgbuf);p_cmsg = CMSG_FIRSTHDR(&msg);p_cmsg->cmsg_level = SOL_SOCKET;p_cmsg->cmsg_type = SCM_RIGHTS;p_cmsg->cmsg_len = CMSG_LEN(sizeof(fd));p_fds = (int *) CMSG_DATA(p_cmsg);*p_fds = fd;msg.msg_name = NULL;msg.msg_namelen = 0;msg.msg_iov = &vec;msg.msg_iovlen = 1;msg.msg_flags = 0;vec.iov_base = &sendchar;vec.iov_len = sizeof(sendchar);ret = sendmsg(sock_fd, &msg, 0);if (ret != 1)ERR_EXIT("sendmsg");
}int recv_fd(const int sock_fd) {int ret;struct msghdr msg;char recvchar;struct iovec vec;int recv_fd;char cmsgbuf[CMSG_SPACE(sizeof(recv_fd))];struct cmsghdr *p_cmsg;int *p_fd;vec.iov_base = &recvchar;vec.iov_len = sizeof(recvchar);msg.msg_name = NULL;msg.msg_namelen = 0;msg.msg_iov = &vec;msg.msg_iovlen = 1;msg.msg_control = cmsgbuf;msg.msg_controllen = sizeof(cmsgbuf);msg.msg_flags = 0;p_fd = (int *) CMSG_DATA(CMSG_FIRSTHDR(&msg));*p_fd = -1;ret = recvmsg(sock_fd, &msg, 0);if (ret != 1)ERR_EXIT("recvmsg");p_cmsg = CMSG_FIRSTHDR(&msg);if (p_cmsg == NULL)ERR_EXIT("no passed fd");p_fd = (int *) CMSG_DATA(p_cmsg);recv_fd = *p_fd;if (recv_fd == -1)ERR_EXIT("no passed fd");return recv_fd;
}
复制代码

编写好这个函数之后,则在main函数中去调用一下:

接着则要编写接受客户端的连接:

#ifndef _SESSION_H_
#define _SESSION_H_#include "common.h"void begin_session(int conn);#endif /* _SESSION_H_ */
复制代码
#include "common.h"
#include "session.h"void begin_session(int conn)
{
}
复制代码

而根据上次介绍的逻辑结构来看:

所以需要创建两个进程:

然后再把这两个进程做的事也模块化,FTP服务进程主要是处理FTP协议相关的一些细节,模块可以叫ftpproto,而nobody进程主要是协助FTP服务进程,只对内,模块可以叫privparent。

所以这里需要建立一个通道来让两进程之间可以相互通信,这里采用socketpair来进行通信:

另外可以定义一个session结构体来代表一个会话,里面包含多个信息:

#ifndef _SESSION_H_
#define _SESSION_H_#include "common.h"typedef struct session
{// 控制连接int ctrl_fd;char cmdline[MAX_COMMAND_LINE];char cmd[MAX_COMMAND];char arg[MAX_ARG];// 父子进程通道int parent_fd;int child_fd;
} session_t;
void begin_session(session_t *sess);#endif /* _SESSION_H_ */
复制代码

上面用到了三个宏,也需要在common.h中进行定义:

这时在main中就得声明一下该session,并将其传递:

这时再回到begin_session方法中,进一步带到父子进程中去处理:

下面则在session的父子进程中进行函数的声明:

ftpproto.h:

#ifndef _FTP_PROTO_H_
#define _FTP_PROTO_H_#include "session.h"void handle_child(session_t *sess);#endif /* _FTP_PROTO_H_ */
复制代码

ftpproto.c:

#include "ftpproto.h"
#include "sysutil.h"void handle_child(session_t *sess)
{}
复制代码

privparent.h:

#ifndef _PRIV_PARENT_H_
#define _PRIV_PARENT_H_#include "session.h"
void handle_parent(session_t *sess);#endif /* _PRIV_PARENT_H_ */
复制代码

privparent.c:

#include "privparent.h"void handle_parent(session_t *sess)
{}
复制代码

在session.c中需要包含这两个头文件:

接下来我们将注意力集中在begin_session函数中,首先我们需要将父进程改成nobody进程,怎么来改呢?这里需要用到一个函数:

下面来编写handle_child()和handle_parent():

另外在连接时,会给客户端一句这样的提示语:

主要还是将经历投射到handle_child()服务进程上来,其它的先不用关心:

而它主要是完成FTP协议相关的功能,所以它的实现放在了ftpproto.c,目前连接成功之后效果是:

其中"USER webor2006"后面是包含"\r\n"的,FTP的协议规定每条指令后面都要包含它,这时handle_child()函数就会收到这个命令并处理,再进行客户端的一些应答,客户端才能够进行下一步的动作,由于目前还没有处理该命令,所以客户端阻塞了,接下来读取该指令来打印一下:

接下来命令中的\r\n,接下来的操作会涉及到一些字符串的处理,所以先来对其进行封装一下,具体字符串的处理函数如下:

str.h:

#ifndef _STR_H_
#define _STR_H_void str_trim_crlf(char *str);
void str_split(const char *str , char *left, char *right, char c);
int str_all_space(const char *str);
void str_upper(char *str);
long long str_to_longlong(const char *str);
unsigned int str_octal_to_uint(const char *str);#endif /* _STR_H_ */
复制代码

str.c:

#include "str.h"
#include "common.h"void str_trim_crlf(char *str)
{}void str_split(const char *str , char *left, char *right, char c)
{}int str_all_space(const char *str)
{return 1;
}void str_upper(char *str)
{
}long long str_to_longlong(const char *str)
{return 0;
}unsigned int str_octal_to_uint(const char *str)
{unsigned int result = 0;return 0;
}
复制代码

①:去除字符串\r\n:rhstr_trim_crlf()

实现思路:

void str_trim_crlf(char *str)
{char *p = &str[strlen(str)-1];while (*p == '\r' || *p == '\n')*p-- = '\0';
}
复制代码

②:解析FTP命令与参数:str_split()

接下来将命令进行分割:

void str_split(const char *str , char *left, char *right, char c)
{//首先查找要分割字符串中首次出现字符的位置char *p = strchr(str, c);if (p == NULL)strcpy(left, str);//表示没有找到,该命令没有参数,则将一整串拷贝到left中else{//表示找到了,该命令有参数strncpy(left, str, p-str);strcpy(right, p+1);}
}
复制代码

③:判断所有的字符是否为空白字符:str_all_space()

④:将字符串转换成大写:str_upper()

其实这个错误是一个很好检验C语言基本功的,修改程序如下:

⑤:将字符串转换为长长整型:str_to_longlong()

可能会想到atoi系统函数可以实现,但是它返回的是一个整型:

但是也有一个现成的函数可以做到:atoll:

long long str_to_longlong(const char *str)
{return atoll(str);
}
复制代码

但是不是所有的系统都支持它,因此这里我们自己来实现,其实现思路也比较简单,规则如下:

12345678=8*(10^0) + 7*(10^1) + 6*(10^2) + ..... + 1*(10^7)

所以实现如下:

⑥:将八进制的整形字符串转换成无符号整型str_octal_to_uint()

其实现原理跟上面的差不多:

123456745=5*(8^0) + 4*(8^1) + 7*(8^2) + .... + 1*(8^8)

代码编写也跟上面函数一样,这里采用另外一种方式来实现,从高位算起:

先拿10进制来进行说明,好理解:

123456745可以经过下面这个换算得到:

0*10+1=1

1*10+2=12

12*10+3=123

123*10+4=1234

....

所以换算成八进制,其原理就是这样:

0*8+1=1

1*8+2=12

12*8+3=123

123*8+4=1234

....

所以依照这个原理就可以进行实现了,由于八进制可能前面为0,如:0123450,所以需要把第一位0给过滤掉,如下:

而公式里面应该是result8+digit来进行计算,这里用位操作来改写,也就是result8=result <<= 3,移位操作效率更加高效,所以最终代码如下:

上一次对字符串工具模块进行了封装,这次主要是对"参数配置模块"的封装,FTP中有很多配置相关的选项,不可能硬编码到代码中,而应该将它们配置到配置文件当中,像vsftpd的配置文件如下:

而对于miniftpd所有的参数配置项如下:

对于上面这些变量应该是与对应的配置项进行一一对应的,所以需要定义三张表格来进行一一对应:

下面定义两个操作配置文件的函数:

下面则开始进行编码,首先先新建配置文件模块文: tunable.h:对其变量进行声明:

#ifndef _TUNABLE_H_
#define _TUNABLE_H_extern int tunable_pasv_enable;
extern int tunable_port_enable;
extern unsigned int tunable_listen_port;
extern unsigned int tunable_max_clients;
extern unsigned int tunable_max_per_ip;
extern unsigned int tunable_accept_timeout;
extern unsigned int tunable_connect_timeout;
extern unsigned int tunable_idle_session_timeout;
extern unsigned int tunable_data_connection_timeout;
extern unsigned int tunable_local_umask;
extern unsigned int tunable_upload_max_rate;
extern unsigned int tunable_download_max_rate;
extern const char *tunable_listen_address;#endif /* _TUNABLE_H_ */
复制代码

另外新建一个配置文件:

接下来还要暴露两个接口出来,对文件和配置项的解析:

parseconf.h:

#ifndef _PARSE_CONF_H_
#define _PARSE_CONF_H_void parseconf_load_file(const char *path);
void parseconf_load_setting(const char *setting);#endif /* _PARSE_CONF_H_ */
复制代码

parseconf.c:

#include "parseconf.h"
#include "common.h"
#include "tunable.h"void parseconf_load_file(const char *path){}void parseconf_load_setting(const char *setting){}
复制代码

另外,由于fgets函数读取的一行字符包含'\n',所以需要将其去掉,可以用我们之前封装的现成方法:

接下来实现命令行的解析函数,在正式解析之前,需要将配置文件中的配置项与配置项变量对应关系表用代码定义出来,如下:

#include "parseconf.h"
#include "common.h"
#include "tunable.h"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 }
};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 }
};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){FILE *fp = fopen(path, "r");if (fp == NULL)ERR_EXIT("fopen");char setting_line[1024] = {0};while (fgets(setting_line, sizeof(setting_line), fp) != NULL){if (strlen(setting_line) == 0|| setting_line[0] == '#'|| str_all_space(setting_line))continue;str_trim_crlf(setting_line);parseconf_load_setting(setting_line);memset(setting_line, 0, sizeof(setting_line));}fclose(fp);
}void parseconf_load_setting(const char *setting){}
复制代码

可见有三种类型的参数,下面一个个来进行解析,对于"pasv_enable=YES"一个配置,可能会写成“ pasv_enable=YES”,所以先去掉左控格:

然后需要将key=pasv_enable;value=YES分隔开,这里可以用之前封装的现成的命令:

但也有可能用户没有配置value,如“pasv_enable=”,所以这是不合法的,也应该做下判断:

接下来,就需要拿这个key在上面的配置表格变量中进行搜索,如果找到了,则将其值赋值给该配置变量,如下:

如果说没有找到话,也就说明当前的配置项不是字符串类型的,这时,还得继续去其它类型的配置项中进行搜寻,如下:

而对于布尔类型,可以有以下几种形式:

AA=YES

AA=yes

AA=TRUE

AA=1

所以,首先将value统一成大写:

当遍历boolean类型配置项中也没有找到时,则需要在无符号整形中进行查找,其中无符号整形有两种形式:一种八进制,以0开头,比如"local_umask=077";另一种是十进制,如:"listen_port=21",所以需要做下判断,代码基本类似:

接下来可以应用某些配置项了:

可见这样代码就变成可配置的了,另外配置文件的文件名可以做成宏:

这节来实现用户登录的验证,首先用客户端来登录vsftpd来演示登录的过程:

接下来实现它,与协议相关的模块都是在ftpproto.c中完成的,目前的代码如下:

#include "ftpproto.h"
#include "sysutil.h"
#include "str.h"void do_user(session_t *sess);
void do_pass(session_t *sess);void handle_child(session_t *sess)
{writen(sess->ctrl_fd, "220 (miniftpd 0.1)\r\n", strlen("220 (miniftpd 0.1)\r\n"));int ret;while (1){memset(sess->cmdline, 0, sizeof(sess->cmdline));memset(sess->cmd, 0, sizeof(sess->cmd));memset(sess->arg, 0, sizeof(sess->arg));ret = readline(sess->ctrl_fd, sess->cmdline, MAX_COMMAND_LINE);if (ret == -1)ERR_EXIT("readline");else if (ret == 0)exit(EXIT_SUCCESS);printf("cmdline=[%s]\n", sess->cmdline);// 去除\r\nstr_trim_crlf(sess->cmdline);printf("cmdline=[%s]\n", sess->cmdline);// 解析FTP命令与参数str_split(sess->cmdline, sess->cmd, sess->arg, ' ');printf("cmd=[%s] arg=[%s]\n", sess->cmd, sess->arg);// 将命令转换为大写str_upper(sess->cmd);// 处理FTP命令if (strcmp("USER", sess->cmd) == 0){do_user(sess);}else if (strcmp("PASS", sess->cmd) == 0){do_pass(sess);}}
}void do_user(session_t *sess)
{//USER jjl
}void do_pass(session_t *sess)
{// PASS 123456
}
复制代码

转载于:https://juejin.im/post/5cefa4c5518825473b4fb9e7

Linux - MiniFtp实现相关推荐

  1. Linux之NTFS、FAT32、exFAT 各种格式硬盘挂载整理

    背景 由于业务需要频繁处理大量视频(几十GB),通过公司内网传输太慢,于是就每次处理视频时需要在服务器挂载硬盘或U盘.业务人员给的硬盘或U盘格式有时不一样,目前遇到的格式:NTFS.FAT32.exF ...

  2. 嵌入式Linux应用程序开发视频教程-曹国辉-专题视频课程

    嵌入式Linux应用程序开发视频教程-1834人已学习 课程介绍         本课程是嵌入式研发精英培养计划的核心课程,重点讲解嵌入式Linux应用程序开发核心技术,包括嵌入式Linux开发工具的 ...

  3. miniFTP项目实战六

    项目简介: 在Linux环境下用C语言开发的Vsftpd的简化版本,拥有部分Vsftpd功能和相同的FTP协议,系统的主要架构采用多进程模型,每当有一个新的客户连接到达,主进程就会派生出一个ftp服务 ...

  4. 过滤Linux下不同大小的文件,linux查找当前目录下 M/G 大小的文件,删除Linux下指定大小的文件

    过滤Linux下不同大小的文件,linux查找当前目录下 M/G 大小的文件,删除Linux下指定大小的文件 find ./ -type f -size +1G| xargs rm 在清理系统日志文件 ...

  5. linux环境下nacos的安装+启动,阿里云服务器安装nacos

    nacos安装+启动(linux环境): 基础:安装java环境 官网下载压缩包:如 nacos-server-1.2.1.tar.gz 放在自定义目录下 # 解压 tar -xvf nacos-se ...

  6. Alibaba Cloud Linux 2.1903 LTS 64位服务器yum源下载404,Alibaba Cloud Linux 2实例中使用docker-ce、epel等YUM源安装软件失败

    [Alibaba Cloud Linux 2.1903 LTS 64位]服务器yum源下载404 failure: repodata/repomd.xml from docker-ce-stable: ...

  7. Linux下创建硬链接,文件访问为空,提示:xxxx: 符号连接的层数过多

    Linux下创建软链接|硬链接,文件访问为空,提示:x x x: 符号连接的层数过多. 原因:创建符号链接的时候未使用绝对路径,无论是源文件路径还是目标路径,都需要使用绝对路径. 如: ln -s / ...

  8. 作为一个java程序员,常用的linux命令(越攒越多)

    本篇记录我在工作中不断遇到的常用的linux命令,并进行总结,时常更新! 1. 升级服务时先停止服务,然后进行替换 linux中杀进程时候,如果你是知道它所占用的端口号的话,可以通过 netstat ...

  9. 设置linux初始root密码

    简单一步设置linux第一个root密码 sudo passwd root #输入当前账户密码 #输入准备设置的root密码 #确认密码 如下所示:

最新文章

  1. oracle 开窗子句,分析函数和开窗函数
  2. 大数据学习,涉及的知识点
  3. 手动安装oracle软件 删软件
  4. 算法------四数相加 II (java 版本)
  5. mysql查看防火墙状态命令_Linux设置允许指定端口通过防火墙centos7
  6. 正则匹配没有闭合标签_RegExRX for Mac(多功能正则表达式开发工具)
  7. angular 定义对象_angularjs – 如何创建一个可以在Angular中使用的自定义对象类
  8. 个性潮流的设计PSD分层模板
  9. Ubuntu18.04下C++编译tensorflow并在QT中使用
  10. ASP操作XML文件的主要方法和实现
  11. 如何切图PS切图&网页切图
  12. 2022 VMware下载安装教程
  13. java uuid 随机数_Java随机数和UUID
  14. 【人在运维囧途_14】打扫干净屋子再请客
  15. Ubuntu中ls之后文件的颜色含义
  16. MOS电平转换电路 stm32的I2C电平转换电路 IIC电平转换电路
  17. mysql1041_mysql8 参考手册--错误代码1036、1041、1046
  18. Flash和JS实现的图片幻灯片切换特效
  19. 纸鸢|物联网云平台倒闭的原因和案例
  20. Hadoop之图解MapReduce与WordCount示例分析

热门文章

  1. 已解决:Unable to register authentication agent: GDBus.Error:org.freedesktop.PolicyKit1.Error.Failed:
  2. web工程中的各种路径(eclipse开发)
  3. iterm2 mac链接linux工具 桌面程序Transmit
  4. 以软件开发生命周期来说明不同的测试的使用情况
  5. AutoMySQLBackup 3.0 Bug:du: WARNING: use --si, not -H
  6. UVA 11578 - Situp Benches(dp)
  7. Android使用 LruCache 缓存图片
  8. 不同配置决定不同的复制的流程
  9. EXCEL数字前补零且转换成文本型
  10. OpenVINO 部署 YOLOv5 转换IR文件