Linux - MiniFtp实现
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实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 这个五月,我拿到了腾讯暑期offer
- 下一篇: linux arpwatch 命令详解