本文基于 C++/Qt 实现一个 WebSocket 测试工具,支持连接 WebSocket 服务器、发送文本消息、接收服务器消息、主动断开连接、记录通信日志等功能。重点不是单纯展示代码,而是从 WebSocket 的通信特点出发,分析一个 WebSocket 客户端应该如何构思,以及如何使用 Qt 中的
QWebSocket完成连接管理和消息收发。
一、WebSocket 是什么?为什么不用普通 HTTP?
在理解 WebSocket 之前,可以先回想一下 HTTP 的工作方式。
普通 HTTP 更像是"一问一答":
客户端:我发一个请求给服务器
服务器:我返回一个响应给客户端
比如 GET 请求文章列表、POST 提交登录信息,这些都属于 HTTP 的典型场景。它的特点是:客户端主动发起请求,服务器被动返回结果。
但是有些场景不太适合这种方式,比如:
聊天消息
实时弹幕
在线游戏
股票价格推送
设备实时状态监控
服务器主动通知客户端
这些场景有一个共同点:服务器可能随时有新数据要发给客户端。
如果用普通 HTTP,就只能让客户端不停地问服务器:
客户端:有新消息吗?
服务器:没有
客户端:有新消息吗?
服务器:没有
客户端:有新消息吗?
服务器:有,给你
这种方式叫轮询,能用,但不够优雅,也比较浪费资源。
WebSocket 就是为了解决这类"实时双向通信"问题的。它的特点是:连接建立之后,客户端和服务器之间会保持一条长连接,双方都可以主动发送消息。
可以简单理解为:
HTTP:打电话问一句,对方回一句,然后挂断
WebSocket:电话一直保持接通,双方可以随时说话
所以 WebSocket 更适合需要实时通信的场景。
WebSocket 地址一般有两种:
ws:// 普通 WebSocket 连接
wss:// 加密 WebSocket 连接,类似 HTTPS
因此在测试工具中,URL 输入框应该要求用户输入完整的 ws:// 或 wss:// 地址。
二、实现 WebSocket 测试工具前,先构思哪些功能?
一个 WebSocket 测试工具,本质上就是一个 WebSocket 客户端。它要做的事情比 HTTP 更像"连接后持续通信"。
使用流程大概是这样:
输入 WebSocket 地址
点击连接服务器
连接成功后发送文本消息
等待服务器返回消息
把发送和接收内容显示到通信日志中
需要时主动断开连接
所以界面上至少需要这些部分:
1. WebSocket 地址输入框:填写 ws:// 或 wss:// 地址
2. 连接按钮:连接服务器
3. 断开按钮:主动关闭连接
4. 发送内容输入框:填写要发送的文本消息
5. 发送按钮:发送消息
6. 通信日志区域:显示连接、发送、接收、错误等信息
在 Qt 里,实现 WebSocket 客户端最核心的类是:
QWebSocket
它可以完成连接、发送、接收和断开操作。当前 FormWebSocketTester 类中,也正是使用 QWebSocket *m_webSocket 作为 WebSocket 客户端对象,同时用 m_connected 保存连接状态,用 m_logEntries 缓存通信日志。
类中的核心成员可以这样设计:
private:
Ui::FormWebSocketTester *ui;
// WebSocket 客户端对象,负责连接、发送和接收消息
QWebSocket *m_webSocket = nullptr;
// 当前是否已经连接服务器
bool m_connected = false;
// 日志缓存,用于保存通信记录
QStringList m_logEntries;
这里的思路很清楚:
m_webSocket:真正负责 WebSocket 通信
m_connected:判断当前能不能发送消息
m_logEntries:记录连接、发送、接收、错误信息
使用 m_connected 的原因是:WebSocket 必须先连接成功,才能发送消息。如果没有连接就允许用户点击发送,程序逻辑就会混乱。
三、初始化界面和信号槽:先把"连接状态"设计清楚
WebSocket 和 HTTP 最大的不同是:HTTP 点一次发送就结束了,而 WebSocket 有明显的"连接中"和"未连接"状态。
所以初始化界面时,要先设置默认提示,并让日志区域只读:
cpp
void FormWebSocketTester::initUi()
{
// 设置 WebSocket 地址输入提示
ui->lineEdit_WebSocketUrl->setPlaceholderText(
"例如:ws://127.0.0.1:8080 或 wss://example.com/ws"
);
// 设置发送内容输入提示
ui->textEdit_WebSocketSendData->setPlaceholderText(
"请输入要发送的WebSocket文本消息..."
);
// 默认填入一条测试消息,方便连接成功后直接发送
ui->textEdit_WebSocketSendData->setPlainText("Hello WebSocket Server.");
// 通信日志只用于显示,不允许手动编辑
ui->textEdit->setReadOnly(true);
ui->textEdit->setPlaceholderText("WebSocket通信日志会显示在这里...");
}
除了界面初始化,WebSocket 还非常依赖信号槽。因为连接成功、断开连接、收到消息、发生错误,这些事件都不是代码立即返回的,而是由 Qt 在对应时机发出信号。
当前实现中,按钮点击信号连接到对应槽函数,同时 QWebSocket 的 connected、disconnected、textMessageReceived 和错误信号也分别连接到了处理函数。
cpp
void FormWebSocketTester::initConnections()
{
// 点击连接按钮,连接 WebSocket 服务器
connect(ui->pushButton_WebSocketConnect, &QPushButton::clicked,
this, &FormWebSocketTester::onConnectClicked);
// 点击断开按钮,关闭 WebSocket 连接
connect(ui->pushButton_WebSocketDisconnect, &QPushButton::clicked,
this, &FormWebSocketTester::onDisconnectClicked);
// 点击发送按钮,发送文本消息
connect(ui->pushButton_WebSocketSend, &QPushButton::clicked,
this, &FormWebSocketTester::onSendClicked);
// WebSocket 连接成功后触发
connect(m_webSocket, &QWebSocket::connected,
this, &FormWebSocketTester::onConnected);
// WebSocket 断开连接后触发
connect(m_webSocket, &QWebSocket::disconnected,
this, &FormWebSocketTester::onDisconnected);
// 收到服务器文本消息时触发
connect(m_webSocket, &QWebSocket::textMessageReceived,
this, &FormWebSocketTester::onTextMessageReceived);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
// Qt6 中的错误信号
connect(m_webSocket, &QWebSocket::errorOccurred,
this, &FormWebSocketTester::onSocketError);
#else
// Qt5 中的错误信号写法
connect(m_webSocket,
QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
this,
&FormWebSocketTester::onSocketError);
#endif
}
这一部分其实是 WebSocket 的核心思想之一:
不是写一个 while 循环一直等消息,而是通过信号槽等待事件发生。
也就是说:
cpp
连接成功了,Qt 自动触发 connected
服务器发消息了,Qt 自动触发 textMessageReceived
连接断开了,Qt 自动触发 disconnected
发生错误了,Qt 自动触发 errorOccurred
这样写出来的程序更符合 Qt 的事件驱动机制。
四、连接服务器与发送消息:先建立长连接,再进行通信
连接 WebSocket 服务器时,第一步不是直接 open(),而是要先检查用户输入的地址是否合法。
比如地址不能为空,必须是合法 URL,并且协议只能是 ws 或 wss。
当前实现中,连接按钮点击后,会读取 URL,检查格式和协议,如果当前 socket 不是未连接状态,还会先 abort() 中止旧连接,然后再通过 open(url) 建立新连接。
cpp
void FormWebSocketTester::onConnectClicked()
{
// 读取用户输入的 WebSocket 地址
const QString urlText = ui->lineEdit_WebSocketUrl->text().trimmed();
// 地址不能为空
if (urlText.isEmpty()) {
QMessageBox::warning(this, "提示", "请输入WebSocket服务器地址。");
return;
}
// 转换成 QUrl,方便判断地址是否合法
const QUrl url(urlText);
// 必须是完整的 URL,并且包含协议和主机名
if (!url.isValid() || url.scheme().isEmpty() || url.host().isEmpty()) {
QMessageBox::warning(
this,
"提示",
"WebSocket地址格式无效,请输入完整的 ws:// 或 wss:// 地址。"
);
return;
}
// WebSocket 只支持 ws 和 wss
if (url.scheme() != "ws" && url.scheme() != "wss") {
QMessageBox::warning(this, "提示", "当前仅支持 ws:// 或 wss:// 地址。");
return;
}
// 如果之前还有连接状态,先强制中止旧连接
if (m_webSocket->state() != QAbstractSocket::UnconnectedState) {
m_webSocket->abort();
}
// 记录连接日志
appendLog(QString("[%1] 正在连接:%2")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(urlText));
// 连接过程中先禁用连接按钮和地址输入框,避免重复操作
ui->pushButton_WebSocketConnect->setEnabled(false);
ui->lineEdit_WebSocketUrl->setEnabled(false);
// 开始连接 WebSocket 服务器
m_webSocket->open(url);
}
这里最关键的是这一句:
cpp
m_webSocket->open(url);
它的含义是:向指定的 WebSocket 服务器发起连接。
但是注意,调用 open(url) 并不代表立刻连接成功。它只是开始连接,真正连接成功后,会触发:
cpp
QWebSocket::connected
然后进入:
cpp
void FormWebSocketTester::onConnected()
{
// 设置为已连接状态
setConnectedState(true);
// 记录连接成功日志
appendLog(QString("[%1] WebSocket连接成功")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")));
}
连接成功后,才可以发送消息。发送消息时,也要先判断是否已经连接:
cpp
void FormWebSocketTester::onSendClicked()
{
// 没有连接时,不允许发送消息
if (!m_webSocket || !m_connected) {
QMessageBox::warning(this, "提示", "请先连接WebSocket服务器。");
return;
}
// 读取用户输入的发送内容
const QString message = ui->textEdit_WebSocketSendData->toPlainText();
// 发送内容不能为空
if (message.trimmed().isEmpty()) {
QMessageBox::warning(this, "提示", "发送内容不能为空。");
return;
}
// 发送文本消息,并返回发送的字节数
const qint64 bytes = m_webSocket->sendTextMessage(message);
// 记录发送日志
appendLog(QString("[%1] [发送] %2 字节:%3")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(bytes)
.arg(message));
}
这里最关键的是:
cpp
m_webSocket->sendTextMessage(message);
它表示向服务器发送一条文本消息。
WebSocket 除了可以发送文本消息,也可以发送二进制消息。不过对于一个基础测试工具来说,先实现文本消息就足够了,比如测试聊天、JSON 指令、普通字符串通信等。
五、接收消息、断开连接与日志记录:让通信过程可观察
WebSocket 是双向通信,所以除了发送消息,还要能接收服务器主动发来的消息。
当服务器发送文本消息时,Qt 会自动触发:
cpp
textMessageReceived(const QString &message)
对应的槽函数可以这样写:
cpp
void FormWebSocketTester::onTextMessageReceived(const QString &message)
{
// 收到服务器消息后,直接追加到通信日志中
appendLog(QString("[%1] [接收] %2")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(message));
}
这里不需要主动循环读取,因为 QWebSocket 已经帮忙完成了底层接收工作。程序只需要在收到消息时,把内容显示出来即可。
主动断开连接也很简单:
cpp
void FormWebSocketTester::onDisconnectClicked()
{
// 如果没有连接,直接返回
if (!m_webSocket || m_webSocket->state() == QAbstractSocket::UnconnectedState) {
return;
}
// 记录主动断开日志
appendLog(QString("[%1] 主动断开连接")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")));
// 关闭 WebSocket 连接
m_webSocket->close();
}
断开后会自动触发 disconnected 信号,然后进入:
cpp
void FormWebSocketTester::onDisconnected()
{
// 设置为未连接状态
setConnectedState(false);
// 记录断开日志
appendLog(QString("[%1] WebSocket连接已断开")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")));
}
错误处理也很重要,比如地址错误、服务器没有启动、连接被拒绝、网络断开等情况,都应该显示到日志中:
cpp
void FormWebSocketTester::onSocketError(QAbstractSocket::SocketError error)
{
Q_UNUSED(error)
// 获取错误信息
const QString errorString = m_webSocket ? m_webSocket->errorString() : "未知错误";
// 记录错误日志
appendLog(QString("[%1] WebSocket错误:%2")
.arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"))
.arg(errorString));
// 出错后恢复为未连接状态
setConnectedState(false);
}
为了让界面状态和连接状态保持一致,可以单独封装一个 setConnectedState() 函数。当前实现中,这个函数会根据连接状态启用或禁用 URL 输入框、连接按钮、断开按钮、发送按钮和消息输入框。
cpp
void FormWebSocketTester::setConnectedState(bool connected)
{
// 保存当前连接状态
m_connected = connected;
// 已连接时,不允许修改地址,也不允许再次点击连接
ui->lineEdit_WebSocketUrl->setEnabled(!connected);
ui->pushButton_WebSocketConnect->setEnabled(!connected);
// 已连接时,才允许断开连接和发送消息
ui->pushButton_WebSocketDisconnect->setEnabled(connected);
ui->pushButton_WebSocketSend->setEnabled(connected);
ui->textEdit_WebSocketSendData->setEnabled(connected);
// 记录状态变化
if (connected) {
appendLog("状态:已连接");
} else {
appendLog("状态:未连接");
}
}
最后,日志显示和日志保存也可以保留。通信日志不只是为了好看,而是为了让测试过程可追踪。
cpp
void FormWebSocketTester::appendLog(const QString &message)
{
// 添加到日志缓存,方便后续保存到文件
m_logEntries.append(message);
// 显示到界面的通信日志区域
if (ui && ui->textEdit) {
ui->textEdit->append(message);
// 光标移动到最后,保证最新日志可见
ui->textEdit->moveCursor(QTextCursor::End);
}
// 同时输出到调试窗口
qDebug() << "WebSocket测试工具日志:" << message;
}
这样每一次连接、断开、发送、接收、错误都会显示在日志区域中,测试时就能清楚看到完整通信过程。
总结
WebSocket 和普通 HTTP 最大的区别在于:HTTP 更偏向"一次请求,一次响应",而 WebSocket 是"建立长连接后,双方都可以随时发送消息"。
实现一个 Qt WebSocket 测试工具,可以按照这个思路来设计:
cpp
1. 使用 QWebSocket 作为客户端对象
2. 输入 ws:// 或 wss:// 地址
3. 调用 open(url) 连接服务器
4. 连接成功后通过 sendTextMessage() 发送文本消息
5. 通过 textMessageReceived 信号接收服务器消息
6. 通过 close() 主动断开连接
7. 使用日志区域记录连接、发送、接收和错误信息
从代码结构上看,几个关键函数分别负责不同任务:
cpp
initUi():初始化界面提示和默认状态
initConnections():绑定按钮和 WebSocket 信号
onConnectClicked():校验地址并发起连接
onSendClicked():发送文本消息
onTextMessageReceived():接收服务器消息
setConnectedState():根据连接状态控制界面
appendLog():记录并显示通信日志
整体来看,这个 WebSocket 测试模块并不复杂,但它和 HTTP 模块的思想明显不同。HTTP 更适合接口请求测试,而 WebSocket 更适合实时通信测试。把 WebSocket 加入网络调试助手后,就可以覆盖聊天、推送、设备状态上报、实时数据返回等更多应用场景。