音视频学习(三十六):websocket协议总结

发布于:2025-07-06 ⋅ 阅读:(14) ⋅ 点赞:(0)

概述

项目 描述
标准 RFC 6455
使用端口 默认 80(ws),443(wss)
基于协议 TCP
特性 全双工、低开销、持久连接、可穿透代理

特点

  • 全双工通信: WebSocket 允许客户端和服务器之间建立一个持久的连接,并且数据可以在两个方向上同时传输。这与传统的 HTTP 请求-响应模型不同,HTTP 每次通信都需要建立新的连接,效率较低。
  • 低延迟: 由于连接是持久的,避免了重复建立连接的开销,WebSocket 在实时通信场景下具有显著的低延迟优势。
  • 兼容性: WebSocket 通过 HTTP 握手进行升级,因此可以在标准的 HTTP 端口(80 和 443)上工作,并兼容 HTTP 代理和中间件,这使得它在 Web 环境中具有良好的兼容性。
  • 轻量级协议: 相较于 HTTP,WebSocket 协议头更小,传输数据时的开销更低。

工作原理

1. 建立连接(握手阶段)

WebSocket 连接的建立使用 HTTP 协议进行握手,然后升级为 WebSocket 协议。

请求(客户端 → 服务端):

GET /chat HTTP/1.1
Host: example.com:80
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

响应(服务端 → 客户端):

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=

Sec-WebSocket-Accept 是服务端基于客户端的 Sec-WebSocket-Key 和魔法字符串计算得到的 SHA1 后 Base64 值,用于验证合法性。

关键字段说明:

字段 说明
Upgrade: websocket 请求升级为 WebSocket
Connection: Upgrade 必须和 Upgrade 一起使用
Sec-WebSocket-Key 客户端生成的随机值,用于服务端验证
Sec-WebSocket-Version 当前协议版本,一般为 13

2. 数据传输阶段

握手成功后,TCP 连接升级为 WebSocket,开始使用 WebSocket 帧格式传输数据:

WebSocket 数据帧格式:

字段 说明
FIN 是否为消息最后一帧
RSV1-3 一般为 0
Opcode 操作码(0x1=文本,0x2=二进制,0x8=关闭,0x9=ping,0xA=pong)
MASK 是否有掩码(客户端必须是1)
Payload length 数据长度(<=125,126, 127为扩展)
Masking-key 客户端发送时存在,用于解码 Payload
Payload data 真实数据

数据帧

结构

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
     +---------------------------------------------------------------+

字段说明

字段 位数 说明
FIN 1 bit 是否为消息最后一帧(1 = 最后一帧)
RSV1-3 3 bits 通常为 0,扩展协议用
Opcode 4 bits 帧类型,如文本、二进制、ping、pong、关闭等
MASK 1 bit 是否有掩码(客户端必须为 1,服务端必须为 0)
Payload Length 7 bits 数据长度: 0-125 表示实际长度 126 表示后跟 16 位扩展长度 127 表示后跟 64 位扩展长度
Extended Payload Length 可选 当 Payload Length 为 126 或 127 时出现
Masking Key 32 bits 客户端发出时必须有,用于解码 Payload
Payload Data 可变 实际的数据内容,若有掩码则是经过掩码异或的

Opcode

Opcode (4 bits) 含义
0x0 continuation(延续帧)
0x1 text(文本帧,UTF-8 编码)
0x2 binary(二进制帧)
0x8 close(关闭连接)
0x9 ping(心跳请求)
0xA pong(心跳响应)

Payload 长度表示规则

Payload Len 描述
0 ~ 125 长度就是值本身
126 后接 2 字节 扩展长度(16 位)
127 后接 8 字节 扩展长度(64 位)

掩码(Masking)

  • 客户端必须使用掩码,服务端不能。
  • Masking key 为 4 字节,掩码算法:
decoded_data[i] = encoded_data[i] XOR masking_key[i % 4]

websocket安全连接(WSS)

握手流程

Client (Browser / App)
        |
        | 1. 建立 TCP 连接到 wss://host:443
        |
        | 2. 启动 TLS 握手  ⇨(证书验证、密钥协商)
        |
        | 3. TLS 握手成功,进入加密通道
        |
        | 4. 发送 WebSocket Upgrade 请求(加密的 HTTP 请求)
        |    GET /chat HTTP/1.1
        |    Host: example.com
        |    Upgrade: websocket
        |    Connection: Upgrade
        |    Sec-WebSocket-Key: xxxxx==
        |    ...
        |
        | 5. 服务器返回 101 Switching Protocols 响应
        |    + Sec-WebSocket-Accept 校验
        |
        | 6. 握手完成,开始加密的数据帧传输(Frame)

