WebSocket
全双工通信协议,在单个 TCP 连接上提供双向、低延迟的实时数据传输。
为什么需要 WebSocket?
HTTP 的局限性
HTTP 是请求-响应模型,只能由客户端发起请求,服务器被动响应。对于需要服务器主动推送数据的场景(实时聊天、股票行情、游戏),HTTP 效率很低:
HTTP 轮询(低效):
客户端 → 有数据吗? → 服务器
客户端 → 有数据吗? → 服务器 ← 大量无效请求
客户端 → 有数据吗? ← 数据来了 ← 偶尔才是有用的WebSocket 的优势
HTTP 请求 → 升级为 WebSocket → 全双工通信
↓
客户端 ↔ 服务器
客户端 ↔ 服务器 ← 任意方向随时发送| 对比 | HTTP 轮询 | WebSocket |
|---|---|---|
| 通信模型 | 请求-响应 | 全双工 |
| 头部开销 | 每次 400~800 字节 | 建立后仅 2~6 字节 |
| 实时性 | 取决于轮询间隔 | 实时推送 |
| 服务器推送 | 不支持 | 原生支持 |
| 连接数 | 每次请求新建/复用 | 单个长连接 |
工作原理
握手过程
WebSocket 通过 HTTP 升级机制建立连接:
客户端 服务器
│ │
├── GET /chat HTTP/1.1 ──────────────────────────→│ ① HTTP 请求
│ Host: example.com │
│ Upgrade: websocket │
│ Connection: Upgrade │
│ Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== │
│ Sec-WebSocket-Version: 13 │
│ │
│←── 101 Switching Protocols ──────────────────────┤ ② 升级响应
│ Upgrade: websocket │
│ Connection: Upgrade │
│ Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZ... │
│ │
│══════ WebSocket 全双工通信 ════════════════════│ ③ 双向通信
│←── WebSocket Frame (data) ──────────────────────┤
├─── WebSocket Frame (data) ──────────────────────→│
│←── WebSocket Frame (data) ──────────────────────┤Sec-WebSocket-Accept 计算
javascript
const crypto = require('crypto')
function generateAccept(key) {
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
const hash = crypto.createHash('sha1')
hash.update(key + GUID)
return hash.digest('base64')
}WebSocket URI
ws://example.com/chat ← 非加密连接(默认端口 80)
wss://example.com/chat ← 加密连接(TLS,默认端口 443)
wss://example.com/chat?token=xxx ← 可携带查询参数(用于认证)数据帧格式
WebSocket 传输的数据以帧为单位,帧格式使用二进制编码:
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) |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+--------------------------------+ - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data (continued) |
+---------------------------------------------------------------+帧字段说明
| 字段 | 长度 | 说明 |
|---|---|---|
| FIN | 1 bit | 是否为最后一帧,1= 消息结束 |
| RSV1/2/3 | 各 1 bit | 预留位,用于扩展协议(如压缩) |
| opcode | 4 bits | 帧类型(见下方表格) |
| MASK | 1 bit | 是否使用掩码,客户端发送必须为 1 |
| Payload length | 7/16/64 bits | 数据长度 |
| Masking-key | 32 bits | 掩码密钥(MASK=1 时存在) |
| Payload Data | 可变 | 实际数据 |
Opcode 帧类型
| opcode | 类型 | 说明 |
|---|---|---|
0x0 | 连续帧 | 上一个帧的后续数据 |
0x1 | 文本帧 | UTF-8 文本数据 |
0x2 | 二进制帧 | 二进制数据 |
0x8 | 关闭帧 | 关闭连接 |
0x9 | Ping | 心跳检测 |
0xA | Pong | Ping 的响应 |
客户端使用
JavaScript(浏览器)
javascript
// 创建连接
const ws = new WebSocket('wss://example.com/chat')
// 连接建立
ws.onopen = () => {
console.log('已连接')
ws.send('Hello 服务器!') // 发送文本
ws.send(new Blob([buffer])) // 发送二进制
}
// 接收消息
ws.onmessage = (event) => {
if (event.data instanceof Blob) {
// 处理二进制数据
} else {
console.log('收到:', event.data) // 文本消息
}
}
// 错误处理
ws.onerror = (error) => {
console.error('WebSocket 错误:', error)
}
// 连接关闭
ws.onclose = (event) => {
console.log('连接关闭:', event.code, event.reason)
}
// 主动关闭
ws.close(1000, '正常关闭')Node.js(ws 库)
bash
npm install wsjavascript
const WebSocket = require('ws')
// 客户端
const ws = new WebSocket('wss://example.com/chat')
ws.on('open', () => {
ws.send('Hello 服务器!')
})
ws.on('message', (data) => {
console.log('收到:', data.toString())
})
ws.on('close', () => {
console.log('连接关闭')
})Java
xml
<!-- Maven 依赖 -->
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.6</version>
</dependency>java
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
public class ChatClient extends WebSocketClient {
public ChatClient(URI uri) {
super(uri);
}
@Override
public void onOpen(ServerHandshake handshake) {
System.out.println("已连接");
send("Hello 服务器!");
}
@Override
public void onMessage(String message) {
System.out.println("收到: " + message);
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("连接关闭: " + reason);
}
@Override
public void onError(Exception ex) {
ex.printStackTrace();
}
public static void main(String[] args) throws Exception {
new ChatClient(new URI("wss://example.com/chat")).connect();
}
}服务端使用
Node.js(ws 库)
javascript
const WebSocket = require('ws')
const server = new WebSocket.Server({ port: 8080 })
server.on('connection', (ws, req) => {
const ip = req.socket.remoteAddress
console.log(`客户端连接: ${ip}`)
// 接收消息
ws.on('message', (data) => {
const msg = data.toString()
console.log(`收到: ${msg}`)
// 回复消息
ws.send(`服务器已收到: ${msg}`)
// 广播给所有客户端
server.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(`${ip}: ${msg}`)
}
})
})
// 发送欢迎消息
ws.send('欢迎加入聊天室!')
// 心跳检测
ws.isAlive = true
ws.on('pong', () => { ws.isAlive = true })
})
// 心跳 Ping
setInterval(() => {
server.clients.forEach((ws) => {
if (!ws.isAlive) return ws.terminate()
ws.isAlive = false
ws.ping()
})
}, 30000)Java(Spring Boot WebSocket)
java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.*;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
// 配置类
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatHandler(), "/chat")
.setAllowedOrigins("*");
}
}
// 处理器
class ChatHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) {
System.out.println("连接建立: " + session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session,
TextMessage message) throws Exception {
String payload = message.getPayload();
System.out.println("收到: " + payload);
session.sendMessage(new TextMessage("已收到: " + payload));
}
@Override
public void afterConnectionClosed(WebSocketSession session,
CloseStatus status) {
System.out.println("连接关闭: " + session.getId());
}
}常见应用场景
| 场景 | 说明 | 替代方案 |
|---|---|---|
| 实时聊天 | 一对一/群聊消息即时推送 | HTTP 轮询、SSE |
| 在线游戏 | 实时操作同步、状态广播 | HTTP 轮询 |
| 股票行情 | 实时价格推送 | SSE |
| 协作编辑 | 多人同时编辑文档 | HTTP 轮询 |
| 物联网 | 设备状态实时上报 | MQTT |
| 实时通知 | 系统通知、告警推送 | 推送通知 |
| 直播弹幕 | 弹幕消息实时收发 | HTTP 轮询 |
客户端 API 参考(浏览器)
| API | 说明 |
|---|---|
new WebSocket(url, [protocols]) | 创建 WebSocket 连接 |
ws.send(data) | 发送数据(String / ArrayBuffer / Blob) |
ws.close([code], [reason]) | 关闭连接 |
ws.onopen | 连接建立回调 |
ws.onmessage | 收到消息回调 |
ws.onclose | 连接关闭回调 |
ws.onerror | 错误回调 |
ws.readyState | 连接状态 |
ws.bufferedAmount | 未发送数据的字节数 |
连接状态
| 常量 | 值 | 说明 |
|---|---|---|
CONNECTING | 0 | 正在连接 |
OPEN | 1 | 已连接,可通信 |
CLOSING | 2 | 正在关闭 |
CLOSED | 3 | 已关闭 / 未连接 |
关闭码
| 码 | 说明 |
|---|---|
| 1000 | 正常关闭 |
| 1001 | 服务器/客户端关闭 |
| 1002 | 协议错误 |
| 1003 | 不支持的数据类型 |
| 1005 | 未指定关闭原因 |
| 1006 | 异常关闭(非主动关闭) |
| 1009 | 消息太大 |
| 1011 | 服务器内部错误 |
| 3000~3999 | 可用于应用层自定义 |
与 Server-Sent Events (SSE) 对比
| 特性 | WebSocket | SSE (EventSource) |
|---|---|---|
| 通信方向 | 双向(全双工) | 单向(服务器 → 客户端) |
| 数据格式 | 文本 / 二进制 | 仅文本 |
| 协议 | 独立协议(基于 HTTP 升级) | 原生 HTTP |
| 自动重连 | 需手动实现 | 内置(EventSource) |
| 浏览器兼容 | 完全支持 | 除 IE 外均支持 |
| 服务端复杂度 | 较高(需要单独处理) | 简单(普通的 HTTP 响应) |
| 适合场景 | 聊天、游戏、协同编辑 | 通知推送、行情、状态更新 |
常见问题
连接不稳定 / 断线重连
javascript
function connect() {
const ws = new WebSocket('wss://example.com/chat')
ws.onopen = () => { console.log('已连接') }
ws.onclose = (event) => {
if (event.code !== 1000) {
// 异常断开,自动重连
setTimeout(connect, 3000)
}
}
ws.onerror = () => {
// 错误时也会触发 onclose
}
}
connect()心跳保活
javascript
// 客户端心跳
const ws = new WebSocket('wss://example.com')
let heartbeatTimer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }))
}
}, 30000)
ws.onclose = () => {
clearInterval(heartbeatTimer)
}发送二进制数据
javascript
const ws = new WebSocket('wss://example.com/data')
// 发送 ArrayBuffer
const buffer = new ArrayBuffer(4)
const view = new DataView(buffer)
view.setUint32(0, 12345)
ws.send(buffer)
// 发送 Blob
const blob = new Blob(['Hello'], { type: 'text/plain' })
ws.send(blob)
// 接收二进制
ws.binaryType = 'arraybuffer' // 或 'blob'
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new DataView(event.data)
console.log(view.getUint32(0))
}
}鉴权实现
javascript
// 方式一:URL 参数(推荐)
const ws = new WebSocket('wss://example.com/chat?token=xxx')
// 服务端验证
const { URL } = require('url')
const server = new WebSocket.Server({ port: 8080 })
server.on('connection', (ws, req) => {
const params = new URL(req.url, 'http://localhost').searchParams
const token = params.get('token')
if (!isValid(token)) {
ws.close(4001, '认证失败')
return
}
})
// 方式二:在 onopen 时发送认证消息
const ws = new WebSocket('wss://example.com/chat')
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'auth', token: 'xxx' }))
}性能与注意事项
- 连接数限制 — 浏览器对单个域名的 WebSocket 连接数有限制(Chrome 约 200),大量连接时需做连接池管理
- 代理兼容性 — 某些 HTTP 代理不支持 WebSocket 升级(仅支持 CONNECT 方法的代理可以),必要时使用
wss://加密通信 - 内存管理 — 大量消息积压时
bufferedAmount会增长,发送前检测bufferedAmount避免内存溢出 - 消息大小 — 单个消息过大会阻塞通道,建议对大数据分片发送(如每片 64KB)
- 安全 — 生产环境始终使用
wss://(TLS 加密),避免明文传输敏感数据