一、基础知识
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; //reqTypeQString name; //name for file or folderfileReq(){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_OBJECTpublic: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 clientchar 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 informationif (UDT::ERROR == UDT::send(m_client, (char*)&size, sizeof(int), 0)){errUDT( "send header size");return false;}// send header informationif (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 filefstream ifs(file, ios::in | ios::binary);ifs.seekg(0, ios::end);int64_t size = ifs.tellg();ifs.seekg(0, ios::beg);// send the headerif (!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 fileint64_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_OBJECTpublic:config(QObject *parent = nullptr);~config();///获取主机名/IPQString getHost() { return m_Host; };///获取端口QString getPort() { return m_Port; };///获取本地保存目录QString getLocalDir() { return m_LocalDir; };///获取服务端待下载文件名QString getServerFile() { return m_ServerFile; };///获取服务端待下载目录QString getServerDir() { return m_ServerDir; };///设置主机名/IPvoid 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:///主机名/IPQString 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 fileQStringList 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 informationbool 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 fileQString 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");}elseresult = 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文库