服务器部分

基于Qt的服务器端实现,核心功能包括:监听客户端连接、接收聊天消息、接收并保存文件。
 

1. main.cpp(服务器入口)

//main.cpp
#include "filetransfertoolsdialog.h"

#include <QApplication>

int main(int argc, char *argv[]) {
  QApplication a(argc, argv);
  FileTransferToolsDialog w;
  w.show();
  return a.exec();
}

初始化Qt应用,创建并显示FileTransferToolsDialog窗口。

2. filetransfertoolsdialog.h(定义服务器对话框类)

//filetransfertoolsdialog.h
#ifndef FILETRANSFERTOOLSDIALOG_H
#define FILETRANSFERTOOLSDIALOG_H

#include <QDateTime>
#include <QDialog>
#include <QFile>
#include <QMessageBox>
#include <QPainter>
#include <QTcpserver>
#include <QtDebug>
#include <QtNetwork>

QT_BEGIN_NAMESPACE
namespace Ui {
class FileTransferToolsDialog;
}
QT_END_NAMESPACE

class FileTransferToolsDialog : public QDialog {
  Q_OBJECT

public:
  FileTransferToolsDialog(QWidget *parent = nullptr);
  ~FileTransferToolsDialog();

  // 网络通信组件
  QTcpServer *P_TCPServerObject;     // TCP服务器
  QTcpSocket *P_TCPTextSocketObject; // 文本传输socket
  QTcpSocket *P_TCPFileSocketObject; // 文件传输socket

  // 文件传输相关变量
  qint64 m_FileTotalBytes;    // 文件总字节数
  qint64 m_FileBytesReceived; // 已接收字节数
  qint64 m_FileNameSizes;     // 文件名长度
  QString m_FileNames;        // 文件名
  QFile *m_LocalFiles;        // 本地文件对象
  QByteArray m_InBlocks;      // 接收数据缓冲区

  // 功能方法
  QString GetLocalIpAddressFunc();  // 获取本地IP地址
  void paintEvent(QPaintEvent *ev); // 重绘事件处理
  void setupUIStyle();              // 设置UI样式

protected:
  void closeEvent(QCloseEvent *event); // 窗口关闭事件

private slots:
  // 服务器槽函数
  void OnNewConnectionFunc(); // 处理新连接

  // 文本通道槽函数
  void OnClientDisconnectedFunc(); // 客户端断开连接
  void OnSocketReadyReadFunc();    // 读取文本数据

  // 文件通道槽函数
  void OnFileClientConnectedFunc();    // 文件客户端连接
  void OnFileClientDisconnectedFunc(); // 文件客户端断开

  // 业务逻辑槽函数
  void UpdateServerProgressFunc(); // 更新进度条
  void DisplayErrorInfoFunc(
      QAbstractSocket::SocketError socketError); // 显示错误信息

  // UI槽函数
  void on_pushButton_StartListening_clicked();
  void on_pushButton_StopListening_clicked();
  void on_pushButton_ExitSystem_clicked();
  void on_pushButton_ClearMsg_clicked();
  void on_pushButton_SendMsg_clicked();

private:
  Ui::FileTransferToolsDialog *ui;
};

#endif // FILETRANSFERTOOLSDIALOG_H

核心成员
QTcpServer:P_TCPServerObject监听客户端连接
QTcpSocket:P_TCPTextSocketObject处理聊天消息、P_TCPFileSocketObject处理文件传输
文件传输变量:文件总大小、已接收、文件名、缓存区QByteArray
主要功能函数
GetLocalIpAddressFunc():获取本地IPv4地址
paintEvent():设置背景
closeEvent():停止监听
主要槽函数
连接:OnNewConnectionFunc:接受新的客户端连接(区分文本通道和文件通道)
文本通道相关:OnSocketReadyReadFunc、OnClientDisconnectedFunc
文件通道:UpdateServerProgressFunc、OnFileClientDisconnectedFunc
其它:启动/停止监听、清空消息、发送消息


3. filetransfertoolsdialog.cpp(服务器实现)

//filetransfertoolsdialog.cpp
#include "filetransfertoolsdialog.h"
#include "ui_filetransfertoolsdialog.h"

FileTransferToolsDialog::FileTransferToolsDialog(QWidget *parent)
    : QDialog(parent), ui(new Ui::FileTransferToolsDialog) {
  ui->setupUi(this);

  // 设置UI样式
  setupUIStyle();

  setFixedSize(this->width(), this->height());

  // 初始化文件传输变量
  m_FileTotalBytes = 0;
  m_FileBytesReceived = 0;
  m_FileNameSizes = 0;

  // 创建TCP服务器和套接字
  P_TCPServerObject = new QTcpServer(this);
  connect(P_TCPServerObject, SIGNAL(newConnection()), this,
          SLOT(OnNewConnectionFunc()));

  P_TCPTextSocketObject = new QTcpSocket(this);
  P_TCPFileSocketObject = new QTcpSocket(this);

  // 设置IP地址
  QString strIp = GetLocalIpAddressFunc();
  ui->comboIP->addItem(strIp);
  qDebug() << strIp;

  // 初始化按钮状态
  ui->pushButton_SendMsg->setEnabled(false);
  ui->pushButton_StopListening->setEnabled(false);
  ui->pushButton_StartListening->setEnabled(true);
}

FileTransferToolsDialog::~FileTransferToolsDialog() { delete ui; }

// 设置UI样式
void FileTransferToolsDialog::setupUIStyle() {
  QStringList labels = {"label_server", "label_port", "groupBox_msglist",
                        "groupBox_sendmsg", "label_fileprogress"};

  for (const QString &labelName : labels) {
    QWidget *widget = findChild<QWidget *>(labelName);
    if (widget) {
      QPalette palette = widget->palette();
      palette.setColor(QPalette::WindowText, Qt::yellow);
      widget->setPalette(palette);

      QFont font = widget->font();
      font.setBold(true);
      widget->setFont(font);
    }
  }
}

