專題十二:實現一個簡單的FTP服務器


引言:

  在本專題中將和大家分享如何自己實現一個簡單的FTP服務器。在我們平時的上網過程中,一般都是使用FTP的客戶端來對商家提供的服務器進行訪問(上傳、下載文件),例如我們經常用到微軟的SkyDrive網盤,115網盤等,然而我們經常用到的都是網頁版本的,網頁版本和客戶端版本的不同,網頁版本的FTP客戶端,它與服務器的交流是使用HTTP協議發出對服務器的請求的,而客戶端版本采用的是FTP協議發出命令對服務器進行請求。然后我們接觸到FTP服務器卻很少的, 所以本專題中將和大家介紹下如何實現一個FTP服務器(不要覺得服務器很深奧一樣的,大家可以簡單的認為服務器也是一個程序,該程序是對客戶端發來的請求做處理的,請求大家可以簡單理解為字符串,從這個角度看, 服務器程序就是一個對字符串解析的過程。),也是為后面的一個專題做一個鋪墊,因為后面專題將和大家介紹下FTP客戶端——文件上傳下載器,有了自己自定義的FTP服務器后, 自定義的FTP客戶端就可以對自定義的FTP服務器進行訪問,使兩者形成一個完整的軟件,從而也讓大家對基於FTP協議的工具有一個初步的了解。

一、基於FTP協議的客戶端和服務器是如何"溝通的"?

  FTP客戶端和FTP服務器之間的“溝通”分為四個階段的:

  1. 啟動FTP

  客戶通過FTP客戶端軟件,發起FTP交互式的命令,就是告訴服務器(也就是一台電腦,服務器上與一個程序(FTP服務)會接收命令,並解析發來的命令,然后發出回復信息)說:“我想和你聊聊天,可以嗎?”  

  2. 建立控制連接

  客戶端TCP層根據客戶給出的服務器IP地址,向服務器提供FTP服務的21號端口發出主動建立連接的請求,服務器接收到請求后,通過3次握手之后,客戶端和服務器之間就建立一個TCP連接(就是一條通道,就好比生活中馬路,有了馬路之后車才能夠在兩地之間運送東西),之后,所有用戶發出的FTP命令和服務器的回應都是通過該連接來傳送的, 所以也把這個TCP連接叫做控制連接,控制連接在用戶退出之前一直存在。

  3. 建立數據連接和進行文件傳輸

  現在客戶端和服務器端已經建立聊天的通道了(控制連接),但是兩者聊天過程中如果互相想贈送禮物要怎么辦呢?(這里形象的把客戶端和服務器端文件的傳輸比喻兩個人通過聊天后互相贈送禮物的過程),此時我們就需要另外一條馬路(數據連接)來進行“禮物的贈送”了,具體贈送禮物的過程如下:

  1.  客戶端通過控制連接向服務器發送一個上傳文件的命令時,會自己分配一個臨時的TCP端口號。
  2.  客戶端通過控制連接向服務器發送一個命令(下面將會介紹的PORT命令)來告訴服務器自己的IP地址和臨時的端口號,然后再發送一條上傳文件的命令(可以理解為——客戶端要送禮物給服務器時,實際上不是簡單的發送一個送禮物命令的,在這之前還需要發送一條自我介紹命令(就是告訴服務器自己的IP地址和端口號)來告訴服務器自己就是剛剛和它聊天的那位,這也很符合我們日常送禮物的流程的,一般大家接到禮物都要弄明白送禮物的人是誰,是不是自己認識的)
  3. 服務器接收到客戶端的IP地址和臨時端口號后,以這個IP地址和端口號為目標,使用服務器上的20端口(該端口是用來傳輸數據的端口)向客戶端發出主動建立連接的請求。
  4.  客戶端收到請求后,通過三次握手后就與服務器之間建立了另外一條TCP連接——數據連接,即用來互相送禮物的通道。
  5.  客戶端在自己的文件系統中選擇要贈送(上傳)的文件
  6.  客戶端將文件寫入到文件傳輸進程中(寫入網絡流中)
  7.  服務器端將傳輸來的文件在服務器端的文件系統中進行存儲
  8.  文件傳輸完成后,由服務器主動關閉該數據的連接

  4 關閉FTP

  當用戶退出FTP時,通關客戶端發送退出命令,之后控制連接被關閉,FTP服務結束。

