天天看點

應用層協定實作系列(三)——FTP伺服器之設計與實作

在實作了HTTP伺服器之後,本人打算再實作一個FTP伺服器。由于FTP協定與HTTP一樣都位于應用層,是以實作原理也類似。在這裡把實作的原理和源碼分享給大家。

首先需要明确的是FTP協定中涉及指令端口和資料端口,即每個用戶端通過指令端口向伺服器發送指令(切換目錄、删除檔案等),通過資料端口從伺服器接收資料(目錄清單、下載下傳上傳檔案等)。這就要求對每個連接配接都必須同時維護兩個端口,如果使用類似于上一篇文章中的多路IO就會複雜很多,是以本文采用了類似Apache的多程序機制,即對每個連接配接建立一個單獨的程序進行管理。

接下來簡要說明一下FTP協定的通信流程,Ftp伺服器向用戶端發送的消息主要由兩部分組成,第一部分是狀态碼(與HTTP類似),第二部分是具體内容(可以為空),兩部分之間以空格分隔,如“220 TS FTP Server ready”就告訴了用戶端已經連接配接上了伺服器;用戶端向伺服器發送的指令也由兩部分組成,第一部分是指令字元串,第二部分是具體内容(可以為空),兩部分之間也以空格分隔,如“USER anonymous”就指定了登入FTP伺服器的使用者名。以一個登入FTP伺服器并擷取目錄清單的流程為例:

220 TS FTP Server ready...
USER anonymous
331 Password required for anonymous
PASS [email protected]
530 Not logged in,password error.
QUIT
221 Goodbye
USER zhaoxy
331 Password required for zhaoxy
PASS 123
230 User zhaoxy logged in
SYST
215 UNIX Type: L8
PWD
257 "/" is current directory.
TYPE I
200 Type set to I
PASV
227 Entering Passive Mode (127,0,0,1,212,54)
SIZE /
550 File not found
PASV
227 Entering Passive Mode (127,0,0,1,212,56)
CWD /
250 CWD command successful. "/" is current directory.
LIST -l
150 Opening data channel for directory list.
16877 8 501 20        272 4 8 114 .
16877 29 501 20        986 4 8 114 ..
33188 1 501 20       6148 3 28 114 .DS_Store
16877 4 501 20        136 2 27 114 css
33279 1 501 20  129639543 6 14 113 haha.pdf
16877 11 501 20        374 2 27 114 images
33261 1 501 20      11930 3 9 114 index.html
16877 6 501 20        204 2 28 114 js
226 Transfer ok.
QUIT
221 Goodbye
           

在一個用戶端連接配接到伺服器後,首先伺服器要向用戶端發送歡迎資訊220,用戶端依此向伺服器發送使用者名和密碼,伺服器校驗之後如果失敗則傳回530,成功則傳回230。一般所有的用戶端第一次連接配接伺服器都會嘗試用匿名使用者進行登入,登入失敗再向使用者詢問使用者名和密碼。接下來,用戶端會與伺服器确認檔案系統的類型,查詢目前目錄以及設定傳輸的資料格式。

FTP協定中主要有兩種格式,二進制和ASCII碼,兩種格式的主要差別在于換行,二進制格式不會對資料進行任何處理,而ASCII碼格式會将回車換行轉換為本機的回車字元,比如Unix下是\n,Windows下是\r\n,Mac下是\r。一般圖檔和執行檔案必須用二進制格式,CGI腳本和普通HTML檔案必須用ASCII碼格式。

在确定了傳輸格式之後,用戶端會設定傳輸模式,Passive被動模式或Active主動模式。在被動模式下,伺服器會再建立一個套接字綁定到一個空閑端口上并開始監聽,同時将本機ip和端口号(h1,h2,h3,h4,p1,p2,其中p1*256+p2等于端口号)發送到用戶端。當之後需要傳輸資料的時候,伺服器會通過150狀态碼通知用戶端,用戶端收到之後會連接配接到之前指定的端口并等待資料。傳輸完成之後,伺服器會發送226狀态碼告訴用戶端傳輸成功。如果用戶端不需要保持長連接配接的話,此時可以向伺服器發送QUIT指令斷開連接配接。在主動模式下,流程與被動模式類似,隻是套接字由用戶端建立并監聽,伺服器連接配接到用戶端的端口上進行資料傳輸。