QString FileTransferToolsDialog::GetLocalIpAddressFunc() {
  QString hostName = QHostInfo::localHostName();
  QHostInfo hostInfo = QHostInfo::fromName(hostName);

  QString StrLocalIP = "";

  QList<QHostAddress> addList = hostInfo.addresses();
  if (!addList.isEmpty()) {
    for (int i = 0; i < addList.count(); i++) {
      QHostAddress aHost = addList.at(i);
      if (aHost.protocol() == QAbstractSocket::IPv4Protocol) {
        StrLocalIP = aHost.toString();
        break;
      }
    }
  }
  return StrLocalIP;
}

void FileTransferToolsDialog::paintEvent(QPaintEvent *ev) {
  QPainter painter(this);
  painter.drawPixmap(rect(), QPixmap(":/new/prefix1/images/bgpng4.png"),
                     QRect());
}

void FileTransferToolsDialog::closeEvent(QCloseEvent *event) {
  if (P_TCPServerObject->isListening()) {
    P_TCPServerObject->close();
  }
  event->accept();
}

void FileTransferToolsDialog::OnNewConnectionFunc() {
  // 处理文本传输通道
  if (P_TCPTextSocketObject->state() == QAbstractSocket::UnconnectedState) {
    P_TCPTextSocketObject = P_TCPServerObject->nextPendingConnection();

    connect(P_TCPTextSocketObject, SIGNAL(disconnected()), this,
            SLOT(OnClientDisconnectedFunc()));
    connect(P_TCPTextSocketObject, SIGNAL(readyread()), this,
            SLOT(OnSocketReadyReadFunc()));
    ui->plainTextEdit_ListMsg->appendPlainText("\n[有客户端连接]*******");
    return;
  }

  // 处理文件传输通道
  if (P_TCPFileSocketObject->state() == QAbstractSocket::UnconnectedState) {
    P_TCPFileSocketObject = P_TCPServerObject->nextPendingConnection();

    connect(P_TCPFileSocketObject, SIGNAL(connected()), this,
            SLOT(OnFileClientConnectedFunc()));
    connect(P_TCPFileSocketObject, SIGNAL(disconnected()), this,
            SLOT(OnFileClientDisconnectedFunc()));
    connect(P_TCPFileSocketObject, SIGNAL(readyread()), this,
            SLOT(UpdateServerProgressFunc()));
    connect(P_TCPFileSocketObject, SIGNAL(error(QAbstractSocket::SocketError)),
            this, SLOT(DisplayErrorInfoFunc));
  }

  ui->plainTextEdit_ListMsg->appendPlainText("\n[有客户端连接]*******");
}

void FileTransferToolsDialog::OnClientDisconnectedFunc() {
  P_TCPTextSocketObject->close();
  ui->plainTextEdit_ListMsg->appendPlainText(
      "[********客户端已断开和服务器的连接********");
}

void FileTransferToolsDialog::OnSocketReadyReadFunc() {
  QDateTime currentDateTime = QDateTime::currentDateTime();
  QString strtemp = currentDateTime.toString("yyyy/mm/dd hh:mm:ss");

  while (P_TCPTextSocketObject->canReadLine()) {
    ui->plainTextEdit_ListMsg->appendPlainText(
        "[客户端消息" + strtemp + "]:" + P_TCPTextSocketObject->readLine());
  }
}

void FileTransferToolsDialog::OnFileClientConnectedFunc() {
  ui->plainTextEdit_ListMsg->appendPlainText("[文件传输通道已连接]");
}

void FileTransferToolsDialog::OnFileClientDisconnectedFunc() {
  P_TCPFileSocketObject->close();
}

void FileTransferToolsDialog::UpdateServerProgressFunc() {
  QString strtemp =
      QDateTime::currentDateTime().toString("yyyy/mm/dd hh:mm:ss");
  QDataStream in(P_TCPFileSocketObject);

  // 接收文件头信息
  if (m_FileBytesReceived <= sizeof(qint64) * 2) {
    if ((P_TCPFileSocketObject->bytesAvailable() >= sizeof(qint64) * 2) &&
        (m_FileNameSizes == 0)) {
      in >> m_FileTotalBytes >> m_FileNameSizes;
      m_FileBytesReceived += sizeof(qint64) * 2;
    }

    if ((P_TCPFileSocketObject->bytesAvailable() >= m_FileNameSizes) &&
        (m_FileNameSizes != 0)) {
      in >> m_FileNames;
      ui->plainTextEdit_EditMsg->appendPlainText(
          tr("[%1 开始接收文件 %2 ...]").arg(strtemp).arg(m_FileNames));
      m_FileBytesReceived += m_FileNameSizes;

      m_LocalFiles =
          new QFile(QApplication::applicationDirPath() + "\\" + m_FileNames);
      if (!m_LocalFiles->open(QFile::WriteOnly)) {
        qDebug() << "文件打开失败,请重新检查!";
        delete m_LocalFiles;
        m_LocalFiles = nullptr;
        return;
      }
    } else {
      return;
    }
  }

  // 接收文件内容
  if (m_FileBytesReceived < m_FileTotalBytes) {
    m_InBlocks = P_TCPFileSocketObject->readAll();
    m_LocalFiles->write(m_InBlocks);
    m_FileBytesReceived += m_InBlocks.size();
    m_InBlocks.clear();

    ui->progressBar->setMaximum(m_FileTotalBytes);
    ui->progressBar->setValue(m_FileBytesReceived);
  }

  // 传输完成处理
  if (m_FileBytesReceived == m_FileTotalBytes) {
    m_LocalFiles->close();
    ui->plainTextEdit_ListMsg->appendPlainText(
        QString("[%1 文件 %2 接收成功]").arg(strtemp).arg(m_FileNames));

    m_FileTotalBytes = 0;
    m_FileBytesReceived = 0;
    m_FileNameSizes = 0;

    delete m_LocalFiles;
    m_LocalFiles = nullptr;
  }
}

