服务器的设计与实现(三)——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

转载请注明出处,谢谢!

服务器的设计与实现(三)——FTP服务器之设计与实现

时间: 2024-11-03 22:11:22

服务器的设计与实现(三)——FTP服务器之设计与实现的相关文章

ftp服务器之vsftp

一.简介 文件传输协议(FTP,File Transfer Protocol),即能够让用户在互联网中上传.下载文件的文件协议,而FTP服务器就是支持FTP传输协议的主机,要想完成文件传输则需要FTP服务端和FTP客户端的配合才行. 通常用户使用FTP客户端软件向FTP服务器发起连接并发送FTP指令,服务器收到用户指令后将执行结果返回客户端. FTP协议占用两个端口号: 21端口:命令连接,用于接收客户端执行的FTP命令. 20端口:数据连接,用于上传.下载文件数据. FTP数据传输的类型: 主

FTP服务器之下载

1.SERVER端 __author__ = 'alex' #coding:utf-8 import socket import os import sys import json def process_bar(num,total): percent = float(num)/float(total) per_int = int(percent*100) # print (percent,per_int) # print (">"*per_int + "%d%%&qu

计算机网络系列:搭建FTP服务器之第二篇:搭建FTP站点

看本篇博文的时候,默认读者已经度过上一篇博文:已经在服务器上边安装好了IIS服务. 接着上一篇的,我们继续说,既然已经安装好了IIS服务,我们就要开始搭建FTP站点了: 开始-->管理工具-->Internet 信息服务(IIS)管理器,会出现如下所示的界面: 在FTP站点,右键选择"新建-->FTP站点": 然后"下一步" (IP地址为这个虚拟机的IP地址,可以在dos界面通过:ipconfig/all 命令查询. 另外,还需要在虚拟机设置:虚拟

计算机网络系列:搭建FTP服务器之第一篇:安装IIS信息服务

学网络课,那天上机课学到了怎么制作一个FTP服务器,然后就跟着老师做了下,其实很简单,但有一些细节性的稍微麻烦一点,还有一些配置文件需要下载,这里一并都放上来了. 首先,你得准备一个win 2003 server或者win xp的系统,然后用虚拟机把系统跑起来,下边我给两个资源的地址供大家下载(本人用的是win2003server): win 2003 server系统:http://pan.baidu.com/s/1hqvdOLA,(我放在了百度网盘里边) IIS配置工具:http://dow

如何在Windows Server 2008 R2下搭建FTP服务

在Windows Server 2008 R2下搭建FTP服务,供客户端读取和上传文件 百度经验:jingyan.baidu.com 工具/原料 Windows Server 2008 R2 百度经验:jingyan.baidu.com 方法/步骤 1 安装FTP服务 开始-->管理工具-->服务器管理器 步骤阅读 2 安装IIS/FTP角色 打开服务器管理器,找到添加角色,然后点击,弹出添加角色对话框,选择下一步 步骤阅读 3 选择Web服务器(IIS),然后选择FTP服务,直到安装完成.

用三种不同的方法访问Linux系统上的FTP服务过程详解

今天向大家介绍一下,如何在Linux上配置ftp服务,并且使用三种不同的方式访问Linux系统上的FTP服务. 实验环境:一台windows7系统虚拟机,一台Linux系统虚拟机. 实验目的:搭建FTP文件传输服务,并进行匿名访问.虚拟用户访问和本地用户验证. 匿名访问用户名为ftp或anonymous,提供任意密码(包括空密码)都可以通过服务器认证 安装软件包,关闭防火墙和关闭selinux功能. 在ftp服务中创建一个wen.txt文件 测试(window7) 匿名登录ftp服务 从ftp服

[转] Linux学习之CentOS(三十六)--FTP服务原理及vsfptd的安装、配置

本篇随笔将讲解FTP服务的原理以及vsfptd这个最常用的FTP服务程序的安装与配置... 一.FTP服务原理 FTP(File Transfer Protocol)是一个非常古老并且应用十分广泛的文件传输协议,FTP协议是现今使用最为广泛的网络文件共享协议之一,我们现在也一直有在用着FTP协议来进行各种文件的传输,FTP为我们提供了一种可靠的方式在网络上进行文件的共享 FTP是C/S架构的服务,拥有一个服务器端和一个客户端,FTP底层通过TCP协议来作为传输协议,所以FTP协议是一种可靠的文件

每日一记:文件服务器之FTP服务器

FTP(File transfer protocol):最主要的功能是在服务器与客户端之间进行档案的传输,使用的是明码传输方式 FTP服务器软件提供的不同等级的用户身份:user.guest.anonymous三种身份的权限和功能有差异FTP可以利用系统的syslogd来进行数据的记录,记录的数据包括用户曾经下达过的命令与用户传输数据的记录FTP可以限制用户仅能在自己的家目录当中活动,登入FTP后,显示的 [根目录]就是自己家目录的内容,这种环境称为change root,简称chrootFTP

使用iptables将内网ftp服务映射到其他内网服务器上

iptables一般的只需要关注两个表,一个是nat表,一个是filter表,其他表暂时用不到,然后nat表里有三个链,filter表里有三个链,总结两个表,五个链 入站数据流向:数据包到达防火墙后首先被PREROUTING链处理(是否修改数据包地址等),然后进行路由选择(判断数据包发往何处),如果数据包的目标地址是防火墙本机(如:Internet用户访问网关的Web服务端口),那么内核将其传递给INPUT链进行处理(决定是否允许通过等). 转发数据流向:来自外界的数据包到达防火墙后首先被PRE