基本FTP客户端
QT C++实现的FTP下载客户端
环境说明
FTP服务器:CentOS7.8 + vsFTPD 3.0.2 安装设置见博文
客户端:win10+QT 5.15.2
实现的不是一个功能全的FTP客户端,而是程序中有从FTP服务器下载文件的需求,主要实现了下载的功能,包括断点续传,没有实现多线程下载。多线程下载的实现与断点续传有点关系,看懂了断点续传,实现多线程下载就简单了。
FTP协议是建立在TCP基础上的,在实现时用的就是Socket编程,客户端和服务端之间发送消息,消息的格式见上篇博文的最后几张图。
示例代码下载:
建立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模式向服务端连接数据端口,然后设置平移量,下载特定大小的字节后就停止下载,最后再本地把几个文件段拼接起来。