Linux - MiniFtp实现
FTP簡(jiǎn)介
文件傳輸協(xié)議FTP(File Transfer Protocol,由RFC 959描述)。
FTP工作在TCP/IP協(xié)議族的應(yīng)用層,其傳輸層使用的是TCP協(xié)議,它是基于客戶/服務(wù)器模式工作的。
FTP支持的文件類型
①、ASCII碼文件,這是FTP默認(rèn)的文本格式【常用】
②、EBCDIC碼文件,它也是一種文本類型文件,用8位代碼表示一個(gè)字符,該文本文件在傳輸時(shí)要求兩端都使用EBCDIC碼【不常用】
③、圖象(Image)文件,也稱二進(jìn)制文件類型,發(fā)送的數(shù)據(jù)為連續(xù)的比特流,通常用于傳輸二進(jìn)制文件【常用】
④、本地文件,字節(jié)的大小有本地主機(jī)定義,也就是說每一字節(jié)的比特?cái)?shù)由發(fā)送方規(guī)定【不常用】
所以我們要實(shí)現(xiàn)的FTP也只支持ASCII碼文件和圖像文件類型,對(duì)于這兩種文件類型,到底有什么區(qū)別呢?下面做一個(gè)簡(jiǎn)要的介紹:
對(duì)于文本文件和二進(jìn)制文件,最直觀的區(qū)別就是文本文件是可以查看,而二進(jìn)制打開看到的是亂碼,實(shí)際上,這兩者在物理結(jié)構(gòu)(或存儲(chǔ)結(jié)構(gòu))上都是由一系列的比特位構(gòu)成的,它們之間的區(qū)別僅僅是在邏輯上:ASCII碼文件是由7個(gè)比特位構(gòu)成,最高位總是0(因?yàn)橐粋€(gè)字節(jié)=8位),所以ASCII碼文件最多能表示的字符數(shù)為2^7=128個(gè),通過man幫助也能看到:
而如果最高位為1則打開就會(huì)是亂碼,也就是二進(jìn)制文件最高位應(yīng)該就是1,這是一個(gè)區(qū)別。
另外一個(gè)區(qū)別就是\r\n換行符,在不同平臺(tái)上是不一樣的:windows上換行是用\r\n表示;linux上換行是用\n表示;mac上換行是用\r表示。如果在傳輸文件的時(shí)候,以這兩種文件類型傳輸實(shí)際上對(duì)\r\n的解析方式是不同的,至于有什么不同,這里通過FTP客戶端連接FTP服務(wù)端來做一個(gè)演示,首先啟動(dòng)FTP服務(wù)器,這里用vsftpd服務(wù)器:
接下來進(jìn)行ftp文件配置:
配置好之后接下來重新啟動(dòng)vsftpd服務(wù):
接下來用一個(gè)ftp客戶端來進(jìn)行連接,連接ftp服務(wù)器的客戶端有很多工具,這里用“LeapFtp”:
接下來新建一個(gè)文件進(jìn)行上傳:
可以用十六進(jìn)制的文本編輯器來查看一下內(nèi)容:
接下來開始上傳它至FTP服務(wù)器:
上傳之后的大小也是8個(gè)字節(jié),來用命令查看一下:
那如果是用二進(jìn)制文件上傳又會(huì)是怎么樣呢?
那這兩種類型難道沒有差別么,實(shí)際上在我的機(jī)器上是沒差別,在有些機(jī)器上是有區(qū)別的,區(qū)別如下:
如果以ASCII方式來傳輸文件,并且從windows->linux會(huì)將\r\n轉(zhuǎn)換成\n,而從linux->windows會(huì)將\n轉(zhuǎn)換成\r\n;而如果以二進(jìn)制文件來傳輸文件,那么不做任何轉(zhuǎn)換。
在C語言階段其實(shí)我們也學(xué)過了打開文件可以以ASCII和二進(jìn)制兩種方式打開,這兩者的區(qū)別也就只是換行符的不同,跟上面一樣。
FTP文件的數(shù)據(jù)結(jié)構(gòu)【僅做了解】
文件結(jié)構(gòu),這是FTP默認(rèn)的方式,文件被認(rèn)為是一個(gè)連續(xù)的字節(jié)流,文件內(nèi)部沒有表示結(jié)構(gòu)的信息。
記錄結(jié)構(gòu),該結(jié)構(gòu)只適用于文本文件(ASCII碼或EBCDIC碼文件)。記錄結(jié)構(gòu)文件是由連續(xù)的記錄構(gòu)成的。
頁結(jié)構(gòu),在FTP中,文件的一個(gè)部分被稱為頁。當(dāng)文件是由非連續(xù)的多個(gè)部分組成時(shí),使用頁結(jié)構(gòu),這種文件稱為隨機(jī)訪問文件。每頁都帶有頁號(hào)發(fā)送,以便收方能隨機(jī)地存儲(chǔ)各頁。
文件的傳輸方式【文件的數(shù)據(jù)結(jié)構(gòu)會(huì)影響傳輸方式】
流方式,這是支持文件傳輸?shù)哪J(rèn)方式,文件以字節(jié)流的形式傳輸。【主流FTP也僅僅實(shí)現(xiàn)了這種方式】
塊方式,文件以一系列塊來傳輸,每塊前面都帶有自己的頭部。頭部包含描述子代碼域(8位)和計(jì)數(shù)域(16位),描述子代碼域定義數(shù)據(jù)塊的結(jié)束標(biāo)志登內(nèi)容,計(jì)數(shù)域說明了數(shù)據(jù)塊的字節(jié)數(shù)。
壓縮方式,用來對(duì)連續(xù)出現(xiàn)的相同字節(jié)進(jìn)行壓縮,現(xiàn)在已很少使用。
FTP工作原理
啟動(dòng)FTP
在客戶端,通過交互式的用戶界面,客戶從終端上輸入啟動(dòng)FTP的用戶交互式命令
建立控制連接
客戶端TCP協(xié)議層根據(jù)用戶命令給出的服務(wù)器IP地址,向服務(wù)器提供FTP服務(wù)的21端口(該端口是TCP協(xié)議層用來傳輸FTP命令的端口)發(fā)出主動(dòng)建立連接的請(qǐng)求。服務(wù)器收到請(qǐng)求后,通過3次握手,就在進(jìn)行FTP命令處理的用戶協(xié)議解釋器進(jìn)程和服務(wù)器協(xié)議解釋器進(jìn)程之間建立了一條TCP連接。
以后所有用戶輸入的FTP命令和服務(wù)器的應(yīng)答都由該連接進(jìn)行傳輸,因此把它叫做控制連接。
建立數(shù)據(jù)連接
當(dāng)客戶通過交互式的用戶界面,向FTP服務(wù)器發(fā)出要下載服務(wù)器上某一文件的命令時(shí),該命令被送到用戶協(xié)議解釋器。
其中用戶的動(dòng)作會(huì)解析成相對(duì)應(yīng)的一些FTP命令,如看到的:
其實(shí)也可以用windows的命令來進(jìn)行FTP連接,也能很清晰地看出用戶的每個(gè)動(dòng)作都會(huì)解析成對(duì)應(yīng)的FTP命令:
FTP命令【先列出來眾覽下,之后會(huì)一一實(shí)現(xiàn)】
FTP應(yīng)答
FTP應(yīng)答格式
服務(wù)器通過控制連接發(fā)送給客戶的FTP應(yīng)答,由ASCII碼形式的3位數(shù)字和一行文本提示信息組成,它們之間用一個(gè)空格分割。應(yīng)答信息的每行文本以回車<CR>和換行<LF>對(duì)結(jié)尾。
如果需要產(chǎn)生一條多行的應(yīng)答,第一行在3位數(shù)字應(yīng)答代碼之后包含一個(gè)連字符“-”,而不是空格符;最后一行包含相同的3位數(shù)字應(yīng)答代碼,后跟一個(gè)空格符,關(guān)于這個(gè)可以實(shí)際查看下:
FTP應(yīng)答作用
確保在文件傳輸過程中的請(qǐng)求和正在執(zhí)行的動(dòng)作保持一致
保證用戶程序總是可以得到服務(wù)器的狀態(tài)信息,用戶可以根據(jù)收到的狀態(tài)信息對(duì)服務(wù)器是否正常執(zhí)行了有關(guān)操作進(jìn)行判定。
FTP應(yīng)答數(shù)字含義【做了解,不需要記,想知道什么含義到時(shí)對(duì)照查看既可】
第一位數(shù)字標(biāo)識(shí)了響應(yīng)是好,壞或者未完成
第二位數(shù)響應(yīng)大概是發(fā)生了什么錯(cuò)誤(比如,文件系統(tǒng)錯(cuò)誤,語法錯(cuò)誤)
第三位為第二位數(shù)字更詳細(xì)的說明
如:
500 Syntax error, command unrecognized. (語法錯(cuò)誤,命令不能被識(shí)別)可能包含因?yàn)槊钚刑L的錯(cuò)誤。
501 Syntax error in parameters or arguments. (參數(shù)語法錯(cuò)誤)
502 Command not implemented. (命令沒有實(shí)現(xiàn))
503 Bad sequence of commands. (命令順序錯(cuò)誤)
504 Command not implemented for that parameter. (沒有實(shí)現(xiàn)這個(gè)命令參數(shù))
FTP應(yīng)答示例【定義的宏,之后程序會(huì)用到,先列出來】
#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是由兩種類型的連接構(gòu)成的,一種是控制連接【主要是接收FTP客戶端發(fā)來的命令請(qǐng)求,并且對(duì)這些命令進(jìn)行應(yīng)答】,一種是數(shù)據(jù)連接【雙方之間進(jìn)行數(shù)據(jù)的傳輸,包括目錄列表的傳輸以及文件的傳輸】,其中控制連接總是由客戶端向服務(wù)器發(fā)起,而數(shù)據(jù)連接則不同了,它有兩種工作模式:主動(dòng)模式【由服務(wù)器向客戶端發(fā)起連接而建立數(shù)據(jù)連接通道】和被動(dòng)模式【由客戶端向服務(wù)器發(fā)起連接而建立數(shù)據(jù)連接通道】。下面來看一下這兩個(gè)工作模式的工作過程:
主動(dòng)模式
FTP客戶端首先向服務(wù)器端的21端口發(fā)起連接,經(jīng)過三次握手建設(shè)立控制連接通道,客戶端本地也會(huì)選擇一個(gè)動(dòng)態(tài)的端口號(hào)AA,一旦控制連接通道建立之后,雙方就可以交換信息了:客戶端可以通過控制連接通道發(fā)起命令請(qǐng)求,服務(wù)器也可以通過它向客戶端對(duì)這些命令請(qǐng)求進(jìn)行應(yīng)答。
接下來,如果要涉及到數(shù)據(jù)的傳輸,勢(shì)必要?jiǎng)?chuàng)建一個(gè)數(shù)據(jù)連接:
在創(chuàng)建數(shù)據(jù)連接之前,要選擇工作模式,如果是PORT模式,客戶端會(huì)上服務(wù)器端發(fā)送PORT命令,這也是通過控制連接通道完成的,向服務(wù)器的21端口傳送一個(gè)PORT命令,并且告知客戶端的一個(gè)端口號(hào)BB,因?yàn)檫@個(gè)信息服務(wù)器端才知道要連接客戶端的哪個(gè)端口號(hào),服務(wù)器端得到了這個(gè)信息,最后就向BB端口號(hào)發(fā)起了一個(gè)請(qǐng)求,建立了一個(gè)數(shù)據(jù)連接通道,數(shù)據(jù)連接通道一旦建立完畢,就可以進(jìn)行數(shù)據(jù)的傳輸了,包含目錄列表、文件的傳輸,一旦數(shù)據(jù)傳輸完畢,數(shù)據(jù)連接通道就會(huì)關(guān)閉掉,它是臨時(shí)的。
這里需要注意一點(diǎn):
接下來用實(shí)驗(yàn)來說明一下雙方建立的詳細(xì)命令,這邊通過登錄一個(gè)客戶端來看一下雙方之間所交換的命令:
接下來進(jìn)行數(shù)據(jù)傳輸,假設(shè)要傳輸一個(gè)列表,刷新一下。在獲得列表之前需要?jiǎng)?chuàng)建一個(gè)數(shù)據(jù)連接,而在創(chuàng)建數(shù)據(jù)連接時(shí)需要根據(jù)模式來創(chuàng)建數(shù)據(jù)連接,這里面采用的是PORT模式:
其整個(gè)的工作過程如下:
被動(dòng)模式
在了解了主動(dòng)模式之后,被動(dòng)模式就比較好理解了,如下:
從中可以發(fā)現(xiàn),主被動(dòng)模式只是連接建立的方向不同而已,同樣的,也通過實(shí)驗(yàn)來查看一下PASV模式所要交換的FTP命令:
這時(shí)同樣請(qǐng)求列表:
其整個(gè)的工作過程如下:
以上就是FTP的兩種工作模式,那為什么要有這兩種模式呢?這實(shí)際上是跟NAT或防火墻對(duì)主被動(dòng)模式有關(guān)系,下面就來了解下:
NAT或防火墻對(duì)主被動(dòng)模式的影響
什么是NAT
NAT的全稱是(Network Address Translation),通過NAT可以將內(nèi)網(wǎng)私有IP地址轉(zhuǎn)換為公網(wǎng)IP地址。一定程度上解決了公網(wǎng)地址不足的問題。
其地址映射關(guān)系可以如下:
192.168.1.100:5678【內(nèi)網(wǎng)IP】 -> 120.35.3.193:5678【NAT轉(zhuǎn)換IP】 -> 50.118.99.200:80【外網(wǎng)IP】
從而就建立了一個(gè)連接,而連接的建立是通過NAT服務(wù)器進(jìn)行地址轉(zhuǎn)換完成的。
FTP客戶端處于NAT或防火墻之后的主動(dòng)模式
建立控制連接通道
因?yàn)镹AT會(huì)主動(dòng)記錄由內(nèi)部發(fā)送外部[相反則無法記錄]的連接信息,而控制連接通道的建立是由客戶向服務(wù)器端連接的,因此這一條接可以順利地建立起來。 復(fù)制代碼客戶端與服務(wù)器端數(shù)據(jù)連接建立時(shí)的通知
客戶端先啟用PORT BB端口,并通過命令通道告知FTP服務(wù)器,且等待服務(wù)器端的主動(dòng)連接。 復(fù)制代碼服務(wù)器主動(dòng)連接客戶端
由于通過NAT轉(zhuǎn)換之后,服務(wù)器只能得知NAT的地址并不知道客戶端的IP地址,因此FTP服務(wù)器會(huì)以20端口主動(dòng)地向NAT的PORT BB端口發(fā)送主動(dòng)連接請(qǐng)求,但NAT并沒有啟用PORT BB端口,因而連接被拒絕。 復(fù)制代碼FTP客戶端處于NAT或防火墻之后的被動(dòng)模式
FTP服務(wù)器處于NAT或防火墻之后的被動(dòng)模式
FTP服務(wù)器處于NAT或防火墻之后的主動(dòng)模式
參數(shù)配置
我們要將程序中的開關(guān)做成可配置的,這里可以看一下VSFTP的配置文件:
空閑斷開
保存并重啟VSFTP服務(wù):
可見過了5秒空閑連接就斷開了,這時(shí)進(jìn)程也結(jié)束了:
限速
也就是上傳跟下載文件的限速功能,下面也來演示一下,默認(rèn)情況下是沒有限速的:
其速度傳輸過程序中會(huì)慢慢降到100K的樣子。
連接數(shù)限制
這里包含兩個(gè)方面的限制:總連接數(shù)的限制,針對(duì)所有IP來說的、同一個(gè)IP連接數(shù)的限制,下面來進(jìn)行配置:
接下來配置同一個(gè)IP的連接數(shù)的限制:
斷點(diǎn)續(xù)載與斷點(diǎn)續(xù)傳
當(dāng)成功連接一個(gè)客戶端時(shí),這時(shí)可以看到創(chuàng)建了兩個(gè)進(jìn)程:
可見該FTP服務(wù)器是采用多進(jìn)程的方式來實(shí)現(xiàn)的,為什么不用多線程的方式呢?
對(duì)于FTP服務(wù)器來講,多線程的方式是絕對(duì)不可取的,因?yàn)?
那為什么連接一個(gè)客戶端要?jiǎng)?chuàng)建兩個(gè)進(jìn)程呢?先看一下系統(tǒng)邏輯結(jié)構(gòu):
從中可以發(fā)現(xiàn),服務(wù)進(jìn)程是直接跟客戶端進(jìn)行通訊,而nobody進(jìn)程并沒有,它僅僅是跟服務(wù)進(jìn)程通信,來協(xié)助服務(wù)進(jìn)程來建立數(shù)據(jù)連接通道,以及需要一些特珠權(quán)限的控制,比如服務(wù)進(jìn)程建立了連接之后,假設(shè)是PORT模式,由于是服務(wù)器端主動(dòng)連接客戶端,服務(wù)器端需要綁定20端口來連接客戶端,而服務(wù)進(jìn)程是沒有權(quán)限來綁定20端口的,也就意味著沒辦法正常建立數(shù)據(jù)連接通道,所以需要加入nobody進(jìn)程。而nobody和服務(wù)進(jìn)程是采用內(nèi)部通信的協(xié)議,這個(gè)協(xié)議對(duì)外是不可見的,完全可以由我們自己來定義,所以可以用UNIX域協(xié)議來進(jìn)行通訊,而不用TCP/IP協(xié)議了。
功能實(shí)現(xiàn)
#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 復(fù)制代碼#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 復(fù)制代碼// // Created by zpw on 2019-06-08. //#include "sysutil.h"/*** tcp_server - 啟動(dòng)TCP服務(wù)器* @param host 服務(wù)器IP地址或者服務(wù)器主機(jī)名* @param port 服務(wù)器端口* @return 成功返回監(jiān)聽套接字*/ int tcp_server(const char *host, unsigned short port) {//創(chuàng)建套接字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) {//證明傳過來的是主機(jī)名而不是點(diǎn)分十進(jìn)制的IP地址,接下來要進(jìn)行轉(zhuǎn)換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);//端口號(hào)//設(shè)置地址重復(fù)利用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");}//監(jiān)聽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 - 設(shè)置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 - 設(shè)置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 - 讀超時(shí)檢測(cè)函數(shù),不含讀操作* @fd: 文件描述符* @wait_seconds: 等待超時(shí)秒數(shù),如果為0表示不檢測(cè)超時(shí)* 成功(未超時(shí))返回0,失敗返回-1,超時(shí)返回-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 - 讀超時(shí)檢測(cè)函數(shù),不含寫操作* @fd: 文件描述符* @wait_seconds: 等待超時(shí)秒數(shù),如果為0表示不檢測(cè)超時(shí)* 成功(未超時(shí))返回0,失敗返回-1,超時(shí)返回-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 - 帶超時(shí)的accept* @fd: 套接字* @addr: 輸出參數(shù),返回對(duì)方地址* @wait_seconds: 等待超時(shí)秒數(shù),如果為0表示正常模式* 成功(未超時(shí))返回已連接套接字,超時(shí)返回-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: 要連接的對(duì)方地址* @wait_seconds: 等待超時(shí)秒數(shù),如果為0表示正常模式* 成功(未超時(shí))返回0,失敗返回-1,超時(shí)返回-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,可能有兩種情況,一種是連接建立成功,一種是套接字產(chǎn)生錯(cuò)誤,*//* 此時(shí)錯(cuò)誤信息不會(huì)保存至errno變量中,因此,需要調(diào)用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 - 讀取固定字節(jié)數(shù)* @fd: 文件描述符* @buf: 接收緩沖區(qū)* @count: 要讀取的字節(jié)數(shù)* 成功返回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 - 發(fā)送固定字節(jié)數(shù)* @fd: 文件描述符* @buf: 發(fā)送緩沖區(qū)* @count: 要讀取的字節(jié)數(shù)* 成功返回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 - 僅僅查看套接字緩沖區(qū)數(shù)據(jù),但不移除數(shù)據(jù)* @sockfd: 套接字* @buf: 接收緩沖區(qū)* @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 - 按行讀取數(shù)據(jù)* @sockfd: 套接字* @buf: 接收緩沖區(qū)* @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; } 復(fù)制代碼編寫好這個(gè)函數(shù)之后,則在main函數(shù)中去調(diào)用一下:
接著則要編寫接受客戶端的連接:
#ifndef _SESSION_H_ #define _SESSION_H_#include "common.h"void begin_session(int conn);#endif /* _SESSION_H_ */ 復(fù)制代碼#include "common.h" #include "session.h"void begin_session(int conn) { } 復(fù)制代碼而根據(jù)上次介紹的邏輯結(jié)構(gòu)來看:
所以需要?jiǎng)?chuàng)建兩個(gè)進(jìn)程:
然后再把這兩個(gè)進(jìn)程做的事也模塊化,FTP服務(wù)進(jìn)程主要是處理FTP協(xié)議相關(guān)的一些細(xì)節(jié),模塊可以叫ftpproto,而nobody進(jìn)程主要是協(xié)助FTP服務(wù)進(jìn)程,只對(duì)內(nèi),模塊可以叫privparent。
所以這里需要建立一個(gè)通道來讓兩進(jìn)程之間可以相互通信,這里采用socketpair來進(jìn)行通信:
另外可以定義一個(gè)session結(jié)構(gòu)體來代表一個(gè)會(huì)話,里面包含多個(gè)信息:
#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];// 父子進(jìn)程通道int parent_fd;int child_fd; } session_t; void begin_session(session_t *sess);#endif /* _SESSION_H_ */ 復(fù)制代碼上面用到了三個(gè)宏,也需要在common.h中進(jìn)行定義:
這時(shí)在main中就得聲明一下該session,并將其傳遞:
這時(shí)再回到begin_session方法中,進(jìn)一步帶到父子進(jìn)程中去處理:
下面則在session的父子進(jìn)程中進(jìn)行函數(shù)的聲明:
ftpproto.h:
#ifndef _FTP_PROTO_H_ #define _FTP_PROTO_H_#include "session.h"void handle_child(session_t *sess);#endif /* _FTP_PROTO_H_ */ 復(fù)制代碼ftpproto.c:
#include "ftpproto.h" #include "sysutil.h"void handle_child(session_t *sess) {} 復(fù)制代碼privparent.h:
#ifndef _PRIV_PARENT_H_ #define _PRIV_PARENT_H_#include "session.h" void handle_parent(session_t *sess);#endif /* _PRIV_PARENT_H_ */ 復(fù)制代碼privparent.c:
#include "privparent.h"void handle_parent(session_t *sess) {} 復(fù)制代碼在session.c中需要包含這兩個(gè)頭文件:
接下來我們將注意力集中在begin_session函數(shù)中,首先我們需要將父進(jìn)程改成nobody進(jìn)程,怎么來改呢?這里需要用到一個(gè)函數(shù):
下面來編寫handle_child()和handle_parent():
另外在連接時(shí),會(huì)給客戶端一句這樣的提示語:
主要還是將經(jīng)歷投射到handle_child()服務(wù)進(jìn)程上來,其它的先不用關(guān)心:
而它主要是完成FTP協(xié)議相關(guān)的功能,所以它的實(shí)現(xiàn)放在了ftpproto.c,目前連接成功之后效果是:
其中"USER webor2006"后面是包含"\r\n"的,FTP的協(xié)議規(guī)定每條指令后面都要包含它,這時(shí)handle_child()函數(shù)就會(huì)收到這個(gè)命令并處理,再進(jìn)行客戶端的一些應(yīng)答,客戶端才能夠進(jìn)行下一步的動(dòng)作,由于目前還沒有處理該命令,所以客戶端阻塞了,接下來讀取該指令來打印一下:
接下來命令中的\r\n,接下來的操作會(huì)涉及到一些字符串的處理,所以先來對(duì)其進(jìn)行封裝一下,具體字符串的處理函數(shù)如下:
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_ */ 復(fù)制代碼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; } 復(fù)制代碼①:去除字符串\r\n:rhstr_trim_crlf()
實(shí)現(xiàn)思路:
void str_trim_crlf(char *str) {char *p = &str[strlen(str)-1];while (*p == '\r' || *p == '\n')*p-- = '\0'; } 復(fù)制代碼②:解析FTP命令與參數(shù):str_split()
接下來將命令進(jìn)行分割:
void str_split(const char *str , char *left, char *right, char c) {//首先查找要分割字符串中首次出現(xiàn)字符的位置char *p = strchr(str, c);if (p == NULL)strcpy(left, str);//表示沒有找到,該命令沒有參數(shù),則將一整串拷貝到left中else{//表示找到了,該命令有參數(shù)strncpy(left, str, p-str);strcpy(right, p+1);} } 復(fù)制代碼③:判斷所有的字符是否為空白字符:str_all_space()
④:將字符串轉(zhuǎn)換成大寫:str_upper()
其實(shí)這個(gè)錯(cuò)誤是一個(gè)很好檢驗(yàn)C語言基本功的,修改程序如下:
⑤:將字符串轉(zhuǎn)換為長長整型:str_to_longlong()
可能會(huì)想到atoi系統(tǒng)函數(shù)可以實(shí)現(xiàn),但是它返回的是一個(gè)整型:
但是也有一個(gè)現(xiàn)成的函數(shù)可以做到:atoll:
long long str_to_longlong(const char *str) {return atoll(str); } 復(fù)制代碼但是不是所有的系統(tǒng)都支持它,因此這里我們自己來實(shí)現(xiàn),其實(shí)現(xiàn)思路也比較簡(jiǎn)單,規(guī)則如下:
12345678=8*(10^0) + 7*(10^1) + 6*(10^2) + ..... + 1*(10^7)
所以實(shí)現(xiàn)如下:
⑥:將八進(jìn)制的整形字符串轉(zhuǎn)換成無符號(hào)整型str_octal_to_uint()
其實(shí)現(xiàn)原理跟上面的差不多:
123456745=5*(8^0) + 4*(8^1) + 7*(8^2) + .... + 1*(8^8)
代碼編寫也跟上面函數(shù)一樣,這里采用另外一種方式來實(shí)現(xiàn),從高位算起:
先拿10進(jìn)制來進(jìn)行說明,好理解:
123456745可以經(jīng)過下面這個(gè)換算得到:
0*10+1=1
1*10+2=12
12*10+3=123
123*10+4=1234
....
所以換算成八進(jìn)制,其原理就是這樣:
0*8+1=1
1*8+2=12
12*8+3=123
123*8+4=1234
....
所以依照這個(gè)原理就可以進(jìn)行實(shí)現(xiàn)了,由于八進(jìn)制可能前面為0,如:0123450,所以需要把第一位0給過濾掉,如下:
而公式里面應(yīng)該是result8+digit來進(jìn)行計(jì)算,這里用位操作來改寫,也就是result8=result <<= 3,移位操作效率更加高效,所以最終代碼如下:
上一次對(duì)字符串工具模塊進(jìn)行了封裝,這次主要是對(duì)"參數(shù)配置模塊"的封裝,FTP中有很多配置相關(guān)的選項(xiàng),不可能硬編碼到代碼中,而應(yīng)該將它們配置到配置文件當(dāng)中,像vsftpd的配置文件如下:
而對(duì)于miniftpd所有的參數(shù)配置項(xiàng)如下:
對(duì)于上面這些變量應(yīng)該是與對(duì)應(yīng)的配置項(xiàng)進(jìn)行一一對(duì)應(yīng)的,所以需要定義三張表格來進(jìn)行一一對(duì)應(yīng):
下面定義兩個(gè)操作配置文件的函數(shù):
下面則開始進(jìn)行編碼,首先先新建配置文件模塊文: tunable.h:對(duì)其變量進(jìn)行聲明:
#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_ */ 復(fù)制代碼另外新建一個(gè)配置文件:
接下來還要暴露兩個(gè)接口出來,對(duì)文件和配置項(xiàng)的解析:
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_ */ 復(fù)制代碼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){} 復(fù)制代碼另外,由于fgets函數(shù)讀取的一行字符包含'\n',所以需要將其去掉,可以用我們之前封裝的現(xiàn)成方法:
接下來實(shí)現(xiàn)命令行的解析函數(shù),在正式解析之前,需要將配置文件中的配置項(xiàng)與配置項(xiàng)變量對(duì)應(yīng)關(guā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){} 復(fù)制代碼可見有三種類型的參數(shù),下面一個(gè)個(gè)來進(jìn)行解析,對(duì)于"pasv_enable=YES"一個(gè)配置,可能會(huì)寫成“ pasv_enable=YES”,所以先去掉左控格:
然后需要將key=pasv_enable;value=YES分隔開,這里可以用之前封裝的現(xiàn)成的命令:
但也有可能用戶沒有配置value,如“pasv_enable=”,所以這是不合法的,也應(yīng)該做下判斷:
接下來,就需要拿這個(gè)key在上面的配置表格變量中進(jìn)行搜索,如果找到了,則將其值賦值給該配置變量,如下:
如果說沒有找到話,也就說明當(dāng)前的配置項(xiàng)不是字符串類型的,這時(shí),還得繼續(xù)去其它類型的配置項(xiàng)中進(jìn)行搜尋,如下:
而對(duì)于布爾類型,可以有以下幾種形式:
AA=YES
AA=yes
AA=TRUE
AA=1
所以,首先將value統(tǒng)一成大寫:
當(dāng)遍歷boolean類型配置項(xiàng)中也沒有找到時(shí),則需要在無符號(hào)整形中進(jìn)行查找,其中無符號(hào)整形有兩種形式:一種八進(jìn)制,以0開頭,比如"local_umask=077";另一種是十進(jìn)制,如:"listen_port=21",所以需要做下判斷,代碼基本類似:
接下來可以應(yīng)用某些配置項(xiàng)了:
可見這樣代碼就變成可配置的了,另外配置文件的文件名可以做成宏:
這節(jié)來實(shí)現(xiàn)用戶登錄的驗(yàn)證,首先用客戶端來登錄vsftpd來演示登錄的過程:
接下來實(shí)現(xiàn)它,與協(xié)議相關(guān)的模塊都是在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命令與參數(shù)str_split(sess->cmdline, sess->cmd, sess->arg, ' ');printf("cmd=[%s] arg=[%s]\n", sess->cmd, sess->arg);// 將命令轉(zhuǎn)換為大寫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 } 復(fù)制代碼轉(zhuǎn)載于:https://juejin.im/post/5cefa4c5518825473b4fb9e7
總結(jié)
以上是生活随笔為你收集整理的Linux - MiniFtp实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 这个五月,我拿到了腾讯暑期offer
- 下一篇: 垃圾回收(GC)浅谈