一、基础知识
UDP(UserDatagramProtocol)是一个简单的面向消息的传输层协议,尽管UDP提供标头和有效负载的完整性验证(通过校验和),但它不保证向上层协议提供消息传递,并且UDP层在发送后不会保留UDP 消息的状态。因此,UDP有时被称为不可靠的数据报协议。如果需要传输可靠性,则必须在用户应用程序中实现。
所以为了利用UDP的传输速度,并保证传输的可靠性,需要在UDP的基础上实现可靠的数据传输协议。为了开发的效率,作者选择了开源的UDT做为基础,并在其基础上构建文件和目录的传输。
二、服务端
数据结构/配置定义
pub.h 定义通用数据类型和转换方法
#pragma once
#include <QObject>
using namespace std;
///请求类型定义
enum reqType
{
finished = -1, ///finished the sending.
file,
folder
};
///请求定义
struct fileReq
{
int type; //reqType
QString name; //name for file or folder
fileReq()
{
type = reqType::file;
}
};
///QString转string
string _Q2S(QString &qstr);
///string转QString
QString _S2Q(string &str);
config.h 定义基础配置信息
#pragma once
#include <QObject>
class config : public QObject
{
Q_OBJECT
public:
config(QObject *parent = nullptr);
~config();
///获取本地存储路径
QString getStoreDir() { return m_StoreDir; };
///获取本地服务端口
QString getPort() { return m_Port; };
///设置本地存储路径
void setStoreDir(QString dir) { m_StoreDir = dir; };
///设置本地服务端口
void setPort(QString port) { m_Port = port; };
private:
///本地存储路径
QString m_StoreDir;
///本地服务端口
QString m_Port;
};
///获取全局配置对象
config *getConfig();
监听和接收连接
建立主线程,接收客户端的连接,并对每个连接开启新的子线程。
void mainThread::run()
{
emit log("listening...");
sockaddr_storage clientaddr;
int addrlen = sizeof(clientaddr);
UDTSOCKET fhandle;
while (m_start)
{
if (UDT::INVALID_SOCK == (fhandle = UDT::accept(m_serv, (sockaddr *)&clientaddr, &addrlen)))
{
emit log(UDT::getlasterror().getErrorMessage());
break;
}
char clienthost[NI_MAXHOST];
char clientservice[NI_MAXSERV];
getnameinfo((sockaddr *)&clientaddr, addrlen, clienthost, sizeof(clienthost), clientservice, sizeof(clientservice), NI_NUMERICHOST | NI_NUMERICSERV);
QString msg = QString("new connection: %1:%2").arg(clienthost).arg(clientservice);
emit log(msg);
startClientThread(fhandle);
}
emit log("Service end.");
}
处理请求
子线程执行主要为三部分:1. 接收和解析请求 2. 处理请求 3. 传输结束
void fileThread::run()
{
getReqs(m_fileReq);
foreach(fileReq req, m_fileReq)
procReq(req);
sendEnd();
}
接收请求方法中,将会从客户端接收请求头的大小,然后接收请求的具体内容,并填充至请求列表。请求支持单个文件或目录类型。
void fileThread::getReqs(QList<fileReq> &reqlist)
{
reqlist.clear();
// aquiring file name information from client
char file[1024];
int len;
if (UDT::ERROR == UDT::recv(m_client, (char*)&len, sizeof(int), 0))
{
QString msg ="getReqs size: " + QString::fromLocal8Bit(UDT::getlasterror().getErrorMessage());
emit log(msg);
return;
}
if (UDT::ERROR == UDT::recv(m_client, file, len, 0))
{
QString msg = "getReqs: " + QString::fromLocal8Bit(UDT::getlasterror().getErrorMessage());
emit log(msg);
return;
}
file[len] = '\0';
QString info = QString::fromUtf8(file);
QStringList reqs = info.split(";");
foreach(QString req, reqs)
{
QStringList file = req.split(",");
if (file.size() > 1)
{
fileReq r;
r.type = file.at(0).toInt();
r.name = file.at(1);
reqlist.append(r);
}
}
}
获取请求列表后,按照文件和目录类型以此处理。
void fileThread::procReq(fileReq &req)
{
if (req.type == reqType::file)
sendFile(req);
else if (req.type == reqType::folder)
sendDir(req);
}
针对目录类型,需要进行递归遍历,获取文件并发送。
每次发送文件之前,需要先发送头信息,以告知客户端发送类型、文件名称和文件大小。
bool fileThread::sendHeader(fileReq &req, int64_t fileSize)
{
QString header = QString("%1,%2,%3").arg(req.type).arg(req.name).arg(fileSize);
QByteArray arr = header.toUtf8();
int size = arr.size();
// send header size information
if (UDT::ERROR == UDT::send(m_client, (char*)&size, sizeof(int), 0))
{
errUDT( "send header size");
return false;
}
// send header information
if (UDT::ERROR == UDT::send(m_client, arr.data(), size, 0))
{
errUDT("send header");
return false;
}
return true;
}
执行文件发送:
bool fileThread::sendFile(fileReq &req, const QString &root, bool keepDir)
{
QString fullname = joinFullPath(getConfig()->getStoreDir(), root, req.name);
fullname = fullname.replace("/", "\\");
string file = _Q2S(fullname);
//open the file
fstream ifs(file, ios::in | ios::binary);
ifs.seekg(0, ios::end);
int64_t size = ifs.tellg();
ifs.seekg(0, ios::beg);
// send the header
if (!keepDir)
{
QFileInfo info(req.name);
req.name = info.fileName();
}
if(!sendHeader(req, size))
return false;
UDT::TRACEINFO trace;
UDT::perfmon(m_client, &trace);
// send the file
int64_t offset = 0;
if (UDT::ERROR == UDT::sendfile(m_client, ifs, offset, size))
{
errUDT("sendfile");
return false;
}
UDT::perfmon(m_client, &trace);
cout << "speed = " << trace.mbpsSendRate << "Mbits/sec" << endl;
QString msg = QString("speed = %1 Mb/s").arg(trace.mbpsSendRate);
emit log(msg);
ifs.close();
return true;
}
三、客户端
config.h配置定义
#pragma once
#include <QObject>
class config : public QObject
{
Q_OBJECT
public:
config(QObject *parent = nullptr);
~config();
///获取主机名/IP
QString getHost() { return m_Host; };
///获取端口
QString getPort() { return m_Port; };
///获取本地保存目录
QString getLocalDir() { return m_LocalDir; };
///获取服务端待下载文件名
QString getServerFile() { return m_ServerFile; };
///获取服务端待下载目录
QString getServerDir() { return m_ServerDir; };
///设置主机名/IP
void setHost(QString host) { m_Host = host; };
///设置端口
void setPort(QString port) { m_Port = port; };
///设置本地保存目录
void setLocalDir(QString dir) { m_LocalDir = dir; };
///设置服务端待下载文件名
void setServerFile(QString file) { m_ServerFile = file; };
///设置服务端待下载目录
void setServerDir(QString dir) { m_ServerDir = dir; };
private:
///主机名/IP
QString m_Host;
///端口
QString m_Port;
///本地保存目录
QString m_LocalDir;
///服务端待下载文件名
QString m_ServerFile;
///服务端待下载目录
QString m_ServerDir;
};
config *getConfig();
客户端负责实现请求的发送和文件接收。
发送请求
bool fileThread::sendReq()
{
// send name information of the requested file
QStringList reqs;
if (!getConfig()->getServerFile().isEmpty())
reqs.append(QString("%1,%2").arg(reqType::file).arg(getConfig()->getServerFile()));
if (!getConfig()->getServerDir().isEmpty())
reqs.append(QString("%1,%2").arg(reqType::folder).arg(getConfig()->getServerDir()));
QByteArray reqFile = reqs.join(";").toUtf8();
int len = reqFile.size();
if (UDT::ERROR == UDT::send(m_client, (char*)&len, sizeof(int), 0))
{
cout << "send: " << UDT::getlasterror().getErrorMessage() << endl;
return false;
}
if (UDT::ERROR == UDT::send(m_client, reqFile.data(), len, 0))
{
cout << "send: " << UDT::getlasterror().getErrorMessage() << endl;
return false;
}
return true;
}
接收文件
先接收头信息,然后接收文件数据。
bool fileThread::recvFile()
{
// get size information
bool result = false;
int len;
if (UDT::ERROR == UDT::recv(m_client, (char*)&len, sizeof(int), 0))
{
errUDT("recv header size");
return result;
}
char buffer[1024];
if (UDT::ERROR == UDT::recv(m_client, buffer, len, 0))
{
errUDT("recv header");
return result;
}
buffer[len] = '\0';
QString info = QString::fromUtf8(buffer);
QStringList ls = info.split(",");
if (ls.size() < 3)
{
emit log("recvFile: illegal params.");
return result;
}
int type = ls.at(0).toInt();
QString filename = ls.at(1);
if (type == reqType::finished)
{
emit log("server side finished.");
return false;
}
else if (type == reqType::folder) ///dir
{
QString path = getConfig()->getLocalDir() + "\\" + filename;
QDir dir;
bool result = dir.mkpath(path);
if (!result)
emit log("failed to make dir: "+ path);
return result;
}
int64_t size = ls.at(2).toInt();
if (size < 0)
{
emit log("no such file: " + filename);
return false;
}
UDT::TRACEINFO trace;
UDT::perfmon(m_client, &trace);
// receive the file
QString path = getConfig()->getLocalDir() + "\\" + filename;
string localFile = _Q2S(path.replace("/","\\"));
fstream ofs(localFile, ios::out | ios::binary | ios::trunc);
int64_t recvsize;
int64_t offset = 0;
if (UDT::ERROR == (recvsize = UDT::recvfile(m_client, ofs, offset, size)))
{
errUDT("recvfile");
}
else
result = true;
UDT::perfmon(m_client, &trace);
emit log(QString("speed = %1 Mb/s").arg(trace.mbpsRecvRate));
ofs.close();
return result;
}
四、程序示例
服务端存储目录
启动服务端
启动客户端
执行下载“测试”目录后的结果如下图:
接收目录:
可执行程序链接:
(27条消息) 【免费】【可执行程序】基于UDT的文件+目录可靠传输(C++,Qt)资源-CSDN文库