websocket协议是基于tcp的协议,它的基本结构如图:

1.建立websocket连接

        建立websocket之间先跟websocket服务器建立tcp连接,再发送http请求实现协议切换,websocket服务器可以使用python搭建一个:

import asyncio
import websockets

async def echo(websocket):
    async for message in websocket:
        await websocket.send(f"服务器回复: {message}")

async def main():
    async with websockets.serve(echo, "0.0.0.0", 8765):
        await asyncio.Future()

asyncio.run(main())

注意:python需要安装websockets库:

pip install websockets

        运行以上脚本就会创建一个websocket的回传服务器,建立回传服务器之后,使用qt的tcp编程连接这个服务器,然后发送http请求,转换为websocket连接,qt主要程序如:

1.1 建立tcp链接:

WebSocketTcpClient::WebSocketTcpClient(QObject *parent)
    : QObject(parent)
    , m_handshakeDone(false)
{
    m_tcpSocket = new QTcpSocket(this);
    m_timer = new QTimer(this);

    connect(m_tcpSocket, &QTcpSocket::connected, this, &WebSocketTcpClient::onTcpConnected);
    connect(m_tcpSocket, &QTcpSocket::readyRead, this, &WebSocketTcpClient::onDataReceived);
    connect(m_timer, &QTimer::timeout, this, &WebSocketTcpClient::sendAutoMessage);

    m_tcpSocket->connectToHost("127.0.0.1", 8765);
}

1.2 切换协议为websocket:


void WebSocketTcpClient::sendUpgradeRequest()
{
    QString key = "dGhlIHNhbXBsZSBub25jZQ==";
    QString req = QString(
        "GET / HTTP/1.1\r\n"
        "Host: 127.0.0.1:8765\r\n"
        "Upgrade: websocket\r\n"
        "Connection: Upgrade\r\n"
        "Sec-WebSocket-Key: %1\r\n"
        "Sec-WebSocket-Version: 13\r\n\r\n"
    ).arg(key);

    m_tcpSocket->write(req.toUtf8());
}

其中key:QString key = "dGhlIHNhbXBsZSBub25jZQ==";

key 必须是一个随机生成的 16 字节数据

然后做 Base64 编码

