現在市面上有很多免費的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); 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_QUIT: memset(send_buf,0,sizeof(send_buf)); sprintf(send_buf,"QUIT\r\n"); ret = write(ctrl_sock,send_buf,strlen(send_buf)); if(ret < 0){ printf("write failed\n"); return -1; } usleep(500*1000); 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)); case CMD_LIST: memset(send_buf,0,sizeof(send_buf)); sprintf(send_buf,"LIST %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)); usleep(100*1000); /*等待一會把266 也接收回來*/ ret = read(ctrl_sock,read_buf,sizeof(send_buf)); if(ret < 0){ printf("read failed\n"); return -1; } // ret = strncmp(read_buf,ack_num,strlen(ack_num)); break; case CMD_STOR: memset(send_buf,0,sizeof(send_buf)); sprintf(send_buf,"STOR %s\r\n",val); ret = write(ctrl_sock,send_buf,strlen(send_buf)); if(ret < 0){ printf("write failed\n"); return -1; } usleep(50*1000); 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_RETR: memset(send_buf,0,sizeof(send_buf)); sprintf(send_buf,"RETR %s\r\n",val); ret = write(ctrl_sock,send_buf,strlen(send_buf)); if(ret < 0){ printf("write failed\n"); return -1; } usleep(50*1000); 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_SIZE_FTP: memset(send_buf,0,sizeof(send_buf)); sprintf(send_buf,"SIZE %s\r\n",val); ret = write(ctrl_sock,send_buf,strlen(send_buf)); if(ret < 0){ printf("write failed\n"); return -1; } usleep(50*1000); 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; } /* 客戶端接收伺服器的響應碼和資訊,正常為 ”213 <size>” */ pos = strstr(read_buf,ack_num); if(pos != NULL){ pos += strlen(ack_num) +1; strcpy(tmp,pos); ret = atoi(tmp); } else{ ret =-1; } break; case CMD_PORT_FTP: memset(send_buf,0,sizeof(send_buf)); sprintf(send_buf,"PORT %s\r\n",val); ret = write(ctrl_sock,send_buf,strlen(send_buf)); if(ret < 0){ printf("write failed\n"); return -1; } usleep(50*1000); 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_MLSD: memset(send_buf,0,sizeof(send_buf)); sprintf(send_buf,"MLSD\r\n"); ret = write(ctrl_sock,send_buf,strlen(send_buf)); if(ret < 0){ printf("write failed\n"); return -1; } break; default:break; } printf("FTP server ack= %s\n",read_buf); return ret; } /* * @brief 進入主動模式,讓伺服器主動連線到客戶端的埠 * @param 無 * @return -1/成功返回client_server_sock */ static int enter_active_mode(int ctrl_sock) { int data_sock,server_sock; struct sockaddr_in name; struct sockaddr_in client_name,loc_addr; unsigned short server_port =0; int ret =-1; int len =0; char send_buf[64] ={0}; char ip[20]={0}; unsigned short ip0,ip1,ip2,ip3,p1,p2; //char read_buf[128] ={0}; struct timeval tv_out; /*設定sock fd 接收超時時間*/ tv_out.tv_sec =3; tv_out.tv_usec =0; /* if(GetAddr("eth0", ip, SIOCGIFADDR) != 0) { printf("get local ip failed\n"); return -1; } printf("local ip =%s\n",ip); */ memset(&name,0,sizeof(name)); memset(&client_name,0,sizeof(client_name)); /*透過ctrl_sock獲取到本機的ip*/ len =sizeof(name); if(getsockname(ctrl_sock,(struct sockaddr*)&name,&len) == -1) { printf("get sock name failed\n"); return -1; } sscanf(inet_ntoa(name.sin_addr),"%hu.%hu.%hu.%hu",&ip0,&ip1,&ip2,&ip3); server_sock = socket(AF_INET,SOCK_STREAM,0); if(server_sock <0){ printf("get sock failed\n"); return -1; } /*設定接收超時*/ setsockopt(server_sock, SOL_SOCKET, SO_RCVTIMEO,&tv_out,sizeof(tv_out)); name.sin_family = AF_INET; name.sin_port = 0; len = sizeof(name); ret = bind(server_sock,(struct sockaddr *)&name,len); if(ret < 0){ printf("bind error\n"); goto err0; } /*透過ctrl_sock獲取到系統分配到的埠*/ len = sizeof(loc_addr); memset(&loc_addr,0,len); ret = getsockname(server_sock,(struct sockaddr *)&loc_addr,&len); if(ret < 0) { printf("get sock name failed\n"); goto err0; } server_port = ntohs(loc_addr.sin_port); p1 = server_port/256; p2 = server_port%256; ret = listen(server_sock,10); if(ret < 0) { printf("listen error\n"); goto err0; } /*給伺服器 命令 “PORT \r\n*/ /*將ip中的.更換為,*/ #if 0 &ip0 = strtok(ip,"."); &ip1 = strtok(NULL,"."); &ip2 = strtok(NULL,"."); &ip3 = strtok(NULL,"."); #endif sprintf(send_buf,"%hu,%hu,%hu,%hu,%hu,%hu",ip0,ip1,ip2,ip3,p1,p2); printf("send_buf =%s server_port=%d\n",send_buf,server_port); ret = send_cmd(ctrl_sock,CMD_PORT_FTP, send_buf,ACK_PORT_NUM); if(ret < 0){ printf("Send PORT failed\n"); goto err0; } return server_sock; err0: close(server_sock); return -1; } /* * @brief 進入被動模式,讓伺服器在資料埠監聽資料 * @param 無 * @return -1/成功返回0 */ static int enter_passive_mode(int ctrl_sock,char *data_ip, int * data_port) { int ret =-1; char read_buf[MAX_BUF]={0}; char tmp_buf[64]={0}; unsigned char ip1,ip2,ip3,ip4,port1,port2; //char *tmp; if((data_ip == NULL) ||(data_port == NULL)){ printf("argc is NULL\n"); return -1; } ret = send_cmd(ctrl_sock,CMD_PASV,NULL,NULL); if(ret < 0){ printf("send_cmd %d failed \n",CMD_PASV); return -1; } usleep(100*1000); ret = read(ctrl_sock,read_buf,sizeof(read_buf)); if(ret < 0){ printf("read failed\n"); return -1; } printf("rev =%d: %s\n",ret,read_buf); if(strstr(read_buf,ACK_PASV_NUM) != NULL){ sscanf(strchr(read_buf,'(')+1,"%hhu,%hhu,%hhu,%hhu,%hhu,%hhu",&ip1,&ip2,&ip3,&ip4,&port1,&port2); //printf("ip1=%d,ip2=%d,ip3=%d,ip4=%d,port1 =%d ,port2 = %d\n",ip1,ip2,ip3,ip4,port1,port2); //snprintf(data_ip,sizeof(data_ip),"%hhu,%hhu,%hhu,%hhu",ip1,ip2,ip3,ip4); //memset(data_ip,0,sizeof(data_ip)); //snprintf(data_ip,sizeof(data_ip),"%d.%d.%d.%d",ip1,ip2,ip3,ip4); //printf("data_ip = %s\n",data_ip); memset(data_ip,0,sizeof(data_ip)); memset(tmp_buf,0,sizeof(tmp_buf)); itoa(ip1,tmp_buf,10); strcat(data_ip,tmp_buf); strcat(data_ip,"."); memset(tmp_buf,0,sizeof(tmp_buf)); itoa(ip2,tmp_buf,10); strcat(data_ip,tmp_buf); strcat(data_ip,"."); memset(tmp_buf,0,sizeof(tmp_buf)); itoa(ip3,tmp_buf,10); strcat(data_ip,tmp_buf); strcat(data_ip,"."); memset(tmp_buf,0,sizeof(tmp_buf)); itoa(ip4,tmp_buf,10); strcat(data_ip,tmp_buf); //printf("data_ip1 = %s\n",data_ip); *data_port = port1*256+port2; } return 0; } /* * @brief 從ftp伺服器上下載檔案 * @param ctrl_sock 控制伺服器sock connect_mode= 0 設定伺服器被動模式下載,非0伺服器主動模式下載 server_filepath_name 要下載檔案的路徑 newfilename 下載到本地的路徑和檔名字 offset 設定下載檔案偏移的位置,不偏移寫0 ,可用於錯誤時續傳。 * @return -1/成功返回未接收到的位元組數 */ int down_file_ftpserver(int ctrl_sock, char *server_filepath_name, const char *newfilename,int connect_mode,int offset,eu_cmd_type typ) { int ret =-1,file_size=0; char rec_buf[2048] ={0}; char stri[128]={0}; int read_size =0; char * pos =NULL; char *tmp = NULL; int flags =O_CREAT|O_RDWR|O_TRUNC;; close_st_info(&server_info); if(server_filepath_name == NULL || newfilename == NULL){ printf("argc is NULL\n"); return -1; } /*提取出server_path file name*/ tmp = server_filepath_name; if(tmp != NULL){ while(tmp != NULL){ tmp = strstr(tmp,"/"); // printf("tmp =%x :%s\n",tmp,tmp); if(tmp != NULL){ pos = tmp; tmp++; /*越過找到的"/"*/ } } if(pos !=NULL){ strncpy(server_info.server_path,server_filepath_name,pos-server_filepath_name+1); strcpy(server_info.server_filename,pos+1); } else{ strcpy(server_info.server_filename,server_filepath_name); } } printf("server path = %s ;file name =%s\n",server_info.server_path,server_info.server_filename); if((typ !=CMD_RETR) && (typ !=CMD_LIST)){ printf("typ value is not CMD_RETR or CMD_LIST\n"); return -1; } if(connect_mode){ /*主動模式*/ server_info.client_server_sock =enter_active_mode(ctrl_sock); if(server_info.client_server_sock< 0) { printf("get data_scok failed\n"); return -1; } }else{ /*被動模式*/ ret = enter_passive_mode(ctrl_sock,server_info.data_ip, &server_info.data_port); if(ret < 0){ printf("set passive mode failed\n"); return -1; } printf("server_info.data_ip =%s, data_port =%d \n",server_info.data_ip,server_info.data_port); server_info.data_sock = get_data_sock(server_info.data_ip,server_info.data_port); if(server_info.data_sock < 0){ printf("get data sock failed\n"); return -1; } } /*更改目錄*/ if(strlen(server_info.server_path) !=0) { ret = send_cmd(ctrl_sock,CMD_CWD,server_info.server_path,ACK_CWD_NUM); if(ret < 0){ printf("set passive mode failed\n"); goto err0; } } if(typ ==CMD_RETR){/*傳送下載檔案命令*/ /*設定下載檔案偏移的位置*/ if(offset >0){ flags =O_CREAT|O_RDWR|O_APPEND; itoa(offset,stri,10); ret = send_cmd(ctrl_sock,CMD_REST,stri,ACK_REST_NUM); if(ret < 0){ printf("set file offsize failed\n"); goto err0; } } ret = send_cmd(ctrl_sock,CMD_RETR,server_info.server_filename,ACK_RETR_NUM); if(ret < 0){ printf("send RETR failed\n"); goto err0; } } else if(typ ==CMD_LIST){ ret = send_cmd(ctrl_sock,CMD_LIST,server_filepath_name,ACK_LIST_NUM); if(ret < 0){ printf("send LIST failed\n"); goto err0; } } if(connect_mode){ server_info.data_sock= get_active_data_sock(server_info.client_server_sock); if(server_info.data_sock <0) { printf("accept failed\n"); goto err0; } } server_info.file_handle = open(newfilename,flags,0766); if(server_info.file_handle < 0){ printf("open file failed\n"); goto err0; } if(offset >0){ lseek(server_info.file_handle,offset, SEEK_SET); read_size += offset; } for(;;){ memset(rec_buf,0,sizeof(rec_buf)); ret = recv(server_info.data_sock,rec_buf,sizeof(rec_buf),0); if(ret < 0) { printf("Read error\n"); goto err1; } else if(ret == 0) { ret = read_size; goto err1; } else if(ret >0) { read_size += ret; ret = write(server_info.file_handle,rec_buf,ret); if(ret < 0) { printf("Write error\n"); goto err1; } //printf("read_buf =%s\n",rec_buf); } } err1: if(server_info.file_handle >0) close(server_info.file_handle); err0: if(server_info.client_server_sock > 0) close(server_info.client_server_sock); if(server_info.data_sock > 0) close(server_info.data_sock); memset(rec_buf,0,sizeof(rec_buf)); /* 客戶端接收伺服器的響應碼和資訊,正常為 ”226 Transfer complete.” */ read(ctrl_sock,rec_buf,sizeof(rec_buf)); /*有時訊息再傳送完命令后里面就能收到,導致會阻塞在這個上面*/ printf("%s \n Download file end!!\n",rec_buf); return ret; } /* * @brief 上傳檔案從ftp伺服器 * @param ctrl_sock 控制伺服器sock connect_mode = 0 設定伺服器被動模式,非0 伺服器主動模式 server_filepath_name 上傳到伺服器上的檔案路徑和名稱 srcfilename 本地要上傳的路徑和檔名字 offset 設定下載檔案偏移的位置,不偏移寫0 ,可用於錯誤時續傳。 * @return -1/成功返回上傳完成的位元組數 */ int up_file_ftpserver(int ctrl_sock, char *server_filepath_name, const char *srcfilename,int connect_mode,int offset) { int ret =-1,file_size=0; int file_handle =0; char rec_buf[2048] ={0}; int read_size =0; char stri[128]={0}; char *tmp = NULL; char * pos =NULL; close_st_info(&server_info); if((server_filepath_name == NULL) ||(srcfilename == NULL)){ printf("argc is NULL\n"); return -1; } /*提取出server_path file name*/ tmp = server_filepath_name; if(tmp != NULL){ while(tmp != NULL){ tmp = strstr(tmp,"/"); // printf("tmp =%x :%s\n",tmp,tmp); if(tmp != NULL){ pos = tmp; tmp++; /*越過找到的"/"*/ } } if(pos !=NULL){ strncpy(server_info.server_path,server_filepath_name,pos-server_filepath_name+1); strcpy(server_info.server_filename,pos+1); } else{ strcpy(server_info.server_filename,server_filepath_name); } } printf("server path = %s ;file name =%s\n",server_info.server_path,server_info.server_filename); if(connect_mode){ /*主動模式*/ server_info.client_server_sock =enter_active_mode(ctrl_sock); if(server_info.client_server_sock <= 0) { printf("get data_scok failed\n"); return -1; } }else{ /*被動模式*/ ret = enter_passive_mode(ctrl_sock,server_info.data_ip, &server_info.data_port); if(ret < 0){ printf("set passive mode failed\n"); return -1; } printf("server_info.data_ip =%s, data_port =%d\n",server_info.data_ip,server_info.data_port); server_info.data_sock = get_data_sock(server_info.data_ip,server_info.data_port); if(server_info.data_sock < 0){ printf("get data sock failed\n"); return -1; } } /*更改目錄*/ if(strlen(server_info.server_path) !=0) { ret = send_cmd(ctrl_sock,CMD_CWD, server_info.server_path,ACK_CWD_NUM); if(ret < 0){ printf("set passive mode failed\n"); goto err0; } } /*設定上傳檔案偏移的位置*/ if(offset >0){ itoa(offset,stri,10); ret = send_cmd(ctrl_sock,CMD_REST,stri,ACK_REST_NUM); if(ret < 0){ printf("set file offsize failed\n"); goto err0; } } /*傳送上傳檔案命令*/ ret = send_cmd(ctrl_sock,CMD_STOR,server_info.server_filename,ACK_STOR_NUM); if(ret < 0){ printf("send STOR failed\n"); goto err0; } if(connect_mode){ server_info.data_sock= get_active_data_sock(server_info.client_server_sock); if(server_info.data_sock <0) { printf("accept failed\n"); goto err0; } } server_info.file_handle = open(srcfilename,O_RDONLY); if(server_info.file_handle < 0){ printf("open file failed\n"); goto err0; } if(offset >0){ lseek(server_info.file_handle,offset, SEEK_SET); read_size += offset; } for(;;){ memset(rec_buf,0,sizeof(rec_buf)); ret = read(server_info.file_handle,rec_buf,sizeof(rec_buf)); if(ret < 0) { printf("read file error\n"); goto err1; } else if(ret == 0) { ret = read_size; goto err1; } else if(ret >0) { //printf("read_buf =%s\n",rec_buf); ret = write(server_info.data_sock,rec_buf,ret); if(ret < 0) { printf("Write failed\n"); goto err1; }else{ read_size += ret; // lseek(file_handle,read_size, SEEK_SET); } } } err1: if(server_info.file_handle >0) close(server_info.file_handle); err0: if(server_info.client_server_sock > 0) close(server_info.client_server_sock); if(server_info.data_sock > 0) close(server_info.data_sock); memset(rec_buf,0,sizeof(rec_buf)); /* 客戶端接收伺服器的響應碼和資訊,正常為 ”226 Transfer complete.” */ read(ctrl_sock,rec_buf,sizeof(rec_buf)); printf("%s\n Up file end!!!\n",rec_buf); return ret; } static int close_st_info(FTP_DATA_INFO * info) { if(info == NULL) return -1; if(info->file_handle >0) close(info->file_handle); if(info->client_server_sock > 0) close(info->file_handle); if(info->data_sock > 0) close(info->file_handle); memset(info,0,sizeof(FTP_DATA_INFO)); return 0; } int get_fsize_ftpserver(int ctrl_sock, char *server_filepath_name) { int ret =-1; if(server_filepath_name ==NULL){ printf("argc is null\n"); return -1; } /*傳送上傳檔案命令*/ ret = send_cmd(ctrl_sock,CMD_SIZE_FTP,server_filepath_name,ACK_SIZE_NUM); if(ret < 0){ printf("send SZIE failed\n"); return -1; } //printf("file size =%d\n",ret); return ret; } /* * @brief 從ftp伺服器退出 * @param ctrl_sock 控制伺服器sock * @return -1/成功返回0 */ int quit_fpt_server(int ctrl_sock) { int ret =-1; close_st_info(&server_info); ret = send_cmd(ctrl_sock,CMD_QUIT,NULL,ACK_QUIT_NUM); if(ret < 0) printf("quit fpt server error\n"); close(ctrl_sock); return ret; }
fpt.h
#ifndef __FTP_H__ #define __FTP_H__ #define FTP_SERVER_PORT 21 typedef enum { CMD_USER =0, /*使用者名稱*/ CMD_PASS, /*密碼*/ CMD_PASV, /*讓伺服器進入被動模式*/ CMD_CWD, /*切換工作目錄*/ CMD_SIZE_FTP, /*獲取檔案大小*/ CMD_RETR, /*下載檔案*/ CMD_REST, /*指定下載檔案的偏移量*/ CMD_QUIT, /*退出命令*/ CMD_LIST, /*列表*/ CMD_STOR, /*上傳檔案*/ CMD_PORT_FTP,/*傳送客戶端埠給伺服器*/ CMD_MLSD, /*列表*/ }eu_cmd_type; int connect_ftp_server(const char* server_ip,const int port); int login_ftp_server(int ctrl_sock,const char *user_name, const char * passwd); int down_file_ftpserver(int ctrl_sock, char *server_filepath_name, const char *newfilename,int connect_mode,int offset,eu_cmd_type typ); int up_file_ftpserver(int ctrl_sock, char *server_filepath_name, const char *srcfilename,int connect_mode,int offset); int get_fsize_ftpserver(int ctrl_sock, char *server_filepath_name); int quit_fpt_server(int ctrl_sock); #endif
遇到的主要問題記錄:
1、實現FTP主動模式的時候,開始的accpet一直無法接收到伺服器的連線請求。
後來用wireshark跟蹤FileZilla與伺服器直接的通訊資料才找到問題所在。原來accept要在
必須要在LIST等下載上傳命令傳送後伺服器才會連線過來。
2、主要一定要關閉掉防火牆
3、ftp還有一些傳送方法型別的選擇,本程式碼中並沒有進行設定,後續用到的時候再進行完善。
4、 程式碼中接收伺服器回覆的時候增加了不少延遲。後來我有在sock 上設定接收超時,延遲太多反正感覺不是太好。
參考資料:
(29條訊息) FTP協議講解_zhubao124的部落格-CSDN部落格_ftp協議