Content Table

Qt WebSocket

Qt 自带了 WebSocket 的客户端,使用起来也很方便,我们在其基础上进行了简单的封装,支持自动重连以及定时心跳发送。WebSocket 客户端的调用者只需要关注下面代码中的几个方法和信号就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MainWidget::MainWidget(QWidget *parent) : QWidget(parent), ui(new Ui::MainWidget) {
ui->setupUi(this);

// 提示: 创建 Websocket 连接,服务器的 IP 根据实际情况填写,端口为 9321
wsClient = new WsClient("127.0.0.1:9321", "gw-1", "gw-1");

// 连接成功或者连接断开
connect(wsClient, &WsClient::connected, [this](bool yes) {
this->ui->stateLabel->setText(yes ? "连接成功" : "连接断开");
});

// 收到服务器发来的消息
connect(wsClient, &WsClient::messageReceived, [this](const QString &message) {
this->ui->responseLabel->setText(message);
});

// 点击按钮发送消息
connect(ui->pushButton, &QPushButton::clicked, [this] {
this->wsClient->sendMessage("ECHO", this->ui->lineEdit->text());
});

// 连接到服务器
wsClient->connectToServer();
}

我们封装的 WebSocket 代码包含下面 2 个文件: WsClient.h 和 WsClient.cpp。在实际使用的时候,根据 WebSocket 服务器端设计稍微修改以下几个地方:

  • URL 参数稍微修改一下: WsClientPrivate::connectUrl()
  • 心跳消息
  • 消息格式: WsClient::sendMessage 中的消息格式

WsClient.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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#ifndef WSCLIENT_H
#define WSCLIENT_H

#include <QString>
#include <QObject>

class WsClientPrivate;
class QWebSocket;
class QTimer;

/**
* @brief Websocket 客户端,实现了自动重连,发送心跳
*/
class WsClient : public QObject {
Q_OBJECT

public:
/**
* @brief 创建 Websocket 客户端
*
* @param serverIpPort 服务器的 Ip 和端口,如: 127.0.0.1:9321
* @param gatewayId 设备网关的 ID,每个设备网关都有唯一的 ID
* @param gatewayName 设备网关的名字,不需要唯一
*/
WsClient(const QString &serverIpPort, const QString &gatewayId, const QString &gatewayName);

~WsClient();

/**
* @brief 连接到服务器
*/
void connectToServer();

/**
* @brief 发送消息到服务器
*
* @param type 消息类型,如 METRICS, ECHO 等
* @param message 消息
*/
void sendMessage(const QString &type, const QString &message);

signals:
/**
* @brief 与服务器连接成功或者连接断开的信号
*
* @param yes 连接成功时 yes 为 true, 连接断开时 yes 为 false
*/
void connected(bool yes);

/**
* @brief 收到消息
*
* @param message 消息
*/
void messageReceived(const QString &message);

private:
WsClientPrivate *d;
};

#endif // WSCLIENT_H

WsClient.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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include "WsClient.h"

#include <QWebSocket>
#include <QTimer>
#include <QDebug>

/*-----------------------------------------------------------------------------|
| WsClientPrivate implementation |
|----------------------------------------------------------------------------*/
class WsClientPrivate {
public:
WsClientPrivate(const QString &serverIpPort, const QString &gatewayId, const QString &gatewayName);
~WsClientPrivate();

// 连接到服务器的 Websocket 连接字符串,例如 ws://127.0.0.1:9321?gatewayId=1&gatewayName=bob
QString connectUrl() const;

QString serverIpPort; // 服务器的 IP Port
QString gatewayId; // 设备网关 Id
QString gatewayName; // 设备网关名字
QWebSocket *socket; // Websocket 对象
bool connected; // 是否已经和服务器连接上
QTimer *heartbeatTimer; // 心跳定时器
QTimer *reconnectTimer; // 重连定时器
int heartbeatInterval = 10000; // 心跳间隔
int reconnectInterval = 5000; // 重连间隔
int reconnectCount = 0; // 重连次数
};