以下是main函數中的代碼:

#include <iostream>
#include "define.h"
#include "CFtpHandler.h"
#include <sys/types.h>
#include <sys/socket.h>

int main(int argc, const char * argv[])
{
    int port = 2100;
    int listenFd = startup(port);
    //ignore SIGCHLD signal, which created by child process when exit, to avoid zombie process
    signal(SIGCHLD,SIG_IGN);
    while (1) {
        int newFd = accept(listenFd, (struct sockaddr *)NULL, NULL);
        if (newFd == -1) {
            //when child process exit, it'll generate a signal which will cause the parent process accept failed.
            //If happens, continue.
            if (errno == EINTR) continue;
            printf("accept error: %s(errno: %d)\n",strerror(errno),errno);
        }
        //timeout of recv
        struct timeval timeout = {3,0};
        setsockopt(newFd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout,sizeof(struct timeval));
        int pid = fork();
        //fork error
        if (pid < 0) {
            printf("fork error: %s(errno: %d)\n",strerror(errno),errno);
        }
        //child process
        else if (pid == 0) {
            //close useless socket
            close(listenFd);
            send(newFd, TS_FTP_STATUS_READY, strlen(TS_FTP_STATUS_READY), 0);
            CFtpHandler handler(newFd);
            int freeTime = 0;
            while (1) {
                char buff[256];
                int len = (int)recv(newFd, buff, sizeof(buff), 0);
                //connection interruption
                if (len == 0) break;
                //recv timeout return -1
                if (len < 0) {
                    freeTime += 3;
                    //max waiting time exceed
                    if (freeTime >= 30) {
                        break;
                    }else {
                        continue;
                    }
                }
                buff[len] = '\0';
                //reset free time
                freeTime = 0;
                if (handler.handleRequest(buff)) {
                    break;
                }
            }
            close(newFd);
            std::cout<<"exit"<<std::endl;
            exit(0);
        }
        //parent process
        else {
            //close useless socket
            close(newFd);
        }
    }
    close(listenFd);
    return 0;
}
           

代碼中先建立了套接字并綁定到指定端口上,然後進入循環開始監聽端口。每監聽到一個新的連接配接就fork出一個子程序。子程序向用戶端發送歡迎資訊後進入循環處理用戶端發送過來的指令,直到收到QUIT指令或者連接配接逾時退出循環。以上代碼中需要注意三個地方,一是子程序在退出之後會向父程序發送SIGCHLD信号,如果父程序不進行處理(調用wait或忽略)就會導緻子程序變為僵屍程序,本文中采用的是忽略的方式;二是accept函數在父程序收到信号時會直接傳回,是以需要判斷如果傳回是由于信号則繼續循環,不fork,否則會無限建立子程序;三是在fork之後需要将不使用的套接字關閉,比如父程序需要關閉新的連接配接套接字,而子程序需要關閉監聽套接字,避免套接字無法完全關閉。

最後通過CFtpHandler類中的handleRequest方法處理用戶端的指令,部分代碼如下:

