現在市面上有很多免費的FTP軟體:如FileZilla ,那如果想自己在代碼中實現與ftp伺服器的上傳下載文件該如何實現那? 本質上ftp協議就是TCP基礎上建立的一種協議,具體如下。 FTP 概述 文件傳輸協議(FTP)作為網路共用文件的傳輸協議,在網路應用軟體中具有廣泛的應用。FTP的目標是提高 ...
現在市面上有很多免費的FTP軟體:如FileZilla ,那如果想自己在代碼中實現與ftp伺服器的上傳下載文件該如何實現那?
本質上ftp協議就是TCP基礎上建立的一種協議,具體如下。
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 的埠向伺服器的命令埠 P,即 21 埠,發起連接,同時開放N +1 埠監聽,並向伺服器發出 “port N+1” 命令,由伺服器從它自己的數據埠 (20) 主動連接到客戶端指定的數據埠 (N+1)。
FTP 的客戶端只是告訴伺服器自己的埠號,讓伺服器來連接客戶端指定的埠。對於客戶端的防火牆來說,這是從外部到內部的連接,可能會被阻塞。
被動模式 (PASV)
為瞭解決伺服器發起到客戶的連接問題,有了另一種 FTP 連接方式,即被動方式。命令連接和數據連接都由客戶端發起,這樣就解決了從伺服器到客戶端的數據埠的連接被防火牆過濾的問題。
被動模式下,當開啟一個 FTP 連接時,客戶端打開兩個任意的本地埠 (N > 1024 和 N+1) 。
第一個埠連接伺服器的 21 埠,提交 PASV 命令。然後,伺服器會開啟一個任意的埠 (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 文件存在,則返回該文件的大小。
CWD: 改變工作目錄。如:“CWD 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 命令退出。
經過測試可以正常上傳下載數據,,測試代碼如下:
main.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "ftp.h" #define FTP_SERVER_IP "XXXXXXXX" #define FTP_SERVER_USER "XXXXX" #define FTP_SERVER_PASS "XXXXXX" #define MAX_BUF_LEN 512 typedef struct{ char usr[32]; char passwd[32]; char ser_filepath[512]; char ser_filename[64]; char new_filename[64]; int control_sock; }ftp_client_st; ftp_client_st ftp_st; int main (int argc , char * argv[]) { char str[MAX_BUF_LEN] ={0}; int ret =-1; // while(1){ printf("*************\n"); //printf("Please input the ftp server ip: "); memset(str,0,sizeof(str)); //scanf("%s",str); //從終端獲取到伺服器ip地址。 strcpy(str,FTP_SERVER_IP); printf("input fpt server ip:%s\n",str); /*連接到伺服器*/ memset(&ftp_st,0,sizeof(ftp_client_st)); ftp_st.control_sock = connect_ftp_server(str,FTP_SERVER_PORT); if(ftp_st.control_sock > 0){/*連接成功*/ ret = -1; while(ret < 0){ strcpy(ftp_st.usr,FTP_SERVER_USER); strcpy(ftp_st.passwd,FTP_SERVER_PASS); printf("input usr:%s passwd:%s\n",ftp_st.usr,ftp_st.passwd); ret = login_ftp_server(ftp_st.control_sock,ftp_st.usr,ftp_st.passwd); if(ret < 0){ printf("\nUser or Passwd is wrong,input agin"); } else{ //列印伺服器當前目錄和列表 while(1){ printf("Get list start:\n"); //ret = down_file_ftpserver(ftp_st.control_sock,"/","/list_mode",0,0,CMD_LIST); /*被動模式*/ ret = down_file_ftpserver(ftp_st.control_sock,"/","../list_passive",1,0,CMD_LIST); /*被動模式獲取文件列表*/ // down_file_ftpserver(ftp_st.control_sock,"/down_test","list1",0,0,CMD_LIST); //printf("\nInput down file dir (Input quit to quit):"); //memset(ftp_st.ser_filepath,0,sizeof(ftp_st.ser_filepath)); //scanf("%s",ftp_st.ser_filepath); //if(strncmp(ftp_st.ser_filepath,"quit",4) ==0) // goto err0; #if 0 printf("\nInput down filename (Input quit to quit):"); memset(ftp_st.ser_filename,0,sizeof(ftp_st.ser_filename)); scanf("%s",ftp_st.ser_filename); if(strncmp(ftp_st.ser_filename,"quit",4) ==0) goto err0; printf("\nInput new filename (Input quit to quit):"); memset(ftp_st.new_filename,0,sizeof(ftp_st.new_filename)); scanf("%s",ftp_st.new_filename); if(strncmp(ftp_st.new_filename,"quit",4) ==0) goto err0; printf("input filename :%s; newfilename:%s; \n",ftp_st.ser_filename,ftp_st.new_filename); printf("down file start:\n"); //ret = down_file_ftpserver(ftp_st.control_sock,ftp_st.ser_filename,ftp_st.new_filename,0,0,CMD_RETR); ret = down_file_ftpserver(ftp_st.control_sock,ftp_st.ser_filename,ftp_st.new_filename,0,0,CMD_RETR); #endif down_file_ftpserver(ftp_st.control_sock,"/down_test/test_ftp.zip","../12.zip",1,0,CMD_RETR); up_file_ftpserver(ftp_st.control_sock, "/down_test/12.zip", "../12.zip", 1, 0); get_fsize_ftpserver(ftp_st.control_sock, "/down_test/12.zip"); goto err0; } } } } } err0: quit_fpt_server(ftp_st.control_sock); return 0; }
fpt.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <arpa/inet.h> #include <sys/unistd.h> #include <sys/ioctl.h> #include <net/if.h> #include "ftp.h" #define MAX_BUF 512 #define IP_LENGTH 16 //正常時伺服器回覆的響應碼 #define ACK_USER_NUM "331" #define ACK_PASS_NUM "230" #define ACK_PASV_NUM "227" #define ACK_CWD_NUM "250" #define ACK_SIZE_NUM "213" #define ACK_RETR_NUM "150" #define ACK_REST_NUM "350" #define ACK_QUIT_NUM "200" #define ACK_LIST_NUM "125" #define ACK_STOR_NUM "150" #define ACK_CONNECT_NUM "220" #define ACK_PORT_NUM "200" /*ftp server info*/ typedef struct { //char szUserName[16]; //char szPassWd[32]; char server_path[128]; char server_filename[64]; char new_filename[128]; int data_sock; char data_ip[32]; int data_port; int client_server_sock; int file_handle; }FTP_DATA_INFO; static int itoa(int value, char * str, int radix); static int send_cmd(int ctrl_sock,eu_cmd_type typ, const char *val,const char *ack_num); static int enter_passive_mode(int ctrl_sock,char *data_ip, int * data_port); static int enter_active_mode(int ctrl_sock); static int get_data_sock(const char* server_ip,const int port); static int get_active_data_sock(int client_server_sock); static int GetAddr(const char *ifname, char *addr, int flag); static int close_st_info(FTP_DATA_INFO * info); FTP_DATA_INFO server_info; static int GetAddr(const char *ifname, char *addr, int flag) { struct sockaddr_in *sin; struct ifreq ifr; int sockfd; if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { printf("socket create error!\n"); return - 1; } memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_ifrn.ifrn_name, ifname, IFNAMSIZ); if(ioctl(sockfd, flag, &ifr) < 0) { close(sockfd); return - 1; } close(sockfd); if(SIOCGIFHWADDR == flag) { memcpy((void *)addr, (const void *)&ifr.ifr_ifru.ifru_hwaddr.sa_data, 6); } else { sin = (struct sockaddr_in *)&ifr.ifr_ifru.ifru_addr; snprintf((char *)addr, IP_LENGTH, "%s", inet_ntoa(sin->sin_addr)); } return 0; } static int itoa(int value, char * str, int radix) { char temp[33]; char *tp = temp; int i; unsigned v; int sign; char *sp; int num= 0; if(radix > 36 || radix < 1) return 0; sign = (radix == 10 && value < 0); //十進位負數 if(sign) v = -value; else v = (unsigned)value; while(v || tp == temp) //轉化操作 { i = v % radix; v = v / radix; if(i < 10) *tp++ = i + '0'; else *tp++ = i + 'a' - 10; } if(str == 0) str = (char*)malloc((tp - temp) + sign + 1); sp = str; if(sign) //是負數的話把負號先加入數組 *sp++ = '-'; while(tp > temp) { *sp++ = *--tp; num++; } *sp = 0; return num; } /* * @brief 連接fpt伺服器 * @param 無 * @return -1/成功建立的套接字 */ int connect_ftp_server(const char* server_ip,const int port) { int control_sock =-1; int ret =-1; struct sockaddr_in server; char read_buf[MAX_BUF]={0}; struct timeval tv_out; memset(&server,0,sizeof(struct sockaddr_in)); if(server_ip == NULL){ printf("argc is NULL\n"); return -1; } control_sock = socket(AF_INET,SOCK_STREAM,0); if(control_sock <0){ printf("socket failed\n"); return -1; } /*設置sock fd 接收超時時間*/ tv_out.tv_sec =0; tv_out.tv_usec =500*1000; setsockopt(control_sock, SOL_SOCKET, SO_RCVTIMEO,&tv_out,sizeof(tv_out)); server.sin_family = AF_INET; server.sin_port = htons(port); server.sin_addr.s_addr = inet_addr(server_ip); ret = connect(control_sock,(struct sockaddr *)&server,sizeof(server)); if(ret < 0){ printf("connect failed\n"); return -1; } ret =1; /*接收服務端的應答消息*/ usleep(100*1000); ret = read(control_sock,read_buf,sizeof(read_buf)); if(ret < 0){ printf("read error\n"); return -1; } printf("%s ret=%d \n",read_buf,ret); if(strncmp(read_buf,ACK_CONNECT_NUM,3) == 0) /*成功*/ { printf("Connect ftp ok\n"); return control_sock; } else { close(control_sock); return -1; } } /* * @brief 被動模式連接獲取伺服器的data_sock * @param 無 * @return -1/成功建立的套接字 */ static int get_data_sock(const char* server_ip,const int port) { int data_sock =-1; int ret =-1; struct sockaddr_in server; char read_buf[MAX_BUF]={0}; memset(&server,0,sizeof(struct sockaddr_in)); if(server_ip == NULL){ printf("argc is NULL\n"); return -1; } data_sock = socket(AF_INET,SOCK_STREAM,0); if(data_sock <0){ printf("socket failed\n"); return -1; } /*設置為非阻塞*/ //int cflags = fcntl(data_sock,F_GETFL,0); //fcntl(data_sock,F_SETFL,cflags|O_NONBLOCK); server.sin_family = AF_INET; server.sin_port = htons(port); server.sin_addr.s_addr = inet_addr(server_ip); ret = connect(data_sock,(struct sockaddr *)&server,sizeof(server)); if(ret < 0){ printf("connect failed\n"); return -1; } /*無應答*/ return data_sock; } /*主動模式獲取data sock 必須要在LIST等下載上傳命令發送後 accept接受才能成功 */ static int get_active_data_sock(int client_server_sock) { int data_sock =-1; struct sockaddr_in client_name; int len; len = sizeof(client_name); data_sock = accept(client_server_sock,(struct sockaddr *)&client_name ,&len); if(data_sock <0) { printf("accept failed\n"); } printf("data_sock = %d\n",data_sock); return data_sock; } /* * @brief 登陸fpt伺服器 * @param 無 * @return -1/成功返回0 */ int login_ftp_server(int ctrl_sock,const char *user_name, const char * passwd) { int ret =-1; if((user_name == NULL) ||(passwd == NULL)){ printf("argc is NULL\n"); return -1; } ret = send_cmd(ctrl_sock,CMD_USER,user_name,ACK_USER_NUM); if(ret < 0){ printf("send_cmd %d failed \n",CMD_USER); return -1; } ret = send_cmd(ctrl_sock,CMD_PASS,passwd,ACK_PASS_NUM); if(ret < 0){ printf("send_cmd %d failed \n",CMD_PASS); return -1; } return 0; } /* * @brief 給伺服器發送指令 * @param * @return 失敗返回-1/成功返回0 SIZE 返回等到的文件大小 */ static int send_cmd(int ctrl_sock,eu_cmd_type typ, const char *val,const char *ack_num) { int ret =-1; char send_buf[MAX_BUF]={0}; char read_buf[MAX_BUF]={0}; char *pos= NULL; char tmp[64] ={0}; if((typ == CMD_USER) ||(typ == CMD_PASS) || (typ == CMD_CWD)){ if((val == NULL) ||(ack_num == NULL)){ printf("argc is NULL\n"); return -1; } } switch(typ){ case CMD_USER: memset(send_buf,0,sizeof(send_buf)); sprintf(send_buf,"USER %s\r\n",val); ret = write(ctrl_sock,send_buf,strlen(send_buf)); if(ret < 0){ printf("write failed\n"); return -1; } memset(read_buf,0,sizeof(read_buf)); ret = read(ctrl_sock,read_buf,sizeof(read_buf)); if(ret < 0){ printf("read failed\n"); return -1; } ret = strncmp(read_buf,ack_num,strlen(ack_num)); break; case CMD_PASS: memset(send_buf,0,sizeof(send_buf)); sprintf(send_buf,"PASS %s\r\n",val); ret = write(ctrl_sock,send_buf,strlen(send_buf)); if(ret < 0){ printf("write failed\n"); return -1; } memset(read_buf,0,sizeof(read_buf)); ret = read(ctrl_sock,read_buf,sizeof(read_buf)); if(ret < 0){ printf("read failed\n"); return -1; } ret = strncmp(read_buf,ack_num,strlen(ack_num)); break; case CMD_PASV: /*只發送命令,函數外面接收提取信息*/ memset(send_buf,0,sizeof(send_buf)); sprintf(send_buf,"PASV\r\n"); ret = write(ctrl_sock,send_buf,strlen(send_buf)); if(ret < 0){ printf("write failed\n"); return -1; } break; case CMD_CWD: memset(send_buf,0,sizeof(send_buf)); sprintf(send_buf,"CWD %s\r\n",val); ret = write(ctrl_sock,send_buf,strlen(send_buf)); if(ret < 0){ printf("write failed\n"); return -1; } usleep(500*1000