void FileTransferToolsDialog::DisplayErrorInfoFunc(
    QAbstractSocket::SocketError socketError) {
  P_TCPFileSocketObject->close();
  ui->progressBar->reset();
}

// UI槽函数
void FileTransferToolsDialog::on_pushButton_StartListening_clicked() {
  QString strIp = ui->comboIP->currentText();
  quint16 port = ui->spinPort->value();

  if (P_TCPServerObject->listen(QHostAddress(strIp), port)) {
    ui->plainTextEdit_ListMsg->appendPlainText("[服务器开始监听]");
    ui->plainTextEdit_ListMsg->appendPlainText(
        "[IP地址:" + strIp + "][端口:" + QString::number(port) + "]");

    ui->pushButton_StartListening->setEnabled(false);
    ui->pushButton_StopListening->setEnabled(true);
    ui->pushButton_SendMsg->setEnabled(true);
  }
}

void FileTransferToolsDialog::on_pushButton_StopListening_clicked() {
  if (P_TCPServerObject->isListening()) {
    P_TCPServerObject->close();
    ui->plainTextEdit_ListMsg->appendPlainText("[服务器已停止监听]");

    ui->pushButton_StartListening->setEnabled(true);
    ui->pushButton_StopListening->setEnabled(false);
    ui->pushButton_SendMsg->setEnabled(false);
  }
}

void FileTransferToolsDialog::on_pushButton_ExitSystem_clicked() {
  this->close();
}

void FileTransferToolsDialog::on_pushButton_ClearMsg_clicked() {
  ui->plainTextEdit_ListMsg->clear();
}

void FileTransferToolsDialog::on_pushButton_SendMsg_clicked() {
  QString strMsg = ui->plainTextEdit_EditMsg->toPlainText().trimmed();
  if (strMsg.isEmpty()) {
    QMessageBox::critical(this, "错误", "系统提示:消息内容不能为空!");
    return;
  }

  QString strtemp =
      QDateTime::currentDateTime().toString("yyyy/mm/dd hh:mm:ss");

  ui->plainTextEdit_ListMsg->appendPlainText("[服务器消息 " + strtemp +
                                             "]:" + strMsg);
  P_TCPTextSocketObject->write((strMsg + "\n").toUtf8());
  ui->plainTextEdit_EditMsg->clear();
}

通过 QTcpServer + 双 socket(分别负责消息通道与文件通道),保证了消息与文件传输互不干扰。文件接收过程分阶段处理:文件头 ---文件数据 ---完成确认。

FileTransferToolsDialog::FileTransferToolsDialog(QWidget *parent)
    : QDialog(parent), ui(new Ui::FileTransferToolsDialog) {
  ...//UI界面、窗口尺寸部分
  // 初始化文件传输变量
  m_FileTotalBytes = 0;
  m_FileBytesReceived = 0;
  m_FileNameSizes = 0;

  // 创建TCP服务器和套接字
  P_TCPServerObject = new QTcpServer(this);
  connect(P_TCPServerObject, SIGNAL(newConnection()), this,
          SLOT(OnNewConnectionFunc()));

  P_TCPTextSocketObject = new QTcpSocket(this);
  P_TCPFileSocketObject = new QTcpSocket(this);

  // 设置IP地址
  QString strIp = GetLocalIpAddressFunc();
  ui->comboIP->addItem(strIp);
  qDebug() << strIp;

  
  // 初始化按钮状态
  ui->pushButton_SendMsg->setEnabled(false);
  ui->pushButton_StopListening->setEnabled(false);
  ui->pushButton_StartListening->setEnabled(true);
}

初始化UI界面、窗口尺寸、网络组件以及按钮状态。
m_FileTotalBytes、m_FileBytesReceived等文件传输变量清零。
P_TCPServerObject作为监听端。connect()连接信号槽函数,当新连接到来newConnection()时,自动调用OnNewConnectionFunc()进行处理;
创建两个socket,分别负责消息通道与文件通道
用GetLocalIpAddressFunc()获取IPv4,填入comboIP,设置IP地址。
按钮状态,未监听时,只允许“开始监听”,禁用“停止监听”和“发送消息”。

void FileTransferToolsDialog::OnNewConnectionFunc() {
  // 处理文本传输通道
  if (P_TCPTextSocketObject->state() == QAbstractSocket::UnconnectedState) {
    P_TCPTextSocketObject = P_TCPServerObject->nextPendingConnection();

    connect(P_TCPTextSocketObject, SIGNAL(disconnected()), this,
            SLOT(OnClientDisconnectedFunc()));
    connect(P_TCPTextSocketObject, SIGNAL(readyread()), this,
            SLOT(OnSocketReadyReadFunc()));
  ui->plainTextEdit_ListMsg->appendPlainText("\n[有客户端连接]*******");

    return;
  }

  // 处理文件传输通道
  if (P_TCPFileSocketObject->state() == QAbstractSocket::UnconnectedState) {
    P_TCPFileSocketObject = P_TCPServerObject->nextPendingConnection();

    connect(P_TCPFileSocketObject, SIGNAL(connected()), this,
            SLOT(OnFileClientConnectedFunc()));
    connect(P_TCPFileSocketObject, SIGNAL(disconnected()), this,
            SLOT(OnFileClientDisconnectedFunc()));
    connect(P_TCPFileSocketObject, SIGNAL(readyread()), this,
            SLOT(UpdateServerProgressFunc()));
    connect(P_TCPFileSocketObject, SIGNAL(error(QAbstractSocket::SocketError)),
            this, SLOT(DisplayErrorInfoFunc));
  }

  ui->plainTextEdit_ListMsg->appendPlainText("\n[有客户端连接]*******");
}