结果就是:Base64 编码后的字符串长度 = 24 个字符(可能末尾带 ==

Base64 编码后长度固定为 24 字符

内容必须随机,不能每次都一样

这个固定 key 只是为了测试

Sec-WebSocket-Version:13 :这个版本是固定的。

1.3 websocket服务器响应成功:


void WebSocketTcpClient::onDataReceived()
{
    QByteArray data = m_tcpSocket->readAll();

    if (!m_handshakeDone) {
        // 打印服务器返回的握手信息(调试用)
        qDebug() << "[WS] 握手响应:\n" << data;

        // 1. 检查是否切换协议成功
        if (data.contains("101 Switching Protocols")) {

            // ==============================================
            // ✅【关键增加】开始校验 Sec-WebSocket-Accept
            // ==============================================
            QString response = QString::fromUtf8(data);

            // ① 从服务器返回的头中,提取 Sec-WebSocket-Accept
            QString serverAccept;
            int acceptIndex = response.indexOf("Sec-WebSocket-Accept:");
            if (acceptIndex != -1) {
                int start = acceptIndex + 21;
                int end = response.indexOf("\r\n", start);
                serverAccept = response.mid(start, end - start).trimmed();
            }

            // ② 客户端自己用【固定算法】计算正确的 Accept
            // 你发送的 Key(必须和发送握手时用的一样!)
            QString clientKey = "dGhlIHNhbXBsZSBub25jZQ==";
            // WebSocket 标准固定串(全球唯一,不能改)
            QString magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

            // SHA1 + Base64 编码
            QByteArray sha1Result = QCryptographicHash::hash(
                (clientKey + magic).toUtf8(),
                QCryptographicHash::Sha1
            );
            QString realAccept = sha1Result.toBase64();

            // ③ 校验:服务器返回的 和 我算的 是否一致
            if (serverAccept == realAccept) {
                qDebug() << "[WS] ✅ 握手成功 + 服务端认证成功!";
                m_handshakeDone = true;
                m_timer->start(1000);
            } else {
                qDebug() << "[WS] ❌ 认证失败!非法服务器!";
                qDebug() << "期望:" << realAccept;
                qDebug() << "收到:" << serverAccept;
                m_tcpSocket->close();
            }

        }
    } else {
        // 已经握手完成,正常解析帧
        parseWebSocketFrame(data);
    }
}

实际握手响应字符串[WS] :

 "HTTP/1.1 101 Switching Protocols\r\n
Date: Sat, 23 May 2026 17:16:42 GMT\r\n
Upgrade: websocket\r\nConnection: Upgrade\r\n
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n
Server: Python/3.12 websockets/16.0\r\n\r\n"

        websocket服务器响应的 Sec-WebSocket-Accept: 计算方法如代码所示。使用 key + WebSocket 标准固定串 做 sha1 哈希运算,再做 Base64 编码得出字符串。

2.websocket数据发送


// ============================
// 发送(标准掩码,正确)
// ============================
void WebSocketTcpClient::sendAutoMessage()
{
    static int count = 0;
    count++;

    QString msg = QString("TCP原生WebSocket消息 %1").arg(count);
    QByteArray payload = msg.toUtf8();

    uchar mask[4] = {0x11,0x22,0x33,0x44};
    QByteArray encoded = payload;

    for (int i=0; i<payload.size(); i++) {
        encoded[i] = payload[i] ^ mask[i%4];
    }

    QByteArray frame;
    frame.append((char)0x81);
    frame.append((char)(0x80 | payload.size()));
    frame.append((char)mask[0]);
    frame.append((char)mask[1]);
    frame.append((char)mask[2]);
    frame.append((char)mask[3]);
    frame.append(encoded);

    m_tcpSocket->write(frame);
    qDebug() << "[SEND] " << msg;
}

发送函数如上所示,把要发送的文本做utf-8编码。

定义掩码(MASK):

  • 掩码是4 个随机字节
  • 我这里写死只是为了演示
  • 真实环境应该随机生成

编码数据:编码后数据 = 原始数据 异或(^) 掩码,如代码所示循环异或所有数据。

第 1 个字节:0x81,二进制:10000001

  • 1 (FIN) 表示这是最后一帧
  • 000 (RSV) 保留位,必须为0
  • 0001 (opcode) 表示这是文本帧

第 2 个字节:0x80 | payload.size():

最高位 1 表示有掩码, 低7位 表示数据长度

追加 4 字节掩码 key,客户端必须发送掩码。

最终发送数据结构:

  • 第1字节:0x81(FIN + 文本帧)
  • 第2字节:0x80 | 长度(掩码+长度)
  • 第3~6字节:4字节掩码
  • 第7字节开始:编码后的 payload 数据

3.websocket数据接收


// ============================
// 解析 WebSocket 帧(含关闭处理)
// ============================
void WebSocketTcpClient::parseWebSocketFrame(const QByteArray &data)
{
    if (data.size() < 2)
        return;

    const uchar *buf = (const uchar*)data.constData();
    int pos = 0;

    // 第1字节:FIN + 操作码
    uchar fin_op = buf[pos++];
    // 第2字节:掩码位 + 长度
    uchar mask_len = buf[pos++];

    // 解析
    bool fin = (fin_op & 0x80) != 0;       // 是否最后一帧
    int opcode = fin_op & 0x0F;             // 帧类型
    bool mask = (mask_len & 0x80) != 0;    // 是否有掩码
    int payload_len = mask_len & 0x7F;     // 数据长度

    // ==========================================
    // ✅ 【关键】如果是 关闭帧(opcode == 0x08)
    // ==========================================
    if (opcode == 0x08) {
        qDebug() << "[WS] 收到服务器关闭帧 → 安全关闭TCP连接";
        m_timer->stop();        // 停止定时器
        m_tcpSocket->close();   // 关闭TCP
        return;
    }

    // 读取掩码key
    QByteArray maskKey;
    if (mask) {
        maskKey = data.mid(pos, 4);
        pos += 4;
    }

    // 读取数据
    QByteArray payload = data.mid(pos, payload_len);

    // 解码(只有mask=1才解码,服务器下发没有掩码)
    if (mask) {
        for (int i = 0; i < payload_len; i++) {
            payload[i] = payload[i] ^ maskKey[i % 4];
        }
    }

    qDebug() << "[RECV] 正确收到:" << QString::fromUtf8(payload);
}

4.关闭websocket

// ============================
// 标准关闭 WebSocket(客户端主动关闭)
// ============================
void WebSocketTcpClient::closeWebSocket()
{
    if (m_tcpSocket->state() == QAbstractSocket::ConnectedState)
    {
        qDebug() << "[WS] 主动发送关闭帧,请求断开连接";

        // 关闭帧格式
        QByteArray closeFrame;
        closeFrame.append((char)0x88);  // FIN=1 + opcode=8 (关闭帧)
        closeFrame.append((char)0x00);  // 长度0,无数据

        m_tcpSocket->write(closeFrame);
        m_tcpSocket->flush();

        m_timer->stop(); // 立即停止发送
    }
}

注意:这里只是发送了关闭帧,websocket回复消息后parseWebSocketFrame自动关闭tcp socket。

5.示例工程

https://github.com/1358484518/websockets/tree/mainhttps://github.com/1358484518/websockets/tree/main

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