WsClientPrivate::WsClientPrivate(const QString &serverIpPort, const QString &gatewayId, const QString &gatewayName) {
this->serverIpPort = serverIpPort;
this->gatewayId = gatewayId;
this->gatewayName = gatewayName;
this->connected = false;
this->socket = new QWebSocket();
this->heartbeatTimer = new QTimer();
this->reconnectTimer = new QTimer();

// 启动心跳定时器,定时给服务器发送心跳消息
}

WsClientPrivate::~WsClientPrivate() {
heartbeatTimer->stop();
heartbeatTimer->deleteLater();
reconnectTimer->stop();
reconnectTimer->deleteLater();
socket->close();
socket->deleteLater();
}

QString WsClientPrivate::connectUrl() const {
return QString("ws://%1?gatewayId=%2&gatewayName=%3")
.arg(serverIpPort)
.arg(gatewayId)
.arg(gatewayName);
}

/*-----------------------------------------------------------------------------|
| WsClient implementation |
|----------------------------------------------------------------------------*/
WsClient::WsClient(const QString &serverIpPort, const QString &gatewayId, const QString &gatewayName) : QObject() {
d = new WsClientPrivate(serverIpPort, gatewayId, gatewayName);

// 连接成功
QObject::connect(d->socket, &QWebSocket::connected, [this] {
d->connected = true;
d->heartbeatTimer->start(d->heartbeatInterval); // 连接成功时启动心跳计时器
d->reconnectTimer->stop(); // 连接成功时关闭重连计时器

emit this->connected(true);
});

// 连接断开
QObject::connect(d->socket, &QWebSocket::disconnected, [this] {
d->connected = false;
d->heartbeatTimer->stop(); // 连接断开时关闭心跳计时器
d->reconnectTimer->start(d->reconnectInterval); // 连接断开时启动重连计时器

emit this->connected(false);
});

// 收到消息
QObject::connect(d->socket, &QWebSocket::textMessageReceived, [this](const QString &message) {
emit this->messageReceived(message);
});

// 定时发送心跳,当服务器端在指定时间内没有收到客户端的心跳消息,服务器会主动断开对应的连接
QObject::connect(d->heartbeatTimer, &QTimer::timeout, [this] {
if (d->connected) {
d->socket->sendTextMessage(R"({"type": "HEARTBEAT"})");
}
});

// 连接断开后定时尝试重连
// 定时 5 秒尝试连接到服务器,如果连接断开了,测试方法: 连接上服务器,然后把服务器关闭,再打开服务器,查看连接状态。
// 如果不这么做,服务器重启后,设备网关连接断开不主动自动重连,那么只有重启设备网关的程序才会再次连接。
QObject::connect(d->reconnectTimer, &QTimer::timeout, [this] {
this->connectToServer(); // 如果已经是连接成功状态,不会重复连接
});
}

WsClient::~WsClient() {
delete d;
}

void WsClient::connectToServer() {
// 1. 如果已经连接,直接返回,不要重复连接
// 2. 如果重连计时器没有启动,则启动他,当没有连接到服务器的时候,自动尝试重连
// 3. 连接到服务器

// [1] 如果已经连接,直接返回,不要重复连接
if (d->connected) {
return;
}

// [2] 如果重连计时器没有启动,则启动他,当没有连接到服务器的时候,自动尝试重连
if (!d->reconnectTimer->isActive()) {
d->reconnectTimer->start(d->reconnectInterval);
}

// [3] 连接到服务器
QString url = d->connectUrl();
d->socket->open(QUrl(url));

d->reconnectCount++;
qDebug().noquote() << QString("第 %1 次尝试连接到服务器 %2 ...").arg(d->reconnectCount).arg(url);
}

void WsClient::sendMessage(const QString &type, const QString &message) {
// 未连接,不发送消息
if (!d->connected) {
return;
}

// 消息使用 JSON 格式,如 {"type": "ECHO", "content": "Hello"}
QString msg = QString(R"({"type": "%1", "content": "%2"})").arg(type).arg(message);
d->socket->sendTextMessage(msg);
}