将QTcpServer队列里的新到连接,按文本通道、文件通道的socket的顺序先后分配出去,并绑定对应的槽函数。
优先分配文本套接字。若P_TCPTextSocketObject仍是UnconnectedState,用nextPendingConnection()取出队首的QTcpSocket(连接),进行赋值。并用connected()绑定信号和槽函数,随后立即return。若文本套接字已分配,再分配文件套接字。

void FileTransferToolsDialog::UpdateServerProgressFunc() {
  QString strtemp =
      QDateTime::currentDateTime().toString("yyyy/mm/dd hh:mm:ss");
  QDataStream in(P_TCPFileSocketObject);

  // 接收文件头信息
  if (m_FileBytesReceived <= sizeof(qint64) * 2) {
    if ((P_TCPFileSocketObject->bytesAvailable() >= sizeof(qint64) * 2) &&
        (m_FileNameSizes == 0)) {
      in >> m_FileTotalBytes >> m_FileNameSizes;
      m_FileBytesReceived += sizeof(qint64) * 2;
    }

    if ((P_TCPFileSocketObject->bytesAvailable() >= m_FileNameSizes) &&
        (m_FileNameSizes != 0)) {
      in >> m_FileNames;
      ui->plainTextEdit_EditMsg->appendPlainText(
          tr("[%1 开始接收文件 %2 ...]").arg(strtemp).arg(m_FileNames));
      m_FileBytesReceived += m_FileNameSizes;

      m_LocalFiles =
          new QFile(QApplication::applicationDirPath() + "\\" + m_FileNames);
      if (!m_LocalFiles->open(QFile::WriteOnly)) {
        qDebug() << "文件打开失败,请重新检查!";
        delete m_LocalFiles;
        m_LocalFiles = nullptr;
        return;
      }
    } else {
      return;
    }
  }

  // 接收文件内容
  if (m_FileBytesReceived < m_FileTotalBytes) {
    m_InBlocks = P_TCPFileSocketObject->readAll();
    m_LocalFiles->write(m_InBlocks);
    m_FileBytesReceived += m_InBlocks.size();
    m_InBlocks.clear();

    ui->progressBar->setMaximum(m_FileTotalBytes);
    ui->progressBar->setValue(m_FileBytesReceived);
  }

  // 传输完成处理
  if (m_FileBytesReceived == m_FileTotalBytes) {
    m_LocalFiles->close();
    ui->plainTextEdit_ListMsg->appendPlainText(
        QString("[%1 文件 %2 接收成功]").arg(strtemp).arg(m_FileNames));

    m_FileTotalBytes = 0;
    m_FileBytesReceived = 0;
    m_FileNameSizes = 0;

    delete m_LocalFiles;
    m_LocalFiles = nullptr;
  }
}

处理文件传输,分阶段接收文件头信息、写入文件内容并更新进度条,直到接收完整个文件。此部分的顺序以及数据类型必须与客户端完全匹配:文件大小(qint64)---文件名长度(qint64)---文件名(QString)---文件内容(二进制)。
1.解析文件头(总文件大小、文件名大小、文件名)
用QDataStream对象in处理有固定结构的文件头。检查当前已接收的字节数是否小于两个qint64(总文件大小和文件名大小)。若接收缓冲区数据量充足,先取出m_FileTotalBytes(文件总字节数)和m_FileNameSizes(文件名长度),同时累加到已接收m_FileBytesReceived。再根据m_FileNameSizes提取文件名m_FileNames,在UI中输出提示信息。最后应用程序运行目录下新建同名文件newQFile(...),准备写入。并作检验,若此文件打开失败,释放内存并置空指针。
2.接收文件内容
当m_FileBytesReceived<m_FileTotalBytes时:从P_TCPFileSocketObject中一次性读取所有可用数据readAll(),写入m_LocalFiles,更新进度条.
3.传输完成处理
若已接收字节数与总字节数相等,关闭并销毁m_LocalFiles,在消息列表输出“文件接收成功”。重置文件传输变量,以便下一次传输。

QString FileTransferToolsDialog::GetLocalIpAddressFunc() {
  QString hostName = QHostInfo::localHostName();
  QHostInfo hostInfo = QHostInfo::fromName(hostName);

  QString StrLocalIP = "";

  QList<QHostAddress> addList = hostInfo.addresses();
  if (!addList.isEmpty()) {
    for (int i = 0; i < addList.count(); i++) {
      QHostAddress aHost = addList.at(i);
      if (aHost.protocol() == QAbstractSocket::IPv4Protocol) {
        StrLocalIP = aHost.toString();
        break;
      }
    }
  }
  return StrLocalIP;
}

自动获取本机可用的IPv4地址。
通过QHostInfo::localHostName()获取主机名,再调用QHostInfo::fromName()得到该主机对应的所有信息。遍历hostInfo.addresses()返回的QList<QHostAddress>,筛选其中符合QAbstractSocket::IPv4Protocol的地址。一旦找到IPv4地址,就将其转换为字符串并返回。若列表为空或未找到IPv4,返回空字符串("")。

void FileTransferToolsDialog::on_pushButton_SendMsg_clicked() {
  QString strMsg = ui->plainTextEdit_EditMsg->toPlainText().trimmed();
  if (strMsg.isEmpty()) {
    QMessageBox::critical(this, "错误", "系统提示:消息内容不能为空!");
    return;
  }

  QString strtemp =
      QDateTime::currentDateTime().toString("yyyy/mm/dd hh:mm:ss");

  ui->plainTextEdit_ListMsg->appendPlainText("[服务器消息 " + strtemp +
                                             "]:" + strMsg);
  P_TCPTextSocketObject->write((strMsg + "\n").toUtf8());
  ui->plainTextEdit_EditMsg->clear();
}