二、從上面的溝通過程中你明白了什么?

  從上面客戶端與服務器端的溝通過程中,這里可以概括幾點:

  (1)客戶端與服務器端進行交互過程中,傳輸層使用的是TCP協議而不是其他傳輸層協議

  (2)溝通過程有兩條TCP連接——一條是控制連接,即傳輸命令和響應信息的通道,另一條是數據連接,即傳輸文件的馬路,並且必須先有控制連接才能建立數據連接,因為要進行文件傳輸首先必須知道客戶的IP地址和端口號,這個過程就是通過控制連接傳送的命令來告知服務器客戶端的IP地址和端口號,之后再在兩者之間建立數據連接來傳輸文件

  (3)在服務器端,控制連接(端口號為21)和數據連接(端口號 為20)使用了不同的端口號

三、贈送禮物的方式?——文件傳輸模式

  客戶端與FTP服務器建立數據連接之后,首先需要告訴服務器采用哪種文件傳輸模式,FTP提供了兩種文件傳輸模式,一種是主動(Port)模式,另一種是被動(Passive)模式。

  主動模式——服務器向客戶端發起數據連接請求,被動模式——客戶端向服務器發起數據請求。

然而兩種模式有什么相同點和不同點呢?

  相同點: 服務器都使用21號端口進行用戶驗證和管理

  不同點: 傳送文件數據的方式不一樣,主動模式的FTP服務器數據端口固定在20,而被動模式的FTP服務器數據端口則在1025~65535之間的隨機數。

3.1 主動模式

  主動模式——服務器主動連接客戶端,然后傳輸文件,在這種模式下,FTP客戶端先用一個端口N(N>1024)向服務器的21號端口發起控制連接,連接成功后,在發出PORT N+1命令告訴服務器自己監聽的端口為N+1;服務器接受到該命令后,用一個新的數據端口(20號端口)與客戶端的端口N+1建立連接,然后進行文件傳輸,而客戶端則通過監聽N+1端口接受文件數據。

  注意: 采用主動模式存在一個問題,如果客戶端安裝了防火牆或在內網時,由於防火牆一般不允許接受外部發起的標准端口以外的連接請求,因此外部FTP服務器就無法使用主動模式穿過防火牆主動連接客戶端(這里與客戶端連接的端口為N+1(N>1024),非標准端口),從而造成無法傳送文件數據,此時就需要采用被動模式傳送文件了。

3.2 被動模式

  被動模式——服務器被動接受客戶端連接請求,即控制連接請求和數據連接請求都是由客戶端發起,在這種模式下,FTP客戶端先隨機開始一個端口N向服務器的21號端口發起控制連接,然后向服務器發送PASV命令。服務器收到該命令后,會用一個新的端口P(P>1024)進行監聽,同時將該端口號告訴客戶端,客戶端接受到響應命令后,再通過新的端口N+1連接服務器的端口P,然后進行文件數據傳輸。

  注意:采用被動模式與主動模式也存在相同的問題,如果服務器安裝了防火牆,客戶端同樣可能無法與服務器端的端口P建立數據請求,因為該請求可能會被防火牆過濾掉。在實際應用中,服務器一般指定一個端口范圍,允許客戶端與該范圍內的端口建立數據連接,而不再這個范圍內的端口會被服務器的防火牆過濾掉,從而在一定程度上消除了針對服務器的惡意攻擊。

四、 FTP協議中有哪些命令的?

  協議簡單說就是一個規范,就好比打牌一樣,制定一個大家都能明白的規則,斗地主的規則被大家都認可的,但是私下我們也可以自定義規則來玩的(例如說三個只能帶一個等這樣的規則),同樣FTP規則也是大家都認可的一個協議,我們當然也可以自定義協議。

  由於.Net平台下目前還沒有提供對FTP服務器端開發的類庫,因此要實現一個FTP服務器端的應用程序,就必須了解FTP協議的詳細內容。

4.1 FTP命令有哪些?

FTP 協議中規定了一些大家都認識的命令和組成。FTP協議中的命令都由3~4個字母組成,命令與參數之間用空格隔開,每個命令用回車換行結束。

(1)訪問命令有:

USER命令——格式為:USER <username>, 指定登錄的用戶名,以便服務器進行身份驗證。這個命令通常是控制連接后第一個發出的命令

PASS命令——格式為:PASS <password>, 指定用戶密碼,該命令必須跟在登錄用戶名命令之后。