关键步骤

TCP + TLS 握手

  • 使用标准的 HTTPS 流程建立 TLS 连接
  • 客户端会校验证书(支持 SNI、可校验 SAN 域名);
  • TLS 会协商对称加密算法、握手密钥、会话密钥;
  • 完成后进入 加密通信通道

发送加密的 HTTP WebSocket 升级请求

  • 浏览器或客户端发送 Upgrade: websocket 的请求;
  • 这实际上是一个 加密的 HTTP 请求,例如:
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

服务器响应并完成握手

  • 服务器返回 HTTP 101 状态码,表示协议切换成功:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Accept 是服务器根据客户端的 key 生成的 SHA1+Base64 值,防止伪造连接。

安全特性

特性 描述
加密通信 所有 WebSocket 帧通过 TLS 加密传输
证书认证 可使用 CA 证书,防止 MITM 攻击
与 HTTPS 共端口 可与 HTTPS 共用 443 端口,适合反向代理
防止劫持 中间人无法篡改握手数据和帧内容
支持 HSTS / CSP 等浏览器安全机制

websocket示例

ws

客户端

// WsClient.h
#pragma once

#include <uwebsockets/App.h>
#include <iostream>
#include <string>
#include <functional>

class WsClient {
public:
    using OnMessageHandler = std::function<void(const std::string_view&)>;
    using OnOpenHandler = std::function<void()>;
    using OnCloseHandler = std::function<void()>;

    WsClient(const std::string& url)
        : url_(url) {}

    void setOnOpen(OnOpenHandler handler) { onOpen_ = handler; }
    void setOnMessage(OnMessageHandler handler) { onMessage_ = handler; }
    void setOnClose(OnCloseHandler handler) { onClose_ = handler; }

    void run() {
        uWS::App().connect(url_, {}, {
            .open = [this](auto *ws) {
                ws_ = ws;
                if (onOpen_) onOpen_();
            },
            .message = [this](auto * /*ws*/, std::string_view msg, uWS::OpCode /*opCode*/) {
                if (onMessage_) onMessage_(msg);
            },
            .close = [this](auto * /*ws*/, int /*code*/, std::string_view /*msg*/) {
                if (onClose_) onClose_();
            },
            .error = [this](auto * /*ws*/, std::error_code ec) {
                std::cerr << "Connection error: " << ec.message() << std::endl;
            }
        }).run();
    }

    void send(const std::string& message) {
        if (ws_) {
            ws_->send(message, uWS::OpCode::TEXT);
        }
    }

    void close() {
        if (ws_) {
            ws_->close();
        }
    }

private:
    std::string url_;
    uWS::WebSocket<false, true>* ws_ = nullptr;
    OnOpenHandler onOpen_;
    OnMessageHandler onMessage_;
    OnCloseHandler onClose_;
};

服务端

// WsServer.h
#pragma once

#include <uwebsockets/App.h>
#include <iostream>
#include <functional>
#include <string_view>

class WsServer {
public:
    using OnMessageHandler = std::function<void(const std::string_view&, uWS::WebSocket<false, true>*)>;
    using OnOpenHandler = std::function<void(uWS::WebSocket<false, true>*)>;
    using OnCloseHandler = std::function<void(uWS::WebSocket<false, true>*)>;

    WsServer(int port)
        : port_(port) {}

    void setOnOpen(OnOpenHandler handler) { onOpen_ = handler; }
    void setOnMessage(OnMessageHandler handler) { onMessage_ = handler; }
    void setOnClose(OnCloseHandler handler) { onClose_ = handler; }

    void run() {
        uWS::App().ws<false>("/*", {
            .open = [this](auto *ws) {
                if (onOpen_) onOpen_(ws);
            },
            .message = [this](auto *ws, std::string_view msg, uWS::OpCode opCode) {
                if (onMessage_) onMessage_(msg, ws);
            },
            .close = [this](auto *ws, int /*code*/, std::string_view /*msg*/) {
                if (onClose_) onClose_(ws);
            }
        }).listen(port_, [this](auto *listen_socket) {
            if (listen_socket) {
                std::cout << "WsServer listening on port " << port_ << std::endl;
            } else {
                std::cerr << "Failed to listen on port " << port_ << std::endl;
            }
        }).run();
    }

private:
    int port_;
    OnOpenHandler onOpen_;
    OnMessageHandler onMessage_;
    OnCloseHandler onClose_;
};

