Qt 实战:即时通讯项目

本文将带你从 0 到 1 完成一个基于 Qt 的 即时通信项目(先用TCP做一个简单的即时通讯,后深度扩展【见Part 6】),覆盖服务端、客户端全流程开发与测试。

Qt学习路线:C++ Qt学习路线一条龙!(桌面开发&嵌入式开发)

Part1开发环境准备

  • Qt 版本:Qt 5.15.2(或 Qt 6.x,核心网络 API 兼容)
  • 编译器:MinGW 8.1.0(或 MSVC 2019/2022)
  • 操作系统:Windows 10(Linux/macOS 流程类似,仅路径、系统 API 细节有差异)

Part2项目架构解析

  • 服务端:实例化 QTcpServer → 监听指定 IP / 端口 → 捕获 newConnection 信号处理新连接 → 通过 QTcpSocket 与客户端双向收发数据
  • 客户端:实例化 QTcpSocket → 主动连接服务端 IP / 端口 → 利用 QTcpSocket 发送数据,并通过 readyRead 信号响应服务端发来的数据

Part3服务端开发

3.1、工程创建

打开 Qt Creator,新建Qt Widgets Application,命名为 TcpServerDemo。

3.2、头文件 tcpserver.h

定义服务端核心类,包含 QTcpServer(监听连接)、QTcpSocket(与客户端通信)及信号槽函数。

#ifndef TCPSERVER_H
#define TCPSERVER_H
#include <QWidget>
#include <QTcpServer>
#include <QTcpSocket>
#include <QDebug>
class TcpServer : public QWidget
{
    Q_OBJECT
public:
    explicit TcpServer(QWidget *parent = nullptr);
    ~TcpServer();
private slots:
    // 处理“新客户端连接”的槽函数
    void onNewConnection();
    // 处理“客户端发数据”的槽函数
    void onReadyRead();
    // 处理“客户端断开连接”的槽函数
    void onDisconnected();
private:
    QTcpServer *tcpServer;   // 服务端核心:负责监听、接收连接
    QTcpSocket *clientSocket; // 与单个客户端通信的套接字(若支持多客户端,可改用列表存储)
};
#endif // TCPSERVER_H

3.3、源文件 tcpserver.cpp

实现服务端 “初始化、监听、处理连接、收发数据” 的完整逻辑。

#include "tcpserver.h"
TcpServer::TcpServer(QWidget *parent)
    : QWidget(parent), tcpServer(nullptr), clientSocket(nullptr)
{
    // 1. 实例化 QTcpServer 对象
    tcpServer = new QTcpServer(this);
    // 2. 开始监听:指定“监听IP”和“端口”
    // QHostAddress::Any 表示监听所有网卡;端口可自定义(如 8888)
    if (!tcpServer->listen(QHostAddress::Any, 8888)) {
        qDebug() << "服务端监听失败:" << tcpServer->errorString();
    } else {
        qDebug() << "服务端启动成功,正在监听端口 8888...";
    }
    // 3. 连接信号:有新客户端连接时,触发 onNewConnection
    connect(tcpServer, &QTcpServer::newConnection, this, &TcpServer::onNewConnection);
}
TcpServer::~TcpServer()
{
    // 析构时关闭服务端和套接字
    if (tcpServer) tcpServer->close();
    if (clientSocket) clientSocket->close();
}
// 处理“新客户端连接”的逻辑
void TcpServer::onNewConnection()
{
    // 4. 获取“待处理的新连接”对应的套接字
    clientSocket = tcpServer->nextPendingConnection();
    qDebug() << "新客户端接入:IP=" << clientSocket->peerAddress().toString()
             << ",端口=" << clientSocket->peerPort();
    // 连接信号:客户端发数据时,触发 onReadyRead
    connect(clientSocket, &QTcpSocket::readyRead, this, &TcpServer::onReadyRead);
    // 连接信号:客户端断开时,触发 onDisconnected
    connect(clientSocket, &QTcpSocket::disconnected, this, &TcpServer::onDisconnected);
    // 服务端主动给新客户端发“欢迎消息”
    clientSocket->write("欢迎连接到 TCP 服务端!");
}
// 处理“客户端发数据”的逻辑
void TcpServer::onReadyRead()
{
    // 5. 读取客户端发来的“所有数据”
    QByteArray data = clientSocket->readAll();
    if (!data.isEmpty()) {
        qDebug() << "[服务端] 收到客户端数据:" << data;
        // 模拟“即时交互”:收到数据后,回复客户端
        clientSocket->write(QString("服务端已收到:%1").arg(QString(data)).toUtf8());
    }
}
// 处理“客户端断开连接”的逻辑
void TcpServer::onDisconnected()
{
    qDebug() << "[服务端] 客户端断开连接";
    // 断开后置空套接字,便于后续新连接复用
    clientSocket = nullptr;
}

Part4客户端开发

4.1、工程创建

新建Qt Widgets Application,命名为 TcpClientDemo。

4.2、头文件 tcpclient.h

定义客户端核心类,包含 QTcpSocket(与服务端通信)及信号槽函数。

#ifndef TCPCLIENT_H
#define TCPCLIENT_H
#include <QWidget>
#include <QTcpSocket>
#include <QDebug>
class TcpClient : public QWidget
{
    Q_OBJECT
public:
    explicit TcpClient(QWidget *parent = nullptr);
    ~TcpClient();
private slots:
    // 主动连接服务端
    void connectToServer();
    // 处理“服务端发数据”的槽函数
    void onReadyRead();
    // 处理“连接成功”的槽函数
    void onConnected();
    // 处理“连接错误”的槽函数
    void onError(QAbstractSocket::SocketError socketError);
private:
    QTcpSocket *tcpSocket; // 客户端与服务端通信的套接字
};
#endif // TCPCLIENT_H

4.3、源文件 tcpclient.cpp

实现客户端 “主动连接、收发数据、处理连接状态” 的完整逻辑。

#include "tcpclient.h"
TcpClient::TcpClient(QWidget *parent)
    : QWidget(parent), tcpSocket(nullptr)
{
    // 1. 实例化 QTcpSocket 对象
    tcpSocket = new QTcpSocket(this);
    // 连接信号:服务端发数据时,触发 onReadyRead
    connect(tcpSocket, &QTcpSocket::readyRead, this, &TcpClient::onReadyRead);
    // 连接信号:连接成功时,触发 onConnected
    connect(tcpSocket, &QTcpSocket::connected, this, &TcpClient::onConnected);
    // 连接信号:连接出错时,触发 onError
    connect(tcpSocket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error),
            this, &TcpClient::onError);
    // 2. 主动连接服务端(此处假设服务端 IP 为 127.0.0.1,端口为 8888)
    connectToServer();
}
TcpClient::~TcpClient()
{
    // 析构时关闭套接字
    if (tcpSocket) tcpSocket->close();
}
// 主动连接服务端的逻辑
void TcpClient::connectToServer()
{
    tcpSocket->abort(); // 断开可能存在的旧连接
    // 3. 连接到服务端的 IP 和端口
    tcpSocket->connectToHost("127.0.0.1", 8888);
}
// 处理“连接成功”的逻辑
void TcpClient::onConnected()
{
    qDebug() << "[客户端] 成功连接到服务端!";
    // 连接成功后,给服务端发“测试消息”
    tcpSocket->write("客户端已就绪,请求通信~");
}
// 处理“服务端发数据”的逻辑
void TcpClient::onReadyRead()
{
    // 4. 读取服务端发来的“所有数据”
    QByteArray data = tcpSocket->readAll();
    if (!data.isEmpty()) {
        qDebug() << "[客户端] 收到服务端数据:" << data;
        // 模拟“即时回复”:简单回复“收到”
        tcpSocket->write("收到,谢谢~");
    }
}
// 处理“连接错误”的逻辑
void TcpClient::onError(QAbstractSocket::SocketError socketError)
{
    qDebug() << "[客户端] 连接错误:" << tcpSocket->errorString();
}

Part5项目测试与运行

5.1、启动服务端

先运行 TcpServerDemo,控制台输出 服务端启动成功,正在监听端口 8888...,表示服务端已就绪。

5.2、启动客户端

再运行 TcpClientDemo,客户端会主动连接服务端:

  • 服务端控制台输出:新客户端接入:IP="127.0.0.1",端口=xxxx(xxxx 为客户端随机端口)。
  • 客户端控制台输出:[客户端] 成功连接到服务端!。

此时,双向即时通信自动触发:

  • 客户端发送 客户端已就绪,请求通信~ → 服务端收到后回复 服务端已收到:客户端已就绪,请求通信~。
  • 服务端启动时发送 欢迎连接到 TCP 服务端! → 客户端收到后回复 收到,谢谢~ → 服务端再收到并回复... 以此循环,实现 “即时交互”。

以上示例是 “最简 TCP 通信”,接下来我们继续往深度扩展:

  • 多客户端支持:服务端用 QList<QTcpSocket*> 存储所有客户端套接字,实现 “群聊” 或 “多设备通信”。
  • 界面化改造:添加 UI 控件(如输入框、按钮、聊天显示区),替代控制台输出,变成 “可视化聊天软件”。
  • 复杂数据传输:用 QDataStream 序列化 / 反序列化自定义对象(如 “用户信息”“文件片段”),实现结构化数据传输。
  • 断线重连 :客户端检测到断开后,定时触发 connectToServer() ,实现 “自动重连”。
  • 加密通信 :使用 QSslSocket 替代 QTcpSocket ,基于 SSL/TLS 加密传输,保障数据安全。
  • 语音 / 视频通话:扩展支持多媒体通信

分享Linux、Unix、C/C++后端开发、面试题等技术知识讲解

C++ Qt学习路线一条龙!(桌面开发&嵌入式开发)

art6多客户端支持实现

多客户端支持是即时通信系统的基础,我们需要管理多个客户端连接并实现消息转发功能。

服务端核心类设计:

tcpserver.cpp

#include "tcpserver.h"
#include <QDateTime>
#include <QUuid>
#include <QDebug>


TcpServer::TcpServer(QObject *parent) : QObject(parent), m_server(nullptr)
{
    m_server = new QTcpServer(this);


    // 连接新连接信号
    connect(m_server, &QTcpServer::newConnection, this, &TcpServer::onNewConnection);
}


bool TcpServer::startServer(quint16 port)
{
    if (m_server->isListening()) {
        m_server->close();
    }


    // 开始监听所有地址的指定端口
    bool success = m_server->listen(QHostAddress::Any, port);
    if (success) {
        emit serverStatusChanged(QString("服务器已启动,监听端口: %1")
                                .arg(m_server->serverPort()));
    } else {
        emit serverStatusChanged(QString("服务器启动失败: %1")
                                .arg(m_server->errorString()));
    }
    return success;
}


void TcpServer::stopServer()
{
    if (m_server->isListening()) {
        m_server->close();


        // 断开所有客户端连接
        QList<QTcpSocket*> sockets = m_clients.values();
        foreach (QTcpSocket* socket, sockets) {
            socket->disconnectFromHost();
        }


        m_clients.clear();
        m_clientIds.clear();


        emit serverStatusChanged("服务器已停止");
    }
}


int TcpServer::getClientCount() const
{
    return m_clients.size();
}


QStringList TcpServer::getClientList() const
{
    return m_clients.keys();
}


void TcpServer::sendToClient(const QString &clientId, const QString &message)
{
    if (m_clients.contains(clientId)) {
        QTcpSocket* socket = m_clients[clientId];
        if (socket->state() == QTcpSocket::ConnectedState) {
            socket->write(message.toUtf8());
        }
    }
}


void TcpServer::broadcast(const QString &message)
{
    QList<QTcpSocket*> sockets = m_clients.values();
    foreach (QTcpSocket* socket, sockets) {
        if (socket->state() == QTcpSocket::ConnectedState) {
            socket->write(message.toUtf8());
        }
    }
}


void TcpServer::disconnectClient(const QString &clientId)
{
    if (m_clients.contains(clientId)) {
        QTcpSocket* socket = m_clients[clientId];
        socket->disconnectFromHost();
    }
}


void TcpServer::onNewConnection()
{
    // 获取新连接的客户端socket
    QTcpSocket* clientSocket = m_server->nextPendingConnection();
    if (!clientSocket) return;


    // 生成客户端ID
    QString clientId = generateClientId();


    // 保存客户端信息
    m_clients[clientId] = clientSocket;
    m_clientIds[clientSocket] = clientId;


    // 连接信号槽
    connect(clientSocket, &QTcpSocket::readyRead, this, &TcpServer::onReadyRead);
    connect(clientSocket, &QTcpSocket::disconnected, this, &TcpServer::onDisconnected);
    connect(clientSocket, &QTcpSocket::errorOccurred, this, &TcpServer::onErrorOccurred);


    // 发送客户端连接信号
    emit clientConnected(clientId, 
                        clientSocket->peerAddress().toString(), 
                        clientSocket->peerPort());


    // 向客户端发送连接成功消息和分配的ID
    QString welcomeMsg = QString("Welcome! Your client ID: %1").arg(clientId);
    clientSocket->write(welcomeMsg.toUtf8());
}


void TcpServer::onReadyRead()
{
    // 获取发送数据的客户端socket
    QTcpSocket* clientSocket = qobject_cast<QTcpSocket*>(sender());
    if (!clientSocket || !m_clientIds.contains(clientSocket)) return;


    // 获取客户端ID
    QString clientId = m_clientIds[clientSocket];


    // 读取数据
    QByteArray data = clientSocket->readAll();
    QString message = QString::fromUtf8(data);


    // 发送收到消息信号
    emit messageReceived(clientId, message);
}


void TcpServer::onDisconnected()
{
    // 获取断开连接的客户端socket
    QTcpSocket* clientSocket = qobject_cast<QTcpSocket*>(sender());
    if (!clientSocket || !m_clientIds.contains(clientSocket)) return;


    // 获取客户端ID
    QString clientId = m_clientIds[clientSocket];


    // 移除客户端信息
    m_clients.remove(clientId);
    m_clientIds.remove(clientSocket);


    // 发送客户端断开信号
    emit clientDisconnected(clientId);


    // 清理socket
    clientSocket->deleteLater();
}


void TcpServer::onErrorOccurred(QAbstractSocket::SocketError error)
{
    QTcpSocket* clientSocket = qobject_cast<QTcpSocket*>(sender());
    if (!clientSocket) return;


    QString clientId = m_clientIds.value(clientSocket, "Unknown");


    emit serverStatusChanged(QString("客户端 %1 错误: %2")
                            .arg(clientId)
                            .arg(clientSocket->errorString()));
}


QString TcpServer::generateClientId()
{
    // 生成唯一ID,使用UUID
    return QUuid::createUuid().toString().replace("{", "").replace("}", "").replace("-", "");
}

tcpserver.h

#ifndef TCPSERVER_H
#define TCPSERVER_H


#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <QList>
#include <QMap>
#include <QString>


class TcpServer : public QObject
{
    Q_OBJECT
public:
    explicit TcpServer(QObject *parent = nullptr);


    // 启动服务器
    bool startServer(quint16 port);


    // 停止服务器
    void stopServer();


    // 获取当前连接的客户端数量
    int getClientCount() const;


    // 获取客户端列表
    QStringList getClientList() const;


signals:
    // 客户端连接信号
    void clientConnected(const QString &clientId, const QString &ip, quint16 port);


    // 客户端断开信号
    void clientDisconnected(const QString &clientId);


    // 收到消息信号
    void messageReceived(const QString &clientId, const QString &message);


    // 服务器状态变化信号
    void serverStatusChanged(const QString &status);


public slots:
    // 向指定客户端发送消息
    void sendToClient(const QString &clientId, const QString &message);


    // 向所有客户端广播消息
    void broadcast(const QString &message);


    // 断开与指定客户端的连接
    void disconnectClient(const QString &clientId);


private slots:
    // 处理新连接
    void onNewConnection();


    // 处理客户端数据
    void onReadyRead();


    // 处理客户端断开连接
    void onDisconnected();


    // 处理错误
    void onErrorOccurred(QAbstractSocket::SocketError error);


private:
    QTcpServer *m_server;                  // TCP服务器
    QMap<QString, QTcpSocket*> m_clients;  // 客户端映射表(clientId -> socket)
    QMap<QTcpSocket*, QString> m_clientIds;// 反向映射表(socket -> clientId)


    // 生成唯一客户端ID
    QString generateClientId();
};


#endif // TCPSERVER_H

Part7界面化改造

将为服务端和客户端分别创建可视化界面

7.1、服务端界面实现:

serverserverwindow.h

#ifndef SERVERWINDOW_H
#define SERVERWINDOW_H


#include <QMainWindow>
#include "tcpserver.h"


QT_BEGIN_NAMESPACE
namespace Ui { class ServerWindow; }
QT_END_NAMESPACE


class ServerWindow : public QMainWindow
{
    Q_OBJECT


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


private slots:
    void on_startButton_clicked();
    void on_stopButton_clicked();
    void on_sendButton_clicked();
    void on_clearLogButton_clicked();
    void on_disconnectButton_clicked();


    // 处理客户端连接
    void handleClientConnected(const QString &clientId, const QString &ip, quint16 port);


    // 处理客户端断开
    void handleClientDisconnected(const QString &clientId);


    // 处理收到的消息
    void handleMessageReceived(const QString &clientId, const QString &message);


    // 处理服务器状态变化
    void handleServerStatusChanged(const QString &status);


private:
    Ui::ServerWindow *ui;
    TcpServer *m_tcpServer;


    // 添加日志
    void addLog(const QString &message);
};
#endif // SERVERWINDOW_H

serverwindow.cpp

#include "serverwindow.h"
#include "ui_serverwindow.h"
#include <QDateTime>
#include <QMessageBox>


ServerWindow::ServerWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::ServerWindow)
    , m_tcpServer(new TcpServer(this))
{
    ui->setupUi(this);
    setWindowTitle("TCP 服务器");


    // 初始化UI状态
    ui->stopButton->setEnabled(false);
    ui->sendButton->setEnabled(false);
    ui->disconnectButton->setEnabled(false);
    ui->portSpinBox->setValue(8888);


    // 连接信号槽
    connect(m_tcpServer, &TcpServer::clientConnected, 
            this, &ServerWindow::handleClientConnected);
    connect(m_tcpServer, &TcpServer::clientDisconnected, 
            this, &ServerWindow::handleClientDisconnected);
    connect(m_tcpServer, &TcpServer::messageReceived, 
            this, &ServerWindow::handleMessageReceived);
    connect(m_tcpServer, &TcpServer::serverStatusChanged, 
            this, &ServerWindow::handleServerStatusChanged);
}


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


void ServerWindow::on_startButton_clicked()
{
    quint16 port = ui->portSpinBox->value();
    if (m_tcpServer->startServer(port)) {
        ui->startButton->setEnabled(false);
        ui->stopButton->setEnabled(true);
        ui->sendButton->setEnabled(true);
        ui->portSpinBox->setEnabled(false);
    }
}


void ServerWindow::on_stopButton_clicked()
{
    m_tcpServer->stopServer();
    ui->clientListWidget->clear();
    ui->startButton->setEnabled(true);
    ui->stopButton->setEnabled(false);
    ui->sendButton->setEnabled(false);
    ui->disconnectButton->setEnabled(false);
    ui->portSpinBox->setEnabled(true);
}


