最近坚定了走网络编程的道路,主要想做云计算的网络虚拟化,本科时曾用C语言实现过简单的FTP客户端,现在决定用C++去重新实现一下,主要利用了类函数的思想。这份代码借助了前辈的劳动成果,我做了修改,只是用做学习,并无恶意。
首先,让我们先回顾一下FTP客户端的原理。
FTP 协议
相比其他协议,如 HTTP 协议,FTP 协议要复杂一些。与一般的 C/S 应用不同点在于一般的C/S 应用程序一般只会建立一个 Socket 连接,这个连接同时处理服务器端和客户端的连接命令和数据传输。而FTP协议中将命令与数据分开传送的方法提高了效率。
FTP 使用 2 个端口,一个数据端口和一个命令端口(也叫做控制端口)。这两个端口一般是21 (命令端口)和 20 (数据端口)。控制 Socket 用来传送命令,数据 Socket 是用于传送数据。每一个 FTP 命令发送之后,FTP 服务器都会返回一个字符串,其中包括一个响应代码和一些说明信息。其中的返回码主要是用于判断命令是否被成功执行了。
命令端口
一般来说,客户端有一个 Socket 用来连接 FTP 服务器的相关端口,它负责 FTP 命令的发送和接收返回的响应信息。一些操作如“登录”、“改变目录”、“删除文件”,依靠这个连接发送命令就可完成。
数据端口
对于有数据传输的操作,主要是显示目录列表,上传、下载文件,我们需要依靠另一个 Socket来完成。
如果使用被动模式,通常服务器端会返回一个端口号。客户端需要用另开一个 Socket 来连接这个端口,然后我们可根据操作来发送命令,数据会通过新开的一个端口传输。
如果使用主动模式,通常客户端会发送一个端口号给服务器端,并在这个端口监听。服务器需要连接到客户端开启的这个数据端口,并进行数据的传输。
主动模式 (PORT)
主动模式下,客户端随机打开一个大于 1024 的端口去连接服务器的well-known端口 ,即 21 端口。同时开放N +1 端口监听,并向服务器发出 “port (N+1)”命令,由服务器从它自己的数据端口 (20) 主动连接到客户端指定的数据端口 (N+1)。
FTP 的客户端只是告诉服务器自己的端口号,让服务器来连接客户端指定的端口。对于客户端的防火墙来说,这是从外部到内部的连接,可能会被阻塞。在程序的测试过程中,我在淘宝购买了一个临时FTP 服务器,是一个windows IIS 自带的FTP server,所以代码中并没有实现PORT的功能。
被动模式 (PASV)
为了解决服务器发起到客户的连接问题,有了另一种 FTP 连接方式,即被动方式。命令连接和数据连接都由客户端发起,这样就解决了从服务器到客户端的数据端口的连接被防火墙过滤的问题。
被动模式下,当开启一个 FTP 连接时,客户端打开两个任意的本地端口 (N > 1024 和 N+1) 。第一个端口连接服务器的 21 端口,提交 PASV 命令。然后,服务器会开启一个任意的端口 (P>1024),返回如“227 entering passive mode (127,0,0,1,4,18)”。它返回了 227 开头的信息,在括号中有以逗号隔开的六个数字,前四个指服务器的地址,最后两个,将倒数第二个乘 256 再加上最后一个数字,这就是 FTP 服务器开放的用来进行数据传输的端口。如得到 227 entering passive mode (h1,h2,h3,h4,p1,p2),那么端口号是 p1*256+p2,ip 地址为h1.h2.h3.h4。这意味着在服务器上有一个端口被开放。客户端收到命令取得端口号之后, 会通过 N+1 号端口连接服务器的端口 P,然后在两个端口之间进行数据传输。下面附上一段代码,利用sscanf的正则表达式去读写数据传输的端口号,从而建立数据连接。