REIN命令——格式為:REIN, 表示重新初始化用戶信息,該命令終止當前USER的傳輸,同時終止正在傳輸的數據,然后重置所有參數,並打開控制連接,以便客戶端再次發生USER命令。

QUIT命令——格式為:QUIT,關閉與服務器的連接

(2)模式設置命令:

PASV命令——格式為:PASV,該命令告訴FTP服務器,讓FTP服務器在指定的數據端口進行監聽,被動接受客戶端的請求。如果未指定任何模式,FTP服務器默認使用PASV模式

PORT命令——格式為:PORT <address>,該命令告訴FTP服務器,客戶端監聽的端口號是address,讓FTP服務器采用主動模式連接客戶端。

TYPE命令——格式為: TYPE <data type>,該命令指定要傳輸的數據類型,有ASCII和BINARY兩種類型。

MODE命令——格式為:MODE <mode>,該命令指定傳輸模式,S表示流,B表示塊,C表示壓縮。

(3)文件管理命令

CWD命令——格式為:CWD <directory>,該命令是用戶可以在不同的目錄或數據集下工作而不用改變登錄信息,directory一般是目錄名或與系統相關的文件集合。

PWD命令——格式為:PWD,該命令返回當前工作目錄。

MKD命令——格式為:MKD <directory>,該命令表示在指定路徑下創建新目錄,directory 表示特定目錄的字符串。

CDUP命令——格式為:CDUP,該命令表示回到上層目錄

RMD命令——格式為:RMD <directory>,刪除指定目錄,directory表示特定目錄的字符串。

LIST命令——格式為:LIST <name>,該命令返回指定路徑下的子目錄及文件列表,name 為路徑。省略路徑時,返回當前路徑下的文件列表。

NLIST命令——格式為:NLIST <directory>,該命令返回指定路徑下的目錄列表,省略路徑時,返回當前目錄。

RNFR命令——格式為:RNFR <old path>,該命令表示重新命名文件,該命令的下一條命令用RNTO指定新的文件名。

RNTO命令——格式為:RNTO <new path>,該命令和RNFR命令共同完成對文件的重命名。

DELE命令——格式為:DELE <filename>,該命令表示刪除指定路徑下的文件

(4)文件傳輸命令:

RETR命令——RETR <filename>,表示下載指定路徑的文件

STOR命令——STOR <filename>,表示上傳一個指定的文件,並將其存儲在指定的位置,如果文件已存在,原文件將被覆蓋,如果文件不存在,則創建新文件。

(5)其他命令

SYST命令——格式為:SYST,該命令返回服務器使用的操作系統。

4.2 FTP響應碼

  客戶端發送FTP命令后,服務器需要返回FTP響應碼,響應碼即是回答,我們平常聊天中別人問了說了話或者問了問題,另外一方就需要回答,FTP協議中定義以響應碼的形式來作為回答,FTP響應碼由ASCII編碼的3位數字開頭,后面接一行文本提示信息,數字和提示信息中有一個空格,如XXX 接收請求。每個響應碼同樣以回車換行結束。

  FTP響應碼的3位數字每位都有特定的意義,具體見下表:

 

響應碼

表示

1

1XX

表示信息已被服務器正確接收,但尚未被處理

2XX

表示信息已被服務器正確處理完畢

3XX

表示信息已被服務器正在接受,並正在處理中

4XX

表示信息處理錯誤(暫時)

5XX

表示信息處理錯誤(永久)

2

X0X

表示語法錯誤

X1X

表示系統狀態與信息

X2X

表示與FTP服務器系統連接狀態

X3X

表示與用戶認證有關的信息

X4X

表示未定義

X5X

表示與文件系統有關的信息

 下表列出了常用的響應碼所代表的意義:

響應碼

意義

響應碼

意義

110

重新啟動標記應答

332

登陸是需要賬戶信息

120

服務在指定時間內准備好

350

請求的文件操作需要進一步命令

125

數據連接打開——開始傳輸

421

服務關閉

150

文件狀態良好,將要打開數據連接

425

不能打開數據連接

200

命令成功

426

關閉連接,終止傳輸

202

命令沒有執行

450

文件不可用

211

系統狀態回復

451

中止請求操作:有本地錯誤

212

目錄狀態回復

452

磁盤空間不足

213

文件狀態回復

500

無效命令

214

幫助信息回復

501

語法錯誤

215

系統類型回復

502

命令未執行

220

服務就緒

503

命令順序錯誤

221

服務關閉控制連接,可以退出登陸

504

