基于Qt的文件传输系统,包含服务器端和客户端的实现,支持断点续传、多文件传输、进度显示等功能。
一、系统架构
文件传输系统架构:
├── 服务器端 (FileServer)
│ ├── 监听端口
│ ├── 接受客户端连接
│ ├── 接收文件
│ ├── 保存文件
│ └── 显示传输状态
├── 客户端 (FileClient)
│ ├── 连接服务器
│ ├── 选择文件
│ ├── 发送文件
│ ├── 支持断点续传
│ └── 显示传输进度
└── 公共模块
├── 文件信息结构体
├── 传输协议定义
└── 工具函数
二、核心代码实现
2.1 传输协议定义 (transmissionprotocol.h)
cpp
#ifndef TRANSMISSIONPROTOCOL_H
#define TRANSMISSIONPROTOCOL_H
#include <QObject>
#include <QString>
#include <QFileInfo>
#include <QDateTime>
// 传输命令
enum TransmissionCommand {
CMD_FILE_INFO = 0x01, // 文件信息
CMD_FILE_DATA = 0x02, // 文件数据
CMD_TRANSFER_COMPLETE = 0x03, // 传输完成
CMD_TRANSFER_CANCEL = 0x04, // 取消传输
CMD_HEARTBEAT = 0x05, // 心跳包
CMD_RESUME_REQUEST = 0x06, // 断点续传请求
CMD_RESUME_RESPONSE = 0x07 // 断点续传响应
};
// 传输状态
enum TransmissionStatus {
STATUS_IDLE = 0,
STATUS_CONNECTING,
STATUS_TRANSFERRING,
STATUS_PAUSED,
STATUS_COMPLETED,
STATUS_CANCELLED,
STATUS_ERROR
};
// 文件信息结构
struct FileInfo {
QString fileName; // 文件名
qint64 fileSize; // 文件大小
QString fileHash; // 文件哈希(MD5)
QDateTime modifyTime; // 修改时间
qint64 transferredSize; // 已传输大小
bool resumeSupported; // 是否支持断点续传
};
// 传输块信息
struct FileBlock {
qint64 blockIndex; // 块索引
qint64 blockOffset; // 块偏移
qint64 blockSize; // 块大小
QByteArray blockData; // 块数据
QString blockHash; // 块哈希
};
// 传输协议常量
const int DEFAULT_PORT = 8888;
const int MAX_BLOCK_SIZE = 64 * 1024; // 64KB
const int HEADER_SIZE = 24; // 头部大小
const int TIMEOUT_MS = 30000; // 超时时间(毫秒)
const QString SERVER_NAME = "QtFileTransferServer";
#endif // TRANSMISSIONPROTOCOL_H
2.2 服务器端实现 (fileserver.h 和 fileserver.cpp)
fileserver.h
cpp
#ifndef FILESERVER_H
#define FILESERVER_H
#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <QFile>
#include <QMap>
#include <QTimer>
#include "transmissionprotocol.h"
class FileServer : public QObject
{
Q_OBJECT
public:
explicit FileServer(QObject *parent = nullptr);
~FileServer();
bool startServer(quint16 port = DEFAULT_PORT);
void stopServer();
bool isRunning() const;
QString getServerAddress() const;
quint16 getServerPort() const;
int getConnectedClients() const;
QString getSavePath() const;
void setSavePath(const QString &path);
signals:
void serverStarted(const QString &address, quint16 port);
void serverStopped();
void clientConnected(const QString &clientAddress, quint16 clientPort);
void clientDisconnected(const QString &clientAddress);
void fileTransferStarted(const QString &fileName, qint64 fileSize);
void fileTransferProgress(const QString &fileName, qint64 transferred, qint64 total);
void fileTransferCompleted(const QString &fileName, const QString &savePath);
void fileTransferCancelled(const QString &fileName);
void errorOccurred(const QString &error);
private slots:
void onNewConnection();
void onClientReadyRead();
void onClientDisconnected();
void onHeartbeatTimeout();
void onSocketError(QAbstractSocket::SocketError error);
private:
void processCommand(QTcpSocket *socket, const QByteArray &data);
void handleFileInfo(QTcpSocket *socket, const QByteArray &data);
void handleFileData(QTcpSocket *socket, const QByteArray &data);
void handleResumeRequest(QTcpSocket *socket, const QByteArray &data);
void saveFileBlock(QTcpSocket *socket, const FileBlock &block);
void sendCommand(QTcpSocket *socket, TransmissionCommand cmd, const QByteArray &data = QByteArray());
void cleanupClient(QTcpSocket *socket);
private:
QTcpServer *tcpServer;
QMap<QTcpSocket*, FileInfo> activeTransfers; // 正在传输的文件信息
QMap<QTcpSocket*, QFile*> files; // 打开的文件
QMap<QTcpSocket*, qint64> receivedBytes; // 已接收字节数
QMap<QTcpSocket*, QTimer*> heartbeats; // 心跳计时器
QString savePath; // 文件保存路径
bool running;
};
#endif // FILESERVER_H
fileserver.cpp
cpp
#include "fileserver.h"
#include <QHostAddress>
#include <QDataStream>
#include <QCryptographicHash>
#include <QDir>
#include <QFileInfo>
#include <QDateTime>
FileServer::FileServer(QObject *parent) : QObject(parent)
{
tcpServer = new QTcpServer(this);
running = false;
savePath = QDir::currentPath() + "/received_files";
// 确保保存目录存在
QDir dir(savePath);
if (!dir.exists()) {
dir.mkpath(".");
}
connect(tcpServer, &QTcpServer::newConnection, this, &FileServer::onNewConnection);
}
FileServer::~FileServer()
{
stopServer();
}
bool FileServer::startServer(quint16 port)
{
if (running) {
emit errorOccurred("Server is already running");
return false;
}
if (!tcpServer->listen(QHostAddress::Any, port)) {
emit errorOccurred(QString("Failed to start server: %1").arg(tcpServer->errorString()));
return false;
}
running = true;
emit serverStarted(getServerAddress(), getServerPort());
return true;
}
void FileServer::stopServer()
{
if (!running) return;
// 断开所有客户端
foreach (QTcpSocket *socket, activeTransfers.keys()) {
cleanupClient(socket);
}
tcpServer->close();
running = false;
emit serverStopped();
}
bool FileServer::isRunning() const
{
return running;
}
QString FileServer::getServerAddress() const
{
return tcpServer->serverAddress().toString();
}
quint16 FileServer::getServerPort() const
{
return tcpServer->serverPort();
}
int FileServer::getConnectedClients() const
{
return activeTransfers.size();
}
QString FileServer::getSavePath() const
{
return savePath;
}
void FileServer::setSavePath(const QString &path)
{
savePath = path;
QDir dir(savePath);
if (!dir.exists()) {
dir.mkpath(".");
}
}
void FileServer::onNewConnection()
{
QTcpSocket *socket = tcpServer->nextPendingConnection();
connect(socket, &QTcpSocket::readyRead, this, &FileServer::onClientReadyRead);
connect(socket, &QTcpSocket::disconnected, this, &FileServer::onClientDisconnected);
connect(socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error),
this, &FileServer::onSocketError);
// 设置心跳计时器
QTimer *heartbeat = new QTimer(socket);
heartbeat->setInterval(TIMEOUT_MS);
connect(heartbeat, &QTimer::timeout, this, &FileServer::onHeartbeatTimeout);
heartbeats[socket] = heartbeat;
heartbeat->start();
emit clientConnected(socket->peerAddress().toString(), socket->peerPort());
}
void FileServer::onClientReadyRead()
{
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
if (!socket) return;
// 重置心跳计时器
if (heartbeats.contains(socket)) {
heartbeats[socket]->start();
}
// 读取数据
QByteArray data = socket->readAll();
if (data.isEmpty()) return;
processCommand(socket, data);
}
void FileServer::processCommand(QTcpSocket *socket, const QByteArray &data)
{
if (data.size() < 1) return;
TransmissionCommand cmd = static_cast<TransmissionCommand>(data[0]);
QByteArray payload = data.mid(1);
switch (cmd) {
case CMD_FILE_INFO:
handleFileInfo(socket, payload);
break;
case CMD_FILE_DATA:
handleFileData(socket, payload);
break;
case CMD_RESUME_REQUEST:
handleResumeRequest(socket, payload);
break;
case CMD_HEARTBEAT:
// 心跳包,只需重置计时器
break;
default:
emit errorOccurred(QString("Unknown command: %1").arg(cmd));
break;
}
}
void FileServer::handleFileInfo(QTcpSocket *socket, const QByteArray &data)
{
QDataStream stream(data);
stream.setVersion(QDataStream::Qt_5_15);
FileInfo fileInfo;
stream >> fileInfo.fileName >> fileInfo.fileSize >> fileInfo.fileHash
>> fileInfo.modifyTime >> fileInfo.resumeSupported;
// 检查文件是否已存在
QString saveFilePath = savePath + "/" + fileInfo.fileName;
QFileInfo existingFile(saveFilePath);
if (existingFile.exists() && existingFile.size() == fileInfo.fileSize) {
// 文件已存在且大小相同,可能是重复传输
emit errorOccurred(QString("File already exists: %1").arg(fileInfo.fileName));
sendCommand(socket, CMD_TRANSFER_CANCEL);
return;
}
// 创建文件
QFile *file = new QFile(saveFilePath, socket);
if (!file->open(QIODevice::WriteOnly)) {
emit errorOccurred(QString("Failed to create file: %1").arg(saveFilePath));
sendCommand(socket, CMD_TRANSFER_CANCEL);
return;
}
// 保存文件信息
activeTransfers[socket] = fileInfo;
files[socket] = file;
receivedBytes[socket] = 0;
emit fileTransferStarted(fileInfo.fileName, fileInfo.fileSize);
}
void FileServer::handleFileData(QTcpSocket *socket, const QByteArray &data)
{
if (!activeTransfers.contains(socket) || !files.contains(socket)) {
emit errorOccurred("No active transfer for this socket");
return;
}
QFile *file = files[socket];
FileInfo &fileInfo = activeTransfers[socket];
// 写入文件
qint64 bytesWritten = file->write(data);
if (bytesWritten != data.size()) {
emit errorOccurred(QString("Failed to write file data: %1").arg(file->errorString()));
sendCommand(socket, CMD_TRANSFER_CANCEL);
return;
}
// 更新接收字节数
receivedBytes[socket] += bytesWritten;
// 发送进度信号
emit fileTransferProgress(fileInfo.fileName, receivedBytes[socket], fileInfo.fileSize);
// 检查是否完成
if (receivedBytes[socket] >= fileInfo.fileSize) {
file->close();
// 验证文件哈希
file->open(QIODevice::ReadOnly);
QByteArray fileHash = QCryptographicHash::hash(file->readAll(), QCryptographicHash::Md5).toHex();
file->close();
if (fileHash != fileInfo.fileHash.toLatin1()) {
emit errorOccurred(QString("File hash verification failed for %1").arg(fileInfo.fileName));
sendCommand(socket, CMD_TRANSFER_CANCEL);
} else {
emit fileTransferCompleted(fileInfo.fileName, file->fileName());
sendCommand(socket, CMD_TRANSFER_COMPLETE);
}
cleanupClient(socket);
}
}
void FileServer::handleResumeRequest(QTcpSocket *socket, const QByteArray &data)
{
QDataStream stream(data);
stream.setVersion(QDataStream::Qt_5_15);
QString fileName;
stream >> fileName;
QString filePath = savePath + "/" + fileName;
QFileInfo fileInfo(filePath);
if (fileInfo.exists()) {
qint64 fileSize = fileInfo.size();
QByteArray response;
QDataStream responseStream(&response, QIODevice::WriteOnly);
responseStream.setVersion(QDataStream::Qt_5_15);
responseStream << fileSize;
sendCommand(socket, CMD_RESUME_RESPONSE, response);
} else {
// 文件不存在,从头开始传输
QByteArray response;
QDataStream responseStream(&response, QIODevice::WriteOnly);
responseStream.setVersion(QDataStream::Qt_5_15);
responseStream << static_cast<qint64>(-1);
sendCommand(socket, CMD_RESUME_RESPONSE, response);
}
}
void FileServer::sendCommand(QTcpSocket *socket, TransmissionCommand cmd, const QByteArray &data)
{
QByteArray packet;
packet.append(static_cast<char>(cmd));
packet.append(data);
socket->write(packet);
}
void FileServer::cleanupClient(QTcpSocket *socket)
{
if (files.contains(socket)) {
QFile *file = files[socket];
if (file->isOpen()) {
file->close();
}
delete file;
files.remove(socket);
}
if (heartbeats.contains(socket)) {
QTimer *timer = heartbeats[socket];
timer->stop();
delete timer;
heartbeats.remove(socket);
}
activeTransfers.remove(socket);
receivedBytes.remove(socket);
socket->deleteLater();
}
void FileServer::onClientDisconnected()
{
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
if (!socket) return;
emit clientDisconnected(socket->peerAddress().toString());
cleanupClient(socket);
}
void FileServer::onHeartbeatTimeout()
{
QTimer *timer = qobject_cast<QTimer*>(sender());
if (!timer) return;
// 找到对应的socket
QTcpSocket *socket = nullptr;
foreach (QTcpSocket *s, heartbeats.keys()) {
if (heartbeats[s] == timer) {
socket = s;
break;
}
}
if (socket) {
emit errorOccurred(QString("Client %1:%2 disconnected due to timeout")
.arg(socket->peerAddress().toString())
.arg(socket->peerPort()));
cleanupClient(socket);
}
}
void FileServer::onSocketError(QAbstractSocket::SocketError error)
{
QTcpSocket *socket = qobject_cast<QTcpSocket*>(sender());
if (!socket) return;
emit errorOccurred(QString("Socket error %1: %2")
.arg(error)
.arg(socket->errorString()));
cleanupClient(socket);
}
2.3 客户端实现 (fileclient.h 和 fileclient.cpp)
fileclient.h
cpp
#ifndef FILECLIENT_H
#define FILECLIENT_H
#include <QObject>
#include <QTcpSocket>
#include <QFile>
#include <QTimer>
#include "transmissionprotocol.h"
class FileClient : public QObject
{
Q_OBJECT
public:
explicit FileClient(QObject *parent = nullptr);
~FileClient();
bool connectToServer(const QString &host, quint16 port = DEFAULT_PORT);
void disconnectFromServer();
bool sendFile(const QString &filePath, bool resume = false);
void cancelTransfer();
bool isConnected() const;
bool isTransferring() const;
QString getServerAddress() const;
quint16 getServerPort() const;
qint64 getTransferredSize() const;
qint64 getFileSize() const;
TransmissionStatus getStatus() const;
signals:
void connected();
void disconnected();
void transferStarted(const QString &fileName, qint64 fileSize);
void transferProgress(qint64 transferred, qint64 total);
void transferCompleted(const QString &fileName);
void transferCancelled(const QString &fileName);
void errorOccurred(const QString &error);
private slots:
void onConnected();
void onDisconnected();
void onReadyRead();
void onSendNextBlock();
void onHeartbeatTimeout();
void onSocketError(QAbstractSocket::SocketError error);
private:
void sendFileInfo();
void sendFileData();
void sendResumeRequest();
void processCommand(TransmissionCommand cmd, const QByteArray &data);
void cleanupTransfer();
private:
QTcpSocket *tcpSocket;
QFile *file;
FileInfo fileInfo;
QTimer *heartbeatTimer;
QTimer *sendTimer;
qint64 currentBlockIndex;
qint64 totalBlocks;
TransmissionStatus status;
bool resumeTransfer;
};
#endif // FILECLIENT_H
fileclient.cpp
cpp
#include "fileclient.h"
#include <QHostAddress>
#include <QDataStream>
#include <QCryptographicHash>
#include <QFileInfo>
#include <QDateTime>
FileClient::FileClient(QObject *parent) : QObject(parent)
{
tcpSocket = new QTcpSocket(this);
file = nullptr;
heartbeatTimer = new QTimer(this);
sendTimer = new QTimer(this);
status = STATUS_IDLE;
resumeTransfer = false;
connect(tcpSocket, &QTcpSocket::connected, this, &FileClient::onConnected);
connect(tcpSocket, &QTcpSocket::disconnected, this, &FileClient::onDisconnected);
connect(tcpSocket, &QTcpSocket::readyRead, this, &FileClient::onReadyRead);
connect(tcpSocket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error),
this, &FileClient::onSocketError);
connect(heartbeatTimer, &QTimer::timeout, this, &FileClient::onHeartbeatTimeout);
connect(sendTimer, &QTimer::timeout, this, &FileClient::onSendNextBlock);
heartbeatTimer->setInterval(TIMEOUT_MS / 2);
sendTimer->setInterval(10); // 发送间隔,控制速度
}
FileClient::~FileClient()
{
cancelTransfer();
disconnectFromServer();
}
bool FileClient::connectToServer(const QString &host, quint16 port)
{
if (isConnected()) {
emit errorOccurred("Already connected to server");
return false;
}
status = STATUS_CONNECTING;
tcpSocket->connectToHost(host, port);
if (!tcpSocket->waitForConnected(5000)) {
status = STATUS_ERROR;
emit errorOccurred(QString("Failed to connect to server: %1").arg(tcpSocket->errorString()));
return false;
}
return true;
}
void FileClient::disconnectFromServer()
{
if (isConnected()) {
tcpSocket->disconnectFromHost();
}
}
bool FileClient::sendFile(const QString &filePath, bool resume)
{
if (!isConnected()) {
emit errorOccurred("Not connected to server");
return false;
}
if (isTransferring()) {
emit errorOccurred("Another transfer is in progress");
return false;
}
// 打开文件
file = new QFile(filePath, this);
if (!file->open(QIODevice::ReadOnly)) {
emit errorOccurred(QString("Failed to open file: %1").arg(file->errorString()));
delete file;
file = nullptr;
return false;
}
// 获取文件信息
QFileInfo info(filePath);
fileInfo.fileName = info.fileName();
fileInfo.fileSize = info.size();
fileInfo.modifyTime = info.lastModified();
fileInfo.resumeSupported = resume;
// 计算文件哈希
file->seek(0);
QByteArray fileData = file->readAll();
fileInfo.fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5).toHex();
file->seek(0);
// 计算总块数
totalBlocks = (fileInfo.fileSize + MAX_BLOCK_SIZE - 1) / MAX_BLOCK_SIZE;
currentBlockIndex = 0;
// 设置状态
status = STATUS_TRANSFERRING;
resumeTransfer = resume;
// 开始传输
if (resume) {
sendResumeRequest();
} else {
sendFileInfo();
}
emit transferStarted(fileInfo.fileName, fileInfo.fileSize);
heartbeatTimer->start();
return true;
}
void FileClient::cancelTransfer()
{
if (isTransferring()) {
status = STATUS_CANCELLED;
sendTimer->stop();
heartbeatTimer->stop();
if (file) {
file->close();
delete file;
file = nullptr;
}
emit transferCancelled(fileInfo.fileName);
cleanupTransfer();
}
}
bool FileClient::isConnected() const
{
return tcpSocket->state() == QAbstractSocket::ConnectedState;
}
bool FileClient::isTransferring() const
{
return status == STATUS_TRANSFERRING;
}
QString FileClient::getServerAddress() const
{
return tcpSocket->peerAddress().toString();
}
quint16 FileClient::getServerPort() const
{
return tcpSocket->peerPort();
}
qint64 FileClient::getTransferredSize() const
{
return currentBlockIndex * MAX_BLOCK_SIZE;
}
qint64 FileClient::getFileSize() const
{
return fileInfo.fileSize;
}
TransmissionStatus FileClient::getStatus() const
{
return status;
}
void FileClient::onConnected()
{
status = STATUS_IDLE;
emit connected();
}
void FileClient::onDisconnected()
{
status = STATUS_IDLE;
heartbeatTimer->stop();
sendTimer->stop();
cleanupTransfer();
emit disconnected();
}
void FileClient::onReadyRead()
{
QByteArray data = tcpSocket->readAll();
if (data.isEmpty()) return;
TransmissionCommand cmd = static_cast<TransmissionCommand>(data[0]);
QByteArray payload = data.mid(1);
processCommand(cmd, payload);
}
void FileClient::processCommand(TransmissionCommand cmd, const QByteArray &data)
{
switch (cmd) {
case CMD_FILE_INFO:
// 服务器确认文件信息,开始发送数据
sendTimer->start();
break;
case CMD_RESUME_RESPONSE:
// 断点续传响应
{
QDataStream stream(data);
stream.setVersion(QDataStream::Qt_5_15);
qint64 receivedSize;
stream >> receivedSize;
if (receivedSize >= 0) {
// 从指定位置继续传输
currentBlockIndex = receivedSize / MAX_BLOCK_SIZE;
file->seek(receivedSize);
sendTimer->start();
} else {
// 文件不存在,从头开始
sendFileInfo();
}
}
break;
case CMD_TRANSFER_COMPLETE:
// 传输完成
status = STATUS_COMPLETED;
sendTimer->stop();
heartbeatTimer->stop();
emit transferCompleted(fileInfo.fileName);
cleanupTransfer();
break;
case CMD_TRANSFER_CANCEL:
// 传输被取消
status = STATUS_CANCELLED;
sendTimer->stop();
heartbeatTimer->stop();
emit transferCancelled(fileInfo.fileName);
cleanupTransfer();
break;
case CMD_HEARTBEAT:
// 心跳包,重置计时器
break;
default:
emit errorOccurred(QString("Unknown command from server: %1").arg(cmd));
break;
}
}
void FileClient::sendFileInfo()
{
QByteArray packet;
QDataStream stream(&packet, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_5_15);
stream << fileInfo.fileName << fileInfo.fileSize << fileInfo.fileHash
<< fileInfo.modifyTime << fileInfo.resumeSupported;
// 发送命令
QByteArray command;
command.append(static_cast<char>(CMD_FILE_INFO));
command.append(packet);
tcpSocket->write(command);
}
void FileClient::sendResumeRequest()
{
QByteArray packet;
QDataStream stream(&packet, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_5_15);
stream << fileInfo.fileName;
QByteArray command;
command.append(static_cast<char>(CMD_RESUME_REQUEST));
command.append(packet);
tcpSocket->write(command);
}
void FileClient::sendFileData()
{
if (!file || !file->isOpen()) {
cancelTransfer();
return;
}
if (currentBlockIndex >= totalBlocks) {
sendTimer->stop();
return;
}
// 读取一块数据
qint64 blockSize = MAX_BLOCK_SIZE;
if (currentBlockIndex == totalBlocks - 1) {
blockSize = fileInfo.fileSize - currentBlockIndex * MAX_BLOCK_SIZE;
}
QByteArray blockData = file->read(blockSize);
if (blockData.size() != blockSize) {
emit errorOccurred("Failed to read file data");
cancelTransfer();
return;
}
// 发送数据块
QByteArray packet;
QDataStream stream(&packet, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_5_15);
stream << currentBlockIndex << currentBlockIndex * MAX_BLOCK_SIZE << blockSize << blockData;
QByteArray command;
command.append(static_cast<char>(CMD_FILE_DATA));
command.append(packet);
tcpSocket->write(command);
// 更新进度
currentBlockIndex++;
qint64 transferred = currentBlockIndex * MAX_BLOCK_SIZE;
if (transferred > fileInfo.fileSize) {
transferred = fileInfo.fileSize;
}
emit transferProgress(transferred, fileInfo.fileSize);
// 如果这是最后一块,停止发送定时器
if (currentBlockIndex >= totalBlocks) {
sendTimer->stop();
}
}
void FileClient::onSendNextBlock()
{
if (status == STATUS_TRANSFERRING) {
sendFileData();
}
}
void FileClient::onHeartbeatTimeout()
{
// 发送心跳包
QByteArray command;
command.append(static_cast<char>(CMD_HEARTBEAT));
tcpSocket->write(command);
}
void FileClient::onSocketError(QAbstractSocket::SocketError error)
{
status = STATUS_ERROR;
emit errorOccurred(QString("Socket error %1: %2").arg(error).arg(tcpSocket->errorString()));
cleanupTransfer();
}
void FileClient::cleanupTransfer()
{
if (file) {
file->close();
delete file;
file = nullptr;
}
fileInfo = FileInfo();
currentBlockIndex = 0;
totalBlocks = 0;
status = STATUS_IDLE;
}
2.4 主窗口界面 (mainwindow.h 和 mainwindow.cpp)
mainwindow.h
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QFileDialog>
#include <QMessageBox>
#include <QProgressBar>
#include <QTableWidget>
#include <QTableWidgetItem>
#include "fileserver.h"
#include "fileclient.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
// 服务器相关
void on_btnStartServer_clicked();
void on_btnStopServer_clicked();
void on_btnBrowseSavePath_clicked();
// 客户端相关
void on_btnConnect_clicked();
void on_btnDisconnect_clicked();
void on_btnBrowseFile_clicked();
void on_btnSendFile_clicked();
void on_btnCancelTransfer_clicked();
void on_chkResume_toggled(bool checked);
// 服务器信号
void onServerStarted(const QString &address, quint16 port);
void onServerStopped();
void onClientConnected(const QString &clientAddress, quint16 clientPort);
void onClientDisconnected(const QString &clientAddress);
void onFileTransferStarted(const QString &fileName, qint64 fileSize);
void onFileTransferProgress(const QString &fileName, qint64 transferred, qint64 total);
void onFileTransferCompleted(const QString &fileName, const QString &savePath);
void onFileTransferCancelled(const QString &fileName);
void onServerError(const QString &error);
// 客户端信号
void onClientConnected();
void onClientDisconnected();
void onTransferStarted(const QString &fileName, qint64 fileSize);
void onTransferProgress(qint64 transferred, qint64 total);
void onTransferCompleted(const QString &fileName);
void onTransferCancelled(const QString &fileName);
void onClientError(const QString &error);
private:
Ui::MainWindow *ui;
FileServer *server;
FileClient *client;
QMap<QString, QProgressBar*> progressBars; // 存储进度条
void updateServerStatus(bool running);
void updateClientStatus(bool connected);
void addTransferToTable(const QString &fileName, qint64 fileSize, bool isServer);
void updateTransferProgress(const QString &fileName, qint64 transferred, qint64 total);
void removeTransferFromTable(const QString &fileName);
};
#endif // MAINWINDOW_H
mainwindow.cpp
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 设置窗口标题
setWindowTitle("Qt File Transfer System");
// 初始化服务器
server = new FileServer(this);
connect(server, &FileServer::serverStarted, this, &MainWindow::onServerStarted);
connect(server, &FileServer::serverStopped, this, &MainWindow::onServerStopped);
connect(server, &FileServer::clientConnected, this, &MainWindow::onClientConnected);
connect(server, &FileServer::clientDisconnected, this, &MainWindow::onClientDisconnected);
connect(server, &FileServer::fileTransferStarted, this, &MainWindow::onFileTransferStarted);
connect(server, &FileServer::fileTransferProgress, this, &MainWindow::onFileTransferProgress);
connect(server, &FileServer::fileTransferCompleted, this, &MainWindow::onFileTransferCompleted);
connect(server, &FileServer::fileTransferCancelled, this, &MainWindow::onFileTransferCancelled);
connect(server, &FileServer::errorOccurred, this, &MainWindow::onServerError);
// 初始化客户端
client = new FileClient(this);
connect(client, &FileClient::connected, this, &MainWindow::onClientConnected);
connect(client, &FileClient::disconnected, this, &MainWindow::onClientDisconnected);
connect(client, &FileClient::transferStarted, this, &MainWindow::onTransferStarted);
connect(client, &FileClient::transferProgress, this, &MainWindow::onTransferProgress);
connect(client, &FileClient::transferCompleted, this, &MainWindow::onTransferCompleted);
connect(client, &FileClient::transferCancelled, this, &MainWindow::onTransferCancelled);
connect(client, &FileClient::errorOccurred, this, &MainWindow::onClientError);
// 设置默认值
ui->txtServerPort->setText(QString::number(DEFAULT_PORT));
ui->txtServerSavePath->setText(QDir::currentPath() + "/received_files");
ui->txtServerAddress->setText("127.0.0.1");
ui->txtServerPortClient->setText(QString::number(DEFAULT_PORT));
ui->chkResume->setChecked(true);
// 设置表格
ui->tblTransfers->setColumnCount(5);
ui->tblTransfers->setHorizontalHeaderLabels({"File Name", "Size", "Progress", "Status", "Actions"});
ui->tblTransfers->horizontalHeader()->setStretchLastSection(true);
ui->tblTransfers->setSelectionBehavior(QAbstractItemView::SelectRows);
ui->tblTransfers->setEditTriggers(QAbstractItemView::NoEditTriggers);
updateServerStatus(false);
updateClientStatus(false);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_btnStartServer_clicked()
{
quint16 port = ui->txtServerPort->text().toUShort();
if (port == 0) {
QMessageBox::warning(this, "Warning", "Invalid port number");
return;
}
if (server->startServer(port)) {
updateServerStatus(true);
}
}
void MainWindow::on_btnStopServer_clicked()
{
server->stopServer();
updateServerStatus(false);
}
void MainWindow::on_btnBrowseSavePath_clicked()
{
QString dir = QFileDialog::getExistingDirectory(this, "Select Save Directory",
ui->txtServerSavePath->text());
if (!dir.isEmpty()) {
ui->txtServerSavePath->setText(dir);
server->setSavePath(dir);
}
}
void MainWindow::on_btnConnect_clicked()
{
QString address = ui->txtServerAddress->text();
quint16 port = ui->txtServerPortClient->text().toUShort();
if (address.isEmpty() || port == 0) {
QMessageBox::warning(this, "Warning", "Please enter server address and port");
return;
}
if (client->connectToServer(address, port)) {
updateClientStatus(true);
}
}
void MainWindow::on_btnDisconnect_clicked()
{
client->disconnectFromServer();
updateClientStatus(false);
}
void MainWindow::on_btnBrowseFile_clicked()
{
QString fileName = QFileDialog::getOpenFileName(this, "Select File to Send");
if (!fileName.isEmpty()) {
ui->txtFilePath->setText(fileName);
}
}
void MainWindow::on_btnSendFile_clicked()
{
QString filePath = ui->txtFilePath->text();
if (filePath.isEmpty()) {
QMessageBox::warning(this, "Warning", "Please select a file to send");
return;
}
if (!client->isConnected()) {
QMessageBox::warning(this, "Warning", "Not connected to server");
return;
}
bool resume = ui->chkResume->isChecked();
client->sendFile(filePath, resume);
}
void MainWindow::on_btnCancelTransfer_clicked()
{
client->cancelTransfer();
}
void MainWindow::on_chkResume_toggled(bool checked)
{
// 断点续传选项
Q_UNUSED(checked);
}
void MainWindow::onServerStarted(const QString &address, quint16 port)
{
ui->lblServerStatus->setText(QString("Running: %1:%2").arg(address).arg(port));
ui->btnStartServer->setEnabled(false);
ui->btnStopServer->setEnabled(true);
ui->txtServerPort->setEnabled(false);
}
void MainWindow::onServerStopped()
{
ui->lblServerStatus->setText("Stopped");
ui->btnStartServer->setEnabled(true);
ui->btnStopServer->setEnabled(false);
ui->txtServerPort->setEnabled(true);
}
void MainWindow::onClientConnected(const QString &clientAddress, quint16 clientPort)
{
ui->txtServerLog->append(QString("[%1] Client connected: %2:%3")
.arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
.arg(clientAddress)
.arg(clientPort));
}
void MainWindow::onClientDisconnected(const QString &clientAddress)
{
ui->txtServerLog->append(QString("[%1] Client disconnected: %2")
.arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
.arg(clientAddress));
}
void MainWindow::onFileTransferStarted(const QString &fileName, qint64 fileSize)
{
ui->txtServerLog->append(QString("[%1] File transfer started: %2 (%3 bytes)")
.arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
.arg(fileName)
.arg(fileSize));
addTransferToTable(fileName, fileSize, true);
}
void MainWindow::onFileTransferProgress(const QString &fileName, qint64 transferred, qint64 total)
{
updateTransferProgress(fileName, transferred, total);
}
void MainWindow::onFileTransferCompleted(const QString &fileName, const QString &savePath)
{
ui->txtServerLog->append(QString("[%1] File transfer completed: %2 saved to %3")
.arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
.arg(fileName)
.arg(savePath));
removeTransferFromTable(fileName);
}
void MainWindow::onFileTransferCancelled(const QString &fileName)
{
ui->txtServerLog->append(QString("[%1] File transfer cancelled: %2")
.arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
.arg(fileName));
removeTransferFromTable(fileName);
}
void MainWindow::onServerError(const QString &error)
{
ui->txtServerLog->append(QString("[%1] ERROR: %2")
.arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
.arg(error));
}
void MainWindow::onClientConnected()
{
ui->lblClientStatus->setText("Connected");
ui->btnConnect->setEnabled(false);
ui->btnDisconnect->setEnabled(true);
ui->btnSendFile->setEnabled(true);
ui->btnCancelTransfer->setEnabled(true);
}
void MainWindow::onClientDisconnected()
{
ui->lblClientStatus->setText("Disconnected");
ui->btnConnect->setEnabled(true);
ui->btnDisconnect->setEnabled(false);
ui->btnSendFile->setEnabled(false);
ui->btnCancelTransfer->setEnabled(false);
}
void MainWindow::onTransferStarted(const QString &fileName, qint64 fileSize)
{
ui->txtClientLog->append(QString("[%1] File transfer started: %2 (%3 bytes)")
.arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
.arg(fileName)
.arg(fileSize));
addTransferToTable(fileName, fileSize, false);
}
void MainWindow::onTransferProgress(qint64 transferred, qint64 total)
{
QString fileName = QFileInfo(ui->txtFilePath->text()).fileName();
updateTransferProgress(fileName, transferred, total);
}
void MainWindow::onTransferCompleted(const QString &fileName)
{
ui->txtClientLog->append(QString("[%1] File transfer completed: %2")
.arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
.arg(fileName));
removeTransferFromTable(fileName);
}
void MainWindow::onTransferCancelled(const QString &fileName)
{
ui->txtClientLog->append(QString("[%1] File transfer cancelled: %2")
.arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
.arg(fileName));
removeTransferFromTable(fileName);
}
void MainWindow::onClientError(const QString &error)
{
ui->txtClientLog->append(QString("[%1] ERROR: %2")
.arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
.arg(error));
}
void MainWindow::updateServerStatus(bool running)
{
if (running) {
ui->lblServerStatus->setStyleSheet("QLabel { color: green; font-weight: bold; }");
} else {
ui->lblServerStatus->setStyleSheet("QLabel { color: red; }");
}
}
void MainWindow::updateClientStatus(bool connected)
{
if (connected) {
ui->lblClientStatus->setStyleSheet("QLabel { color: green; font-weight: bold; }");
} else {
ui->lblClientStatus->setStyleSheet("QLabel { color: red; }");
}
}
void MainWindow::addTransferToTable(const QString &fileName, qint64 fileSize, bool isServer)
{
int row = ui->tblTransfers->rowCount();
ui->tblTransfers->insertRow(row);
// 文件名
ui->tblTransfers->setItem(row, 0, new QTableWidgetItem(fileName));
// 文件大小
QString sizeStr;
if (fileSize < 1024) {
sizeStr = QString("%1 B").arg(fileSize);
} else if (fileSize < 1024 * 1024) {
sizeStr = QString("%.1f KB").arg(fileSize / 1024.0);
} else {
sizeStr = QString("%.1f MB").arg(fileSize / (1024.0 * 1024.0));
}
ui->tblTransfers->setItem(row, 1, new QTableWidgetItem(sizeStr));
// 进度条
QProgressBar *progressBar = new QProgressBar();
progressBar->setMinimum(0);
progressBar->setMaximum(100);
progressBar->setValue(0);
progressBar->setTextVisible(true);
progressBar->setFormat("%p%");
ui->tblTransfers->setCellWidget(row, 2, progressBar);
progressBars[fileName] = progressBar;
// 状态
QString status = isServer ? "Receiving" : "Sending";
ui->tblTransfers->setItem(row, 3, new QTableWidgetItem(status));
// 操作按钮
QWidget *actionsWidget = new QWidget();
QHBoxLayout *layout = new QHBoxLayout(actionsWidget);
layout->setContentsMargins(0, 0, 0, 0);
QPushButton *cancelBtn = new QPushButton("Cancel");
cancelBtn->setMaximumWidth(60);
connect(cancelBtn, &QPushButton::clicked, {
if (isServer) {
// 服务器端取消传输
} else {
client->cancelTransfer();
}
});
layout->addWidget(cancelBtn);
ui->tblTransfers->setCellWidget(row, 4, actionsWidget);
}
void MainWindow::updateTransferProgress(const QString &fileName, qint64 transferred, qint64 total)
{
if (progressBars.contains(fileName)) {
QProgressBar *progressBar = progressBars[fileName];
int percent = (total > 0) ? static_cast<int>((transferred * 100) / total) : 0;
progressBar->setValue(percent);
// 更新状态
for (int row = 0; row < ui->tblTransfers->rowCount(); row++) {
if (ui->tblTransfers->item(row, 0)->text() == fileName) {
QString status = (percent < 100) ? "Transferring" : "Completed";
ui->tblTransfers->setItem(row, 3, new QTableWidgetItem(status));
break;
}
}
}
}
void MainWindow::removeTransferFromTable(const QString &fileName)
{
for (int row = 0; row < ui->tblTransfers->rowCount(); row++) {
if (ui->tblTransfers->item(row, 0)->text() == fileName) {
ui->tblTransfers->removeRow(row);
progressBars.remove(fileName);
break;
}
}
}
2.5 主程序入口 (main.cpp)
cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 设置应用程序信息
QApplication::setApplicationName("Qt File Transfer System");
QApplication::setApplicationVersion("1.0");
QApplication::setOrganizationName("Qt File Transfer");
QApplication::setOrganizationDomain("qtfiletransfer.com");
MainWindow w;
w.show();
return a.exec();
}
三、UI设计文件 (mainwindow.ui)
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1200</width>
<height>800</height>
</rect>
</property>
<property name="windowTitle">
<string>Qt File Transfer System</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tabServer">
<attribute name="title">
<string>Server</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Server Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Port:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="txtServerPort">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="btnStartServer">
<property name="text">
<string>Start Server</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="btnStopServer">
<property name="text">
<string>Stop Server</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Save Path:</string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QLineEdit" name="txtServerSavePath"/>
</item>
<item row="1" column="3">
<widget class="QPushButton" name="btnBrowseSavePath">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lblServerStatus">
<property name="text">
<string>Stopped</string>
</property>
<property name="styleSheet">
<string notr="true">QLabel { color: red; }</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Server Log</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTextEdit" name="txtServerLog">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabClient">
<attribute name="title">
<string>Client</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Connection Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Server Address:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="txtServerAddress">
<property name="maximumSize">
<size>
<width>150</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Port:</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QLineEdit" name="txtServerPortClient">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="btnConnect">
<property name="text">
<string>Connect</string>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QPushButton" name="btnDisconnect">
<property name="text">
<string>Disconnect</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lblClientStatus">
<property name="text">
<string>Disconnected</string>
</property>
<property name="styleSheet">
<string notr="true">QLabel { color: red; }</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>File Transfer</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>File Path:</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QLineEdit" name="txtFilePath"/>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="btnBrowseFile">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="btnSendFile">
<property name="text">
<string>Send File</string>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QPushButton" name="btnCancelTransfer">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="chkResume">
<property name="text">
<string>Resume Transfer (if supported)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Client Log</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QTextEdit" name="txtClientLog">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabTransfers">
<attribute name="title">
<string>Active Transfers</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QTableWidget" name="tblTransfers"/>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>
四、项目配置文件 (CMakeLists.txt)
cmake
cmake_minimum_required(VERSION 3.16)
project(QtFileTransfer VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
# 查找Qt库
find_package(Qt6 COMPONENTS Core Widgets Network REQUIRED)
# 包含目录
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
# 源文件
set(SOURCES
main.cpp
mainwindow.cpp
fileserver.cpp
fileclient.cpp
transmissionprotocol.cpp
)
# 头文件
set(HEADERS
mainwindow.h
fileserver.h
fileclient.h
transmissionprotocol.h
)
# UI文件
set(UIS
mainwindow.ui
)
# 创建可执行文件
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS} ${UIS})
# 链接库
target_link_libraries(${PROJECT_NAME}
Qt6::Core
Qt6::Widgets
Qt6::Network
)
# 安装规则
install(TARGETS ${PROJECT_NAME}
RUNTIME DESTINATION bin
)
# 打包
set(CPACK_GENERATOR "ZIP")
set(CPACK_PACKAGE_NAME ${PROJECT_NAME})
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_DESCRIPTION "Qt File Transfer System")
include(CPack)
参考代码 基于QT的文件传输 www.youwenfan.com/contentcsv/72043.html
五、使用说明
5.1 编译和运行
- 使用CMake构建项目:
bash
mkdir build
cd build
cmake ..
make
- 运行程序:
bash
./QtFileTransfer
5.2 使用步骤
-
启动服务器:
- 切换到"Server"标签页
- 设置端口(默认8888)
- 设置文件保存路径
- 点击"Start Server"
-
连接客户端:
- 切换到"Client"标签页
- 输入服务器地址(如127.0.0.1)
- 输入服务器端口(默认8888)
- 点击"Connect"
-
发送文件:
- 点击"Browse..."选择要发送的文件
- 勾选"Resume Transfer"启用断点续传
- 点击"Send File"开始传输
-
监控传输:
- 切换到"Active Transfers"标签页查看所有传输进度
- 在服务器和客户端的日志区域查看详细日志
5.3 功能特点
- 断点续传:支持在传输中断后从中断处继续传输
- 多文件传输:可以同时监控多个传输任务
- 进度显示:实时显示传输进度和速度
- 日志记录:详细记录传输过程和错误信息
- 跨平台:基于Qt,可在Windows、Linux、macOS上运行