void ServerWindow::on_sendButton_clicked()
{
    QString message = ui->messageEdit->toPlainText().trimmed();
    if (message.isEmpty()) return;


    // 判断是广播还是发送给指定客户端
    if (ui->broadcastRadio->isChecked()) {
        m_tcpServer->broadcast(message);
        addLog(QString("已广播消息: %1").arg(message));
    } else if (ui->clientListWidget->currentItem()) {
        QString clientId = ui->clientListWidget->currentItem()->text();
        m_tcpServer->sendToClient(clientId, message);
        addLog(QString("已向 %1 发送消息: %1").arg(clientId, message));
    } else {
        QMessageBox::warning(this, "警告", "请选择一个客户端");
    }


    ui->messageEdit->clear();
}


void ServerWindow::on_clearLogButton_clicked()
{
    ui->logTextEdit->clear();
}


void ServerWindow::on_disconnectButton_clicked()
{
    if (ui->clientListWidget->currentItem()) {
        QString clientId = ui->clientListWidget->currentItem()->text();
        m_tcpServer->disconnectClient(clientId);
    } else {
        QMessageBox::warning(this, "警告", "请选择一个客户端");
    }
}


void ServerWindow::handleClientConnected(const QString &clientId, const QString &ip, quint16 port)
{
    ui->clientListWidget->addItem(clientId);
    addLog(QString("客户端连接: %1 (%2:%3)")
           .arg(clientId).arg(ip).arg(port));
    ui->clientCountLabel->setText(QString("客户端数量: %1")
                                 .arg(m_tcpServer->getClientCount()));
    ui->disconnectButton->setEnabled(true);
}


void ServerWindow::handleClientDisconnected(const QString &clientId)
{
    QList<QListWidgetItem*> items = ui->clientListWidget->findItems(clientId, Qt::MatchExactly);
    foreach (QListWidgetItem* item, items) {
        delete ui->clientListWidget->takeItem(ui->clientListWidget->row(item));
    }


    addLog(QString("客户端断开: %1").arg(clientId));
    ui->clientCountLabel->setText(QString("客户端数量: %1")
                                 .arg(m_tcpServer->getClientCount()));


    if (m_tcpServer->getClientCount() == 0) {
        ui->disconnectButton->setEnabled(false);
    }
}


void ServerWindow::handleMessageReceived(const QString &clientId, const QString &message)
{
    addLog(QString("收到来自 %1 的消息: %2").arg(clientId, message));
}


void ServerWindow::handleServerStatusChanged(const QString &status)
{
    addLog(status);
    ui->statusBar->showMessage(status);
}


void ServerWindow::addLog(const QString &message)
{
    QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss");
    ui->logTextEdit->append(QString("[%1] %2").arg(timestamp, message));


    // 自动滚动到底部
    QTextCursor cursor = ui->logTextEdit->textCursor();
    cursor.movePosition(QTextCursor::End);
    ui->logTextEdit->setTextCursor(cursor);
}

7.2、客户端界面实现

clientwindow.cpp

#include "clientwindow.h"
#include "ui_clientwindow.h"
#include <QDateTime>
#include <QMessageBox>
#include <QHostAddress>


ClientWindow::ClientWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::ClientWindow)
    , m_socket(nullptr)
    , m_reconnectTimer(nullptr)
{
    ui->setupUi(this);
    setWindowTitle("TCP 客户端");


    // 初始化UI状态
    ui->disconnectButton->setEnabled(false);
    ui->sendButton->setEnabled(false);
    ui->ipLineEdit->setText("127.0.0.1");
    ui->portSpinBox->setValue(8888);


    // 初始化重连计时器
    m_reconnectTimer = new QTimer(this);
    m_reconnectTimer->setInterval(5000); // 5秒重连一次
    m_reconnectTimer->setSingleShot(false);
    connect(m_reconnectTimer, &QTimer::timeout, this, &ClientWindow::reconnect);
}


ClientWindow::~ClientWindow()
{
    if (m_socket && m_socket->state() == QTcpSocket::ConnectedState) {
        m_socket->disconnectFromHost();
    }
    delete ui;
}


void ClientWindow::on_connectButton_clicked()
{
    if (m_socket && m_socket->state() == QTcpSocket::ConnectedState) {
        return;
    }


    // 创建socket
    if (!m_socket) {
        m_socket = new QTcpSocket(this);


        // 连接信号槽
        connect(m_socket, &QTcpSocket::connected, this, &ClientWindow::onConnected);
        connect(m_socket, &QTcpSocket::disconnected, this, &ClientWindow::onDisconnected);
        connect(m_socket, &QTcpSocket::readyRead, this, &ClientWindow::onReadyRead);
        connect(m_socket, &QTcpSocket::errorOccurred, this, &ClientWindow::onErrorOccurred);
    }


    // 连接到服务器
    QString ip = ui->ipLineEdit->text();
    quint16 port = ui->portSpinBox->value();
    m_socket->connectToHost(ip, port);


    addStatusMessage(QString("正在连接到 %1:%2...").arg(ip).arg(port));
}


void ClientWindow::on_disconnectButton_clicked()
{
    if (m_socket && m_socket->state() == QTcpSocket::ConnectedState) {
        m_socket->disconnectFromHost();
        m_reconnectTimer->stop();
    }
}


void ClientWindow::on_sendButton_clicked()
{
    if (!m_socket || m_socket->state() != QTcpSocket::ConnectedState) {
        return;
    }


    QString message = ui->messageEdit->toPlainText().trimmed();
    if (message.isEmpty()) return;


    // 发送消息
    m_socket->write(message.toUtf8());


    // 在本地显示自己发送的消息
    addChatMessage("我", message, true);


    ui->messageEdit->clear();
}


void ClientWindow::on_clearChatButton_clicked()
{
    ui->chatTextEdit->clear();
}


void ClientWindow::onConnected()
{
    addStatusMessage(QString("已连接到服务器 %1:%2")
                    .arg(m_socket->peerAddress().toString())
                    .arg(m_socket->peerPort()));


    // 更新UI状态
    updateConnectionStatus(true);


    // 停止重连计时器
    m_reconnectTimer->stop();
}


void ClientWindow::onDisconnected()
{
    addStatusMessage("与服务器断开连接");


    // 更新UI状态
    updateConnectionStatus(false);


    // 如果不是手动断开连接,则启动重连
    if (ui->autoReconnectCheckBox->isChecked()) {
        addStatusMessage("将在5秒后尝试重连...");
        m_reconnectTimer->start();
    }
}


void ClientWindow::onReadyRead()
{
    if (!m_socket) return;


    QByteArray data = m_socket->readAll();
    QString message = QString::fromUtf8(data);


    // 检查是否是欢迎消息(包含客户端ID)
    if (message.startsWith("Welcome! Your client ID: ")) {
        m_clientId = message.mid(23); // 提取客户端ID
        addStatusMessage(QString("已分配客户端ID: %1").arg(m_clientId));
        setWindowTitle(QString("TCP 客户端 - %1").arg(m_clientId));
        return;
    }


    // 显示收到的消息(简单处理,实际应用中应解析消息格式)
    addChatMessage("服务器", message);
}


void ClientWindow::onErrorOccurred(QAbstractSocket::SocketError error)
{
    Q_UNUSED(error);
    addStatusMessage(QString("错误: %1").arg(m_socket->errorString()));
}


void ClientWindow::reconnect()
{
    if (m_socket->state() == QTcpSocket::UnconnectedState) {
        QString ip = ui->ipLineEdit->text();
        quint16 port = ui->portSpinBox->value();
        m_socket->connectToHost(ip, port);
        addStatusMessage(QString("尝试重连到 %1:%2...").arg(ip).arg(port));
    }
}


void ClientWindow::addChatMessage(const QString &sender, const QString &message, bool isSelf)
{
    QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss");


    // 根据是否是自己发送的消息,使用不同的样式
    QString html;
    if (isSelf) {
        html = QString("<p style='text-align: right; margin: 5px 0;'>"
                      "<span style='font-weight: bold; color: #0066cc;'>%1</span> "
                      "<span style='color: #999; font-size: 0.8em;'>%2</span><br>"
                      "<span style='background-color: #e6f7ff; padding: 4px 8px; "
                      "border-radius: 4px;'>%3</span></p>")
              .arg(sender).arg(timestamp).arg(message);
    } else {
        html = QString("<p style='text-align: left; margin: 5px 0;'>"
                      "<span style='font-weight: bold; color: #cc6600;'>%1</span> "
                      "<span style='color: #999; font-size: 0.8em;'>%2</span><br>"
                      "<span style='background-color: #f5f5f5; padding: 4px 8px; "
                      "border-radius: 4px;'>%3</span></p>")
              .arg(sender).arg(timestamp).arg(message);
    }


    ui->chatTextEdit->insertHtml(html);


    // 自动滚动到底部
    QTextCursor cursor = ui->chatTextEdit->textCursor();
    cursor.movePosition(QTextCursor::End);
    ui->chatTextEdit->setTextCursor(cursor);
}


void ClientWindow::addStatusMessage(const QString &message)
{
    QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss");
    QString html = QString("<p style='margin: 3px 0;'><span style='color: #666; "
                          "font-style: italic; font-size: 0.9em;'>[%1] %2</span></p>")
                  .arg(timestamp).arg(message);


    ui->chatTextEdit->insertHtml(html);


    // 自动滚动到底部
    QTextCursor cursor = ui->chatTextEdit->textCursor();
    cursor.movePosition(QTextCursor::End);
    ui->chatTextEdit->setTextCursor(cursor);
}


void ClientWindow::updateConnectionStatus(bool connected)
{
    if (connected) {
        ui->connectButton->setEnabled(false);
        ui->disconnectButton->setEnabled(true);
        ui->sendButton->setEnabled(true);
        ui->ipLineEdit->setEnabled(false);
        ui->portSpinBox->setEnabled(false);
        ui->statusBar->showMessage("已连接");
    } else {
        ui->connectButton->setEnabled(true);
        ui->disconnectButton->setEnabled(false);
        ui->sendButton->setEnabled(false);
        ui->ipLineEdit->setEnabled(true);
        ui->portSpinBox->setEnabled(true);
        ui->statusBar->showMessage("未连接");
    }
}