FTP_API CFTPManager::createDataLink(int data_fd)
{
    assert(data_fd != INVALID_SOCKET);     //这是一个宏的预处理
    int nPort = 0 ;
    char strServerIp[16];

    std::string parseStr = Pasv();

    if (parseStr.size() <= 0)
    {
        return -1;
    }
   /* regular expression*/
    sscanf(parseStr.c_str(),"%*[^(](%d,%d,%d,%d,%d,%d)",&addr[0],&addr[1],&addr[2],&addr[3],&addr[4],&addr[5]);  

    sprintf(strServerIp, "%d.%d.%d.%d", addr[0],addr[1],addr[2],addr[3]);
    nPort=256*addr[4]+addr[5];

    if (Connect(data_fd, strServerIp, nPort) < 0)
    {
        return -1;
    }

    return 0;
 }

返回码
识别FTP服务器的返回码是FTP client 编程的重要环节,我利用了wireshark的抓包识别了这些返回码,为了方便后面的回顾,我列出了部分的返回码,具体要求请参见请抓包。

响应代码 解释说明
125 打开数据连接,开始传输
150 打开连接
200 成功
220 服务就绪
221 退出网络
225 打开数据连接
226 结束数据连接
227 进入被动模式(IP 地址、ID 端口)
230 登录因特网
250 文件行为完成
257 路径名建立
331 要求密码
332 要求帐号
350 文件行为暂停
421 服务关闭
425 无法打开数据连接

我主要在main函数中是实现返回码的识别,在类函数中返回返回码,下面附main函数中的主要代码。

            if(carry==220){
                carry=ftptest.inputUserName(buffer);
                continue;
            }
            if(carry==331){
                carry=ftptest.inputPassWord(buffer);
                continue;
            }
            if(carry==226||carry==257||carry==230||carry==125){
                if(strncmp(buffer,"pwd",3)==0){
                    printf("Response: %s\n", ftptest.PWD().c_str());
                }
                if(strncmp(buffer,"cd",2)==0){
                    carry=ftptest.CD(buffer+3);
                    printf("%d\n",carry);
                }
                if(strncmp(buffer,"ls",2)==0){
                    printf("%s\n",ftptest.Dir("").c_str());
                    continue;
                }
                if(strncmp(buffer,"binary",6)==0){
                    ftptest.setTransferMode(ftptest.binary);
                }
                if(strncmp(buffer,"ascii",5)==0){
                    ftptest.setTransferMode(ftptest.ascii);
                }
                if(strncmp(buffer,"get",3)==0){
                    for(i=4;i<strlen(buffer);i++)
                    {
                        filename[i-4]=buffer[i];
                    }
                    filename[i-5]='\0'; //证明第五个字符是结束符
                    ftptest.Get(filename,filename);
                    continue;
                }
                if(strncmp(buffer,"put",3)==0){
                    ftptest.Put(buffer+4,buffer+4);
                    continue;
                }
                if(strncmp(buffer,"quit",4)==0)
                    ftptest.~CFTPManager();
                }

程序的登陆步骤
/* 登陆步骤
login2Server
|
inputUserName
|
inputPassWord
|
具体操作
|
quit
*/
这个FTP client只完成了简单功能:登陆,退出,更改路径,打印路径,上传,下载,设置数据传送的模式 。刚刚开始学习c++,所以代码写的非常幼稚和冗余,主要借助了前辈的API。下面的代码主要介绍客户端的登陆过程。登陆的过程中连接了服务器的默认端口号(21),并要求输入账号和密码。

FTP_API CFTPManager::login2Server(const std::string &serverIP)
{
    std::string strPort;
    int pos = serverIP.find_first_of(":");

    if (pos > 0)
    {
        strPort = serverIP.substr(pos + 1, serverIP.length() - pos);
    }
    else
    {
        pos = serverIP.length();
        strPort = FTP_DEFAULT_PORT;
    }

    m_strServerIP = serverIP.substr(0, pos);   /*server' ip*/
    m_nServerPort = atol(strPort.c_str());     /*server' port*/

    trace("IP: %s port: %d\n", m_strServerIP.c_str(), m_nServerPort);

    if (Connect(m_cmdSocket, m_strServerIP, m_nServerPort) < 0)
    {

        return -1;
    }

    m_strResponse = serverResponse(m_cmdSocket);
    printf("loginResponse:\n%s", m_strResponse.c_str());
    std::cout<<"your name:"<<std::endl;


    return  parseResponse(m_strResponse);
}

接下来是设置数据传输模式的代码,头文件中枚举了ASCII和BINARY两种传输模式,我也是第一次接触枚举,就直接把这两个枚举变量当做类变量来使用,也不知道是不是正确。

