在Qt中使用WebSocket

1. 前言

WebSocket 是一种全双工通信协议,允许客户端和服务器之间建立持久化的双向通信连接。使用 WebSocket 可以在单个 TCP 连接上实现客户端与服务器之间的实时、低延迟的数据传输,有效解决了在使用HTTP通信时的局限性。如果还不清楚什么是WebSocket请先行阅读WebSocket 详解

如果涉及到网路通信一般会有两个角色,分别是是客户端服务器端。在Qt的标准库中为我们提供了相关的操作类,一共有两个,分别是 QWebSocketQWebSocketServer

  • QWebSocket :Qt 提供的用于实现 WebSocket 的数据通信类。
  • QWebSocketServer: Qt 提供的一个用于实现 WebSocket 服务器的类。它允许创建一个 WebSocket 服务器,接受客户端连接,并与这些客户端进行双向通信(通信过程需要使用 QWebSocket 类)。

Qt提供的这两个类相当于是对 WebSocket 协议进行了封装,不论是建立连接,还是数据通信已经全然看不到协议原有的样子了,我们只需要调用类提供的接口函数就可以非常轻松的实现客户端和服务器端的数据通信。

由于 WebSocket 协议在传输层使用的是 TCP,并且可以实现双工的持久化通信,所以 Qt 中的 WebSocket 提供的接口类 QWebSocket、QWebSocketServer 和 Qt 中的 TcpSocket 提供的接口类QTcpSocket、QTcpServer中提供的函数接口有很多类似的地方,使用方法也大致相同。

另外,在 Qt6 中如果想要使用 WebSocket 协议进行网路通信,需要额外安装相关的模块,如下图:

展开Additional Libraries节点之后,找它的子选项Qt WebSocket:

熟悉 Qt 的小伙伴都知道,Qt 官方提供了非常丰富且详细的文档。在使用 WebSocket 相关类的 API 时,建议先查看官方对各个函数的介绍和说明,这对学习和编程都大有裨益。下面给大家介绍一下这两个类中常用的一些 API 函数。

使用 QWebSocketQWebSocketServer 可以轻松地创建客户端和服务器,它们之间可以互相通信。以下是基本的交互过程(以发送文本格式的数据为例):

  • 服务器端

    1. 创建一个 QWebSocketServer 对象并监听一个端口。
    2. 接收到新的客户端连接时,通过 nextPendingConnection() 获取客户端对象,并进行相应的信号槽连接处理,用于和客户端的通信。
    3. 监测textMessageReceived 信号,通过它的槽函数接收客户端消息,处理并回复消息sendTextMessage
  • 客户端

    1. 创建一个 QWebSocket 对象并连接到服务器。
    2. 通过 connected 信号检测是否已经成功和服务器建立了连接。
    3. 通过步骤1中得到的QWebSocket对象和服务器进行通信。
      • 通过textMessageReceived信号通知服务器数据已到达,并在其对应的槽函数中进行处理
      • 回复数据:sendTextMessage

2. WebSocket 客户端

QWebSocket 类是 Qt 提供的可以用于实现 WebSocket 客户端的类,它可以实现与 WebSocket 服务器之间的双向通信。以下是关于 QWebSocket 的主要功能和特性的介绍:

  1. 建立连接
    • 使用 open() 方法连接到 WebSocket 服务器,可以指定 URL 和可选的协议。
  2. 发送和接收消息
    • 支持发送文本和二进制消息,使用 sendTextMessage()sendBinaryMessage() 方法。
    • 通过信号接收消息:textMessageReceived()binaryMessageReceived() 信号分别用于接收文本和二进制消息。
  3. 连接管理
    • 支持连接的打开和关闭,close() 方法可以关闭与服务器的连接。
    • 通过 stateChanged() 信号可以监测连接状态的变化(例如连接成功、断开等)。
  4. 支持 SSL
    • 可以通过 QWebSocket 的构造函数启用安全连接(SSL)。
  5. 处理 ping/pong
    • 内置处理 WebSocket 协议中的 ping/pong 心跳机制,以保持连接活跃。

