7.4在Qt中使用WebSocket
1. 前言WebSocket 是一种全双工通信协议允许客户端和服务器之间建立持久化的双向通信连接。使用 WebSocket 可以在单个 TCP 连接上实现客户端与服务器之间的实时、低延迟的数据传输有效解决了在使用HTTP通信时的局限性。如果还不清楚什么是WebSocket请先行阅读7.3WebSocket 详解如果涉及到网路通信一般会有两个角色分别是是客户端和服务器端。在Qt的标准库中为我们提供了相关的操作类一共有两个分别是 QWebSocket 和 QWebSocketServer。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 函数。使用 QWebSocket 和 QWebSocketServer 可以轻松地创建客户端和服务器它们之间可以互相通信。以下是基本的交互过程以发送文本格式的数据为例服务器端创建一个 QWebSocketServer 对象并监听一个端口。接收到新的客户端连接时通过 nextPendingConnection() 获取客户端对象并进行相应的信号槽连接处理用于和客户端的通信。监测textMessageReceived 信号通过它的槽函数接收客户端消息处理并回复消息sendTextMessage。客户端创建一个 QWebSocket 对象并连接到服务器。通过 connected 信号检测是否已经成功和服务器建立了连接。通过步骤1中得到的QWebSocket对象和服务器进行通信。通过textMessageReceived信号通知服务器数据已到达并在其对应的槽函数中进行处理回复数据sendTextMessage2. WebSocket 客户端QWebSocket 类是 Qt 提供的可以用于实现 WebSocket 客户端的类它可以实现与 WebSocket 服务器之间的双向通信。以下是关于 QWebSocket 的主要功能和特性的介绍建立连接使用 open() 方法连接到 WebSocket 服务器可以指定 URL 和可选的协议。发送和接收消息支持发送文本和二进制消息使用 sendTextMessage() 和 sendBinaryMessage() 方法。通过信号接收消息:textMessageReceived() 和 binaryMessageReceived() 信号分别用于接收文本和二进制消息。连接管理支持连接的打开和关闭close() 方法可以关闭与服务器的连接。通过 stateChanged() 信号可以监测连接状态的变化例如连接成功、断开等。支持 SSL可以通过 QWebSocket 的构造函数启用安全连接SSL。处理 ping/pong内置处理 WebSocket 协议中的 ping/pong 心跳机制以保持连接活跃。2.1 信号当成功连接到服务器时会触发此信号。void connected();当连接断开时会触发此信号。void disconnected();当接收到一个文本消息帧时会触发此信号。void textFrameReceived(const QString frame, bool isLastFrame);当接收到完整的文本消息时会触发此信号。void textMessageReceived(const QString message);当接收到一个二进制帧时会触发此信号。void binaryFrameReceived(const QByteArray frame, bool isLastFrame);当接收到完整的二进制消息时会触发此信号。void binaryMessageReceived(const QByteArray message);当连接状态改变时会触发此信号。void stateChanged(QAbstractSocket::SocketState state);关于上面的信号QWebSocket::binaryFrameReceived 和 QWebSocket::binaryMessageReceived 相同点是都与接收二进制数据相关区别在于处理数据的粒度和方式1.QWebSocket::binaryFrameReceived 信号触发条件当 WebSocket 接收到一个二进制帧时会触发此信号。参数该信号的参数是 QByteArray frame即接收到的二进制帧。特点WebSocket 协议支持将一个完整的消息拆分为多个帧进行传输这个信号会在每次收到一个帧时触发而不论这个帧是否是完整的消息。如果消息被分成多个帧发送程序会收到多次 binaryFrameReceived 信号。使用这个信号时开发者需要自己重组这些帧才能得到完整的消息。2.QWebSocket::binaryMessageReceived 信号触发条件当 WebSocket 接收到一个完整的二进制消息时会触发此信号。参数该信号的参数是 QByteArray message即接收到的完整二进制消息。特点与 binaryFrameReceived 不同这个信号是在所有帧组成一个完整的消息后触发的。开发者不需要手动重组帧因为 WebSocket 会自动处理帧的组合并将完整消息传递给你。一般来说如果你只想处理完整的数据建议使用 binaryMessageReceived它更方便。如果你需要深入控制帧或处理流式数据才会用到 binaryFrameReceived。同理我们也就可以想明白信号QWebSocket::textFrameReceived和 QWebSocket::textMessageReceived之间的区别了此处不再过多的进行赘述。2.2 重要的 API 函数构造函数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:443version指定 WebSocket 协议版本默认使用最新版本目前为 13。parent指向父对象的指针通常用于对象树管理可选参数默认为 nullptr。连接服务器[slot] void open(const QUrl url);关于函数参数的描述:url客户端连接到指定的 WebSocket URL。发送数据qint64 sendTextMessage(const QString message); 1 qint64 sendBinaryMessage(const QByteArray message); 2 [slot] void ping(const QByteArray payload QByteArray()); 3作者: 苏丙榅函数1发送文本消息返回已发送的字节数。函数2发送二进制消息返回已发送的字节数。函数3发送 Ping 帧用于检查连接状态。关闭连接[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 信号每当有新的连接可用时就会发出此信号。void newConnection();当服务器关闭时触发该信号。void closed();3.2 重要的 API 函数构造函数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。设置服务器监听的地址和端口信息bool listen(const QHostAddress address QHostAddress::Any, quint16 port 0);下面是关于函数参数的简要说明1.address服务器监听的地址信息如果 address 为 QHostAddress::Any服务器将监听所有网络接口。2.port服务器监听的端口当port为0时系统会自动选择一个可用端口。函数返回值成功时返回true失败返回false。从等待处理的连接队列中获取下一个已经完成握手的 WebSocket 连接用于处理客户端的连接请求QWebSocket *nextPendingConnection();关于该函数它的功能我们需要掌握以下几点1.当客户端向 QWebSocketServer 发起连接并完成 WebSocket 握手后该连接会进入一个队列nextPendingConnection() 用于从这个队列中获取下一个连接。2.每次调用此函数服务器会从连接队列中移除一个 QWebSocket 连接并将它返回给调用者。3.通常情况下nextPendingConnection() 会和 QWebSocketServer::newConnection() 信号一起使用。newConnection() 信号在每次有新的客户端连接时触发表明服务器有新的待处理连接。4.在 newConnection() 信号的槽函数中调用 nextPendingConnection()可以获取具体的 QWebSocket 对象从而进行消息传输和处理。下面是关于函数返回值的简要说明返回一个指向 QWebSocket 对象的指针该对象可以用来与客户端进行通信例如发送或接收消息。如果没有等待的客户端连接则返回 nullptr。关闭服务器服务器将不再监听传入的连接void close();4. 示例代码我们可以先搭建一个如下图所示的客户端和服务器界面:4.1 客户端代码client.h#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_Hclient.cpp#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#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_Hserver.cpp#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实例对象每个实例对应一个客户端此时就需要在服务器端通过容器对这些对象进行存储以保证在通信的时候能够快速找到它们。