Skip to content

WebSocket

github-actions[bot] edited this page Jul 12, 2026 · 9 revisions

WebSocket 协议

帧编解码(websocket/WebSocketFrame.cpp

enum class WebSocketOpcode : UCHAR {
    Continuation=0x0, Text=0x1, Binary=0x2, Close=0x8, Ping=0x9, Pong=0xA
};

常量:client key 16 字节(base64 24)、Accept 28、掩码键 4、帧头最大 14、控制帧 payload ≤125。

WebSocketCodec(静态):

  • GenerateClientKey:16 随机字节(CNG)→ base64,用后清零。
  • ComputeAcceptValue:SHA-1(key + 258EAFA5-...) → base64。
  • ValidateServerHandshake:要求 101、HTTP/1.1、Connection: UpgradeUpgrade: websocket恰好一个 Sec-WebSocket-Accept常量时间比对);只接受已请求且参数合法的 permessage-deflate,未请求扩展、未知扩展、重复/非法参数一律拒绝;子协议须为请求过的之一。
  • EncodeClientFrame:客户端帧必掩码(payload XOR 4 字节键);控制帧必须 FIN 且 ≤125。
  • DecodeFrameHeader:RSV2/RSV3 必须为 0;RSV1 仅供已协商的 permessage-deflate 首个 Text/Binary 片使用;被掩码的服务端帧 → STATUS_INVALID_NETWORK_RESPONSE;扩展长度强制最短编码。

