C++的简单FTP客户端实现(二)编程

基本FTP客户端

QT C++实现的FTP下载客户端

环境说明

FTP服务器:CentOS7.8 + vsFTPD 3.0.2 安装设置见博文

CentOS vsftpd设置

客户端:win10+QT 5.15.2

实现的不是一个功能全的FTP客户端,而是程序中有从FTP服务器下载文件的需求,主要实现了下载的功能,包括断点续传,没有实现多线程下载。多线程下载的实现与断点续传有点关系,看懂了断点续传,实现多线程下载就简单了。

FTP协议是建立在TCP基础上的,在实现时用的就是Socket编程,客户端和服务端之间发送消息,消息的格式见上篇博文的最后几张图。

C++的简单FTP客户端实现(一)FTP基础知识

示例代码下载:

QT C++实现的FTP客户端,带断点续传功能

建立Socket连接

WSADATA dat;
int ret;

//初始化,很重要
if (::WSAStartup(MAKEWORD(2,2),&dat) != 0)  //Windows Sockets Asynchronous启动
{
    cout<<"Init Failed: "<<GetLastError()<<endl;
    emit emitInfo(network , "Init Failed!\n");
    return -1;
}

//创建Socket
controlSocket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(controlSocket==INVALID_SOCKET)
{
    cout<<"Creating Control Socket Failed: "<<GetLastError()<<endl;
    emit emitInfo(network , "Creating Control Socket Failed.\n");
    return -1;
}

//构建服务器访问参数结构体
serverAddr.sin_family=AF_INET;
serverAddr.sin_addr.S_un.S_addr=inet_addr(ip_addr.c_str()); //地址
serverAddr.sin_port=htons(PORT);            //端口
memset(serverAddr.sin_zero,0,sizeof(serverAddr.sin_zero));