無效命令參數

225

數據連接打開,無傳輸正在進行

530

未登陸

226

關閉數據連接,請求的文件操作成功

532

存儲文件需要賬戶信息

227

進入被動模式

550

未執行請求操作

230

用戶已登陸

551

請求操作終止:頁類型未知

250

請求的文件操作完成

552

請求文件操作終止:超過存儲分配

257

創建路徑名

553

為執行請求的操作:文件名不合法

331

用戶名正確,需要口令

 

 

五、實現自定義的FTP服務器

  相信大家看完上面的介紹對FTP協議以及FTP客戶端和FTP服務器的交互過程有一定的理解的,這時候大家知道理論后就一定很想知道知道這些之后可以做什么的?答案就是可以制作一個簡單的FTP服務器,大家可以根據代碼來進一步理解FTP協議。下面是程序中一些核心代碼片段:

  1 // 啟動服務器
  2         private void btnFtpServerStartStop_Click(object sender, EventArgs e)
  3         {
  4             if (myTcpListener == null)
  5             {
  6                 listenThread = new Thread(ListenClientConnect);
  7                 listenThread.IsBackground = true;
  8                 listenThread.Start();
  9 
 10                 lstboxStatus.Enabled = true;
 11                 lstboxStatus.Items.Clear();
 12                 lstboxStatus.Items.Add("啟動Ftp服務...");
 13                 btnFtpServerStartStop.Text = "停止";
 14             }
 15             else
 16             {
 17                 myTcpListener.Stop();
 18                 myTcpListener = null;
 19                 listenThread.Abort();
 20                 lstboxStatus.Items.Add("Ftp服務已停止!");
 21                 lstboxStatus.TopIndex = lstboxStatus.Items.Count - 1;
 22 
 23                 btnFtpServerStartStop.Text = "啟動";
 24             }
 25         }
 26 
 27         // 監聽端口,處理客戶端連接
 28         private void ListenClientConnect()
 29         {
 30             myTcpListener = new TcpListener(IPAddress.Parse(tbxFtpServerIp.Text), int.Parse(tbxFtpServerPort.Text));
 31             // 開始監聽傳入的請求
 32             myTcpListener.Start();
 33             AddInfo("啟動成功!");
 34             AddInfo("Ftp服務運行中...[單機”停止“退出]");
 35             while (true)
 36             {
 37                 try
 38                 {
 39                     // 接收連接請求
 40                     TcpClient tcpClient = myTcpListener.AcceptTcpClient();
 41                     AddInfo(string.Format("客戶端({0})與本機({1})建立Ftp連接", tcpClient.Client.RemoteEndPoint, myTcpListener.LocalEndpoint));
 42                     User user = new User();
 43                     user.commandSession = new UserSeesion(tcpClient);
 44                     user.workDir = tbxFtpRoot.Text;
 45                     Thread t = new Thread(UserProcessing);
 46                     t.IsBackground = true;
 47                     t.Start(user);
 48                 }
 49                 catch
 50                 {
 51                     break;
 52                 }
 53             }
 54         }
 55 
 56         // 處理客戶端用戶請求
 57         private void UserProcessing(object obj)
 58         {
 59             User user = (User)obj;
 60             string sendString = "220 FTP Server v1.0";
 61             RepleyCommandToUser(user, sendString);
 62             while (true)
 63             {
 64                 string receiveString = null;
 65                 try
 66                 {
 67                     // 讀取客戶端發來的請求信息
 68                     receiveString = user.commandSession.streamReader.ReadLine();
 69                 }
 70                 catch(Exception ex)
 71                 {
 72                     if (user.commandSession.tcpClient.Connected == false)
 73                     {
 74                         AddInfo(string.Format("客戶端({0}斷開連接!)", user.commandSession.tcpClient.Client.RemoteEndPoint));
 75                     }
 76                     else
 77                     {
 78                         AddInfo("接收命令失敗!" + ex.Message);
 79                     }
 80 
 81                     break;
 82                 }
 83 
 84                 if (receiveString == null)
 85                 {
 86                     AddInfo("接收字符串為null,結束線程!");
 87                     break;
 88                 }
 89 
 90                 AddInfo(string.Format("來自{0}:[{1}]", user.commandSession.tcpClient.Client.RemoteEndPoint, receiveString));
 91                 
 92                 // 分解客戶端發來的控制信息中的命令和參數
 93                 string command = receiveString;
 94                 string param = string.Empty;
 95                 int index = receiveString.IndexOf(' ');
 96                 if (index != -1)
 97                 {
 98                     command = receiveString.Substring(0, index).ToUpper();
 99                     param = receiveString.Substring(command.Length).Trim();
100                 }
101 
102                 // 處理不需登錄即可響應的命令(這里只處理QUIT)
103                 if (command == "QUIT")
104                 {
105                     // 關閉TCP連接並釋放與其關聯的所有資源
106                     user.commandSession.Close();
107                     return;
108                 }
109                 else
110                 {
111                     switch (user.loginOK)
112                     {
113                         // 等待用戶輸入用戶名:
114                         case 0:
115                             CommandUser(user, command, param);
116                             break;
117 
118                         // 等待用戶輸入密碼
119                         case 1:
120                             CommandPassword(user, command, param);
121                             break;
122 
123                         // 用戶名和密碼驗證正確后登陸
124                         case 2:
125                             switch (command)
126                             {
127                                 case "CWD":
128                                     CommandCWD(user, param);
129                                     break;
130                                 case "PWD":
131                                     CommandPWD(user);
132                                     break;
133                                 case "PASV":
134                                     CommandPASV(user);
135                                     break;
136                                 case "PORT":
137                                     CommandPORT(user, param);
138                                     break;
139                                 case "LIST":
140                                     CommandLIST(user, param);
141                                     break;
142                                 case "NLIST":
143                                     CommandLIST(user, param);
144                                     break;
145                                 // 處理下載文件命令
146                                 case "RETR":
147                                     CommandRETR(user, param);
148                                     break;
149                                 // 處理上傳文件命令
150                                 case "STOR":
151                                     CommandSTOR(user, param);
152                                     break;
153                                 // 處理刪除命令
154                                 case "DELE":
155                                     CommandDELE(user, param);
156                                     break;
157                                 // 使用Type命令在ASCII和二進制模式進行變換
158                                 case "TYPE":
159                                     CommandTYPE(user, param);
160                                     break;
161                                 default:
162                                     sendString = "502 command is not implemented.";
163                                     RepleyCommandToUser(user, sendString);
164                                     break;
165                             }
166 
167                             break;
168                     }          
169                 }
170             }       
171         }
View Code