产品 API(wknet::websocket,见 产品 API

// 连接(同步/异步,URL 或 ConnectConfig)
wknet::websocket::Connect / ConnectEx / ConnectAsync / ConnectAsyncEx ; wknet::websocket::AsyncGetWebSocket
// 发送(各有 *Ex 带 SendOptions{ bool FinalFragment })
wknet::websocket::SendText / SendBinary / SendContinuation / SendPing / SendPong
// 接收 / 关闭 / 查询
wknet::websocket::Receive / ReceiveEx ; wknet::websocket::Close / CloseEx ; wknet::websocket::SelectedSubprotocol
enum class wknet::websocket::MsgType { Text, Binary, Close, Continuation, Ping, Pong };
struct wknet::websocket::Message { MsgType Type; const UCHAR* Data; SIZE_T DataLength; bool Final; bool FinalFragment; };
struct wknet::websocket::ReceiveOptions { SIZE_T MaxMessageBytes; bool AutoAllocate=true; MessageCallback OnMessage; void* CallbackContext; bool DeliverFragments=false; };
struct wknet::websocket::Header { const char* Name; SIZE_T NameLength; const char* Value; SIZE_T ValueLength; };
struct wknet::websocket::ConnectConfig { const char* Url; SIZE_T UrlLength; const char* Subprotocol; SIZE_T SubprotocolLength;
                            const Header* Headers; SIZE_T HeaderCount;
                            websocket::PerMessageDeflateOptions PerMessageDeflate;
                            wknet::http::TlsConfig Tls; wknet::http::AddressFamily Family; SIZE_T MaxMessageBytes; bool AutoReplyPing=true; };

Opening handshake headers

  • ConnectConfig.Headers 可传调用方自定义握手头,例如 OriginAuthorizationCookie
  • 为防止握手被篡改,库受控头会被拒绝:HostConnectionUpgradeContent-LengthTransfer-EncodingSec-WebSocket-KeySec-WebSocket-VersionSec-WebSocket-ProtocolSec-WebSocket-Extensions
  • 字段名/值走普通 HTTP header 文本校验,拒绝空名、超限长度、控制字符与 CRLF 注入。
  • 默认不跟随 opening-handshake 的 redirect、401/407 challenge:3xx/401/407 返回 STATUS_NOT_SUPPORTED 并保留握手状态码;其它非 101 响应返回 STATUS_INVALID_NETWORK_RESPONSE。跨源 redirect、认证重放等若未来支持,必须通过显式 opt-in 并沿用 HTTP redirect 安全规则。

permessage-deflate(显式 opt-in)

  • 默认关闭:DefaultConnectConfig() 不启用压缩,零初始化配置也不启用压缩;未请求时服务端返回任何扩展都会拒绝。
  • 启用方式:设置 ConnectConfig.PerMessageDeflate.Enable=true。可配置 ClientNoContextTakeoverServerNoContextTakeoverClientMaxWindowBitsServerMaxWindowBits,window bits 只接受 8..15,非法配置返回 STATUS_INVALID_PARAMETER
  • 握手:HTTP/1.1 Upgrade 与 RFC 8441 HTTP/2 Extended CONNECT 使用同一 offer 生成逻辑;调用方仍不能在 Headers 中手写 Sec-WebSocket-Extensions
  • 数据路径:发送 Text/Binary 首片压缩并设置 RSV1,Continuation 不设置 RSV1;接收时只允许首片 Text/Binary 携带 RSV1,control frame、Continuation、未协商压缩时出现 RSV1 均按协议错误关闭。
  • 安全边界:解压受 MaxMessageBytes、输出容量与膨胀比限制约束;context takeover 按协商结果保留或在每条消息后重置。

分片(已支持

  • 发送wknet::websocket::SendContinuation(Ex) 续帧;SendText/SendBinary*Ex 可带 FinalFragment=false 开启分片。客户端会按帧缓冲自动分块(首帧用真实 opcode,后续用 Continuation),并对文本消息跨分片增量 UTF-8 校验,最终片不完整码点 → STATUS_INVALID_PARAMETER
  • 接收:默认 ReceiveOptions.DeliverFragments=falseReceiveOptions.OnMessage 回调或默认返回式都返回客户端已重组的完整消息finalFragment=true。显式设置 DeliverFragments=true 后,接收路径按 wire fragment 交付:首帧返回 Text/Binary,续帧返回 Continuation,finalFragment 承载真实 FIN;文本消息仍跨分片做增量 UTF-8 校验,最终片不完整码点 → close 1007 / STATUS_INVALID_NETWORK_RESPONSEMessage.Data 指向内部缓冲,下次收/关前有效。

行为与时序

  • 控制帧:AutoReplyPing(默认开)自动回 Pong;单次接收控制帧 > 100 → close 1008(WsMaxControlFramesPerReceive)。
  • 校验失败均以 close 帧失败连接:被掩码帧/分片状态错 → 1002;文本/close payload 非法 UTF-8 → 1007;累计超 MaxMessageBytes1009STATUS_BUFFER_TOO_SMALL)。
  • 合法接收 close 码:1000–1014(除 1004/1005/1006)或 3000–4999;长度恰为 1 的 close payload → 协议错误。
  • close 握手:主动 Close 发空 close 后 WaitForPeerCloseWskCloseTimeoutMilliseconds = 3000 超时,吞掉终止/超时为成功);被动收到 peer close 则 echo 后关 transport。CloseEx reason ≤123 字节。
  • 连接:解析 ≤8 地址逐个尝试;wss 默认 WS 握手 ALPN 为 h2,http/1.1,协商到 h2 后走 RFC 8441,peer 未启用 SETTINGS_ENABLE_CONNECT_PROTOCOL 时按 Auto 规则重试 HTTP/1.1;显式 Http11Only 只走 HTTP/1.1,ws:// 不隐式走 h2c;每地址都做 TLS1.2 确认重连;取消令牌贯穿。
  • 全双工时序Close 不得与同句柄「新 I/O 发起」并发;最安全单线程内 连接→发→收→关。

边界 / 非目标

wknet::websocket::ConnectConfig 默认 TransportMode::Autowss 可通过 RFC 8441 extended CONNECT over HTTP/2 建立隧道,不支持时回到 HTTP/1.1 Upgrade;Http11Only 可强制 HTTP/1.1。

