阅读 32

Nginx网络epoll多进程系列:应用层协议实现系列(三)——FTP服务器之设计与实现

blog.csdn.net/zhaoxy_thu/…

在实现了HTTP服务器之后,本人打算再实现一个FTP服务器。由于FTP协议与HTTP一样都位于应用层,所以实现原理也类似。在这里把实现的原理和源码分享给大家。

首先需要明确的是FTP协议中涉及命令端口和数据端口,即每个客户端通过命令端口向服务器发送命令(切换目录、删除文件等),通过数据端口从服务器接收数据(目录列表、下载上传文件等)。这就要求对每个连接都必须同时维护两个端口,如果使用类似于上一篇文章中的多路IO就会复杂很多,因此本文采用了类似Apache的多进程机制,即对每个连接创建一个单独的进程进行管理。

接下来简要说明一下FTP协议的通信流程,Ftp服务器向客户端发送的消息主要由两部分组成,第一部分是状态码(与HTTP类似),第二部分是具体内容(可以为空),两部分之间以空格分隔,如“220 TS FTP Server ready”就告诉了客户端已经连接上了服务器;客户端向服务器发送的命令也由两部分组成,第一部分是命令字符串,第二部分是具体内容(可以为空),两部分之间也以空格分隔,如“USER anonymous”就指定了登录FTP服务器的用户名。以一个登录FTP服务器并获取目录列表的流程为例:

[plain]  view plain  copy

  1. 220 TS FTP Server ready...  
  2. USER anonymous  
  3. 331 Password required for anonymous  
  4. PASS chrome@example.com  
  5. 530 Not logged in,password error.  
  6. QUIT  
  7. 221 Goodbye  
  8. USER zhaoxy  
  9. 331 Password required for zhaoxy  
  10. PASS 123  
  11. 230 User zhaoxy logged in  
  12. SYST  
  13. 215 UNIX Type: L8  
  14. PWD  
  15. 257 "/" is current directory.  
  16. TYPE I  
  17. 200 Type set to I  
  18. PASV  
  19. 227 Entering Passive Mode (127,0,0,1,212,54)  
  20. SIZE /  
  21. 550 File not found  
  22. PASV  
  23. 227 Entering Passive Mode (127,0,0,1,212,56)  
  24. CWD /  
  25. 250 CWD command successful. "/" is current directory.  
  26. LIST -l  
  27. 150 Opening data channel for directory list.  
  28. 16877 8 501 20        272 4 8 114 .  
  29. 16877 29 501 20        986 4 8 114 ..  
  30. 33188 1 501 20       6148 3 28 114 .DS_Store  
  31. 16877 4 501 20        136 2 27 114 css  
  32. 33279 1 501 20  129639543 6 14 113 haha.pdf  
  33. 16877 11 501 20        374 2 27 114 images  
  34. 33261 1 501 20      11930 3 9 114 index.html  
  35. 16877 6 501 20        204 2 28 114 js  
  36. 226 Transfer ok.  
  37. QUIT  
  38. 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函数中的代码:

[cpp]  view plain  copy

  1. #include <iostream>  
  2. #include "define.h"  
  3. #include "CFtpHandler.h"  
  4. #include <sys/types.h>  
  5. #include <sys/socket.h>  
  6.   
  7. int main(int argc, const char * argv[])  
  8. {  
  9.     int port = 2100;  
  10.     int listenFd = startup(port);  
  11.     //ignore SIGCHLD signal, which created by child process when exit, to avoid zombie process  
  12.     signal(SIGCHLD,SIG_IGN);  
  13.     while (1) {  
  14.         int newFd = accept(listenFd, (struct sockaddr *)NULL, NULL);  
  15.         if (newFd == -1) {  
  16.             //when child process exit, it'll generate a signal which will cause the parent process accept failed.  
  17.             //If happens, continue.  
  18.             if (errno == EINTR) continue;  
  19.             printf("accept error: %s(errno: %d)\n",strerror(errno),errno);  
  20.         }  
  21.         //timeout of recv  
  22.         struct timeval timeout = {3,0};  
  23.         setsockopt(newFd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout,sizeof(struct timeval));  
  24.         int pid = fork();  
  25.         //fork error  
  26.         if (pid < 0) {  
  27.             printf("fork error: %s(errno: %d)\n",strerror(errno),errno);  
  28.         }  
  29.         //child process  
  30.         else if (pid == 0) {  
  31.             //close useless socket  
  32.             close(listenFd);  
  33.             send(newFd, TS_FTP_STATUS_READY, strlen(TS_FTP_STATUS_READY), 0);  
  34.             CFtpHandler handler(newFd);  
  35.             int freeTime = 0;  
  36.             while (1) {  
  37.                 char buff[256];  
  38.                 int len = (int)recv(newFd, buff, sizeof(buff), 0);  
  39.                 //connection interruption  
  40.                 if (len == 0) break;  
  41.                 //recv timeout return -1  
  42.                 if (len < 0) {  
  43.                     freeTime += 3;  
  44.                     //max waiting time exceed  
  45.                     if (freeTime >= 30) {  
  46.                         break;  
  47.                     }else {  
  48.                         continue;  
  49.                     }  
  50.                 }  
  51.                 buff[len] = '\0';  
  52.                 //reset free time  
  53.                 freeTime = 0;  
  54.                 if (handler.handleRequest(buff)) {  
  55.                     break;  
  56.                 }  
  57.             }  
  58.             close(newFd);  
  59.             std::cout<<"exit"<<std::endl;  
  60.             exit(0);  
  61.         }  
  62.         //parent process  
  63.         else {  
  64.             //close useless socket  
  65.             close(newFd);  
  66.         }  
  67.     }  
  68.     close(listenFd);  
  69.     return 0;  
  70. }  

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

最后通过CFtpHandler类中的handleRequest方法处理客户端的命令,部分代码如下:

[cpp]  view plain  copy

  1. //handle client request  
  2. bool CFtpHandler::handleRequest(char *buff) {  
  3.     stringstream recvStream;  
  4.     recvStream<<buff;  
  5.       
  6.     cout<<buff;  
  7.     string command;  
  8.     recvStream>>command;  
  9.       
  10.     bool isClose = false;  
  11.     string msg;  
  12.     //username  
  13.     if (command == COMMAND_USER) {  
  14.         recvStream>>username;  
  15.         msg = TS_FTP_STATUS_PWD_REQ(username);  
  16.     }  
  17.     //password  
  18.     else if (command == COMMAND_PASS) {  
  19.         recvStream>>password;  
  20.         if (username == "zhaoxy" && password == "123") {  
  21.             msg = TS_FTP_STATUS_LOG_IN(username);  
  22.         }else {  
  23.             msg = TS_FTP_STATUS_PWD_ERROR;  
  24.         }  
  25.     }  
  26.     //quit  
  27.     else if (command == COMMAND_QUIT) {  
  28.         msg = TS_FTP_STATUS_BYE;  
  29.         isClose = true;  
  30.     }  
  31.     //system type  
  32.     else if (command == COMMAND_SYST) {  
  33.         msg = TS_FTP_STATUS_SYSTEM_TYPE;  
  34.     }  
  35.     //current directory  
  36.     else if (command == COMMAND_PWD) {  
  37.         msg = TS_FTP_STATUS_CUR_DIR(currentPath);  
  38.     }  
  39.     //transmit type  
  40.     else if (command == COMMAND_TYPE) {  
  41.         recvStream>>type;  
  42.         msg = TS_FTP_STATUS_TRAN_TYPE(type);  
  43.     }  
  44.     //passive mode  
  45.     else if (command == COMMAND_PASSIVE) {  
  46.         int port = 0;  
  47.         if (m_dataFd) {  
  48.             close(m_dataFd);  
  49.         }  
  50.         m_dataFd = startup(port);  
  51.           
  52.         stringstream stream;  
  53.         stream<<TS_FTP_STATUS_PASV<<port/256<<","<<port%256<<")";  
  54.         msg = stream.str();  
  55.           
  56.         //active passive mode  
  57.         m_isPassive = true;  
  58.     }  
  59.     //active mode  
  60.     else if (command == COMMAND_PORT) {  
  61.         string ipStr;  
  62.         recvStream>>ipStr;  
  63.           
  64.         char ipC[32];  
  65.         strcpy(ipC, ipStr.c_str());  
  66.         char *ext = strtok(ipC, ",");  
  67.         m_clientPort = 0; m_clientIp = 0;  
  68.         m_clientIp = atoi(ext);  
  69.         int count = 0;  
  70.         //convert string to ip address and port number  
  71.         //be careful, the ip should be network endianness  
  72.         while (1) {  
  73.             if ((ext = strtok(NULL, ","))==NULL) {  
  74.                 break;  
  75.             }  
  76.             switch (++count) {  
  77.                 case 1:  
  78.                 case 2:  
  79.                 case 3:  
  80.                     m_clientIp |= atoi(ext)<<(count*8);  
  81.                     break;  
  82.                 case 4:  
  83.                     m_clientPort += atoi(ext)*256;  
  84.                     break;  
  85.                 case 5:  
  86.                     m_clientPort += atoi(ext);  
  87.                     break;  
  88.                 default:  
  89.                     break;  
  90.             }  
  91.         }  
  92.         msg = TS_FTP_STATUS_PORT_SUCCESS;  
  93.     }  
  94.     //file size  
  95.     else if (command == COMMAND_SIZE) {  
  96.         recvStream>>fileName;  
  97.         string filePath = ROOT_PATH+currentPath+fileName;  
  98.         long fileSize = filesize(filePath.c_str());  
  99.         if (fileSize) {  
  100.             stringstream stream;  
  101.             stream<<TS_FTP_STATUS_FILE_SIZE<<fileSize;  
  102.             msg = stream.str();  
  103.         }else {  
  104.             msg = TS_FTP_STATUS_FILE_NOT_FOUND;  
  105.         }  
  106.     }  
  107.     //change directory  
  108.     else if (command == COMMAND_CWD) {  
  109.         string tmpPath;  
  110.         recvStream>>tmpPath;  
  111.         string dirPath = ROOT_PATH+tmpPath;  
  112.         if (isDirectory(dirPath.c_str())) {  
  113.             currentPath = tmpPath;  
  114.             msg = TS_FTP_STATUS_CWD_SUCCESS(currentPath);  
  115.         }else {  
  116.             msg = TS_FTP_STATUS_CWD_FAILED(currentPath);  
  117.         }  
  118.     }  
  119.     //show file list  
  120.     else if (command == COMMAND_LIST || command == COMMAND_MLSD) {  
  121.         string param;  
  122.         recvStream>>param;  
  123.           
  124.         msg = TS_FTP_STATUS_OPEN_DATA_CHANNEL;  
  125.         sendResponse(m_connFd, msg);  
  126.         int newFd = getDataSocket();  
  127.         //get files in directory  
  128.         string dirPath = ROOT_PATH+currentPath;  
  129.         DIR *dir = opendir(dirPath.c_str());  
  130.         struct dirent *ent;  
  131.         struct stat s;  
  132.         stringstream stream;  
  133.         while ((ent = readdir(dir))!=NULL) {  
  134.             string filePath = dirPath + ent->d_name;  
  135.             stat(filePath.c_str(), &s);  
  136.             struct tm tm = *gmtime(&s.st_mtime);  
  137.             //list with -l param  
  138.             if (param == "-l") {  
  139.                 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;  
  140.             }else {  
  141.                 stream<<ent->d_name<<endl;  
  142.             }  
  143.         }  
  144.         closedir(dir);  
  145.         //send file info  
  146.         string fileInfo = stream.str();  
  147.         cout<<fileInfo;  
  148.         send(newFd, fileInfo.c_str(), fileInfo.size(), 0);  
  149.         //close client  
  150.         close(newFd);  
  151.         //send transfer ok  
  152.         msg = TS_FTP_STATUS_TRANSFER_OK;  
  153.     }  
  154.     //send file  
  155.     else if (command == COMMAND_RETRIEVE) {  
  156.         recvStream>>fileName;  
  157.         msg = TS_FTP_STATUS_TRANSFER_START(fileName);  
  158.         sendResponse(m_connFd, msg);  
  159.         int newFd = getDataSocket();  
  160.         //send file  
  161.         std::ifstream file(ROOT_PATH+currentPath+fileName);  
  162.         file.seekg(0, std::ifstream::beg);  
  163.         while(file.tellg() != -1)  
  164.         {  
  165.             char *p = new char[1024];  
  166.             bzero(p, 1024);  
  167.             file.read(p, 1024);  
  168.             int n = (int)send(newFd, p, 1024, 0);  
  169.             if (n < 0) {  
  170.                 cout<<"ERROR writing to socket"<<endl;  
  171.                 break;  
  172.             }  
  173.             delete p;  
  174.         }  
  175.         file.close();  
  176.         //close client  
  177.         close(newFd);  
  178.         //send transfer ok  
  179.         msg = TS_FTP_STATUS_FILE_SENT;  
  180.     }  
  181.     //receive file  
  182.     else if (command == COMMAND_STORE) {  
  183.         recvStream>>fileName;  
  184.         msg = TS_FTP_STATUS_UPLOAD_START;  
  185.         sendResponse(m_connFd, msg);  
  186.         int newFd = getDataSocket();  
  187.         //receive file  
  188.         ofstream file;  
  189.         file.open(ROOT_PATH+currentPath+fileName, ios::out | ios::binary);  
  190.         char buff[1024];  
  191.         while (1) {  
  192.             int n = (int)recv(newFd, buff, sizeof(buff), 0);  
  193.             if (n<=0) break;  
  194.             file.write(buff, n);  
  195.         }  
  196.         file.close();  
  197.         //close client  
  198.         close(newFd);  
  199.         //send transfer ok  
  200.         msg = TS_FTP_STATUS_FILE_RECEIVE;  
  201.     }  
  202.     //get support command  
  203.     else if (command == COMMAND_FEAT) {  
  204.         stringstream stream;  
  205.         stream<<"211-Extension supported"<<endl;  
  206.         stream<<COMMAND_SIZE<<endl;  
  207.         stream<<"211 End"<<endl;;  
  208.         msg = stream.str();  
  209.     }  
  210.     //get parent directory  
  211.     else if (command == COMMAND_CDUP) {  
  212.         if (currentPath != "/") {  
  213.             char path[256];  
  214.             strcpy(path, currentPath.c_str());  
  215.             char *ext = strtok(path, "/");  
  216.             char *lastExt = ext;  
  217.             while (ext!=NULL) {  
  218.                 ext = strtok(NULL, "/");  
  219.                 if (ext) lastExt = ext;  
  220.             }  
  221.             currentPath = currentPath.substr(0, currentPath.length()-strlen(lastExt)-1);  
  222.         }  
  223.         msg = TS_FTP_STATUS_CDUP(currentPath);  
  224.     }  
  225.     //delete file  
  226.     else if (command == COMMAND_DELETE) {  
  227.         recvStream>>fileName;  
  228.         //delete file  
  229.         if (remove((ROOT_PATH+currentPath+fileName).c_str()) == 0) {  
  230.             msg = TS_FTP_STATUS_DELETE;  
  231.         }else {  
  232.             printf("delete error: %s(errno: %d)\n",strerror(errno),errno);  
  233.             msg = TS_FTP_STATUS_DELETE_FAILED;  
  234.         }  
  235.     }  
  236.     //other  
  237.     else if (command == COMMAND_NOOP || command == COMMAND_OPTS){  
  238.         msg = TS_FTP_STATUS_OK;  
  239.     }  
  240.       
  241.     sendResponse(m_connFd, msg);  
  242.     return isClose;  
  243. }  

以上代码针对每种命令进行了不同的处理,在这里不详细说明。需要注意的是,文中采用的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中,感兴趣的朋友可以前往下载

\

如果大家觉得对自己有帮助的话,还希望能帮顶一下,谢谢:)

个人博客: blog.csdn.net/zhaoxy2850

本文地址: blog.csdn.net/zhaoxy_thu/…

转载请注明出处,谢谢!

\

文章分类
代码人生
文章标签