clientwindow.h

#ifndef CLIENTWINDOW_H
#define CLIENTWINDOW_H


#include <QMainWindow>
#include <QTcpSocket>
#include <QTimer>


QT_BEGIN_NAMESPACE
namespace Ui { class ClientWindow; }
QT_END_NAMESPACE


class ClientWindow : public QMainWindow
{
    Q_OBJECT


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


private slots:
    void on_connectButton_clicked();
    void on_disconnectButton_clicked();
    void on_sendButton_clicked();
    void on_clearChatButton_clicked();


    // 处理连接成功
    void onConnected();


    // 处理断开连接
    void onDisconnected();


    // 处理收到的数据
    void onReadyRead();


    // 处理错误
    void onErrorOccurred(QAbstractSocket::SocketError error);


    // 断线重连
    void reconnect();


private:
    Ui::ClientWindow *ui;
    QTcpSocket *m_socket;
    QTimer *m_reconnectTimer;  // 重连计时器
    QString m_clientId;        // 客户端ID


    // 添加聊天消息
    void addChatMessage(const QString &sender, const QString &message, bool isSelf = false);


    // 添加状态消息
    void addStatusMessage(const QString &message);


    // 更新连接状态
    void updateConnectionStatus(bool connected);
};
#endif // CLIENTWINDOW_H

Part8复杂数据传输(序列化)

在实际应用中,我们通常需要传输结构化数据,如用户信息、文件片段等。Qt 提供了 QDataStream 类来实现数据的序列化和反序列化。

消息协议与序列化实现:

message.cpp

#include "message.h"
#include <QDataStream>
#include <QBuffer>


Message::Message()
    : m_type(TextMessage), m_timestamp(QDateTime::currentDateTime())
{
}


MessageType Message::type() const
{
    return m_type;
}


QString Message::senderId() const
{
    return m_senderId;
}


void Message::setSenderId(const QString &id)
{
    m_senderId = id;
}


QString Message::receiverId() const
{
    return m_receiverId;
}


void Message::setReceiverId(const QString &id)
{
    m_receiverId = id;
}


QDateTime Message::timestamp() const
{
    return m_timestamp;
}


QByteArray Message::serialize() const
{
    QByteArray data;
    QDataStream out(&data, QIODevice::WriteOnly);


    // 写入消息类型
    out << static_cast<quint8>(m_type);


    // 写入发送者ID
    out << m_senderId;


    // 写入接收者ID
    out << m_receiverId;


    // 写入时间戳
    out << m_timestamp;


    return data;
}


bool Message::deserialize(const QByteArray &data)
{
    QDataStream in(data);


    // 读取消息类型
    quint8 type;
    in >> type;
    m_type = static_cast<MessageType>(type);


    // 读取发送者ID
    in >> m_senderId;


    // 读取接收者ID
    in >> m_receiverId;


    // 读取时间戳
    in >> m_timestamp;


    return !in.status();
}


TextMessage::TextMessage()
{
    m_type = TextMessage;
}


QString TextMessage::content() const
{
    return m_content;
}


void TextMessage::setContent(const QString &content)
{
    m_content = content;
}


QByteArray TextMessage::serialize() const
{
    QByteArray data = Message::serialize();
    QDataStream out(&data, QIODevice::Append);


    // 写入消息内容
    out << m_content;


    return data;
}


bool TextMessage::deserialize(const QByteArray &data)
{
    if (!Message::deserialize(data)) {
        return false;
    }


    QDataStream in(data);


    // 跳过基类已经读取的数据
    quint8 type;
    in >> type;
    QString senderId, receiverId;
    QDateTime timestamp;
    in >> senderId >> receiverId >> timestamp;


    // 读取消息内容
    in >> m_content;


    return !in.status();
}


UserInfoMessage::UserInfoMessage()
{
    m_type = UserInfoMessage;
}


UserInfo UserInfoMessage::userInfo() const
{
    return m_userInfo;
}


void UserInfoMessage::setUserInfo(const UserInfo &info)
{
    m_userInfo = info;
}


QByteArray UserInfoMessage::serialize() const
{
    QByteArray data = Message::serialize();
    QDataStream out(&data, QIODevice::Append);


    // 写入用户信息
    out << m_userInfo;


    return data;
}


bool UserInfoMessage::deserialize(const QByteArray &data)
{
    if (!Message::deserialize(data)) {
        return false;
    }


    QDataStream in(data);


    // 跳过基类已经读取的数据
    quint8 type;
    in >> type;
    QString senderId, receiverId;
    QDateTime timestamp;
    in >> senderId >> receiverId >> timestamp;


    // 读取用户信息
    in >> m_userInfo;


    return !in.status();
}


FileRequestMessage::FileRequestMessage()
    : m_fileSize(0)
{
    m_type = FileRequestMessage;
}


QString FileRequestMessage::fileName() const
{
    return m_fileName;
}


void FileRequestMessage::setFileName(const QString &name)
{
    m_fileName = name;
}


qint64 FileRequestMessage::fileSize() const
{
    return m_fileSize;
}


void FileRequestMessage::setFileSize(qint64 size)
{
    m_fileSize = size;
}


QByteArray FileRequestMessage::serialize() const
{
    QByteArray data = Message::serialize();
    QDataStream out(&data, QIODevice::Append);


    // 写入文件名和大小
    out << m_fileName << m_fileSize;


    return data;
}


bool FileRequestMessage::deserialize(const QByteArray &data)
{
    if (!Message::deserialize(data)) {
        return false;
    }


    QDataStream in(data);


    // 跳过基类已经读取的数据
    quint8 type;
    in >> type;
    QString senderId, receiverId;
    QDateTime timestamp;
    in >> senderId >> receiverId >> timestamp;


    // 读取文件名和大小
    in >> m_fileName >> m_fileSize;


    return !in.status();
}


FileDataMessage::FileDataMessage()
    : m_fileOffset(0), m_isLastPacket(false)
{
    m_type = FileDataMessage;
}


qint64 FileDataMessage::fileOffset() const
{
    return m_fileOffset;
}


void FileDataMessage::setFileOffset(qint64 offset)
{
    m_fileOffset = offset;
}


QByteArray FileDataMessage::fileData() const
{
    return m_fileData;
}


void FileDataMessage::setFileData(const QByteArray &data)
{
    m_fileData = data;
}


bool FileDataMessage::isLastPacket() const
{
    return m_isLastPacket;
}


void FileDataMessage::setIsLastPacket(bool isLast)
{
    m_isLastPacket = isLast;
}


QByteArray FileDataMessage::serialize() const
{
    QByteArray data = Message::serialize();
    QDataStream out(&data, QIODevice::Append);


    // 写入文件偏移、数据和是否为最后一个包
    out << m_fileOffset << m_fileData << m_isLastPacket;


    return data;
}


bool FileDataMessage::deserialize(const QByteArray &data)
{
    if (!Message::deserialize(data)) {
        return false;
    }


    QDataStream in(data);


    // 跳过基类已经读取的数据
    quint8 type;
    in >> type;
    QString senderId, receiverId;
    QDateTime timestamp;
    in >> senderId >> receiverId >> timestamp;


    // 读取文件偏移、数据和是否为最后一个包
    in >> m_fileOffset >> m_fileData >> m_isLastPacket;


    return !in.status();
}


Message* MessageFactory::createMessage(MessageType type)
{
    switch (type) {
    case TextMessage:
        return new TextMessage();
    case UserInfoMessage:
        return new UserInfoMessage();
    case FileRequestMessage:
        return new FileRequestMessage();
    case FileDataMessage:
        return new FileDataMessage();
    default:
        return nullptr;
    }
}


Message* MessageFactory::parseMessage(const QByteArray &data)
{
    if (data.isEmpty()) {
        return nullptr;
    }


    // 读取消息类型
    QDataStream in(data);
    quint8 type;
    in >> type;


    // 创建对应类型的消息
    Message* msg = createMessage(static_cast<MessageType>(type));
    if (msg && !msg->deserialize(data)) {
        delete msg;
        return nullptr;
    }


    return msg;
}

message.h

#ifndef MESSAGE_H
#define MESSAGE_H


#include <QString>
#include <QDateTime>
#include <QDataStream>


// 消息类型
enum MessageType {
    TextMessage,       // 文本消息
    UserInfoMessage,   // 用户信息消息
    FileRequestMessage,// 文件请求消息
    FileDataMessage,   // 文件数据消息
    StatusMessage      // 状态消息
};


// 用户信息结构体
struct UserInfo {
    QString userId;     // 用户ID
    QString userName;   // 用户名
    QString avatar;     // 头像路径
    bool online;        // 是否在线


    // 序列化操作符
    friend QDataStream &operator<<(QDataStream &out, const UserInfo &info) {
        out << info.userId << info.userName << info.avatar << info.online;
        return out;
    }


    // 反序列化操作符
    friend QDataStream &operator>>(QDataStream &in, UserInfo &info) {
        in >> info.userId >> info.userName >> info.avatar >> info.online;
        return in;
    }
};


// 消息基类
class Message {
public:
    Message();
    virtual ~Message() = default;


    // 获取消息类型
    MessageType type() const;


    // 获取发送者ID
    QString senderId() const;
    void setSenderId(const QString &id);


    // 获取接收者ID
    QString receiverId() const;
    void setReceiverId(const QString &id);


    // 获取消息时间戳
    QDateTime timestamp() const;


    // 序列化
    virtual QByteArray serialize() const;


    // 反序列化
    virtual bool deserialize(const QByteArray &data);


protected:
    MessageType m_type;
    QString m_senderId;
    QString m_receiverId;
    QDateTime m_timestamp;
};


// 文本消息
class TextMessage : public Message {
public:
    TextMessage();


    QString content() const;
    void setContent(const QString &content);


