- WebSocket 是 HTML5 定义的全双工、持久化网络通信协议,基于 TCP 连接,允许客户端与服务器之间实时双向数据传输;
- 相比 HTTP 1.1 的 “请求 - 响应” 模式 (单向、无状态、需频繁建连),WebSocket 仅需一次握手建立连接,之后双方可随时发送数据,延迟更低、带宽占用更少,适用于实时聊天、直播弹幕、协同编辑、实时数据监控等场景;
WebSocket 核心特性
-
全双工通信:客户端和服务器可 同时发送 / 接收数据,无需等待对方响应; -
持久连接:一次 TCP 握手后保持连接,避免 HTTP 重复建连的开销; -
轻量协议:帧头部仅 2-14 字节 (HTTP 头部通常数百字节),数据传输效率高; -
跨域支持:原生支持跨域通信,无需额外配置 (如 CORS); -
基于 TCP:依赖 TCP 可靠性 (有序、无丢失、无重复),但需自行处理重连、心跳等场景; -
文本 / 二进制兼容:支持传输 UTF-8 文本 (如 JSON) 和二进制数据 (如图片、视频流);
WebSocket 协议流程
握手阶段(HTTP 升级请求)
-
WebSocket 连接建立依赖 HTTP 握手升级,客户端先发送 HTTP 请求,告知服务器 “要切换到 WebSocket 协议”:
-
客户端请求头 (关键字段):
GET /ws-endpoint HTTP/1.1 Host: example.com Connection: Upgrade # 表示要升级协议 Upgrade: websocket # 目标协议为 WebSocket Sec-WebSocket-Version: 13 # 必须为 13(标准版本) Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== # 随机字符串(用于服务器验证) Sec-WebSocket-Protocol: chat, game # 可选:协商子协议(如聊天、游戏) -
服务器响应头 (关键字段):
HTTP/1.1 101 Switching Protocols # 状态码 101 表示协议切换成功 Connection: Upgrade Upgrade: websocket Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= # 由 Sec-WebSocket-Key 计算得出(验证握手合法性) Sec-WebSocket-Protocol: chat # 可选:确认最终使用的子协议
数据传输阶段
握手成功后,HTTP 连接升级为 WebSocket 连接,双方通过帧 (Frame) 传输数据:
- 帧结构包含:操作码 (OPCODE)、掩码位、数据长度、载荷数据;
- 关键操作码:
- 0x00:继续帧 (分片数据的后续帧);
- 0x01:文本帧 (UTF-8 编码);
- 0x02:二进制帧;
- 0x08:关闭帧 (主动关闭连接);
- 0x09:Ping 帧 (心跳检测,需响应 Pong);
- 0x0A:Pong 帧 (响应 Ping);
关闭阶段
任意一方发送 0x08 关闭帧,对方响应后断开 TCP 连接 (优雅关闭);
若超时未响应,直接断开连接 (强制关闭);
客户端 API 详解(浏览器原生)
浏览器内置 WebSocket 构造函数,无需依赖第三方库,API 简洁易用;
建立连接
if (typeof window.WebSocket == 'function') { // 检测一下浏览器是否支持 Websocket
// 语法:new WebSocket(url[, protocols])
// url:WebSocket 服务地址(ws:// 非加密,wss:// 加密,类似 http/https)
// protocols:可选,子协议数组(如 ["chat", "json"]),需服务器支持
const ws = new WebSocket('wss://example.com/ws-endpoint');
} else {
alert("您的浏览器不支持 websocket");
}
核心事件(回调函数)
-
WebSocket 实例通过事件监听处理连接状态、数据收发,常用事件如下:
事件名 触发时机 回调参数 说明 open 连接建立成功时 event (无关键数据) 仅触发一次 message 收到服务器发送的数据时 event (data 字段为数据) 数据类型:String/Blob/ArrayBuffer error 连接出错时 (如网络中断、协议错误) event 出错后连接会关闭 close 连接关闭时 (主动关闭 / 异常关闭) event (code/reason) code:关闭码,reason:原因 -
事件监听示例:
// 1. 连接成功 ws.onopen = () => { console.log('WebSocket 连接已建立'); // 连接成功后可立即发送数据 ws.send('Hello, Server!'); }; // 2. 接收服务器数据 ws.onmessage = (event) => { // 数据类型判断(根据服务器发送的格式处理) if (typeof event.data === 'string') { console.log('收到文本数据:', event.data); } else if (event.data instanceof Blob) { console.log('收到二进制 Blob:', event.data); // 如需解析 Blob(如图片),可通过 FileReader 转换 const reader = new FileReader(); reader.onload = (e) => console.log('Blob 解析结果:', e.target.result); reader.readAsDataURL(event.data); } else if (event.data instanceof ArrayBuffer) { console.log('收到 ArrayBuffer:', event.data); } }; // 3. 连接出错 ws.onerror = (event) => { console.error('WebSocket 错误:', event); }; // 4. 连接关闭 ws.onclose = (event) => { console.log(`连接关闭:状态码 ${event.code},原因 ${event.reason}`); // 常见关闭码:1000(正常关闭)、1001(客户端/服务器离开)、1006(异常关闭) };
核心方法
-
发送数据:
ws.send(data)- 支持发送文本、二进制数据 (Blob / ArrayBuffer / ArrayBufferView);
- 注意:send() 仅在 open 事件触发后调用 (连接建立前调用会报错);
// 1. 发送文本(最常用,如 JSON 字符串) ws.send(JSON.stringify({ type: 'chat', content: 'Hello' })); // 2. 发送 Blob(如文件) const fileInput = document.getElementById('file-input'); const file = fileInput.files[0]; if (file) ws.send(file); // 3. 发送 ArrayBuffer(二进制数据) const buffer = new ArrayBuffer(4); const view = new Uint8Array(buffer); view.set([0x10, 0x20, 0x30, 0x40]); ws.send(buffer); -
关闭连接:
ws.close([code[, reason]])- 主动关闭连接,可选参数:
- code:关闭码 (默认 1000,需符合 RFC 6455 标准);
- reason:关闭原因 (字符串,UTF-8 编码,长度不超过 123 字节);
// 正常关闭连接 ws.close(1000, '客户端主动断开连接'); - 主动关闭连接,可选参数:
核心属性
| 属性名 | 类型 | 说明 |
|---|---|---|
| readyState | 数字 | 连接状态 (0:CONNECTING,1:OPEN,2:CLOSING,3:CLOSED) |
| bufferedAmount | 数字 | 已发送但未被服务器确认的字节数 (判断发送队列是否拥堵) |
| protocol | 字符串 | 最终协商的子协议 (如 “chat”) |
| url | 字符串 | 连接的 WebSocket 服务地址 |
// 判断连接状态
if (ws.readyState === WebSocket.OPEN) {
console.log('连接正常,可发送数据');
} else if (ws.readyState === WebSocket.CONNECTING) {
console.log('连接正在建立中...');
}
服务器端实现 node
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 }); // 监听 8080 端口
// 新客户端连接时触发
wss.on('connection', (ws) => {
console.log('新客户端连接');
// 接收客户端数据
ws.on('message', (data) => {
console.log('收到客户端数据:', data.toString());
// 回复客户端(支持广播:wss.clients.forEach 遍历所有连接)
ws.send(`服务器回复:${data.toString()}`);
});
// 客户端断开连接
ws.on('close', (code, reason) => {
console.log(`客户端断开:${code} - ${reason}`);
});
// 错误处理
ws.on('error', (err) => {
console.error('服务器错误:', err);
});
});
关键实战场景与解决方案
心跳检测(防止连接被网关断开)
-
多数网关 (如 Nginx) 会断开长时间无数据传输的连接,需通过 Ping/Pong 帧 保持连接;
-
示例代码:
// 客户端心跳逻辑 let pingInterval; ws.onopen = () => { // 每 30 秒发送一次 Ping pingInterval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'ping' })); // 自定义 Ping 消息(或用原生 Ping 帧) // 原生 Ping 帧(部分浏览器支持,无需手动序列化) // ws.ping(); } }, 30000); }; ws.onclose = () => { clearInterval(pingInterval); // 关闭时清除定时器 }; // 服务器收到 Ping 后回复 Pong // Node.js 示例(ws 库) ws.on('message', (data) => { const msg = JSON.parse(data); if (msg.type === 'ping') { ws.send(JSON.stringify({ type: 'pong' })); } });
自动重连(网络中断后恢复)
-
连接异常关闭时,客户端需自动重试连接 (避免用户手动刷新);
-
示例代码:
let ws; let reconnectInterval; const MAX_RETRY = 10; // 最大重连次数 let retryCount = 0; // 初始化连接 function initWebSocket() { ws = new WebSocket('wss://example.com/ws-endpoint'); ws.onopen = () => { console.log('连接成功'); retryCount = 0; // 重置重连次数 clearInterval(reconnectInterval); }; ws.onclose = () => { console.log('连接关闭,尝试重连...'); if (retryCount < MAX_RETRY) { retryCount++; // 指数退避重连(1s、2s、4s...,避免频繁重试) reconnectInterval = setTimeout(initWebSocket, 1000 * Math.pow(2, retryCount)); } else { console.log('重连失败,请刷新页面'); } }; // 其他事件(message/error)... } // 启动连接 initWebSocket();
跨域处理
WebSocket 原生支持跨域,无需额外配置,但需注意:
- 服务器需正确响应 Sec-WebSocket-Accept 头部;
- 若客户端指定 Sec-WebSocket-Protocol,服务器需返回匹配的子协议;
- 部分旧版服务器可能需配置允许跨域源 (如 Nginx 转发时无需额外处理);
安全考量
优先使用 wss:// (加密传输,类似 HTTPS),避免数据被窃听或篡改;
握手时可添加身份验证 (如在 URL 中携带 Token:wss://example.com/ws?token=xxx,服务器验证 Token 合法性);
限制单客户端连接数,防止恶意连接攻击;
对接收的数据进行校验 (如 JSON 格式、数据长度),避免注入攻击;
WebSocket 与 HTTP/2、SSE 的区别
| 特性 | WebSocket | HTTP/2 | SSE(Server-Sent Events) |
|---|---|---|---|
通信方向 |
全双工 (双向) | 全双工 (但基于请求 - 响应) | 单工 (服务器→客户端) |
连接类型 |
持久 TCP 连接 | 持久 TCP 连接 | 持久 HTTP 连接 |
适用场景 |
实时聊天、弹幕、协同编辑 | 多路复用 HTTP 请求 | 实时通知、数据推送 (如股票行情) |
客户端兼容性 |
所有现代浏览器 (IE10+) | 现代浏览器 (IE 不支持) | 现代浏览器 (IE 不支持) |
服务器复杂度 |
中等 (需帧解析) | 高 (需 TLS 支持) | 低 (基于 HTTP 响应) |
数据格式 |
文本 / 二进制 | 文本 / 二进制 | 仅文本 (UTF-8) |
Socket.IO chat 案例
<!DOCTYPE html>
<html>
<head>
<title>Socket.IO chat</title>
<style>
body {
margin: 0;
padding-bottom: 3rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
#form {
background: rgba(0, 0, 0, 0.15);
padding: 0.25rem;
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
height: 3rem;
box-sizing: border-box;
backdrop-filter: blur(10px);
}
#input {
border: none;
padding: 0 1rem;
flex-grow: 1;
border-radius: 2rem;
margin: 0.25rem;
}
#input:focus {
outline: none;
}
#form>button {
background: #333;
border: none;
padding: 0 1rem;
margin: 0.25rem;
border-radius: 3px;
outline: none;
color: #fff;
}
#messages {
list-style-type: none;
margin: 0;
padding: 0;
}
#messages>li {
padding: 0.5rem 1rem;
}
#messages>li:nth-child(odd) {
background: #efefef;
}
</style>
</head>
<body>
<ul id="messages"></ul>
<form id="form" action="">
<input id="input" autocomplete="off" /><button>Send</button>
</form>
<!-- socket.io(对原生的 WebSocket 进行了封装,使用起来更方便,客户端/服务端要同时使用) -->
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
var messages = document.getElementById('messages');
var form = document.getElementById('form');
var input = document.getElementById('input');
form.addEventListener('submit', function(e) {
e.preventDefault();
if (input.value) {
socket.emit('chat message', input.value);
input.value = '';
}
});
socket.on('chat message', function(msg) {
var item = document.createElement('li');
item.textContent = msg;
messages.appendChild(item);
window.scrollTo(0, document.body.scrollHeight);
});
</script>
</body>
</html>
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html'); // http://localhost:3000
});
io.on('connection', (socket) => {
socket.on('chat message', msg => {
io.emit('chat message', msg);
});
});
http.listen(port, () => {
console.log(`Socket.IO server running at http://localhost:${port}/`);
});
API 篇:Fetch Api
上一篇