//连接
ret = ::connect(controlSocket,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
if(ret==SOCKET_ERROR)
{
    cout<<"Control Socket Connecting Failed: "<<GetLastError()<<endl;
    emit emitInfo(network , "Control Socket Connecting Failed\n");
    return -1;
}

用户名密码登录:

//用户名
executeCmd("USER " + username);
if(recvControl(331) != 0)
{
    emit emitInfo(userpass, "");
}

//密码
executeCmd("PASS " + password);
if(recvControl(230) != 0)
{
    emit emitInfo(userpass, "用户名或密码错误!");
    return -1;
}

更改目录

executeCmd("CWD "+tardir);
if(recvControl(250) != 0)
{
    emit emitInfo(directory, "FTP目录不存在!");
    return -1;
}

切换Binary模式

memset(buf, 0, BUFLEN);
executeCmd("TYPE I");
if(recvControl(200) != 0)
{
    emit emitInfo(filename, "切换BINARY模式失败!");
    return -1;
}

列出当前目录下所有文件

int FtpClient::listPwd()
{
    intoPasv();
    executeCmd("LIST -al");
    recvControl(150);
    memset(databuf, 0, DATABUFLEN);
    string fulllist;
    int ret = recv(dataSocket, databuf, DATABUFLEN-1, 0);
    while(ret>0)
    {
        databuf[ret] = '\0';
        fulllist += databuf;
        ret = recv(dataSocket, databuf, DATABUFLEN-1, 0);
    }

    removeSpace(fulllist);

    int lastp, lastq, p, q;
    vector<string> eachrow;
    string rawrow;
    string item;
    filelist.clear();
    p = fulllist.find("\r\n");
    lastp = 0;

    while(p>=0)
    {
        eachrow.clear();
        rawrow = fulllist.substr(lastp, p-lastp);

        q = rawrow.find(' ');
        lastq = 0;
        for(int i=0; i<8; i++)
        {
            item = rawrow.substr(lastq, q-lastq);
            eachrow.push_back(item);
            lastq = q + 1;
            q = rawrow.find(' ', lastq);
        }

        item = rawrow.substr(lastq);
        eachrow.push_back(item);
        filelist.push_back(eachrow);

        lastp = p + 2;
        p = fulllist.find("\r\n", lastp);
    }

    closesocket(dataSocket);
    recvControl(226);
    return 0;
}

切换成PASV模式

int dataPort, ret;
//切换到被动模式
executeCmd("PASV");
recvControl(227);

//返回的信息格式为---h1,h2,h3,h4,p1,p2
//其中h1,h2,h3,h4为服务器的地址,p1*256+p2为数据端口
dataPort = getPortNum();
//客户端数据传输socket
dataSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
serverAddr.sin_port = htons(dataPort);    //更改连接参数中的port值
ret = ::connect(dataSocket,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
if(ret == SOCKET_ERROR)
{
    cout<<"Data Socket connecting Failed: "<<GetLastError()<<endl;
    return -1;
}

cout<<"Data Socket connecting is success."<<endl;
return 0;

下载文件:

int cur = 0;
ofstream ofile;
string localFile = localDir + "/" + localName;
QFileInfo fileinfo(QString::fromStdString(localFile));
int ss = fileinfo.size();

ofile.open(localFile, ios_base::binary);

if(intoPasv() == -1)
{
    ofile.close();
    emit emitInfo(network, "进入pasv模式失败!");
    return -1;
}

executeCmd("RETR "+remoteName);
if(recvControl(150) != 0)
{
    ofile.close();
    emit emitInfo(filename, "FTP文件不存在!");
    return -1;
}

memset(databuf, 0, DATABUFLEN);
int ret = recv(dataSocket, databuf, DATABUFLEN, 0);

while(ret > 0)
{
    cur += ret;
    //cout << cur << " : " << size;

    emit emitProcess(cur, size);

    ofile.write(databuf, ret);
    ofile.flush();
    ret = recv(dataSocket, databuf, DATABUFLEN, 0);

    if(ret == -1)
    {
        cout << "sending file, socker error!" << endl;
        emit emitInfo(network, "传输文件时失败,网络断开!");
        break;
    }
}

ofile.close();

断点续传

实现断点续传,主要用到REST命令,从特定的偏移量开始传输文件。
先获取本地文件大小,与服务器文件比较,如果小于服务器文件大小,则开始断点续传,本地文件用append模式打开,从文件末尾写入。
设置偏移量REST 本地文件大小,之前还要切换成Binary模式,一般FTP服务器默认的是Ascii模式,Ascii模式是不能进行断点续传的。
核心代码:

int cur = 0;
ofstream ofile;
string localFile = localDir + "/" + localName;
QFileInfo fileinfo(QString::fromStdString(localFile));
int ss = fileinfo.size();

if(resume)
{
    if(ss > 0)
    {
        if(ss >= size)
        {
            // 本地文件比ftp上大或相等,默认覆盖
            cout << "s >= size-----" << endl;
            //ofile.seekp(0, std::ios::beg);
            ofile.open(localFile, ios_base::binary);
        }
        else
        {
            if(setTypeI() == -1)
            {
                //cout << "设置BINARY模式失败,不能断点续传,从头开始!" << endl;
                //ofile.seekp(0, std::ios::beg); // 设置BINARY模式失败,不能断点续传,从头开始
                //ofile.open(localFile, ios_base::binary);

                emit emitInfo(network, "设置BINARY模式失败!");
                return -1;
            }
            else
            {
                if(restFile(ss) == -1)
                {
                    //ofile.open(localFile, ios_base::binary);
                    //ofile.seekp(0, std::ios::beg); // 设置断点续传失败,从头开始
                    emit emitInfo(network, "设置续传模式失败!");
                    return -1;
                }
                else
                {
                    cout << "begin resume break-point!" << endl;
                    ofile.open(localFile, ios_base::binary|ios_base::app);
                    cur += ss;
                }
            }
        }
    }
    else
    {
        ofile.open(localFile, ios_base::binary);
    }
}
else
{
    ofile.open(localFile, ios_base::binary);
}

if(intoPasv() == -1)
{
    ofile.close();
    emit emitInfo(network, "进入pasv模式失败!");
    return -1;
}

executeCmd("RETR "+remoteName);
if(recvControl(150) != 0)
{
    ofile.close();
    emit emitInfo(filename, "FTP文件不存在!");
    return -1;
}

memset(databuf, 0, DATABUFLEN);
int ret = recv(dataSocket, databuf, DATABUFLEN, 0);

while(ret > 0)
{
    cur += ret;
    //cout << cur << " : " << size;

    emit emitProcess(cur, size);

    ofile.write(databuf, ret);
    ofile.flush();
    ret = recv(dataSocket, databuf, DATABUFLEN, 0);

    if(ret == -1)
    {
        cout << "sending file, socker error!" << endl;
        emit emitInfo(network, "传输文件时失败,网络断开!");
        break;
    }
}

ofile.close();

类封装

主要包含了三个类:
class FtpClient : public QObject

class ClientThread : public QThread

class ClientManager : public QObject


ClientManager类是对外的接口类,FtpClient类是ftp客户端,进行与服务器进行交互的类,ClientThread是线程执行类,把一系列的ftpclient类的调用封装在一个线程函数中。
FtpClient类向ClientManager类通过signal报告信息,是否登录成功,文件下载进度等。ClientManager类接收到后根据需要把重要的信息向上再次抛出signal。

上层程序调用是首先响应signal
connect(&m_Client, SIGNAL(emitProcess(int,int)), this, SLOT(on_emitDownloadSize(int,int)));
connect(&m_Client, SIGNAL(emitError(int,QString)), this, SLOT(on_emitError(int,QString)));
然后就是调用两个函数实现文件下载
m_ClientManager.setDownloadInfo(ui->textEdit1->toPlainText(), ui->textEdit1_2->toPlainText(), ui->textEdit1_3->toPlainText(), ui->textEdit1_4->toPlainText(), ui->textEdit1_5->toPlainText(), ui->textEdit1_6->toPlainText(), ui->textEdit1_7->toPlainText());
m_ ClientManager.startDownload();

把程序运行过程中与FTP服务器交互的主要信息打印出来如下:

C++的简单FTP客户端实现(二)编程

 

多线程下载

我的程序中没有实现多线程下载。如果要做也是要用REST命令。根据线程数,计算每个线程下载的偏移量。每个线程各自通过pasv模式向服务端连接数据端口,然后设置平移量,下载特定大小的字节后就停止下载,最后再本地把几个文件段拼接起来。

上一篇:本题要求实现两个函数:一个函数判断给定正整数的各位数字之和是否等于5;另一个函数统计给定区间内有多少个满足上述要求的整数,并计算这些整数的和。


下一篇:利用python写一个简易图书管理系统