示例

wknet::websocket::WebSocket* ws = nullptr;
if (NT_SUCCESS(wknet::websocket::Connect(session, "wss://echo.example/ws", 21, &ws))) {
    wknet::websocket::SendText(ws, "hello", 5);
    wknet::websocket::Message msg = {};
    if (NT_SUCCESS(wknet::websocket::Receive(ws, &msg)) && msg.Type == wknet::websocket::MsgType::Text) {
        // 使用 msg.Data / msg.DataLength(下次收/关前有效)
    }
    wknet::websocket::Close(ws);
}

English

WebSocket Protocol

Framing lives in the internal wknet::ws layer; the public API is wknet::websocket. Client frames are masked on HTTP/1.1 and unmasked inside RFC 8441 DATA tunnels. Server handshake validation compares Sec-WebSocket-Accept in constant time and rejects unrequested or invalid extensions.

Fragmentation: send is fully granular via wknet::websocket::SendContinuation(Ex) / FinalFragment=false (auto-chunked to the frame buffer, with incremental cross-fragment UTF-8 validation of text). Receive defaults to ReceiveOptions.DeliverFragments=false, so callbacks and the default return form deliver a client-reassembled complete message with finalFragment=true. Set DeliverFragments=true to receive wire fragments: the first frame reports Text/Binary, continuations report Continuation, and finalFragment reflects the real FIN bit. Text receive still uses incremental cross-fragment UTF-8 validation; an unfinished code point on the final fragment closes with 1007 / STATUS_INVALID_NETWORK_RESPONSE. Message.Data is valid until the next receive/close.

ConnectConfig.Headers accepts caller-supplied opening-handshake headers such as Origin, Authorization, and Cookie; controlled handshake headers (Host, Connection, Upgrade, Content-Length, Transfer-Encoding, and Sec-WebSocket-*) plus invalid header text are rejected.

permessage-deflate: supported as an explicit opt-in through ConnectConfig.PerMessageDeflate.Enable=true (and the lower-level connect options). Defaults stay off: DefaultConnectConfig() and zero-initialized configs do not offer compression, and any server extension is rejected when not requested. ClientNoContextTakeover, ServerNoContextTakeover, ClientMaxWindowBits, and ServerMaxWindowBits are configurable; window bits must be 8..15 or connect returns STATUS_INVALID_PARAMETER. HTTP/1.1 Upgrade and RFC 8441 HTTP/2 Extended CONNECT share the same offer/validation path. Callers still cannot hand-write Sec-WebSocket-Extensions. The send path sets RSV1 only on the first compressed Text/Binary fragment; continuations and control frames never set RSV1. Receive rejects RSV1 on control frames, continuations, or unnegotiated connections, and inflate is bounded by MaxMessageBytes, output capacity, and expansion-ratio limits.

Behavior: auto-Pong (toggleable); >100 control frames per receive -> close 1002 lineage (masked/fragment-state errors), 1007 (bad UTF-8), 1008 (control flood), 1009 (over MaxMessageBytes). Valid incoming close codes 1000-1014 (minus 1004/1005/1006) or 3000-4999. Active close sends an empty close then waits for peer close (3 s timeout, swallowed as success); passive close echoes then closes. wss handshake ALPN defaults to h2,http/1.1 and uses RFC 8441 when h2 is negotiated and enabled by the peer; Auto retries HTTP/1.1 when RFC 8441 is unsupported, Http11Only forces HTTP/1.1, and ws:// does not implicitly use h2c. Opening-handshake redirects and 401/407 challenges are not followed by default: 3xx/401/407 return STATUS_NOT_SUPPORTED with the response status visible to the caller, while other non-101 responses return STATUS_INVALID_NETWORK_RESPONSE. Never run Close concurrently with new I/O on the same handle. Boundaries: no compression by default, no handshake redirect/401/407 following.

Clone this wiki locally