C + + simple FTP client programming

Posted by bernouli on Wed, 02 Feb 2022 09:27:24 +0100

Basic FTP client

FTP download client implemented by QT C + +

Environmental description

FTP server: centos7 8 + vsftpd 3.0.2 installation settings see blog

CentOS vsftpd settings

Client: win10+QT 5.15.2

The implementation is not a fully functional FTP client, but the program has the need to download files from the FTP server. It mainly realizes the download function, including breakpoint continuation, and does not realize multi-threaded download. The implementation of multi-threaded download has something to do with breakpoint continuation. It is simple to realize multi-threaded download after understanding breakpoint continuation.

FTP protocol is based on TCP. Socket programming is used in the implementation. Messages are sent between the client and the server. See the last pictures of the previous blog post for the format of messages.

Simple FTP client implementation of C + + (I) basic knowledge of FTP

Sample code download:

FTP client implemented by QT C + +, with breakpoint continuation function

Establish Socket connection

WSADATA dat;
int ret;

//Initialization is very important
if (::WSAStartup(MAKEWORD(2,2),&dat) != 0)  //Windows Sockets Asynchronous startup
{
    cout<<"Init Failed: "<<GetLastError()<<endl;
    emit emitInfo(network , "Init Failed!\n");
    return -1;
}

//Create 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;
}

//Build server access parameter structure
serverAddr.sin_family=AF_INET;
serverAddr.sin_addr.S_un.S_addr=inet_addr(ip_addr.c_str()); //address
serverAddr.sin_port=htons(PORT);            //port
memset(serverAddr.sin_zero,0,sizeof(serverAddr.sin_zero));

//connect
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;
}

User name password login:

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

//password
executeCmd("PASS " + password);
if(recvControl(230) != 0)
{
    emit emitInfo(userpass, "Wrong user name or password!");
    return -1;
}

change directory

executeCmd("CWD "+tardir);
if(recvControl(250) != 0)
{
    emit emitInfo(directory, "FTP Directory does not exist!");
    return -1;
}

Toggle Binary mode

memset(buf, 0, BUFLEN);
executeCmd("TYPE I");
if(recvControl(200) != 0)
{
    emit emitInfo(filename, "switch BINARY Mode failed!");
    return -1;
}

List all files in the current directory

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;
}

Switch to PASV mode

int dataPort, ret;
//Switch to passive mode
executeCmd("PASV");
recvControl(227);

//The returned information format is -- h1,h2,h3,h4,p1,p2
//Where H1, H2, H3 and H4 are server addresses and p1*256+p2 are data ports
dataPort = getPortNum();
//Client data transmission socket
dataSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
serverAddr.sin_port = htons(dataPort);    //Change the port value in the connection parameters
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;

Download File:

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, "get into pasv Mode failed!");
    return -1;
}

executeCmd("RETR "+remoteName);
if(recvControl(150) != 0)
{
    ofile.close();
    emit emitInfo(filename, "FTP File does not exist!");
    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, "Failed to transfer file, network disconnected!");
        break;
    }
}

ofile.close();

Breakpoint continuation

To realize the continuous transmission of breakpoints, the REST command is mainly used to transfer files from a specific offset.
First obtain the local file size and compare it with the server file. If it is smaller than the server file size, start breakpoint continuous transmission. The local file is opened in append mode and written from the end of the file.
Before setting the offset REST local file size, you need to switch to Binary mode. Generally, the default mode of FTP server is Ascii mode, and Ascii mode cannot continue transmission at breakpoints.
Core code:

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)
        {
            // The local file is larger than or equal to that on ftp. It is overwritten by default
            cout << "s >= size-----" << endl;
            //ofile.seekp(0, std::ios::beg);
            ofile.open(localFile, ios_base::binary);
        }
        else
        {
            if(setTypeI() == -1)
            {
                //Cout < < "failed to set BINARY mode, unable to resume transmission at breakpoint, start from scratch!"<< endl;
                //ofile.seekp(0, std::ios::beg); //  Failed to set BINARY mode, unable to resume transmission at breakpoint, start from scratch
                //ofile.open(localFile, ios_base::binary);

                emit emitInfo(network, "set up BINARY Mode failed!");
                return -1;
            }
            else
            {
                if(restFile(ss) == -1)
                {
                    //ofile.open(localFile, ios_base::binary);
                    //ofile.seekp(0, std::ios::beg); //  Failed to set breakpoint to resume transmission. Start from scratch
                    emit emitInfo(network, "Failed to set resume mode!");
                    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, "get into pasv Mode failed!");
    return -1;
}

executeCmd("RETR "+remoteName);
if(recvControl(150) != 0)
{
    ofile.close();
    emit emitInfo(filename, "FTP File does not exist!");
    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, "Failed to transfer file, network disconnected!");
        break;
    }
}

ofile.close();

Class encapsulation

It mainly includes three classes:
class FtpClient : public QObject

class ClientThread : public QThread

class ClientManager : public QObject


ClientManager class is an external interface class, ftpclient class is an ftp client class that interacts with the server, and ClientThread is a thread execution class that encapsulates a series of calls of ftpclient class in a thread function.
The FtpClient class reports information to the ClientManager class through signal, such as whether the login is successful, file download progress, etc. After receiving the important information, the ClientManager class throws up the signal again as needed.

The upper layer program calls respond to signal first
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)));
Then it calls two functions to download files
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();

Print out the main information of interaction with FTP server during program operation as follows:

 

Multithreaded Download

There is no multi-threaded download in my program. If you want to do it, you also need to use the REST command. Calculate the download offset of each thread according to the number of threads. Each thread connects the data port to the server through pasv mode, then sets the translation amount, stops downloading after downloading bytes of a specific size, and finally splices several file segments locally.

Topics: C++ Qt