FTP_API CFTPManager::setTransferMode(type mode)
{
    std::string strCmdLine;

    switch (mode)
    {
        case binary:
            strCmdLine = parseCommand(FTP_COMMAND_TYPE_MODE, "I");
            break;
        case ascii:
            strCmdLine = parseCommand(FTP_COMMAND_TYPE_MODE, "A");
            break;
        default:
            break;
    }

    if (Send(m_cmdSocket, strCmdLine.c_str()) < 0)
    {
        assert(false);
    }
    else
    {
        m_strResponse  = serverResponse(m_cmdSocket);
        printf("@@@@Response: %s", m_strResponse.c_str());

        return parseResponse(m_strResponse);
    }
}

接下来我们再介绍get和put的方法,这大概是一个FTP client最重要的功能了,代码中使用了PASV()数据连接模式去连接服务器,应用了c++的文件I/O方法,我感觉和C语言大同小异,后期准备写一个FTP的server,到时候再一同摸索吧,现在只能大概理解代码。这里的数据连接过程主要使用非阻塞的模式,而我以前的程序主要是使用了阻塞模式(本程序中的main函数使用了阻塞模式,用于把标准输入读入buffer中)。关于阻塞和非阻塞模式,稍后应该写一篇博文再具体分析。

FTP_API CFTPManager::Connect(int socketfd, const std::string &serverIP, unsigned int nPort)
{
    if (socketfd == INVALID_SOCKET)
    {
        return -1;
    }

    unsigned int argp = 1;
    int error = -1;
    int len = sizeof(int);
    struct sockaddr_in  addr;
    bool ret = false;
    timeval stime;
    fd_set  set;

    ioctl(socketfd, FIONBIO, &argp);  //设置为非阻塞模式

    memset(&addr, 0, sizeof(struct sockaddr_in));
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(nPort);
    addr.sin_addr.s_addr = inet_addr(serverIP.c_str());
    bzero(&(addr.sin_zero), 8);

    trace("Address: %s %d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));

    if (connect(socketfd, (struct sockaddr*)&addr, sizeof(struct sockaddr)) == -1)   //若直接返回 则说明正在进行TCP三次握手
    {
        stime.tv_sec = 20;  //设置为1秒超时
        stime.tv_usec = 0;
        FD_ZERO(&set);
        FD_SET(socketfd, &set);

        if (select(socketfd + 1, NULL, &set, NULL, &stime) > 0)   ///在这边等待 阻塞 返回可以读的描述符 或者超时返回0  或者出错返回-1
        {
            getsockopt(socketfd, SOL_SOCKET, SO_ERROR, &error, (socklen_t*)&len);
            if (error == 0)
            {
                ret = true;
            }
            else
            {
                ret = false;
            }
        }
    }
    else
    {   trace("Connect Immediately!!!\n");
        ret = true;
    }

    argp = 0;
    ioctl(socketfd, FIONBIO, &argp);

    if (!ret)
    {
        close(socketfd);
        fprintf(stderr, "cannot connect server!!\n");
        return -1;
    }

    //fprintf(stdout, "Connect!!!\n");

    return 0;
}

在创建本地文件夹的过程中,遇到了一点障。创建的文件名一直出现换行,即文件名下面出现一行空行。c++11中规定可以使用c风格的字符串,char[]数组可以自动转换为c++中字符串,而c++字符串加c_str()函数就可以转换为char[]的c风格字符串。但是我的buffer在初始化的过程中留了太多的空白字符,所以可能导致转换后的c++字符串过长。解决的在main函数中,给char[]数组的最后一个字符加上一个’\0’结束符,但是结束符位置应该引起注意。
程序 quit的时候,使用了析构函数,这个函数博大精深,需要继续参悟。

CFTPManager::~CFTPManager(void)
{
    std::string strCmdLine = parseCommand(FTP_COMMAND_QUIT, "");

    Send(m_cmdSocket, strCmdLine.c_str());
    close(m_cmdSocket);
    m_bLogin = false;
}

刚刚学习了git的版本控制,应该稍后会把我使用的代码上传到github。如果想使用前辈代码,请参照:http://blog.csdn.net/the_king_cloud/article/details/8090699

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