    QByteArray serialize() const override;
    bool deserialize(const QByteArray &data) override;


private:
    QString m_content;
};


// 用户信息消息
class UserInfoMessage : public Message {
public:
    UserInfoMessage();


    UserInfo userInfo() const;
    void setUserInfo(const UserInfo &info);


    QByteArray serialize() const override;
    bool deserialize(const QByteArray &data) override;


private:
    UserInfo m_userInfo;
};


// 文件请求消息
class FileRequestMessage : public Message {
public:
    FileRequestMessage();


    QString fileName() const;
    void setFileName(const QString &name);


    qint64 fileSize() const;
    void setFileSize(qint64 size);


    QByteArray serialize() const override;
    bool deserialize(const QByteArray &data) override;


private:
    QString m_fileName;
    qint64 m_fileSize;
};


// 文件数据消息
class FileDataMessage : public Message {
public:
    FileDataMessage();


    qint64 fileOffset() const;
    void setFileOffset(qint64 offset);


    QByteArray fileData() const;
    void setFileData(const QByteArray &data);


    bool isLastPacket() const;
    void setIsLastPacket(bool isLast);


    QByteArray serialize() const override;
    bool deserialize(const QByteArray &data) override;


private:
    qint64 m_fileOffset;
    QByteArray m_fileData;
    bool m_isLastPacket;
};


// 消息工厂类,用于创建不同类型的消息
class MessageFactory {
public:
    static Message* createMessage(MessageType type);
    static Message* parseMessage(const QByteArray &data);
};


#endif // MESSAGE_H

Part9断线重连功能

断线重连是提高系统可靠性的重要功能,我们在客户端实现自动重连机制。

断线重连实现(已集成在客户端代码中)

主要实现思路:

  1. 使用 QTimer 设置定时重连
  2. 监测连接状态,在意外断开时启动重连计时器
  3. 重连成功后停止计时器
  4. 提供手动触发重连的机制

Part10加密通信实现

为保障通信安全,我们使用 QSslSocket 替代 QTcpSocket,实现基于 SSL/TLS 的加密通信。

加密通信实现:

sslclient.cpp

#include "sslclient.h"
#include <QFile>
#include <QDebug>


SslClient::SslClient(QObject *parent) : QObject(parent)
    , m_sslSocket(new QSslSocket(this))
    , m_reconnectTimer(new QTimer(this))
    , m_port(0)
    , m_reconnectAttempts(0)
    , m_autoReconnect(false)
{
    // 配置重连计时器
    m_reconnectTimer->setSingleShot(true);
    connect(m_reconnectTimer, &QTimer::timeout, this, &SslClient::attemptReconnect);


    // 连接SSL信号槽
    connect(m_sslSocket, &QSslSocket::encrypted, this, &SslClient::onEncrypted);
    connect(m_sslSocket, &QSslSocket::readyRead, this, &SslClient::onReadyRead);
    connect(m_sslSocket, &QSslSocket::disconnected, this, &SslClient::onDisconnected);
    connect(m_sslSocket, QOverload<QAbstractSocket::SocketError>::of(&QSslSocket::error),
            this, &SslClient::onErrorOccurred);
    connect(m_sslSocket, &QSslSocket::sslErrors, this, &SslClient::onSslErrors);
}


void SslClient::connectToHostEncrypted(const QString &hostName, quint16 port)
{
    m_hostName = hostName;
    m_port = port;
    m_reconnectAttempts = 0;


    // 连接到SSL服务器
    m_sslSocket->connectToHostEncrypted(hostName, port);
}


void SslClient::disconnectFromHost()
{
    m_sslSocket->disconnectFromHost();
    m_reconnectTimer->stop();
}


qint64 SslClient::send(const QByteArray &data)
{
    if (m_sslSocket->state() == QSslSocket::ConnectedState) {
        return m_sslSocket->write(data);
    }
    return -1;
}


bool SslClient::loadCaCertificates(const QString &caFile)
{
    QFile caFileObj(caFile);
    if (!caFileObj.open(QIODevice::ReadOnly)) {
        emit errorOccurred(QString("无法打开CA证书文件: %1").arg(caFileObj.errorString()));
        return false;
    }


    QList<QSslCertificate> certificates = QSslCertificate::fromDevice(&caFileObj);
    caFileObj.close();


    if (certificates.isEmpty()) {
        emit errorOccurred("无效的CA证书文件");
        return false;
    }


    m_sslSocket->setCaCertificates(certificates);
    return true;
}


void SslClient::setAutoReconnect(bool enable, int interval)
{
    m_autoReconnect = enable;
    m_reconnectTimer->setInterval(interval);
}


bool SslClient::isConnected() const
{
    return m_sslSocket->state() == QSslSocket::ConnectedState;
}


void SslClient::onEncrypted()
{
    emit connected();
    m_reconnectAttempts = 0; // 重置重连尝试次数
}


void SslClient::onReadyRead()
{
    emit readyRead(m_sslSocket->readAll());
}


void SslClient::onDisconnected()
{
    emit disconnected();


    // 如果启用了自动重连,则启动重连计时器
    if (m_autoReconnect && m_sslSocket->state() != QSslSocket::ConnectingState) {
        m_reconnectTimer->start();
    }
}


void SslClient::onErrorOccurred(QAbstractSocket::SocketError error)
{
    Q_UNUSED(error);
    emit errorOccurred(m_sslSocket->errorString());


    // 如果是连接错误且启用了自动重连,则尝试重连
    if (m_autoReconnect && 
        (error == QAbstractSocket::ConnectionRefusedError ||
         error == QAbstractSocket::HostNotFoundError ||
         error == QAbstractSocket::NetworkError)) {
        m_reconnectTimer->start();
    }
}


void SslClient::onSslErrors(const QList<QSslError> &errors)
{
    QString errorString;
    foreach (const QSslError &error, errors) {
        errorString += error.errorString() + "\n";
    }


    emit errorOccurred(QString("SSL错误:\n%1").arg(errorString));


    // 忽略自签名证书错误(仅用于测试环境)
    // 在生产环境中不应该忽略任何SSL错误
    m_sslSocket->ignoreSslErrors();
}


void SslClient::attemptReconnect()
{
    if (m_sslSocket->state() == QSslSocket::ConnectedState) {
        return;
    }


    m_reconnectAttempts++;
    emit reconnectAttempt(m_reconnectAttempts);


    // 尝试重新连接
    m_sslSocket->connectToHostEncrypted(m_hostName, m_port);
}

sslclient.h

#ifndef SSLCLIENT_H
#define SSLCLIENT_H


#include <QSslSocket>
#include <QTimer>


class SslClient : public QObject
{
    Q_OBJECT
public:
    explicit SslClient(QObject *parent = nullptr);


    // 连接到SSL服务器
    void connectToHostEncrypted(const QString &hostName, quint16 port);


    // 断开连接
    void disconnectFromHost();


    // 发送数据
    qint64 send(const QByteArray &data);


    // 加载CA证书
    bool loadCaCertificates(const QString &caFile);


    // 启用/禁用自动重连
    void setAutoReconnect(bool enable, int interval = 5000);


    // 获取连接状态
    bool isConnected() const;


signals:
    // 连接成功信号
    void connected();


    // 断开连接信号
    void disconnected();


    // 收到数据信号
    void readyRead(const QByteArray &data);


    // 错误信号
    void errorOccurred(const QString &errorString);


    // 重连尝试信号
    void reconnectAttempt(int attemptNumber);


private slots:
    // 处理加密完成
    void onEncrypted();


    // 处理读取数据
    void onReadyRead();


    // 处理断开连接
    void onDisconnected();


    // 处理错误
    void onErrorOccurred(QAbstractSocket::SocketError error);


    // 处理SSL错误
    void onSslErrors(const QList<QSslError> &errors);


    // 尝试重连
    void attemptReconnect();


private:
    QSslSocket *m_sslSocket;       // SSL套接字
    QTimer *m_reconnectTimer;      // 重连计时器
    QString m_hostName;            // 主机名
    quint16 m_port;                // 端口
    int m_reconnectAttempts;       // 重连尝试次数
    bool m_autoReconnect;          // 是否自动重连
};


#endif // SSLCLIENT_H

sslserver.cpp

#include "sslserver.h"
#include <QFile>
#include <QDebug>


SslServer::SslServer(QObject *parent) : QTcpServer(parent)
{
}


bool SslServer::loadSslConfiguration(const QString &certFile, const QString &keyFile)
{
    // 加载证书
    QFile certFileObj(certFile);
    if (!certFileObj.open(QIODevice::ReadOnly)) {
        qWarning() << "无法打开证书文件:" << certFileObj.errorString();
        return false;
    }


    m_certificate = QSslCertificate(&certFileObj);
    certFileObj.close();


    if (m_certificate.isNull()) {
        qWarning() << "无效的证书文件";
        return false;
    }


    // 加载私钥
    QFile keyFileObj(keyFile);
    if (!keyFileObj.open(QIODevice::ReadOnly)) {
        qWarning() << "无法打开私钥文件:" << keyFileObj.errorString();
        return false;
    }


    m_privateKey = QSslKey(&keyFileObj, QSsl::Rsa);
    keyFileObj.close();


    if (m_privateKey.isNull()) {
        qWarning() << "无效的私钥文件";
        return false;
    }


    // 配置SSL
    m_sslConfig.setLocalCertificate(m_certificate);
    m_sslConfig.setPrivateKey(m_privateKey);
    m_sslConfig.setProtocol(QSsl::TlsV1_2OrLater);


    return true;
}


QSslConfiguration SslServer::sslConfiguration() const
{
    return m_sslConfig;
}