处理用户输入并通过TCP套接字将消息发送到客户端,同时在消息窗口中显示发送记录。
从plainTextEdit_EditMsg输入框中读取用户输入,调用trimmed()去除首尾空格。若输入为空,用QMessageBox::critical()弹出提示框,阻止执行。生成带时间戳的消息记录,在消息列表框plainTextEdit_ListMsg中显示发送的消息。
调用P_TCPTextSocketObject->write()将消息内容以UTF-8编码形式发送给客户端。最后清空输入框plainTextEdit_EditMsg。

客户端部分

基于Qt的客户端实现,核心功能包括:消息聊天、文件传输、进度条显示、系统托盘等。

1. main.cpp(客户端入口)

//main.cpp
#include "maindialog.h"

#include <QApplication>

int main(int argc, char *argv[]) {
  QApplication a(argc, argv);
  MainDialog w;

  // 显示窗口最小化按钮
  w.setWindowFlags(w.windowFlags() | Qt::WindowMinimizeButtonHint);

  w.show();
  return a.exec();
}

初始化Qt应用,创建并显示MainDialog窗口。用QApplication启动事件循环。

2. maindialog.h(定义客户端对话框类)

//maindialog.h
#ifndef MAINDIALOG_H
#define MAINDIALOG_H

#include <QAction>
#include <QDataStream>
#include <QDateTime>
#include <QDialog>
#include <QFile>
#include <QFileDevice>
#include <QFileDialog>
#include <QHostAddress>
#include <QHostInfo>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QPainter>
#include <QSystemTrayIcon>
#include <QTcpSocket>

QT_BEGIN_NAMESPACE
namespace Ui {
class MainDialog;
}
QT_END_NAMESPACE

class MainDialog : public QDialog {
  Q_OBJECT

public:
  MainDialog(QWidget *parent = nullptr);
  ~MainDialog();

private:
  // 网络通信组件
  QTcpSocket *m_TcpMsgClient;  // 聊天消息socket
  QTcpSocket *m_TcpFileClient; // 文件传输socket

  // 文件传输相关变量
  QFile *m_LocalFiles;       // 本地文件对象
  qint64 m_TotalBytes;       // 文件总大小
  qint64 m_BytesWrites;      // 已发送字节数
  qint64 m_BytesToWrites;    // 剩余字节数
  qint64 m_LoadSizes;        // 每次发送大小
  QString m_FileNames;       // 文件名及路径
  QByteArray m_OutDataBlock; // 发送数据缓冲区

  // 系统托盘组件
  QSystemTrayIcon *MySystemTrays; // 系统托盘
  QMenu *pContextMenu;            // 托盘菜单
  QAction *qMiniAction;           // 最小化动作
  QAction *qRestoreAction;        // 恢复动作
  QAction *qExitAction;           // 退出动作

  // 功能方法
  void paintEvent(QPaintEvent *event); // 重绘事件处理
  QString GetLocalIPAddress();         // 获取本机IP地址
  void closeEvent(QCloseEvent *event); // 窗口关闭事件

private slots:
  // 网络连接槽函数
  void OnConnectedFunc();                              // 客户端连接成功
  void OnDisconnectedFunc();                           // 客户端断开连接
  void OnSocketReadyReadFunc();                        // 读取服务器数据
  void DisplayErrorFunc(QAbstractSocket::SocketError); // 显示错误信息

  // 文件传输槽函数
  void UpdateClientProgressFunc(qint64); // 更新进度条

  // 系统托盘槽函数
  void MySystemTrayFunc(); // 系统托盘处理

  // UI槽函数
  void on_pushButton_ConnectServer_clicked();
  void on_pushButton_DisconnectServer_clicked();
  void on_pushButton_Exit_clicked();
  void on_pushButton_SendMsg_clicked();
  void on_pushButton_ClearMsg_clicked();
  void on_pushButton_SelectFile_clicked();
  void on_pushButton_SendFile_clicked();

private:
  Ui::MainDialog *ui;
};

#endif // MAINDIALOG_H

核心成员(几乎完全与服务器端相对应)
两个QTcpSocket:m_TcpMsgClient处理聊天消息、m_TcpFileClient处理文件传输
文件传输相关变量:文件总大小、已发送字节数、剩余字节、每次发送大小、缓存区等
系统托盘相关成员:托盘图标、菜单和相关动作(最小化、恢复、退出)
槽函数
连接与断开服务器:(OnConnectedFunc、OnDisconnectedFunc)
消息收发:(OnSocketReadyReadFunc、on_pushButton_SendMsg_clicked)
文件选择与发送:(on_pushButton_SelectFile_clicked、on_pushButton_SendFile_clicked)
更新进度条:(UpdateClientProgressFunc)
异常处理:(DisplayErrorFunc)

3. maindialog.cpp(客户端实现)

//maindialog.cpp
#include "maindialog.h"
#include "ui_maindialog.h"

