前言
FTP 客戶端如 FlashFXP,File Zilla 被廣泛應用,原理上都是用底層的 Socket 來實現。FTP 客戶端與服務器端進行數據交換必須建立兩個套接字,一個作爲命令通道,一個作爲數據通道。前者用於客戶端向服務器發送命令,如登錄,刪除某個文件,後者用於發送或接收數據,例如下載或上傳文件等。本文詳細闡述如何調用系統接口發送 FTP 命令實現文件上傳下載等 FTP 客戶端功能,讓讀者對 FTP 客戶端的原理有一個深入的瞭解。
FTP 概述
文件傳輸協議(FTP)作爲網絡共享文件的傳輸協議,在網絡應用軟件中具有廣泛的應用。FTP的目標是提高文件的共享性和可靠高效地傳送數據。
在傳輸文件時,FTP 客戶端程序先與服務器建立連接,然後向服務器發送命令。服務器收到命令後給予響應,並執行命令。FTP 協議與操作系統無關,任何操作系統上的程序只要符合 FTP 協議,就可以相互傳輸數據。本文主要基於 Linux 平臺,對 FTP 客戶端的實現原理進行詳盡的解釋並闡述如何使用 C 語言編寫一個簡單的 FTP 客戶端。
FTP 協議
相比其他協議,如 HTTP 協議,FTP 協議要複雜一些。與一般的 C/S 應用不同點在於一般的C/S 應用程序一般只會建立一個 Socket 連接,這個連接同時處理服務器端和客戶端的連接命令和數據傳輸。而FTP協議中將命令與數據分開傳送的方法提高了效率。
FTP 使用 2 個端口,一個數據端口和一個命令端口(也叫做控制端口)。命令端口一般爲21,數據端口爲20或其它。命令 Socket 用來傳送命令,數據 Socket 用於傳送數據。每一個 FTP 命令發送之後,FTP 服務器都會返回一個字符串,其中包括一個響應代碼和一些說明信息。其中的返回碼主要是用於判斷命令是否被成功執行了。
命令端口
一般來說,客戶端有一個 Socket 用來連接 FTP 服務器的相關端口,它負責 FTP 命令的發送和接收返回的響應信息。一些操作如“登錄”、“改變目錄”、“刪除文件”,依靠這個連接發送命令就可完成。
數據端口
對於有數據傳輸的操作,主要是顯示目錄列表,上傳、下載文件,我們需要依靠另一個 Socket來完成。
如果使用被動模式,通常服務器端會返回一個端口號。客戶端需要用另開一個 Socket 來連接這個端口,然後我們可根據操作來發送命令,數據會通過新開的一個端口傳輸。
如果使用主動模式,通常客戶端會發送一個端口號給服務器端,並在這個端口監聽。服務器需要連接到客戶端開啓的這個數據端口,並進行數據的傳輸。
下面對 FTP 的主動模式和被動模式做一個簡單的介紹。
主動模式 (PORT)
主動模式下,客戶端隨機打開一個大於 1024 的端口向服務器的命令端口 21端口發起連接,同時開放N +1 端口監聽,並向服務器發出 “port N+1” 命令,由服務器從它自己的數據端口 20端口,主動連接到客戶端指定的數據端口 (N+1)。
FTP 的客戶端只是告訴服務器自己的端口號,讓服務器來連接客戶端指定的端口。對於客戶端的防火牆來說,這是從外部到內部的連接,可能會被阻塞。
被動模式 (PASV)
爲了解決服務器發起到客戶的連接問題,有了另一種 FTP 連接方式,即被動方式。命令連接和數據連接都由客戶端發起,這樣就解決了從服務器到客戶端的數據端口的連接被防火牆過濾的問題。
被動模式下,當開啓一個 FTP 連接時,客戶端打開兩個任意的本地端口 (N > 1024 和 N+1) 。
第一個端口連接服務器的命令 端口 21端口,提交 PASV 命令。然後,服務器會開啓一個任意的數據端口P (P > 1024 ),返回如“227 entering passive mode (127,0,0,1,4,18)”。 它返回了 227 開頭的信息,在括號中有以逗號隔開的六個數字,前四個指服務器的地址,最後兩個,將倒數第二個乘 256 再加上最後一個數字,這就是 FTP 服務器開放的用來進行數據傳輸的端口。如得到 227 entering passive mode (h1,h2,h3,h4,p1,p2),那麼端口號是 p1*256+p2,ip 地址爲h1.h2.h3.h4。這意味着在服務器上有一個端口被開放。客戶端收到命令取得端口號之後, 會通過 N+1 號端口連接服務器的端口 P,然後在兩個端口之間進行數據傳輸。
主要用到的 FTP 命令
FTP 每個命令都有 3 到 4 個字母組成,命令後面跟參數,用空格分開。每個命令都以 "\r\n"結束。
要下載或上傳一個文件,首先要登入 FTP 服務器,然後發送命令,最後退出。這個過程中,主要用到的命令有 USER、PASS、SIZE、REST、CWD、RETR、PASV、PORT、QUIT。
USER: 指定用戶名。通常是控制連接後第一個發出的命令。“USER gaoleyi\r\n”: 用戶名爲gaoleyi 登錄。
PASS: 指定用戶密碼。該命令緊跟 USER 命令後。“PASS gaoleyi\r\n”:密碼爲 gaoleyi。
SIZE: 從服務器上返回指定文件的大小。“SIZE file.txt\r\n”:如果 file.txt 文件存在,則返回該文件的大小。
TYPE:設置傳輸模式。如"TYPE I\r\n"爲Binary傳輸模式,"TYPE A\r\n"爲Ascii方式。
CWD: 改變工作目錄。如:“CWD dirname\r\n”,"CWD \\\r\n"爲進入根目錄。
LIST:從服務器上返回指定目錄的文件列表信息。如:"LIST dirname\r\n"
PASV: 讓服務器在數據端口監聽,進入被動模式。如:“PASV\r\n”。
PORT: 告訴 FTP 服務器客戶端監聽的端口號,讓 FTP 服務器採用主動模式連接客戶端。如:“PORT h1,h2,h3,h4,p1,p2”。
RETR: 下載文件。“RETR file.txt \r\n”:下載文件 file.txt。
STOR: 上傳文件。“STOR file.txt\r\n”:上傳文件 file.txt。
REST: 該命令並不傳送文件,而是略過指定點後的數據。此命令後應該跟其它要求文件傳輸的 FTP 命令。“REST 100\r\n”:重新指定文件傳送的偏移量爲 100 字節。
QUIT: 關閉與服務器的連接。
FTP 響應碼
客戶端發送 FTP 命令後,服務器返回響應碼。
響應碼用三位數字編碼表示:
第一個數字給出了命令狀態的一般性指示,比如響應成功、失敗或不完整。
第二個數字是響應類型的分類,如 2 代表跟連接有關的響應,3 代表用戶認證。
第三個數字提供了更加詳細的信息。
第一個數字的含義如下:
1 表示服務器正確接收信息,還未處理。
2 表示服務器已經正確處理信息。
3 表示服務器正確接收信息,正在處理。
4 表示信息暫時錯誤。
5 表示信息永久錯誤。
第二個數字的含義如下:
0 表示語法。
1 表示系統狀態和信息。
2 表示連接狀態。
3 表示與用戶認證有關的信息。
4 表示未定義。
5 表示與文件系統有關的信息。
Socket 編程的幾個重要步驟
Socket 客戶端編程主要步驟如下:
- socket() 創建一個 Socket
- connect() 與服務器連接
- write() 和 read() 進行會話
- close() 關閉 Socket
Socket 服務器端編程主要步驟如下:
- socket() 創建一個 Socket
- bind()
- listen() 監聽
- accept() 接收連接的請求
- write() 和 read() 進行會話
- close() 關閉 Socket
實現 FTP 客戶端上傳下載功能
下面讓我們通過一個例子來對 FTP 客戶端有一個深入的瞭解。本文實現的 FTP 客戶端有下列功能:
- 客戶端和 FTP 服務器建立 Socket 連接。
- 向服務器發送 USER、PASS 命令登錄 FTP 服務器。
- 使用 PASV 命令得到服務器監聽的端口號,建立數據連接。
- 使用 RETR/STOR 命令下載/上傳文件。
- 在下載完畢後斷開數據連接併發送 QUIT 命令退出。
本例中使用的 FTP 服務器爲 filezilla。在整個交互的過程中,控制連接始終處於連接的狀態,數據連接在每傳輸一個文件時先打開,後關閉。
客戶端和 FTP 服務器建立 Socket 連接
當客戶端與服務器建立連接後,服務器會返回 220 的響應碼和一些歡迎信息。
清單 1. 客戶端連接到 FTP 服務器,接收歡迎信息
SOCKET control_sock; struct hostent *hp; struct sockaddr_in server; memset(&server, 0, sizeof(struct sockaddr_in)); /* 初始化socket */ control_sock = socket(AF_INET, SOCK_STREAM, 0); hp = gethostbyname(server_name); memcpy(&server.sin_addr, hp->h_addr, hp->h_length); server.sin_family = AF_INET; server.sin_port = htons(port); /* 連接到服務器端 */ connect(control_sock,(struct sockaddr *)&server, sizeof(server)); /* 客戶端接收服務器端的一些歡迎信息 */ read(control_sock, read_buf, read_len);
客戶端登錄 FTP 服務器
當客戶端發送用戶名和密碼,服務器驗證通過後,會返回 230 的響應碼。然後客戶端就可以向服務器端發送命令了。
清單 2. 客戶端發送用戶名和密碼,登入 FTP 服務器
/* 命令 ”<strong>USER username\r\</strong>n” */ sprintf(send_buf,"USER %s\r\n",username); /*客戶端發送用戶名到服務器端 */ write(control_sock, send_buf, strlen(send_buf)); /* 客戶端接收服務器的響應碼和信息,正常爲 ”<strong>331 User name okay, need password.” </strong>*/ read(control_sock, read_buf, read_len); /* 命令 ”<strong>PASS password\r\n</strong>” */ sprintf(send_buf,"PASS %s\r\n",password); /* 客戶端發送密碼到服務器端 */ write(control_sock, send_buf, strlen(send_buf)); /* 客戶端接收服務器的響應碼和信息,正常爲 ”<strong>230 User logged in, proceed.” </strong>*/ read(control_sock, read_buf, read_len);
客戶端讓 FTP 服務器進入被動模式
當客戶端在下載/上傳文件前,要先發送命令讓服務器進入被動模式。服務器會打開數據端口並監聽。並返回響應碼 227 和數據連接的端口號。
清單 3. 讓服務器進入被動模式,在數據端口監聽
/* 命令 ”<strong>PASV\r\n</strong>” */ sprintf(send_buf,"PASV\r\n"); /* 客戶端告訴服務器用被動模式 */ write(control_sock, send_buf, strlen(send_buf)); /*客戶端接收服務器的響應碼和新開的端口號, * 正常爲 <strong>”227 Entering passive mode (<h1,h2,h3,h4,p1,p2>)”</strong> */ read(control_sock, read_buf, read_len);
客戶端通過被動模式下載文件
當客戶端發送命令下載文件。服務器會返回響應碼 150,並向數據連接發送文件內容。
清單 4. 客戶端連接到 FTP 服務器的數據端口並下載文件
/* 連接服務器新開的數據端口 */ connect(data_sock,(struct sockaddr *)&server, sizeof(server)); /* 命令 ”<strong>CWD dirname\r\n</strong>” */ sprintf(send_buf,"CWD %s\r\n", dirname); /* 客戶端發送命令改變工作目錄 */ write(control_sock, send_buf, strlen(send_buf)); /* 客戶端接收服務器的響應碼和信息,正常爲 ”<strong>250 Command okay.” </strong>*/ read(control_sock, read_buf, read_len); /* 命令 ”<strong>SIZE filename\r\n</strong>” */ sprintf(send_buf,"SIZE %s\r\n",filename); /* 客戶端發送命令從服務器端得到下載文件的大小 */ write(control_sock, send_buf, strlen(send_buf)); /* 客戶端接收服務器的響應碼和信息,正常爲 ”<strong>213 <size>” </strong>*/ read(control_sock, read_buf, read_len); /* 命令 ”<strong>RETR filename\r\n</strong>” */ sprintf(send_buf,"RETR %s\r\n",filename); /* 客戶端發送命令從服務器端下載文件 */ write(control_sock, send_buf, strlen(send_buf)); /* 客戶端接收服務器的響應碼和信息,正常爲 ”<strong>150 Opening data connection.” </strong>*/ read(control_sock, read_buf, read_len); /* 客戶端創建文件 */ file_handle = open(disk_name, CRFLAGS, RWXALL); for( ; ; ) { ... ... /* 客戶端通過數據連接 從服務器接收文件內容 */ read(data_sock, read_buf, read_len); /* 客戶端寫文件 */ write(file_handle, read_buf, read_len); ... ... } /* 客戶端關閉文件 */ rc = close(file_handle);
客戶端退出服務器
當客戶端下載完畢後,發送命令退出服務器,並關閉連接。服務器會返回響應碼 200。
清單 5. 客戶端關閉數據連接,退出 FTP 服務器並關閉控制連接
/* 客戶端關閉數據連接 */ close(data_sock); /* 客戶端接收服務器的響應碼和信息,正常爲 ”226 Transfer complete.” */ read(control_sock, read_buf, read_len); /* 命令 ”QUIT\r\n” */ sprintf(send_buf,"QUIT\r\n"); /* 客戶端將斷開與服務器端的連接 */ write(control_sock, send_buf, strlen(send_buf)); /* 客戶端接收服務器的響應碼,正常爲 ”200 Closes connection.” */ read(control_sock, read_buf, read_len); /* 客戶端關閉控制連接 */ close(control_sock);
至此,下載文件已經完成。需要注意的是發送 FTP 命令的時候,在命令後要緊跟 “\r\n”,否則服務器不會返回信息。回車換行符號 “\r\n” 是 FTP 命令的結尾符號,當服務器接收到這個符號時,認爲客戶端發送的命令已經結束,開始處理。否則會繼續等待。
讓我們來看一下 FTP 服務器這一端的響應情況:
清單 6. 客戶端下載文件時,FTP 服務器的響應輸出
(not logged in) (127.0.0.1)> Connected, sending welcome message... (not logged in) (127.0.0.1)> 220-FileZilla Server version 0.9.36 beta (not logged in) (127.0.0.1)> 220 hello gaoleyi (not logged in) (127.0.0.1)> USER gaoleyi (not logged in) (127.0.0.1)> 331 Password required for gaoleyi (not logged in) (127.0.0.1)> PASS ********* gaoleyi (127.0.0.1)> 230 Logged on gaoleyi (127.0.0.1)> PWD gaoleyi (127.0.0.1)> 257 "/" is current directory. gaoleyi (127.0.0.1)> SIZE file.txt gaoleyi (127.0.0.1)> 213 4096 gaoleyi (127.0.0.1)> <strong>PASV</strong> gaoleyi (127.0.0.1)> 227 Entering Passive Mode (127,0,0,1,13,67) gaoleyi (127.0.0.1)> <strong>RETR file.txt</strong> gaoleyi (127.0.0.1)> 150 Connection accepted gaoleyi (127.0.0.1)> 226 Transfer OK gaoleyi (127.0.0.1)> QUIT gaoleyi (127.0.0.1)> 221 Goodbye
首先,服務器準備就緒後返回 220。客戶端接收到服務器端返回的響應碼後,相繼發送“USER username” 和 “PASS password” 命令登錄。隨後,服務器返回的響應碼爲 230 開頭,說明客戶端已經登入了。這時,客戶端發送 PASV 命令讓服務器進入被動模式。服務器返回如 “227 Entering Passive Mode (127,0,0,1,13,67)”,客戶端從中得到端口號,然後連接到服務器的數據端口。接下來,客戶端發送下載命令,服務器會返回響應碼 150,並從數據端口發送數據。最後,服務器返回 “226 transfer complete”,表明數據傳輸完成。
需要注意的三點:
1、客戶端不要一次發送多條命令,例如我們要打開一個目錄並且顯示這個目錄,我們得發送 CWD dirname,PASV,LIST。在發送完 CWD dirname 之後等待響應代碼,然後再發送後面一條。當 PASV 返回之後,我們打開另一個 Socket 連接到相關端口上。然後發送 LIST,返回 125 之後在開始接收數據,最後返回 226 表明完成。
2、在傳輸多個文件的過程中,需要注意的是每次新的傳輸都必須重新使用 PASV 獲取新的端口號,接收完數據後應該關閉該數據連接,這樣服務器纔會返回一個 2XX 成功的響應。然後客戶端可以繼續下一個文件的傳輸。
3、當數據端口成功接收完數據(如RETR、LIST命令接收數據完成)後服務器端還會發回一條以226響應碼開頭的響應信息,這一點要留意做相應的處理。
上傳文件與下載文件相比,登入驗證和切換被動模式都如出一轍,只需要改變發送到服務器端的命令,並通過數據連接發送文件內容。
客戶端通過被動模式向服務器上傳文件
當客戶端發送命令上傳文件,服務器會從數據連接接收文件。
客戶端通過主動模式向服務器上傳文件
到目前爲止,本文介紹的都是客戶端用被動模式進行文件的上傳和下載。下面將介紹客戶端用主動模式下載文件。
<strong>清單 7. 用主動模式從 FTP 服務器下載文件的示例 C 程序</strong> ... ... SOCKET data_sock; data_sock = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in name; name.sin_family = AF_INET; name.sin_addr.s_addr = htons(INADDR_ANY); server_port = p1*256+p2; length = sizeof(name); name.sin_port = htons(server_port); bind(server_sock, (struct sockaddr *)&name, length); struct sockaddr_in client_name; length = sizeof(client_name); /* 客戶端開始監聽端口p1*256+p2 */ listen(server_sock, 64); /* 命令 ”<strong>PORT \r\n</strong>” */ sprintf(send_buf,"PORT 1287,0,0,1,%d,%d\r\n", p1, p2); write(control_sock, send_buf,strlen(send_buf)); /* 客戶端接收服務器的響應碼和信息,正常爲 ”<strong>200 Port command successful</strong>” */ read(control_sock, read_buf, read_len); sprintf(send_buf,"RETR filename.txt\r\n"); write(control_sock, send_buf, strlen(send_buf)); /* 客戶端接收服務器的響應碼和信息,正常爲 ”<strong>150 Opening data channel for file transfer.</strong>” */ read(control_sock, read_buf, read_len); /* ftp客戶端接受服務器端的連接請求 */ data_sock = accept(server_sock,(struct sockaddr *)&client_name, &length); ... ... file_handle = open(disk_name, ROFLAGS, RWXALL); for( ; ; ) { ... ... read(data_sock, read_buf, read_len); write(file_handle, read_buf, read_len); ... ... } close(file_handle);
客戶端通過 PORT 命令告訴服務器連接自己的 p1*256+p2 端口。隨後在這個端口進行監聽,等待 FTP 服務器連接上來, 再通過這個數據端口來傳輸文件。PORT 方式在傳送數據時,FTP 客戶端其實就相當於一個服務器端,由 FTP 服務器主動連接自己。
斷點續傳
由於網絡不穩定,在傳輸文件的過程中,可能會發生連接斷開的情況,這時候需要客戶端支持斷點續傳的功能,下次能夠從上次終止的地方開始接着傳送。需要使用命令 REST。如果在斷開連接前,一個文件已經傳輸了 512 個字節。則斷點續傳開始的位置爲 512,服務器會跳過傳輸文件的前 512 字節。
清單 8. 從 FTP 服務器斷點續傳下載文件
... ... /* 命令 ”<strong>REST offset\r\n</strong>” */ sprintf(send_buf,"REST %ld\r\n", offset); /* 客戶端發送命令指定下載文件的偏移量 */ write(control_sock, send_buf, strlen(send_buf)); /* 客戶端接收服務器的響應碼和信息, *正常爲 ”350 Restarting at <position>. Send STORE or RETRIEVE to initiate transfer.” */ read(control_sock, read_buf, read_len); ... ... /* 命令 ”<strong>RETR filename\r\n</strong>” */ sprintf(send_buf,"RETR %s\r\n",filename); /* 客戶端發送命令從服務器端下載文件, 並且跳過該文件的前offset字節*/ write(control_sock, send_buf, strlen(send_buf)); /* 客戶端接收服務器的響應碼和信息,* *正常爲 ”150 Connection accepted, restarting at offset <position>” */ read(control_sock, read_buf, read_len); ... ... file_handle = open(disk_name, CRFLAGS, RWXALL); /* 指向文件寫入的初始位置 */ lseek(file_handle, offset, SEEK_SET); ... ...
結束語
本文從應用實現的角度,介紹了 FTP 協議。並用詳盡的例子分析瞭如何用主動模式和被動模式實現 FTP 客戶端上傳下載文件,如何進行斷點續傳。通過本文可以讓讀者對 FTP 客戶端的原理有一個深入的瞭解。
原文地址:http://www.ibm.com/developerworks/cn/linux/l-cn-socketftp/