void SslServer::incomingConnection(qintptr socketDescriptor)
{
    // 创建SSL socket
    QSslSocket *sslSocket = new QSslSocket(this);


    // 设置socket描述符
    if (!sslSocket->setSocketDescriptor(socketDescriptor)) {
        qWarning() << "设置socket描述符失败:" << sslSocket->errorString();
        sslSocket->deleteLater();
        return;
    }


    // 配置SSL
    sslSocket->setSslConfiguration(m_sslConfig);


    // 连接信号槽
    connect(sslSocket, &QSslSocket::encrypted, this, [this, sslSocket]() {
        emit newSslConnection(sslSocket);
    });


    connect(sslSocket, &QSslSocket::sslErrors, this, [this](const QList<QSslError> &errors) {
        emit sslErrorOccurred(errors);
    });


    // 开始加密握手
    sslSocket->startServerEncryption();
}

sslserver.h

#ifndef SSLSERVER_H
#define SSLSERVER_H


#include <QTcpServer>
#include <QSslSocket>
#include <QSslCertificate>
#include <QSslKey>


class SslServer : public QTcpServer
{
    Q_OBJECT
public:
    explicit SslServer(QObject *parent = nullptr);


    // 加载SSL证书和密钥
    bool loadSslConfiguration(const QString &certFile, const QString &keyFile);


    // 获取SSL配置
    QSslConfiguration sslConfiguration() const;


protected:
    // 重写 incomingConnection 方法
    void incomingConnection(qintptr socketDescriptor) override;


signals:
    // 新的SSL连接建立信号
    void newSslConnection(QSslSocket *socket);


    // SSL错误信号
    void sslErrorOccurred(const QList<QSslError> &errors);


private:
    QSslConfiguration m_sslConfig;  // SSL配置
    QSslCertificate m_certificate;  // SSL证书
    QSslKey m_privateKey;           // 私钥
};


#endif // SSLSERVER_H

Part11语音 / 视频通话

音视频通信需要额外的库支持,以下是项目配置要点:

11.1、项目文件 (.pro) 配置

QT       += core gui network widgets multimedia
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = VideoCall
TEMPLATE = app
SOURCES += \
    main.cpp \
    audiomanager.cpp \
    videomanager.cpp \
    rtptransmitter.cpp \
    callwindow.cpp
HEADERS += \
    audiomanager.h \
    videomanager.h \
    rtptransmitter.h \
    callwindow.h
FORMS += \
    callwindow.ui
# OPUS 音频编解码库
unix {
    LIBS += -lopus
}
win32 {
    LIBS += -lopus
    INCLUDEPATH += $$PWD/opus/include
    LIBS += -L$$PWD/opus/lib
}
# VP8 视频编解码库 (libvpx)
unix {
    LIBS += -lvpx
}
win32 {
    LIBS += -lvpx
    INCLUDEPATH += $$PWD/vpx/include
    LIBS += -L$$PWD/vpx/lib
}

11.2、依赖库安装

  • OPUS 库:用于音频编解码
# Ubuntu/Debian
sudo apt-get install libopus-dev
# macOS
brew install opus

libvpx 库:用于视频编解码

11.3、音频传输功能

音频通信是视频通话的基础,我们先实现点对点的音频传输功能。

audiomanager.cpp

#include "audiomanager.h"
#include <QAudioDeviceInfo>
#include <QAudioFormat>
#include <QDebug>
#include <QIODevice>


AudioManager::AudioManager(QObject *parent) : QObject(parent)
    , m_audioInput(nullptr)
    , m_audioOutput(nullptr)
    , m_inputDevice(nullptr)
    , m_outputDevice(nullptr)
    , m_isTransmitting(false)
{
    // 初始化音频格式 (16kHz, 16位, 单声道)
    m_format.setSampleRate(16000);
    m_format.setChannelCount(1);
    m_format.setSampleSize(16);
    m_format.setCodec("audio/pcm");
    m_format.setByteOrder(QAudioFormat::LittleEndian);
    m_format.setSampleType(QAudioFormat::SignedInt);


    // 检查格式是否支持
    QAudioDeviceInfo info(QAudioDeviceInfo::defaultInputDevice());
    if (!info.isFormatSupported(m_format)) {
        qWarning() << "默认音频格式不支持,使用 nearest";
        m_format = info.nearestFormat(m_format);
    }


    // 初始化编解码器
    initOpusCodec();
}


AudioManager::~AudioManager()
{
    stopTransmission();


    // 释放编解码器资源
    if (m_encoder) {
        opus_encoder_destroy(m_encoder);
    }
    if (m_decoder) {
        opus_decoder_destroy(m_decoder);
    }
}


void AudioManager::startTransmission()
{
    if (m_isTransmitting) return;


    // 创建音频输入
    m_audioInput = new QAudioInput(m_format, this);
    m_inputDevice = m_audioInput->start();
    connect(m_inputDevice, &QIODevice::readyRead, this, &AudioManager::onAudioInputReady);


    // 创建音频输出
    m_audioOutput = new QAudioOutput(m_format, this);
    m_outputDevice = m_audioOutput->start();


    m_isTransmitting = true;
    emit transmissionStateChanged(true);
}


void AudioManager::stopTransmission()
{
    if (!m_isTransmitting) return;


    // 停止音频输入
    if (m_audioInput) {
        m_audioInput->stop();
        delete m_audioInput;
        m_audioInput = nullptr;
    }
    m_inputDevice = nullptr;


    // 停止音频输出
    if (m_audioOutput) {
        m_audioOutput->stop();
        delete m_audioOutput;
        m_audioOutput = nullptr;
    }
    m_outputDevice = nullptr;


    m_isTransmitting = false;
    emit transmissionStateChanged(false);
}


bool AudioManager::isTransmitting() const
{
    return m_isTransmitting;
}


void AudioManager::setRemoteAddress(const QString &address, quint16 port)
{
    m_remoteAddress = address;
    m_remotePort = port;
}


void AudioManager::onAudioInputReady()
{
    if (!m_inputDevice) return;


    // 读取音频数据 (每次读取20ms的数据)
    const int frameSize = m_format.sampleRate() / 50; // 20ms
    const int bytesPerSample = m_format.sampleSize() / 8;
    const int bufferSize = frameSize * m_format.channelCount() * bytesPerSample;


    QByteArray pcmData = m_inputDevice->read(bufferSize);
    if (pcmData.isEmpty()) return;


    // 编码PCM数据
    QByteArray encodedData = encodeAudio(pcmData);
    if (!encodedData.isEmpty()) {
        // 发送编码后的数据
        emit audioDataReady(encodedData);
    }
}


void AudioManager::playAudioData(const QByteArray &encodedData)
{
    if (!m_outputDevice || !m_isTransmitting) return;


    // 解码音频数据
    QByteArray pcmData = decodeAudio(encodedData);
    if (!pcmData.isEmpty()) {
        // 播放PCM数据
        m_outputDevice->write(pcmData);
    }
}


bool AudioManager::initOpusCodec()
{
    int error;


    // 创建编码器
    m_encoder = opus_encoder_create(m_format.sampleRate(), 
                                  m_format.channelCount(), 
                                  OPUS_APPLICATION_VOIP, 
                                  &error);
    if (error != OPUS_OK || !m_encoder) {
        qWarning() << "创建OPUS编码器失败:" << error;
        return false;
    }


    // 设置编码质量 (0-10, 10为最高质量)
    opus_encoder_ctl(m_encoder, OPUS_SET_QUALITY(8));


    // 创建解码器
    m_decoder = opus_decoder_create(m_format.sampleRate(), 
                                  m_format.channelCount(), 
                                  &error);
    if (error != OPUS_OK || !m_decoder) {
        qWarning() << "创建OPUS解码器失败:" << error;
        if (m_encoder) {
            opus_encoder_destroy(m_encoder);
            m_encoder = nullptr;
        }
        return false;
    }


    return true;
}


QByteArray AudioManager::encodeAudio(const QByteArray &pcmData)
{
    if (!m_encoder) return QByteArray();


    // OPUS每次编码的最大数据大小
    const int maxEncodedSize = 4000;
    unsigned char encodedData[maxEncodedSize];


    // 计算样本数
    int frameSize = pcmData.size() / (m_format.sampleSize() / 8);


    // 编码PCM数据
    int result = opus_encode(m_encoder, 
                           reinterpret_cast<const opus_int16*>(pcmData.data()),
                           frameSize,
                           encodedData,
                           maxEncodedSize);


    if (result < 0) {
        qWarning() << "OPUS编码失败:" << result;
        return QByteArray();
    }


    return QByteArray(reinterpret_cast<const char*>(encodedData), result);
}


QByteArray AudioManager::decodeAudio(const QByteArray &encodedData)
{
    if (!m_decoder || encodedData.isEmpty()) return QByteArray();


    // 计算解码后的样本数
    int frameSize = m_format.sampleRate() / 50; // 20ms
    opus_int16 decodedData[frameSize * m_format.channelCount()];


    // 解码数据
    int result = opus_decode(m_decoder,
                           reinterpret_cast<const unsigned char*>(encodedData.data()),
                           encodedData.size(),
                           decodedData,
                           frameSize,
                           0);


    if (result < 0) {
        qWarning() << "OPUS解码失败:" << result;
        return QByteArray();
    }


    // 转换为QByteArray
    return QByteArray(reinterpret_cast<const char*>(decodedData), 
                     result * m_format.channelCount() * (m_format.sampleSize() / 8));
}

11.4、视频通信实现

视频通信相对复杂,需要处理更高的数据量和更严格的实时性要求。

videomanager.cpp

#include "videomanager.h"
#include <QCameraInfo>
#include <QVideoFrame>
#include <QPainter>
#include <QDebug>
#include <QTimer>


VideoManager::VideoManager(QObject *parent) : QObject(parent)
    , m_camera(nullptr)
    , m_videoSurface(nullptr)
    , m_isStreaming(false)
    , m_frameRate(15) // 15 FPS
    , m_width(640)
    , m_height(480)
{
    // 初始化视频表面用于捕获帧
    m_videoSurface = new VideoSurface(this);
    connect(m_videoSurface, &VideoSurface::frameAvailable, 
            this, &VideoManager::onVideoFrameAvailable);


    // 初始化VP8编码器和解码器
    initVp8Codec();


    // 设置帧定时器控制帧率
    m_frameTimer = new QTimer(this);
    m_frameTimer->setInterval(1000 / m_frameRate); // 计算每帧间隔
    connect(m_frameTimer, &QTimer::timeout, this, &VideoManager::captureFrame);
}