MainDialog::MainDialog(QWidget *parent)
    : QDialog(parent), ui(new Ui::MainDialog) {
  ui->setupUi(this);
  ui->plainTextEdit_MsgList->setReadOnly(true);

  setFixedSize(width(), height());

  // 初始化文件传输变量
  m_TotalBytes = 0;
  m_BytesWrites = 0;
  m_BytesToWrites = 0;
  m_LoadSizes = 1024 * 4;

  // 创建TCP套接字
  m_TcpMsgClient = new QTcpSocket(this);
  m_TcpFileClient = new QTcpSocket(this);

  // 设置IP地址
  QString strLocalIPs = GetLocalIPAddress();
  QString strIP = ("本地IP地址:" + strLocalIPs);
  ui->label_LocalIP->setText(strIP);
  ui->comboBox_ServerIP->addItem(strLocalIPs);

  // 连接信号槽
  connect(m_TcpMsgClient, SIGNAL(connected()), this, SLOT(OnConnectedFunc()));
  connect(m_TcpMsgClient, SIGNAL(disconnected()), this,
          SLOT(OnDisconnectedFunc()));
  connect(m_TcpMsgClient, SIGNAL(readyRead()), this,
          SLOT(OnSocketReadyReadFunc()));
  connect(m_TcpFileClient, SIGNAL(bytesWritten(qint64)), this,
          SLOT(UpdateClientProgressFunc(qint64)));
  connect(m_TcpFileClient, SIGNAL(error(QAbstractSocket::SocketError)), this,
          SLOT(DisplayErrorFunc(QAbstractSocket::SocketError)));

  MySystemTrayFunc();
}

MainDialog::~MainDialog() { delete ui; }

void MainDialog::paintEvent(QPaintEvent *event) {
  QPainter painter(this);
  painter.drawPixmap(rect(), QPixmap(":/new/prefix1/images/bgpng.png"),
                     QRect());
}

QString MainDialog::GetLocalIPAddress() {
  QString strHostName = QHostInfo::localHostName();
  QHostInfo HostInfos = QHostInfo::fromName(strHostName);

  QString strLocalIP = "";

  QList<QHostAddress> addressList = HostInfos.addresses();
  if (!addressList.isEmpty()) {
    for (int i = 0; i < addressList.count(); i++) {
      QHostAddress sHost = addressList.at(i);
      if (sHost.protocol() == QAbstractSocket::IPv4Protocol) {
        strLocalIP = sHost.toString();
        break;
      }
    }
  }

  return strLocalIP;
}

void MainDialog::closeEvent(QCloseEvent *event) {
  if (m_TcpMsgClient->state() == QAbstractSocket::ConnectedState)
    m_TcpMsgClient->disconnectFromHost();

  if (m_TcpFileClient->state() == QAbstractSocket::ConnectedState)
    m_TcpFileClient->disconnectFromHost();

  event->accept();
}

void MainDialog::OnConnectedFunc() {
  ui->plainTextEdit_MsgList->appendPlainText(
      "[*****客户端与服务器连接成功*****]");
  ui->plainTextEdit_MsgList->appendPlainText(
      "IP Address:" + m_TcpMsgClient->peerAddress().toString() +
      "[Port:" + QString::number(m_TcpMsgClient->peerPort()) + "]");

  ui->pushButton_ConnectServer->setEnabled(false);
  ui->pushButton_DisconnectServer->setEnabled(true);
}

void MainDialog::OnDisconnectedFunc() {
  ui->plainTextEdit_MsgList->appendPlainText(
      "[客户端已断开与服务器的连接!]\n");

  ui->pushButton_ConnectServer->setEnabled(true);
  ui->pushButton_DisconnectServer->setEnabled(false);
}

void MainDialog::OnSocketReadyReadFunc() {
  QString strDateTimes =
      QDateTime::currentDateTime().toString("yyyy/mm/dd hh:mm:ss");
  while (m_TcpMsgClient->canReadLine())
    ui->plainTextEdit_MsgList->appendPlainText(
        "[服务器消息 " + strDateTimes + "]" + m_TcpMsgClient->readLine());
}

void MainDialog::MySystemTrayFunc() {
  QIcon qIcons(":/new/prefix1/images/logo.ico");
  MySystemTrays = new QSystemTrayIcon(this);
  MySystemTrays->setIcon(qIcons);
  MySystemTrays->setToolTip("Tcp客户端!");

  qMiniAction = new QAction("客户端最小化", this);
  connect(qMiniAction, SIGNAL(triggered()), this, SLOT(hide()));

  qRestoreAction = new QAction("恢复客户端", this);
  connect(qRestoreAction, SIGNAL(triggered()), this, SLOT(showNormal()));

  qExitAction = new QAction("退出系统", this);
  connect(qExitAction, SIGNAL(triggered()), this, SLOT(quit()));

  pContextMenu = new QMenu(this);
  pContextMenu->addAction(qMiniAction);
  pContextMenu->addAction(qRestoreAction);
  pContextMenu->addAction(qExitAction);

  MySystemTrays->setContextMenu(pContextMenu);
  // MySystemTrays->show();
}

void MainDialog::UpdateClientProgressFunc(qint64 numBytes) {
  QString strDateTimes =
      QDateTime::currentDateTime().toString("yyyy/mm/dd hh:mm:ss");

  m_BytesWrites += (int)numBytes;
  if (m_BytesToWrites > 0) {
    // 从文件中读取数据块(4KB)
    m_OutDataBlock = m_LocalFiles->read(qMin(m_BytesToWrites, m_LoadSizes));

    // 发送数据并更新剩余字节数
    m_BytesToWrites -= (int)m_TcpFileClient->write(m_OutDataBlock);
    m_OutDataBlock.resize(0);
  } else {
    m_LocalFiles->close();
  }

  // 更新进度条
  ui->progressBar->setMaximum(m_TotalBytes);
  ui->progressBar->setValue(m_BytesWrites);

  if (m_BytesWrites == m_TotalBytes) {
    m_LocalFiles->close();
    ui->plainTextEdit_MsgList->appendPlainText(
        QString("[%1 文件已传输完成!!!路径为:%2]")
            .arg(strDateTimes)
            .arg(m_FileNames));
  }
}

void MainDialog::DisplayErrorFunc(QAbstractSocket::SocketError) {
  m_TcpFileClient->close();
  ui->progressBar->reset();
}