2.1 信号

  • 当成功连接到服务器时,会触发此信号。

    1
    void connected();
  • 当连接断开时,会触发此信号。

    1
    void disconnected();
  • 当接收到一个文本消息帧时,会触发此信号。

    1
    void textFrameReceived(const QString &frame, bool isLastFrame);
  • 当接收到完整的文本消息时,会触发此信号。

    1
    void textMessageReceived(const QString &message);
  • 当接收到一个二进制帧时,会触发此信号。

    1
    void binaryFrameReceived(const QByteArray &frame, bool isLastFrame);
  • 当接收到完整的二进制消息时,会触发此信号。

    1
    void binaryMessageReceived(const QByteArray &message);
  • 当连接状态改变时,会触发此信号。

1
void stateChanged(QAbstractSocket::SocketState state);

关于上面的信号QWebSocket::binaryFrameReceivedQWebSocket::binaryMessageReceived 相同点是都与接收二进制数据相关,区别在于处理数据的粒度和方式:

  1. QWebSocket::binaryFrameReceived 信号

    • 触发条件:当 WebSocket 接收到一个二进制帧时,会触发此信号。

    • 参数:该信号的参数是 QByteArray &frame,即接收到的二进制帧。

    • 特点:

      • WebSocket 协议支持将一个完整的消息拆分为多个帧进行传输,这个信号会在每次收到一个帧时触发,而不论这个帧是否是完整的消息。
      • 如果消息被分成多个帧发送,程序会收到多次 binaryFrameReceived 信号。
      • 使用这个信号时,开发者需要自己重组这些帧,才能得到完整的消息。
  2. QWebSocket::binaryMessageReceived 信号

    • 触发条件:当 WebSocket 接收到一个完整的二进制消息时,会触发此信号。

    • 参数:该信号的参数是 QByteArray &message,即接收到的完整二进制消息。

    • 特点:

      • binaryFrameReceived 不同,这个信号是在所有帧组成一个完整的消息后触发的。
      • 开发者不需要手动重组帧,因为 WebSocket 会自动处理帧的组合并将完整消息传递给你。

一般来说,如果你只想处理完整的数据,建议使用 binaryMessageReceived,它更方便。如果你需要深入控制帧或处理流式数据,才会用到 binaryFrameReceived

同理,我们也就可以想明白信号QWebSocket::textFrameReceivedQWebSocket::textMessageReceived之间的区别了,此处不再过多的进行赘述。

2.2 重要的 API 函数

  • 构造函数

    1
    2
    3
    explicit QWebSocket(const QString &origin = QString(), 
    QWebSocketProtocol::Version version = QWebSocketProtocol::VersionLatest,
    QObject *parent = nullptr);

    关于函数参数的描述:

    • origin:指定 WebSocket 的源(可选),通常格式为 protocol://host:port,例如:
      • 非安全连接:"ws://example.com"
      • 安全连接:"wss://example.com:443"
    • version:指定 WebSocket 协议版本(默认使用最新版本,目前为 13)。
    • parent:指向父对象的指针,通常用于对象树管理(可选参数,默认为 nullptr)。
  • 连接服务器

    1
    [slot] void open(const QUrl &url);

    关于函数参数的描述:

    • url:客户端连接到指定的 WebSocket URL。
  • 发送数据

    1
    2
    3
    qint64 sendTextMessage(const QString &message);             <1>
    qint64 sendBinaryMessage(const QByteArray &message); <2>
    [slot] void ping(const QByteArray &payload = QByteArray()); <3>
    • 函数<1>:发送文本消息,返回已发送的字节数。
    • 函数<2>:发送二进制消息,返回已发送的字节数。
    • 函数<3>:发送 Ping 帧,用于检查连接状态。
  • 关闭连接

    1
    2
    [slot] void close(QWebSocketProtocol::CloseCode closeCode = QWebSocketProtocol::CloseCodeNormal, 
    const QString &reason = QString());

    关于函数参数的描述:

    • closeCode:链接关闭对应的状态码。QWebSocketProtocol::CloseCodeNormal 表示正常关闭。
    • reason:描述关闭的原因。默认情况下是一个空字符串。