VideoManager::~VideoManager()
{
    stopStreaming();


    // 释放VP8资源
    if (m_encoder) {
        vpx_codec_destroy(&m_encoder);
    }
    if (m_decoder) {
        vpx_codec_destroy(&m_decoder);
    }
}


QList<QString> VideoManager::availableCameras()
{
    QList<QString> cameraNames;
    foreach (const QCameraInfo &cameraInfo, QCameraInfo::availableCameras()) {
        cameraNames.append(cameraInfo.description());
    }
    return cameraNames;
}


void VideoManager::startStreaming(int cameraIndex)
{
    if (m_isStreaming) return;


    // 获取可用相机列表
    QList<QCameraInfo> cameras = QCameraInfo::availableCameras();
    if (cameraIndex < 0 || cameraIndex >= cameras.size()) {
        qWarning() << "无效的相机索引";
        return;
    }


    // 创建相机实例
    m_camera = new QCamera(cameras[cameraIndex], this);


    // 设置相机视图finder
    m_camera->setViewfinder(m_videoSurface);


    // 配置相机设置
    QCameraViewfinderSettings settings;
    settings.setResolution(m_width, m_height);
    settings.setMinimumFrameRate(m_frameRate);
    settings.setMaximumFrameRate(m_frameRate);
    m_camera->setViewfinderSettings(settings);


    // 启动相机
    m_camera->start();


    // 启动帧定时器
    m_frameTimer->start();


    m_isStreaming = true;
    emit streamingStateChanged(true);
}


void VideoManager::stopStreaming()
{
    if (!m_isStreaming) return;


    // 停止定时器
    m_frameTimer->stop();


    // 停止相机
    if (m_camera) {
        m_camera->stop();
        delete m_camera;
        m_camera = nullptr;
    }


    m_isStreaming = false;
    emit streamingStateChanged(false);
}


bool VideoManager::isStreaming() const
{
    return m_isStreaming;
}


void VideoManager::setResolution(int width, int height)
{
    m_width = width;
    m_height = height;
}


void VideoManager::setFrameRate(int frameRate)
{
    m_frameRate = frameRate;
    m_frameTimer->setInterval(1000 / m_frameRate);
}


void VideoManager::setRemoteAddress(const QString &address, quint16 port)
{
    m_remoteAddress = address;
    m_remotePort = port;
}


void VideoManager::displayVideoFrame(const QByteArray &encodedData)
{
    // 解码视频数据
    QImage image = decodeVideo(encodedData);
    if (!image.isNull()) {
        emit frameReady(image);
    }
}


void VideoManager::onVideoFrameAvailable(const QVideoFrame &frame)
{
    // 保存当前帧供定时器处理
    m_lastFrame = frame;
}


void VideoManager::captureFrame()
{
    if (!m_lastFrame.isValid()) return;


    // 将视频帧转换为QImage
    QImage image = frameToImage(m_lastFrame);
    if (image.isNull()) return;


    // 编码图像
    QByteArray encodedData = encodeVideo(image);
    if (!encodedData.isEmpty()) {
        // 发送编码后的数据
        emit videoDataReady(encodedData);
    }
}


bool VideoManager::initVp8Codec()
{
    int res;


    // 初始化编码器
    vpx_codec_enc_cfg_t enc_cfg;
    vpx_codec_iface_t *encoder_iface = vpx_codec_vp8_cx();


    res = vpx_codec_enc_config_default(encoder_iface, &enc_cfg, 0);
    if (res) {
        qWarning() << "无法获取默认编码器配置";
        return false;
    }


    // 设置编码参数
    enc_cfg.g_w = m_width;
    enc_cfg.g_h = m_height;
    enc_cfg.g_timebase.num = 1;
    enc_cfg.g_timebase.den = m_frameRate;
    enc_cfg.rc_target_bitrate = 500; // 500 kbps
    enc_cfg.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;


    // 初始化编码器实例
    if (vpx_codec_enc_init(&m_encoder, encoder_iface, &enc_cfg, 0)) {
        qWarning() << "无法初始化VP8编码器";
        return false;
    }


    // 初始化解码器
    vpx_codec_iface_t *decoder_iface = vpx_codec_vp8_dx();
    if (vpx_codec_dec_init(&m_decoder, decoder_iface, NULL, 0)) {
        qWarning() << "无法初始化VP8解码器";
        vpx_codec_destroy(&m_encoder);
        return false;
    }


    return true;
}


QImage VideoManager::frameToImage(const QVideoFrame &frame)
{
    QVideoFrame cloneFrame(frame);
    cloneFrame.map(QAbstractVideoBuffer::ReadOnly);


    QImage::Format imageFormat = QVideoFrame::imageFormatFromPixelFormat(cloneFrame.pixelFormat());


    // 处理YUYV格式(常见于摄像头)
    if (imageFormat == QImage::Format_Invalid) {
        // 这里简化处理,实际应用中需要完整的YUYV转RGB实现
        imageFormat = QImage::Format_RGB32;
    }


    QImage image(cloneFrame.bits(), 
                cloneFrame.width(), 
                cloneFrame.height(), 
                cloneFrame.bytesPerLine(), 
                imageFormat);


    // 转换为RGB格式以便处理
    image = image.convertToFormat(QImage::Format_RGB888);


    cloneFrame.unmap();
    return image;
}


QByteArray VideoManager::encodeVideo(const QImage &image)
{
    if (vpx_codec_encode(&m_encoder, NULL, 0, 1, 0, VPX_DL_REALTIME) != VPX_CODEC_OK) {
        qWarning() << "VP8编码失败:" << vpx_codec_error(&m_encoder);
        return QByteArray();
    }


    // 获取编码后的数据
    vpx_codec_iter_t iter = NULL;
    const vpx_codec_cx_pkt_t *pkt;
    QByteArray encodedData;


    while ((pkt = vpx_codec_get_cx_data(&m_encoder, &iter)) != NULL) {
        if (pkt->kind == VPX_CODEC_CX_FRAME_PKT) {
            encodedData.append(reinterpret_cast<const char*>(pkt->data.frame.buf), 
                             pkt->data.frame.sz);
        }
    }


    return encodedData;
}


QImage VideoManager::decodeVideo(const QByteArray &encodedData)
{
    if (encodedData.isEmpty()) return QImage();


    // 解码数据
    if (vpx_codec_decode(&m_decoder, 
                       reinterpret_cast<const uint8_t*>(encodedData.data()),
                       encodedData.size(), 
                       NULL, 0) != VPX_CODEC_OK) {
        qWarning() << "VP8解码失败:" << vpx_codec_error(&m_decoder);
        return QImage();
    }


    // 获取解码后的帧
    vpx_codec_iter_t iter = NULL;
    const vpx_image_t *img;
    QImage resultImage;


    while ((img = vpx_codec_get_frame(&m_decoder, &iter)) != NULL) {
        // 转换vpx_image到QImage
        resultImage = QImage(img->w, img->h, QImage::Format_RGB888);


        // 处理YV12格式(VP8常用输出格式)
        for (int y = 0; y < img->h; y++) {
            for (int x = 0; x < img->w; x++) {
                // 简化的YUV到RGB转换(实际应用中应使用更精确的转换)
                uint8_t *yPlane = img->planes[0] + y * img->stride[0];
                uint8_t y = yPlane[x];


                int uvWidth = img->w / 2;
                int uvHeight = img->h / 2;
                int ux = x / 2;
                int uy = y / 2;


                uint8_t *uPlane = img->planes[1] + uy * img->stride[1];
                uint8_t *vPlane = img->planes[2] + uy * img->stride[2];
                int u = uPlane[ux] - 128;
                int v = vPlane[ux] - 128;


                // YUV到RGB转换公式
                int r = qBound(0, y + (int)(1.402f * v), 255);
                int g = qBound(0, y - (int)(0.344f * u + 0.714f * v), 255);
                int b = qBound(0, y + (int)(1.772f * u), 255);


                resultImage.setPixel(x, y, qRgb(r, g, b));
            }
        }
    }


    return resultImage;
}


// VideoSurface 实现
VideoSurface::VideoSurface(QObject *parent) : QAbstractVideoSurface(parent)
{
}


QList<QVideoFrame::PixelFormat> VideoSurface::supportedPixelFormats(
        QAbstractVideoBuffer::HandleType handleType) const
{
    Q_UNUSED(handleType);
    // 支持常见的像素格式
    return QList<QVideoFrame::PixelFormat>()
            << QVideoFrame::Format_ARGB32
            << QVideoFrame::Format_ARGB32_Premultiplied
            << QVideoFrame::Format_RGB32
            << QVideoFrame::Format_RGB24
            << QVideoFrame::Format_RGB565
            << QVideoFrame::Format_RGB555
            << QVideoFrame::Format_YUV420P
            << QVideoFrame::Format_YUYV;
}


bool VideoSurface::present(const QVideoFrame &frame)
{
    if (frame.isValid()) {
        QVideoFrame cloneFrame(frame);
        cloneFrame.map(QAbstractVideoBuffer::ReadOnly);
        emit frameAvailable(cloneFrame);
        cloneFrame.unmap();
        return true;
    }
    return false;
}

11.5、RTP 传输实现

实时传输协议 (RTP) 是用于音视频实时传输的标准协议,我们基于 UDP 实现 RTP 封装传输。

rtptransmitter.cpp

#include "rtptransmitter.h"
#include <QHostAddress>
#include <QDateTime>
#include <QDebug>