// UI槽函数
void MainDialog::on_pushButton_ConnectServer_clicked() {
  QString ipaddress = ui->comboBox_ServerIP->currentText();
  quint16 port = ui->spinBox_ServerPort->value();

  // 连接消息传输套接字
  m_TcpMsgClient->connectToHost(ipaddress, port);

  // 连接文件传输套接字
  m_TcpFileClient->connectToHost(ipaddress, port);
}

void MainDialog::on_pushButton_DisconnectServer_clicked() {
  // 断开消息传输套接字
  if (m_TcpMsgClient->state() == QAbstractSocket::ConnectedState)
    m_TcpMsgClient->disconnectFromHost();

  // 断开文件传输套接字
  if (m_TcpFileClient->state() == QAbstractSocket::ConnectedState)
    m_TcpFileClient->disconnectFromHost();
}

void MainDialog::on_pushButton_Exit_clicked() {
  m_TcpMsgClient->close();
  m_TcpFileClient->close();
  this->close();
}

void MainDialog::on_pushButton_SendMsg_clicked() {
  QString strDateTimes =
      QDateTime::currentDateTime().toString("yyyy/mm/dd hh:mm:ss");
  QString strMsg = ui->plainTextEdit_SendMsg->toPlainText();

  if (strMsg.isEmpty()) {
    QMessageBox::critical(this, "错误", "发消息为空!", QMessageBox::Yes);
    ui->plainTextEdit_SendMsg->setFocus();
    return;
  }

  ui->plainTextEdit_MsgList->appendPlainText("[客户端消息 " + strDateTimes +
                                             "]:" + strMsg);
  ui->plainTextEdit_SendMsg->clear();
  ui->plainTextEdit_SendMsg->setFocus();

  QByteArray bytearray = strMsg.toUtf8();
  bytearray.append('\n');
  m_TcpMsgClient->write(bytearray);
}

void MainDialog::on_pushButton_ClearMsg_clicked() {
  ui->plainTextEdit_MsgList->clear();
}

void MainDialog::on_pushButton_SelectFile_clicked() {
  QString strDateTimes =
      QDateTime::currentDateTime().toString("yyyy/mm/dd hh:mm:ss");

  m_FileNames = QFileDialog::getOpenFileName(this, "请选择要传输的文件");
  if (!m_FileNames.isEmpty()) {
    ui->plainTextEdit_MsgList->appendPlainText(
        QString("[%1 客户端打开文件:%2 成功!]")
            .arg(strDateTimes)
            .arg(m_FileNames));
    ui->pushButton_SendFile->setEnabled(true);
  }
}

void MainDialog::on_pushButton_SendFile_clicked() {
  // 检查文件传输套接字是否连接
  if (m_TcpFileClient->state() != QAbstractSocket::ConnectedState) {
    QString ipaddress = ui->comboBox_ServerIP->currentText();
    quint16 port = ui->spinBox_ServerPort->value();
    m_TcpFileClient->connectToHost(ipaddress, port);
    return; // 等待连接完成后再发送
  }

  m_LocalFiles = new QFile(m_FileNames);
  if (!m_LocalFiles->open(QFile::ReadOnly)) {
    qDebug() << "打开文件错误,请检查!" << endl;
    return;
  }

  m_TotalBytes = m_LocalFiles->size();

  QDataStream sendDataOut(&m_OutDataBlock, QIODevice::WriteOnly);
  QString strCurrentFileName =
      m_FileNames.right(m_FileNames.size() - m_FileNames.lastIndexOf('/') - 1);

  // 写入文件头信息
  sendDataOut << qint64(0) << qint64(0) << strCurrentFileName;
  m_TotalBytes += m_OutDataBlock.size();

  sendDataOut.device()->seek(0);
  sendDataOut << m_TotalBytes
              << qint64((m_OutDataBlock.size() - sizeof(qint64) * 2));

  m_BytesToWrites = m_TotalBytes - m_TcpFileClient->write(m_OutDataBlock);
  m_OutDataBlock.resize(0);
}

初始化客户端系统。设置UI界面,绑定网络通信变量与信号槽函数。

MainDialog::MainDialog(QWidget *parent)
    : QDialog(parent), ui(new Ui::MainDialog) {
  ui->setupUi(this);
  ui->plainTextEdit_MsgList->setReadOnly(true);

  setFixedSize(width(), height());

  // 初始化文件传输变量
  m_TotalBytes = 0;
  m_BytesWrites = 0;
  m_BytesToWrites = 0;
  m_LoadSizes = 1024 * 4;

  // 创建TCP套接字
  m_TcpMsgClient = new QTcpSocket(this);
  m_TcpFileClient = new QTcpSocket(this);

  // 设置IP地址
  QString strLocalIPs = GetLocalIPAddress();
  QString strIP = ("本机IP地址:" + strLocalIPs);
  ui->label_LocalIP->setText(strIP);
  ui->comboBox_ServerIP->addItem(strLocalIPs);

  // 连接信号槽
  connect(m_TcpMsgClient, SIGNAL(connected()), this, SLOT(OnConnectedFunc()));
  connect(m_TcpMsgClient, SIGNAL(disconnected()), this,
          SLOT(OnDisconnectedFunc()));
  connect(m_TcpMsgClient, SIGNAL(readyRead()), this,
          SLOT(OnSocketReadyReadFunc()));
  connect(m_TcpFileClient, SIGNAL(bytesWritten(qint64)), this,
          SLOT(UpdateClientProgressFunc(qint64)));
  connect(m_TcpFileClient, SIGNAL(error(QAbstractSocket::SocketError)), this,
          SLOT(DisplayErrorFunc(QAbstractSocket::SocketError)));

  MySystemTrayFunc();
}


初始化文件传输相关的内部变量(m_TotalBytes、m_BytesWrites等)。
新建两个TCP套接字分别用于文本消息、和文件传输处理(与服务器端相对应)。
调用GetLocalIPAddress()获取本机IP地址,并将其显示在界面标签label_LocalIP中,并加入到下拉框comboBox_ServerIP。当m_TcpMsgClient连接成功、断开连接、有数据可读、发生错误或文件写入进度更新时时,触发相应的槽函数。
最后调用MySystemTrayFunc()初始化系统托盘功能。

void MainDialog::on_pushButton_SendFile_clicked() {
  m_LocalFiles = new QFile(m_FileNames);
  if (!m_LocalFiles->open(QFile::ReadOnly)) {
    qDebug() << "打开文件错误,请检查!" << endl;
    return;
  }

  m_TotalBytes = m_LocalFiles->size();

  QDataStream sendDataOut(&m_OutDataBlock, QIODevice::WriteOnly);
  QString strCurrentFileName =
      m_FileNames.right(m_FileNames.size() - m_FileNames.lastIndexOf('/') - 1);

  // 写入文件头信息
  sendDataOut << qint64(0) << qint64(0) << strCurrentFileName;
  m_TotalBytes += m_OutDataBlock.size(b);

  sendDataOut.device()->seek(0);
  sendDataOut << m_TotalBytes
              << qint64((m_OutDataBlock.size() - sizeof(qint64) * 2));

  m_BytesToWrites = m_TotalBytes - m_TcpFileClient->write(m_OutDataBlock);
  m_OutDataBlock.resize(0);
}

响应“发送文件”按钮的点击事件,准备本地文件并通过TCP套接字进行传输。
首先,根据用户选择的文件路径m_FileNames创建QFile对象m_LocalFiles,尝试以只读方式打开。若失败,输出提示错误信息并终止。
若成功,通过m_LocalFiles->size()获取文件大小,存入m_TotalBytes以便后续统计进度。用QDataStreamsendDataOut向缓存区m_OutDataBlock写入文件头信息(此部分与服务器端完全对应一致)随后,函数将文件的总大小(文件本身大小+文件头大小)和文件名大小回填到前两个占位字段中。使接收端首先读取到总大小与文件名信息。最后,通过m_TcpFileClient->write(m_OutDataBlock)将准备好的文件头一次性写入,并计算尚未发送的字节数m_BytesToWrites,同时清空m_OutDataBlock。

void MainDialog::UpdateClientProgressFunc(qint64 numBytes) {
  QString strDateTimes =
      QDateTime::currentDateTime().toString("yyyy/mm/dd hh:mm:ss");

  m_BytesWrites += (int)numBytes;
  if (m_BytesToWrites > 0) {
    // 从文件中读取数据块(4KB)
    m_OutDataBlock = m_LocalFiles->read(qMin(m_BytesToWrites, m_LoadSizes));

    // 发送数据并更新剩余字节数
    m_BytesToWrites -= (int)m_TcpFileClient->write(m_OutDataBlock);
    m_OutDataBlock.resize(0);
  } else {
    m_LocalFiles->close();
  }

  // 更新进度条
  ui->progressBar->setMaximum(m_TotalBytes);
  ui->progressBar->setValue(m_BytesWrites);

  if (m_BytesWrites == m_TotalBytes) {
    m_LocalFiles->close();
    ui->plainTextEdit_MsgList->appendPlainText(
        QString("[%1 文件已传输完成!!!路径为:%2]")
            .arg(strDateTimes)
            .arg(m_FileNames));
  }
}

处理文件发送过程中的UI进度更新。每当TCP套接字成功写入一部分数据时,该槽函数会被自动触发。
附加时间戳后,将本次写入的字节数累加到m_BytesWrites中,用于统计已发送的总量。如果还有未发送的字节,就从本地文件m_LocalFiles中读取一个数据块,大小为剩余字节数与缓存块大小m_LoadSizes(4KB)中的较小值。读取后的数据被写入m_TcpFileClient,并更新剩余待发送的字节数。完成后,清空m_OutDataBlock,以便下次使用。若剩余字节数为零,说明文件数据已经全部写入,此时关闭文件m_LocalFiles。
函数也会实时更新UI界面的进度条:progressBar的最大值设为文件总字节数m_TotalBytes,当前值设为已写入字节数m_BytesWrites。当m_BytesWrites与m_TotalBytes相等时,说明整个文件传输已经完成。此时关闭文件,并在消息显示区plainTextEdit_MsgList输出提示信息。

void MainDialog::MySystemTrayFunc() {
  QIcon qIcons(":/new/prefix1/images/logo.ico");
  MySystemTrays = new QSystemTrayIcon(this);
  MySystemTrays->setIcon(qIcons);
  MySystemTrays->setToolTip("Tcp客户端!");

  qMiniAction = new QAction("客户端最小化", this);
  connect(qMiniAction, SIGNAL(triggered()), this, SLOT(hide()));

  qRestoreAction = new QAction("恢复客户端", this);
  connect(qRestoreAction, SIGNAL(triggered()), this, SLOT(showNormal()));

  qExitAction = new QAction("退出系统", this);
  connect(qExitAction, SIGNAL(triggered()), this, SLOT(quit()));

  pContextMenu = new QMenu(this);
  pContextMenu->addAction(qMiniAction);
  pContextMenu->addAction(qRestoreAction);
  pContextMenu->addAction(qExitAction);

  MySystemTrays->setContextMenu(pContextMenu);
  MySystemTrays->show();
}

实现系统托盘功能。应用能在后台运行,并通过托盘图标进行操作。
通过QIcon设置了托盘图标logo.ico,并将其绑定到QSystemTrayIcon对象MySystemTrays,同时设置提示信息。接着,定义三个动作:qMiniAction(客户端最小化)、qRestoreAction(恢复客户端)、qExitAction(退出系统)。将三项动作加入到右键菜单pContextMenu,并通过MySystemTrays->setContextMenu()绑定到托盘图标。最后调用MySystemTrays->show(),使托盘图标正式显示在系统任务栏。


Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