在实现了HTTP服务器之后,本人打算再实现一个FTP服务器。由于FTP协议与HTTP一样都位于应用层,所以实现原理也类似。在这里把实现的原理和源码分享给大家。
首先需要明确的是FTP协议中涉及命令端口和数据端口,即每个客户端通过命令端口向服务器发送命令(切换目录、删除文件等),通过数据端口从服务器接收数据(目录列表、下载上传文件等)。这就要求对每个连接都必须同时维护两个端口,如果使用类似于上一篇文章中的多路IO就会复杂很多,因此本文采用了类似Apache的多进程机制,即对每个连接创建一个单独的进程进行管理。
接下来简要说明一下FTP协议的通信流程,Ftp服务器向客户端发送的消息主要由两部分组成,第一部分是状态码(与HTTP类似),第二部分是具体内容(可以为空),两部分之间以空格分隔,如“220 TS FTP Server ready”就告诉了客户端已经连接上了服务器;客户端向服务器发送的命令也由两部分组成,第一部分是命令字符串,第二部分是具体内容(可以为空),两部分之间也以空格分隔,如“USER anonymous”就指定了登录FTP服务器的用户名。以一个登录FTP服务器并获取目录列表的流程为例:
[plain] view plain copy
- 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
在一个客户端连接到服务器后,首先服务器要向客户端发送欢迎信息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
- #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方法处理客户端的命令,部分代码如下:
[cpp] view plain copy
- //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中,感兴趣的朋友可以前往下载。
\
如果大家觉得对自己有帮助的话,还希望能帮顶一下,谢谢:)
个人博客: blog.csdn.net/zhaoxy2850
本文地址: blog.csdn.net/zhaoxy_thu/…
转载请注明出处,谢谢!
\