概述
项目 | 描述 |
---|---|
标准 | 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 数据时,通常的工作流程如下:
- 服务器生成 FLV 数据流: 媒体服务器(如 Nginx-RTMP-Module, SRS 等)接收到原始音视频流(例如 RTMP 推流),将其实时封装成 FLV 格式。
- WebSocket 连接建立: 客户端(通常是 Web 浏览器中的 JavaScript 播放器)通过 WebSocket 握手与服务器建立连接。
- FLV 数据通过 WebSocket 推送: 服务器将实时生成的 FLV 数据(包括 FLV 头、Metadata 和音视频 Tag)通过已建立的 WebSocket 连接持续地推送到客户端。
- 客户端解析和播放: 客户端的 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 有些相似,但关键在于数据封装格式的不同:
- 服务器生成 fMP4 数据: 媒体服务器将原始音视频流实时转码并封装成 fMP4 格式。这通常涉及到生成 初始化片段 (Initialization Segment)(包含音视频轨道的元数据,如编码信息、时间尺度等)和后续的 媒体片段 (Media Segments)(包含实际的音视频数据)。
- WebSocket 连接建立: 客户端通过 WebSocket 握手与服务器建立连接。
- fMP4 数据通过 WebSocket 推送: 服务器首先发送初始化片段,然后持续地将实时生成的媒体片段通过 WebSocket 连接推送到客户端。
- 客户端解析和播放: 客户端的 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 结构、时间戳、序列号等,因此服务器端和客户端的实现会更复杂。
- 初始延迟: 由于需要先传输初始化片段,可能会有微小的初始延迟。