wss

客户端

#pragma once

#include <uwebsockets/App.h>
#include <iostream>
#include <string>
#include <functional>

class WsClientTLS {
public:
    using OnMessageHandler = std::function<void(const std::string_view&)>;
    using OnOpenHandler = std::function<void()>;
    using OnCloseHandler = std::function<void()>;

    WsClientTLS(const std::string& url)
        : url_(url) {}

    void setOnOpen(OnOpenHandler handler) { onOpen_ = handler; }
    void setOnMessage(OnMessageHandler handler) { onMessage_ = handler; }
    void setOnClose(OnCloseHandler handler) { onClose_ = handler; }

    void run() {
        uWS::App().connect(url_, {}, {
            .open = [this](auto *ws) {
                ws_ = ws;
                if (onOpen_) onOpen_();
            },
            .message = [this](auto * /*ws*/, std::string_view msg, uWS::OpCode /*opCode*/) {
                if (onMessage_) onMessage_(msg);
            },
            .close = [this](auto * /*ws*/, int /*code*/, std::string_view /*msg*/) {
                if (onClose_) onClose_();
            },
            .error = [this](auto * /*ws*/, std::error_code ec) {
                std::cerr << "Connection error: " << ec.message() << std::endl;
            }
        }).run();
    }

    void send(const std::string& message) {
        if (ws_) {
            ws_->send(message, uWS::OpCode::TEXT);
        }
    }

    void close() {
        if (ws_) {
            ws_->close();
        }
    }

private:
    std::string url_;
    uWS::WebSocket<true, true>* ws_ = nullptr;
    OnOpenHandler onOpen_;
    OnMessageHandler onMessage_;
    OnCloseHandler onClose_;
};

服务端

#pragma once

#include <uwebsockets/App.h>
#include <iostream>
#include <functional>
#include <string_view>

class WsServerTLS {
public:
    using OnMessageHandler = std::function<void(const std::string_view&, uWS::WebSocket<true, true>*)>;
    using OnOpenHandler = std::function<void(uWS::WebSocket<true, true>*)>;
    using OnCloseHandler = std::function<void(uWS::WebSocket<true, true>*)>;

    WsServerTLS(int port, const std::string& certFile, const std::string& keyFile)
        : port_(port), certFile_(certFile), keyFile_(keyFile) {}

    void setOnOpen(OnOpenHandler handler) { onOpen_ = handler; }
    void setOnMessage(OnMessageHandler handler) { onMessage_ = handler; }
    void setOnClose(OnCloseHandler handler) { onClose_ = handler; }

    void run() {
        uWS::App()
            .sslContextOptions(
                SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 // 禁用旧协议
            )
            .ws<true>("/*", {
                .open = [this](auto *ws) {
                    if (onOpen_) onOpen_(ws);
                },
                .message = [this](auto *ws, std::string_view msg, uWS::OpCode opCode) {
                    if (onMessage_) onMessage_(msg, ws);
                },
                .close = [this](auto *ws, int /*code*/, std::string_view /*msg*/) {
                    if (onClose_) onClose_(ws);
                }
            })
            .listen(port_, uWS::SocketOptions {
                .cert_file_name = certFile_.c_str(),
                .key_file_name = keyFile_.c_str()
            }, [this](auto *listen_socket) {
                if (listen_socket) {
                    std::cout << "TLS WsServer listening on port " << port_ << std::endl;
                } else {
                    std::cerr << "Failed to listen on port " << port_ << std::endl;
                }
            })
            .run();
    }

private:
    int port_;
    std::string certFile_;
    std::string keyFile_;

    OnOpenHandler onOpen_;
    OnMessageHandler onMessage_;
    OnCloseHandler onClose_;
};

websocket-flv

FLV (Flash Video) 是一种流媒体封装格式,主要用于在 Adobe Flash Player 中播放视频。虽然 Flash 已经逐渐被淘汰,但 FLV 格式因其 简单、易于解析 的特点,仍在一些直播场景中被广泛使用,尤其是在基于 WebSocket 的实时传输中。

工作原理

