本文将带你从 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++后端开发、面试题等技术知识讲解
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断线重连功能
断线重连是提高系统可靠性的重要功能,我们在客户端实现自动重连机制。
断线重连实现(已集成在客户端代码中)
主要实现思路:
- 使用 QTimer 设置定时重连
- 监测连接状态,在意外断开时启动重连计时器
- 重连成功后停止计时器
- 提供手动触发重连的机制
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文档、大厂面经、编程交流圈子等等。
859

被折叠的 条评论
为什么被折叠?