// RTP 头部结构 (简化版)
struct RtpHeader {
    uint8_t version : 2;    // 版本号 (2 bits)
    uint8_t padding : 1;    // 填充标志 (1 bit)
    uint8_t extension : 1;  // 扩展标志 (1 bit)
    uint8_t csrcCount : 4;  // CSRC计数 (4 bits)
    uint8_t marker : 1;     // 标记位 (1 bit)
    uint8_t payloadType : 7;// 负载类型 (7 bits)
    uint16_t sequenceNumber;// 序列号 (16 bits)
    uint32_t timestamp;     // 时间戳 (32 bits)
    uint32_t ssrc;          // 同步源标识 (32 bits)
};


RtpTransmitter::RtpTransmitter(QObject *parent) : QObject(parent)
    , m_udpSocket(new QUdpSocket(this))
    , m_sequenceNumber(0)
    , m_ssrc(qrand()) // 随机生成SSRC
    , m_audioPayloadType(0)
    , m_videoPayloadType(96)
{
    // 绑定到随机端口
    m_udpSocket->bind(QHostAddress::Any, 0);


    // 连接接收信号
    connect(m_udpSocket, &QUdpSocket::readyRead, this, &RtpTransmitter::onReadyRead);
}


quint16 RtpTransmitter::localPort() const
{
    return m_udpSocket->localPort();
}


void RtpTransmitter::setRemoteAddress(const QString &address, quint16 port)
{
    m_remoteAddress = address;
    m_remotePort = port;
}


void RtpTransmitter::sendAudioData(const QByteArray &data)
{
    sendRtpPacket(data, m_audioPayloadType, false);
}


void RtpTransmitter::sendVideoData(const QByteArray &data)
{
    // 视频数据可能较大,需要分片
    const int maxPacketSize = 1400; // 避免IP分片
    int offset = 0;


    while (offset < data.size()) {
        int chunkSize = qMin(maxPacketSize, data.size() - offset);
        QByteArray chunk = data.mid(offset, chunkSize);
        bool isLast = (offset + chunkSize >= data.size());


        sendRtpPacket(chunk, m_videoPayloadType, isLast);
        offset += chunkSize;
    }
}


void RtpTransmitter::onReadyRead()
{
    while (m_udpSocket->hasPendingDatagrams()) {
        QByteArray datagram;
        datagram.resize(m_udpSocket->pendingDatagramSize());


        QHostAddress sender;
        quint16 senderPort;


        m_udpSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);


        // 解析RTP包
        parseRtpPacket(datagram);
    }
}


void RtpTransmitter::sendRtpPacket(const QByteArray &payload, uint8_t payloadType, bool marker)
{
    if (m_remoteAddress.isEmpty() || m_remotePort == 0) {
        qWarning() << "未设置远程地址和端口";
        return;
    }


    // 计算RTP包大小
    int packetSize = sizeof(RtpHeader) + payload.size();
    QByteArray packet(packetSize, 0);


    // 填充RTP头部
    RtpHeader *header = reinterpret_cast<RtpHeader*>(packet.data());
    header->version = 2; // RTP版本2
    header->padding = 0;
    header->extension = 0;
    header->csrcCount = 0;
    header->marker = marker ? 1 : 0;
    header->payloadType = payloadType;
    header->sequenceNumber = qToBigEndian(++m_sequenceNumber);


    // 时间戳 (毫秒级)
    uint32_t timestamp = QDateTime::currentMSecsSinceStartOfDay();
    header->timestamp = qToBigEndian(timestamp);


    header->ssrc = qToBigEndian(m_ssrc);


    // 填充负载数据
    memcpy(packet.data() + sizeof(RtpHeader), payload.data(), payload.size());


    // 发送RTP包
    m_udpSocket->writeDatagram(packet, QHostAddress(m_remoteAddress), m_remotePort);
}


void RtpTransmitter::parseRtpPacket(const QByteArray &packet)
{
    if (packet.size() < sizeof(RtpHeader)) {
        qWarning() << "RTP包太小,无法解析";
        return;
    }


    // 解析RTP头部
    const RtpHeader *header = reinterpret_cast<const RtpHeader*>(packet.data());
    uint8_t payloadType = header->payloadType;
    uint16_t sequenceNumber = qFromBigEndian(header->sequenceNumber);
    uint32_t timestamp = qFromBigEndian(header->timestamp);
    uint32_t ssrc = qFromBigEndian(header->ssrc);
    bool marker = header->marker;


    // 提取负载数据
    QByteArray payload = packet.mid(sizeof(RtpHeader));


    // 根据负载类型分发数据
    if (payloadType == m_audioPayloadType) {
        // 处理音频数据
        emit audioDataReceived(payload);
    } else if (payloadType == m_videoPayloadType) {
        // 处理视频数据(可能需要重组分片)
        handleVideoFragment(payload, marker);
    }
}


void RtpTransmitter::handleVideoFragment(const QByteArray &fragment, bool isLast)
{
    // 收集视频分片
    m_videoBuffer.append(fragment);


    // 如果是最后一个分片,发射完整视频数据
    if (isLast) {
        emit videoDataReceived(m_videoBuffer);
        m_videoBuffer.clear();
    }
}

11.6、音视频通话界面

整合音频、视频和传输模块,实现完整的音视频通话界面。

callwindow.cpp

#include "callwindow.h"
#include "ui_callwindow.h"
#include <QCameraInfo>
#include <QMessageBox>
#include <QTimer>
#include <QPalette>


CallWindow::CallWindow(const QString &peerName, const QString &peerAddress, 
                     quint16 peerPort, QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::CallWindow),
    m_peerName(peerName),
    m_peerAddress(peerAddress),
    m_peerPort(peerPort),
    m_audioManager(new AudioManager(this)),
    m_videoManager(new VideoManager(this)),
    m_rtpTransmitter(new RtpTransmitter(this))
{
    ui->setupUi(this);
    setWindowTitle(QString("正在与 %1 通话").arg(peerName));


    // 初始化UI
    ui->localVideoWidget->setBackgroundRole(QPalette::Dark);
    ui->remoteVideoWidget->setBackgroundRole(QPalette::Dark);
    ui->localVideoWidget->setAutoFillBackground(true);
    ui->remoteVideoWidget->setAutoFillBackground(true);


    // 显示可用相机
    ui->cameraComboBox->addItems(m_videoManager->availableCameras());


    // 连接信号槽
    connectSignals();


    // 设置远程地址
    m_rtpTransmitter->setRemoteAddress(peerAddress, peerPort);
    m_audioManager->setRemoteAddress(peerAddress, peerPort);
    m_videoManager->setRemoteAddress(peerAddress, peerPort);


    // 启动音频传输
    m_audioManager->startTransmission();
}


CallWindow::~CallWindow()
{
    stopCall();
    delete ui;
}


void CallWindow::connectSignals()
{
    // 音频信号
    connect(m_audioManager, &AudioManager::audioDataReady,
            m_rtpTransmitter, &RtpTransmitter::sendAudioData);
    connect(m_rtpTransmitter, &RtpTransmitter::audioDataReceived,
            m_audioManager, &AudioManager::playAudioData);


    // 视频信号
    connect(m_videoManager, &VideoManager::videoDataReady,
            m_rtpTransmitter, &RtpTransmitter::sendVideoData);
    connect(m_rtpTransmitter, &RtpTransmitter::videoDataReceived,
            m_videoManager, &VideoManager::displayVideoFrame);
    connect(m_videoManager, &VideoManager::frameReady,
            this, &CallWindow::updateRemoteVideo);


    // 本地视频预览
    connect(m_videoSurface, &VideoSurface::frameAvailable,
            this, &CallWindow::updateLocalVideo);
}


void CallWindow::on_startVideoButton_clicked()
{
    if (!m_videoManager->isStreaming()) {
        int cameraIndex = ui->cameraComboBox->currentIndex();
        m_videoManager->startStreaming(cameraIndex);


        // 设置本地视频预览
        m_videoManager->setViewfinder(m_videoSurface);


        ui->startVideoButton->setText("关闭视频");
    } else {
        m_videoManager->stopStreaming();
        ui->startVideoButton->setText("开启视频");
    }
}


void CallWindow::on_muteAudioButton_clicked()
{
    if (m_audioManager->isTransmitting()) {
        m_audioManager->stopTransmission();
        ui->muteAudioButton->setText("开启声音");
    } else {
        m_audioManager->startTransmission();
        ui->muteAudioButton->setText("静音");
    }
}


void CallWindow::on_endCallButton_clicked()
{
    stopCall();
    close();
}


void CallWindow::stopCall()
{
    m_videoManager->stopStreaming();
    m_audioManager->stopTransmission();
}


void CallWindow::updateLocalVideo(const QVideoFrame &frame)
{
    QImage image = frameToImage(frame);
    if (!image.isNull()) {
        ui->localVideoWidget->setPixmap(QPixmap::fromImage(image.scaled(
            ui->localVideoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)));
    }
}


void CallWindow::updateRemoteVideo(const QImage &image)
{
    if (!image.isNull()) {
        ui->remoteVideoWidget->setPixmap(QPixmap::fromImage(image.scaled(
            ui->remoteVideoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)));
    }
}


QImage CallWindow::frameToImage(const QVideoFrame &frame)
{
    QVideoFrame cloneFrame(frame);
    cloneFrame.map(QAbstractVideoBuffer::ReadOnly);


    QImage::Format imageFormat = QVideoFrame::imageFormatFromPixelFormat(cloneFrame.pixelFormat());
    if (imageFormat == QImage::Format_Invalid) {
        imageFormat = QImage::Format_RGB32;
    }


    QImage image(cloneFrame.bits(),
                cloneFrame.width(),
                cloneFrame.height(),
                cloneFrame.bytesPerLine(),
                imageFormat);


    cloneFrame.unmap();
    return image;
}

点击下方关注公众号【Linux教程】,获取 Qt技术栈学习路线、项目教程、简历模板、大厂面试题pdf文档、大厂面经、编程交流圈子等等。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值