程序演示截圖:

 首先在F:\盤下新建文件夾MyFtpServerRoot,在其中創建目錄結構並放一些文件資源,例如圖片,文檔等,程序中演示的目錄結構如下圖:

 這樣,本地的FTP服務站點就已經建好了,運行FTP服務器程序,然后點擊“啟動”按鈕后就啟動了FTP服務器,運行結果如下圖所示:

然后配合上個專題中實現的FTP客戶端來完成與FTP服務器的“聊天”演示,因為FTP服務器程序中已經初始化用戶名和密碼(都為admin),所以FTP客戶端中取消選擇“匿名復選框”,直接輸入用戶名和密碼為admin后點擊“登錄”按鈕后就完成了用戶驗證的過程,並與FTP服務器建立了控制連接和數據連接。運行結果如下圖:

當然用戶可以通過"上傳"、“下載”和刪除按鈕來對FTP服務器上的文件進行操作,這里就不貼出運行圖片了, 大家可以下載源碼來測試下的。

六、內容的結尾,說說后面的計划吧

這個專題介紹完后,我這個C#網絡編程系列也就介紹完了,這個系列中主要介紹網絡編程的一些入門知識,對於朋友在留言中經常提到的“打洞”技術以及一些網絡編程中一些更難的內容還大家一起努力來學習的,同時我也會在后面和大家分享下一些實際開發過程中的網絡編程的內容(在后面的文章打算和大家分享一個下載器的實現),最后,希望這個系列可以讓大家對網絡協議有一個最初的入門,這樣在實際的開發過程中才知道這些實現背后的原理。之后我總結下我這個系列的所有文章的索引,以便讓大家更好的閱讀和查找關於這個系列的所有文章。

 

源碼下載:http://files.cnblogs.com/zhili/FtpServer.zip,大家如果覺得不錯的話,還請大家推薦下,謝謝大家的支持

 用來演示的服務器目錄:http://files.cnblogs.com/zhili/MyFtpServerRoot.zip

 上個專題FTP文件上傳下載器源碼:http://files.cnblogs.com/zhili/FTPUpDownloader.zip

 

轉自:http://www.cnblogs.com/zhili/archive/2012/10/18/FTPServer.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM