zoukankan      html  css  js  c++  java
  • 应用层协议实现系列(三)——FTPserver之设计与实现

    在实现了HTTPserver之后。本人打算再实现一个FTPserver。

    因为FTP协议与HTTP一样都位于应用层,所以实现原理也类似。

    在这里把实现的原理和源代码分享给大家。

    首先须要明白的是FTP协议中涉及命令port和数据port,即每一个client通过命令port向server发送命令(切换文件夹、删除文件等),通过数据port从server接收数据(文件夹列表、下载上传文件等)。这就要求对每一个连接都必须同一时候维护两个port,假设使用类似于上一篇文章中的多路IO就会复杂非常多,因此本文採用了类似Apache的多进程机制,即对每一个连接创建一个单独的进程进行管理。

    接下来简要说明一下FTP协议的通信流程。Ftpserver向client发送的消息主要由两部分组成。第一部分是状态码(与HTTP类似),第二部分是详细内容(能够为空)。两部分之间以空格分隔,如“220 TS FTP Server ready”就告诉了client已经连接上了server;client向server发送的命令也由两部分组成。第一部分是命令字符串。第二部分是详细内容(能够为空),两部分之间也以空格分隔。如“USER anonymous”就指定了登录FTPserver的username。以一个登录FTPserver并获取文件夹列表的流程为例:

    220 TS FTP Server ready...
    USER anonymous
    331 Password required for anonymous
    PASS chrome@example.com
    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
    在一个client连接到server后,首先server要向client发送欢迎信息220,client依此向server发送username和password,server校验之后假设失败则返回530,成功则返回230。一般全部的client第一次连接server都会尝试用匿名用户进行登录。登录失败再向用户询问username和password。接下来,client会与server确认文件系统的类型,查询当前文件夹以及设定传输的数据格式。

    FTP协议中主要有两种格式,二进制和ASCII码,两种格式的主要差别在于换行。二进制格式不会对数据进行不论什么处理,而ASCII码格式会将回车换行转换为本机的回车字符。比方Unix下是 ,Windows下是 ,Mac下是 。一般图片和运行文件必须用二进制格式。CGI脚本和普通HTML文件必须用ASCII码格式。

    在确定了传输格式之后,client会设定传输模式,Passive被动模式或Active主动模式。在被动模式下,server会再创建一个套接字绑定到一个空暇port上并開始监听,同一时候将本机ip和port号(h1,h2,h3,h4,p1,p2,当中p1*256+p2等于port号)发送到client。

    当之后须要数据传输的时候。server会通过150状态码通知client。client收到之后会连接到之前指定的port并等待数据。

    传输完毕之后,server会发送226状态码告诉client传输成功。

    假设client不须要保持长连接的话,此时能够向server发送QUIT命令断开连接。在主动模式下。流程与被动模式类似。仅仅是套接字由client创建并监听,server连接到client的port上进行数据传输。

    下面是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)
    ",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)
    ",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] = '';
                    //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;
    }

    代码中先创建了套接字并绑定到指定port上,然后进入循环開始监听port。

    每监听到一个新的连接就fork出一个子进程。子进程向client发送欢迎信息后进入循环处理client发送过来的命令,直到收到QUIT命令或者连接超时退出循环。以上代码中须要注意三个地方。一是子进程在退出之后会向父进程发送SIGCHLD信号。假设父进程不进行处理(调用wait或忽略)就会导致子进程变为僵尸进程,本文中採用的是忽略的方式;二是accept函数在父进程收到信号时会直接返回。因此须要推断假设返回是因为信号则继续循环,不fork,否则会无限创建子进程;三是在fork之后须要将不使用的套接字关闭,比方父进程须要关闭新的连接套接字,而子进程须要关闭监听套接字。避免套接字无法全然关闭。

    最后通过CFtpHandler类中的handleRequest方法处理client的命令,部分代码例如以下:

    //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)
    ",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);二是类似Httpserver中的方法,将每一个命令以及对应的处理函数存到hashmap中,收到一个命令时能够通过hash直接调用对应的函数,时间复杂度相同为O(1)。

    另外,以上代码中的PORT命令处理时涉及对ip地址的解析。须要注意本机字节顺序和网络字节顺序的差别。如127.0.0.1转换成整数应逆序转换,以网络字节顺序存到s_addr变量中。

    以上源代码已经上传到GitHub中,感兴趣的朋友能够前往下载


    假设大家认为对自己有帮助的话,还希望能帮顶一下,谢谢:)
    转载请注明出处。谢谢!

  • 相关阅读:
    linux定时任务crontab介绍
    kafka跨集群同步方案
    hadoop配置参数速查大全
    kafka生产者消费者示例代码
    storysnail的Linux串口编程笔记
    storysnail的Windows串口编程笔记
    botbrew下写glib2程序
    codeblocks配置GLUT
    使用Code::blocks在windows下写网络程序
    使用pango-Cairo列出系统中的有效字体
  • 原文地址:https://www.cnblogs.com/gcczhongduan/p/5097030.html
Copyright © 2011-2022 走看看