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