3. WebSocket 服务器

QWebSocketServer 类是 Qt 提供的一个用于实现 WebSocket 服务器的类。它允许创建一个 WebSocket 服务器,接受客户端连接,并与这些客户端进行双向通信。以下是 QWebSocketServer 类的主要功能和特性:

  1. 启动和监听
    • 使用 listen() 函数在指定的地址和端口上启动服务器,开始接受连接请求。
    • 可以选择监听所有网络接口或特定的地址。
  2. 接受客户端连接
    • 当有新的客户端连接请求时,发出 newConnection() 信号。
    • 使用 nextPendingConnection() 函数获取待处理的客户端连接。
  3. 关闭服务器
    • 使用 close() 函数停止服务器,并关闭所有当前连接的客户端。
  4. 安全模式
    • 支持 SSL 模式,通过构造函数参数指定是否启用 SSL(安全的 WebSocket 连接)。
  5. 管理连接
    • 通过 QWebSocket 对象与客户端进行通信,支持文本和二进制消息的发送和接收。
    • 支持管理多个客户端连接。

3.1 信号

  • 每当有新的连接可用时,就会发出此信号。

    1
    void newConnection();
  • 当服务器关闭时,触发该信号。

    1
    void closed();

3.2 重要的 API 函数

  • 构造函数

    1
    2
    3
    QWebSocketServer(const QString &serverName, 
    QWebSocketServer::SslMode secureMode,
    QObject *parent = nullptr);

    下面是关于函数参数的简要说明:

    1. serverName: 服务器的名称,通常用于标识服务器。
    2. secureMode: 指定服务器的安全模式,可以是以下值之一:
      • QWebSocketServer::NonSecureMode:非安全的 WebSocket 连接(ws://)。
      • QWebSocketServer::SecureMode:安全的 WebSocket 连接(wss://),需要 SSL/TLS 配置。
    3. parent: 指向父对象的指针,通常用于对象树管理(可选参数,默认为 nullptr)。
  • 设置服务器监听的地址和端口信息

    1
    bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0);

    下面是关于函数参数的简要说明:

    1. address:服务器监听的地址信息,如果 addressQHostAddress::Any,服务器将监听所有网络接口。
    2. port:服务器监听的端口,当port0时,系统会自动选择一个可用端口。

    函数返回值:成功时返回true,失败返回false

  • 从等待处理的连接队列中获取下一个已经完成握手的 WebSocket 连接,用于处理客户端的连接请求

    1
    QWebSocket *nextPendingConnection();

    关于该函数,它的功能我们需要掌握以下几点:

    1. 当客户端向 QWebSocketServer 发起连接并完成 WebSocket 握手后,该连接会进入一个队列,nextPendingConnection() 用于从这个队列中获取下一个连接。
    2. 每次调用此函数,服务器会从连接队列中移除一个 QWebSocket 连接,并将它返回给调用者。
    3. 通常情况下,nextPendingConnection() 会和 QWebSocketServer::newConnection() 信号一起使用。newConnection() 信号在每次有新的客户端连接时触发,表明服务器有新的待处理连接。
    4. newConnection() 信号的槽函数中调用 nextPendingConnection(),可以获取具体的 QWebSocket 对象,从而进行消息传输和处理。

    下面是关于函数返回值的简要说明:

    • 返回一个指向 QWebSocket 对象的指针,该对象可以用来与客户端进行通信(例如发送或接收消息)。
    • 如果没有等待的客户端连接,则返回 nullptr
  • 关闭服务器,服务器将不再监听传入的连接

    1
    void close();

4. 示例代码

我们可以先搭建一个如下图所示的客户端和服务器界面:

image-20240923001418765

4.1 客户端代码

client.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef CLIENT_H
#define CLIENT_H

#include <QMainWindow>
#include <QWebSocket>

QT_BEGIN_NAMESPACE
namespace Ui {
class Client;
}
QT_END_NAMESPACE

class Client : public QMainWindow
{
Q_OBJECT

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

private:
Ui::Client *ui;
QWebSocket m_client;
};
#endif // CLIENT_H

client.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "client.h"
#include "ui_client.h"

Client::Client(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::Client)
{
ui->setupUi(this);
setWindowTitle("Websocket - 客户端");

connect(&m_client, &QWebSocket::connected, this, [=](){
ui->recvMsgInfo->append("恭喜, 连接服务器成功!");
m_client.sendTextMessage("你好, 服务器!");
});
connect(&m_client, &QWebSocket::disconnected, this, [=](){
ui->recvMsgInfo->append("服务器已经断开了和客户端的连接!");
});
connect(&m_client, &QWebSocket::textMessageReceived, this, [=](const QString& msg){
ui->recvMsgInfo->append("服务器: " + msg);
});
connect(ui->sendBtn, &QPushButton::clicked, this, [=](){
QString msg = ui->sendMsgInfo->toPlainText();
m_client.sendTextMessage(msg);
ui->recvMsgInfo->append("客户端: " + msg);
ui->sendMsgInfo->clear();
});
connect(ui->connectBtn, &QPushButton::clicked, this, [=](){
QString url = ui->url->text();
m_client.open(url);
});
}

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

4.2 服务器代码

server.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#ifndef SERVER_H
#define SERVER_H

#include <QMainWindow>
#include <QWebSocket>
#include <QWebSocketServer>

QT_BEGIN_NAMESPACE
namespace Ui {
class Server;
}
QT_END_NAMESPACE

class Server : public QMainWindow
{
Q_OBJECT

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

void onNewConnection();

private:
Ui::Server *ui;
QWebSocket *m_socket;
QWebSocketServer* m_server;
};
#endif // SERVER_H

server.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include "server.h"
#include "ui_server.h"


Server::Server(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::Server)
{
ui->setupUi(this);
setWindowTitle("Websocket - 服务器");

// 实例化Websocket服务器对象
m_server = new QWebSocketServer("MyServer", QWebSocketServer::NonSecureMode, this);

connect(ui->listenBtn, &QPushButton::clicked, this, [=](){
quint16 port = ui->port->text().toInt();
if(m_server->listen(QHostAddress::Any, port))
{
connect(m_server, &QWebSocketServer::newConnection, this, &Server::onNewConnection);
ui->listenBtn->setDisabled(true);
}
});
connect(ui->sendBtn, &QPushButton::clicked, this, [=](){
QString msg = ui->sendMsgInfo->toPlainText();
if(m_socket)
{
m_socket->sendTextMessage(msg);
}
ui->sendMsgInfo->clear();
ui->recvMsgInfo->append("服务器: " + msg);
});
}

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

void Server::onNewConnection()
{
m_socket = m_server->nextPendingConnection();
connect(m_socket, &QWebSocket::textMessageReceived, this, [=](const QString &msg){
ui->recvMsgInfo->append("客户端: " + msg);
});
connect(m_socket, &QWebSocket::disconnected, m_socket, &QWebSocket::deleteLater);
}

由于服务器端是可以同时和多个客户端建立连接的,因此在使用 Websocket 进行网路通信的时候,在服务器端可能会得到多个QWebSocket实例对象,每个实例对应一个客户端,此时就需要在服务器端通过容器对这些对象进行存储,以保证在通信的时候能够快速找到它们。