//handle client request
bool CFtpHandler::handleRequest(char *buff) {
    stringstream recvStream;
    recvStream<<buff;
    
    cout<<buff;
    string command;
    recvStream>>command;
    
    bool isClose = false;
    string msg;
    //username
    if (command == COMMAND_USER) {
        recvStream>>username;
        msg = TS_FTP_STATUS_PWD_REQ(username);
    }
    //password
    else if (command == COMMAND_PASS) {
        recvStream>>password;
        if (username == "zhaoxy" && password == "123") {
            msg = TS_FTP_STATUS_LOG_IN(username);
        }else {
            msg = TS_FTP_STATUS_PWD_ERROR;
        }
    }
    //quit
    else if (command == COMMAND_QUIT) {
        msg = TS_FTP_STATUS_BYE;
        isClose = true;
    }
    //system type
    else if (command == COMMAND_SYST) {
        msg = TS_FTP_STATUS_SYSTEM_TYPE;
    }
    //current directory
    else if (command == COMMAND_PWD) {
        msg = TS_FTP_STATUS_CUR_DIR(currentPath);
    }
    //transmit type
    else if (command == COMMAND_TYPE) {
        recvStream>>type;
        msg = TS_FTP_STATUS_TRAN_TYPE(type);
    }
    //passive mode
    else if (command == COMMAND_PASSIVE) {
        int port = 0;
        if (m_dataFd) {
            close(m_dataFd);
        }
        m_dataFd = startup(port);
        
        stringstream stream;
        stream<<TS_FTP_STATUS_PASV<<port/256<<","<<port%256<<")";
        msg = stream.str();
        
        //active passive mode
        m_isPassive = true;
    }
    //active mode
    else if (command == COMMAND_PORT) {
        string ipStr;
        recvStream>>ipStr;
        
        char ipC[32];
        strcpy(ipC, ipStr.c_str());
        char *ext = strtok(ipC, ",");
        m_clientPort = 0; m_clientIp = 0;
        m_clientIp = atoi(ext);
        int count = 0;
        //convert string to ip address and port number
        //be careful, the ip should be network endianness
        while (1) {
            if ((ext = strtok(NULL, ","))==NULL) {
                break;
            }
            switch (++count) {
                case 1:
                case 2:
                case 3:
                    m_clientIp |= atoi(ext)<<(count*8);
                    break;
                case 4:
                    m_clientPort += atoi(ext)*256;
                    break;
                case 5:
                    m_clientPort += atoi(ext);
                    break;
                default:
                    break;
            }
        }
        msg = TS_FTP_STATUS_PORT_SUCCESS;
    }
    //file size
    else if (command == COMMAND_SIZE) {
        recvStream>>fileName;
        string filePath = ROOT_PATH+currentPath+fileName;
        long fileSize = filesize(filePath.c_str());
        if (fileSize) {
            stringstream stream;
            stream<<TS_FTP_STATUS_FILE_SIZE<<fileSize;
            msg = stream.str();
        }else {
            msg = TS_FTP_STATUS_FILE_NOT_FOUND;
        }
    }
    //change directory
    else if (command == COMMAND_CWD) {
        string tmpPath;
        recvStream>>tmpPath;
        string dirPath = ROOT_PATH+tmpPath;
        if (isDirectory(dirPath.c_str())) {
            currentPath = tmpPath;
            msg = TS_FTP_STATUS_CWD_SUCCESS(currentPath);
        }else {
            msg = TS_FTP_STATUS_CWD_FAILED(currentPath);
        }
    }
    //show file list
    else if (command == COMMAND_LIST || command == COMMAND_MLSD) {
        string param;
        recvStream>>param;
        
        msg = TS_FTP_STATUS_OPEN_DATA_CHANNEL;
        sendResponse(m_connFd, msg);
        int newFd = getDataSocket();
        //get files in directory
        string dirPath = ROOT_PATH+currentPath;
        DIR *dir = opendir(dirPath.c_str());
        struct dirent *ent;
        struct stat s;
        stringstream stream;
        while ((ent = readdir(dir))!=NULL) {
            string filePath = dirPath + ent->d_name;
            stat(filePath.c_str(), &s);
            struct tm tm = *gmtime(&s.st_mtime);
            //list with -l param
            if (param == "-l") {
                stream<<s.st_mode<<" "<<s.st_nlink<<" "<<s.st_uid<<" "<<s.st_gid<<" "<<setw(10)<<s.st_size<<" "<<tm.tm_mon<<" "<<tm.tm_mday<<" "<<tm.tm_year<<" "<<ent->d_name<<endl;
            }else {
                stream<<ent->d_name<<endl;
            }
        }
        closedir(dir);
        //send file info
        string fileInfo = stream.str();
        cout<<fileInfo;
        send(newFd, fileInfo.c_str(), fileInfo.size(), 0);
        //close client
        close(newFd);
        //send transfer ok
        msg = TS_FTP_STATUS_TRANSFER_OK;
    }
    //send file
    else if (command == COMMAND_RETRIEVE) {
        recvStream>>fileName;
        msg = TS_FTP_STATUS_TRANSFER_START(fileName);
        sendResponse(m_connFd, msg);
        int newFd = getDataSocket();
        //send file
        std::ifstream file(ROOT_PATH+currentPath+fileName);
        file.seekg(0, std::ifstream::beg);
        while(file.tellg() != -1)
        {
            char *p = new char[1024];
            bzero(p, 1024);
            file.read(p, 1024);
            int n = (int)send(newFd, p, 1024, 0);
            if (n < 0) {
                cout<<"ERROR writing to socket"<<endl;
                break;
            }
            delete p;
        }
        file.close();
        //close client
        close(newFd);
        //send transfer ok
        msg = TS_FTP_STATUS_FILE_SENT;
    }
    //receive file
    else if (command == COMMAND_STORE) {
        recvStream>>fileName;
        msg = TS_FTP_STATUS_UPLOAD_START;
        sendResponse(m_connFd, msg);
        int newFd = getDataSocket();
        //receive file
        ofstream file;
        file.open(ROOT_PATH+currentPath+fileName, ios::out | ios::binary);
        char buff[1024];
        while (1) {
            int n = (int)recv(newFd, buff, sizeof(buff), 0);
            if (n<=0) break;
            file.write(buff, n);
        }
        file.close();
        //close client
        close(newFd);
        //send transfer ok
        msg = TS_FTP_STATUS_FILE_RECEIVE;
    }
    //get support command
    else if (command == COMMAND_FEAT) {
        stringstream stream;
        stream<<"211-Extension supported"<<endl;
        stream<<COMMAND_SIZE<<endl;
        stream<<"211 End"<<endl;;
        msg = stream.str();
    }
    //get parent directory
    else if (command == COMMAND_CDUP) {
        if (currentPath != "/") {
            char path[256];
            strcpy(path, currentPath.c_str());
            char *ext = strtok(path, "/");
            char *lastExt = ext;
            while (ext!=NULL) {
                ext = strtok(NULL, "/");
                if (ext) lastExt = ext;
            }
            currentPath = currentPath.substr(0, currentPath.length()-strlen(lastExt)-1);
        }
        msg = TS_FTP_STATUS_CDUP(currentPath);
    }
    //delete file
    else if (command == COMMAND_DELETE) {
        recvStream>>fileName;
        //delete file
        if (remove((ROOT_PATH+currentPath+fileName).c_str()) == 0) {
            msg = TS_FTP_STATUS_DELETE;
        }else {
            printf("delete error: %s(errno: %d)\n",strerror(errno),errno);
            msg = TS_FTP_STATUS_DELETE_FAILED;
        }
    }
    //other
    else if (command == COMMAND_NOOP || command == COMMAND_OPTS){
        msg = TS_FTP_STATUS_OK;
    }
    
    sendResponse(m_connFd, msg);
    return isClose;
}
           

以上代碼針對每種指令進行了不同的處理,在這裡不詳細說明。需要注意的是,文中采用的if-else方法判斷指令效率是很低的,時間複雜度為O(n)(n為指令總數),有兩種方法可以進行優化,一是由于FTP指令都是4個字母組成的,可以将4個字母的ascii碼拼接成一個整數,使用switch進行判斷,時間複雜度為O(1);二是類似Http伺服器中的方法,将每個指令以及相應的處理函數存到hashmap中,收到一個指令時可以通過hash直接調用相應的函數,時間複雜度同樣為O(1)。

另外,以上代碼中的PORT指令處理時涉及對ip位址的解析,需要注意本機位元組順序和網絡位元組順序的差別,如127.0.0.1轉換成整數應逆序轉換,以網絡位元組順序存到s_addr變量中。

以上源碼已經上傳到GitHub中,感興趣的朋友可以前往下載下傳。

如果大家覺得對自己有幫助的話,還希望能幫頂一下,謝謝:) 個人部落格: http://blog.csdn.net/zhaoxy2850 本文位址: http://blog.csdn.net/zhaoxy_thu/article/details/25369437 轉載請注明出處,謝謝!

繼續閱讀