当通过 WebSocket 传输 FLV 数据时,通常的工作流程如下:

  1. 服务器生成 FLV 数据流: 媒体服务器(如 Nginx-RTMP-Module, SRS 等)接收到原始音视频流(例如 RTMP 推流),将其实时封装成 FLV 格式。
  2. WebSocket 连接建立: 客户端(通常是 Web 浏览器中的 JavaScript 播放器)通过 WebSocket 握手与服务器建立连接。
  3. FLV 数据通过 WebSocket 推送: 服务器将实时生成的 FLV 数据(包括 FLV 头、Metadata 和音视频 Tag)通过已建立的 WebSocket 连接持续地推送到客户端。
  4. 客户端解析和播放: 客户端的 JavaScript 播放器(例如 flv.js)接收到 FLV 数据后,解析其中的音视频 Tag,并利用 Media Source Extensions (MSE) API 将音视频数据喂给 HTML <video> 标签进行播放。

优缺点

优点

  • 实现简单: FLV 格式本身相对简单,解析起来比较容易,因此基于 WebSocket-FLV 的实现相对直观。
  • 低延迟: 结合 WebSocket 的实时性,WebSocket-FLV 可以实现较低的直播延迟。
  • 兼容性: 对于支持 MSE 的现代浏览器,flv.js 等库可以很好地支持 FLV 播放。

缺点

  • Flash 遗产: FLV 格式与 Flash 关联较深,虽然现在通过 MSE 可以在浏览器中播放,但其“出身”仍然让一些开发者对其现代化程度有所疑虑。
  • 移动端原生支持欠佳: 对于 iOS 等移动端原生应用,通常不支持直接播放 FLV,需要进行转码或使用第三方播放器。

websocket-fmp4

fMP4 (fragmented MP4),即 分段 MP4,是一种将 MP4 文件分割成小段(fragments)的格式。每个分段都包含独立的音视频数据,并且可以独立解码和播放。这种分段特性使其非常适合流媒体传输,尤其是对于 HTTP Live Streaming (HLS)MPEG-DASH 这样的自适应流媒体技术。

当与 WebSocket 结合时,WebSocket-fMP4 意味着通过 WebSocket 连接传输分段的 MP4 数据。

工作原理

WebSocket-fMP4 的工作流程与 WebSocket-FLV 有些相似,但关键在于数据封装格式的不同:

  1. 服务器生成 fMP4 数据: 媒体服务器将原始音视频流实时转码并封装成 fMP4 格式。这通常涉及到生成 初始化片段 (Initialization Segment)(包含音视频轨道的元数据,如编码信息、时间尺度等)和后续的 媒体片段 (Media Segments)(包含实际的音视频数据)。
  2. WebSocket 连接建立: 客户端通过 WebSocket 握手与服务器建立连接。
  3. fMP4 数据通过 WebSocket 推送: 服务器首先发送初始化片段,然后持续地将实时生成的媒体片段通过 WebSocket 连接推送到客户端。
  4. 客户端解析和播放: 客户端的 JavaScript 播放器(如基于 MSE 的自定义播放器或 shaka-player, hls.js 等)接收到 fMP4 数据后,将初始化片段和媒体片段依次添加到 Media Source Buffer 中,由浏览器进行解码和播放。

优缺点

优点

  • 标准和通用性: fMP4 是 ISO 标准,与 MPEG-DASH 和 HLS 等主流流媒体协议兼容,因此具有更好的通用性和未来发展潜力。
  • 更好的兼容性: 现代浏览器通过 Media Source Extensions (MSE) 对 fMP4 有原生支持,可以实现更高效的硬件解码和播放。
  • 自适应码率流 (ABR) 潜力: fMP4 的分段特性使其更容易实现自适应码率切换,根据网络状况动态调整视频质量。尽管 WebSocket 本身不直接提供 ABR 机制,但基于 fMP4 的数据流可以为上层应用实现 ABR 提供基础。
  • Seek(快进/快退)支持: fMP4 的分段结构使得在流媒体中进行快进/快退操作更加容易和高效。

缺点

  • 实现复杂性: 相对于 FLV,fMP4 格式的封装更为复杂,涉及到 Box 结构、时间戳、序列号等,因此服务器端和客户端的实现会更复杂。
  • 初始延迟: 由于需要先传输初始化片段,可能会有微小的初始延迟。

网站公告

今日签到

点亮在社区的每一天
去签到