进阶篇:WebSocket
WebSocket 是一套应用层双向通信协议,2008 年诞生,2011 年正式成为国际标准,现代浏览器全部原生支持。
核心特性:服务端可以主动向客户端推送消息,客户端也可以主动发消息,真正实现全双工通信,是服务端推送技术的主流方案。
主要特点:
- 底层基于 TCP,服务端实现简单;
- 握手请求为标准 HTTP,默认端口 80、443,可顺利穿过 HTTP 代理、防火墙,兼容性好;
- 协议数据帧格式轻量,报文开销小,通信效率高;
- 支持传输文本数据、二进制数据(ArrayBuffer、Blob);
- 无同源策略限制,浏览器客户端可以对接任意后端服务;
- 协议标识:
ws://(明文)、wss://(加密)。
注意:Ajax 适用于一次性请求-响应场景,WebSocket 更适合长连接实时双向通信场景,二者不能互相完全替代。
工作原理
WebSocket 不是脱离 TCP 的全新协议,借助 HTTP 完成握手,握手成功之后彻底脱离 HTTP,使用自定义二进制帧传输业务数据。
WebSocket 连接必须由客户端发起。
客户端握手请求
客户端发送标准 HTTP GET 请求,携带升级相关请求头,申请把 HTTP 连接升级为 WebSocket 协议。
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: xxxxxxxx-client-random
Sec-WebSocket-Version: 13
请求头释义:
Connection: Upgrade:告知服务器,本次请求要做协议升级。Upgrade: websocket:目标升级协议为 WebSocket。Sec-WebSocket-Version: 13:客户端协议版本,主流固定为 13;服务端不支持则返回服务端支持的版本列表。Sec-WebSocket-Key:客户端随机 Base64 字符串,用于简单校验防护,防止误连接、恶意请求。
服务端握手响应
服务端校验通过后,返回 101 Switching Protocols 状态码,完成协议切换。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: xxxxxxxx-server-hash
响应头释义:
101 Switching Protocols:HTTP 协议切换,握手完成。Sec-WebSocket-Accept:计算规则:将客户端Sec-WebSocket-Key拼接固定魔数258EAFA5-E914-47DA-95CA-C5AB0DC85B11,做 SHA-1 哈希,再转为 Base64 字符串。客户端收到后做校验,确认服务端合法。
握手完成之后,TCP 连接保留,不再使用 HTTP 协议,全部通信走 WebSocket 自定义帧格式,双方自由收发数据。
wss:// 加密连接流程:先建立 TLS 加密通道(和 HTTPS 一致),再在加密通道之上完成 WebSocket 握手,底层由 SSL/TLS 保障传输安全。
备注:TCP 本身原生支持全双工;HTTP 受限于「一问一答」的请求-响应模型,无法做到服务端主动推送,WebSocket 就是为了解决这个限制。
浏览器客户端 API
浏览器原生 WebSocket 对象,核心能力:建立、关闭连接,收发数据,处理异常。
const ws = new WebSocket('ws://localhost:8080/ws');
实例化即立刻发起握手连接。
实例属性(只读):
readyState:连接状态0 CONNECTING:正在建立连接1 OPEN:连接已打开,可以通信2 CLOSING:正在关闭3 CLOSED:已关闭或连接失败
binaryType:二进制数据类型,blob、arraybufferbufferedAmount:待发送队列的字节数,可以判断发送缓冲区积压url:实例对应的完整地址extensions、protocol:协商得到的扩展与子协议
事件:
open:握手成功,连接打开message:收到服务端消息close:连接关闭error:发生错误
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onopen = (e) => {
console.log('连接已打开');
ws.send('hello server');
};
ws.onmessage = (e) => {
console.log('收到服务端消息:', e.data);
};
ws.onclose = (e) => {
// e.code 关闭状态码,1000 为正常关闭
console.log('连接关闭', e.code, e.reason);
};
ws.onerror = (err) => {
console.error('连接异常', err);
};
// addEventListener 写法
ws.addEventListener('message', e => {});
实例方法:
- send(data):发送数据,支持字符串、Blob、ArrayBuffer、ArrayBufferView。
- close(code?, reason?):主动关闭连接,可以携带关闭码与关闭原因。
ws.send('文本消息');
const buf = new Uint8Array([0x01, 0x02]).buffer;
ws.send(buf);
ws.close(1000, '正常关闭');
服务端实现
WebSocket 服务端各个语言均有成熟库。
Node.js 生态常用:
ws:轻量高性能标准实现,最常用。uWebSockets.js:性能极高,底层 C++ 实现。Socket.IO:不是标准 WebSocket,是上层封装库,内置降级(轮询、长轮询兜底)、断线重连、心跳、房间广播等能力;自定义协议,原生 WebSocket 客户端无法直连。
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (clientWs) => {
console.log('有客户端接入');
clientWs.on('message', (data) => {
console.log('收到客户端:', data.toString());
clientWs.send('服务端回执:' + data);
});
const timer = setInterval(() => {
clientWs.send('服务端时间:' + Date.now());
}, 1000);
clientWs.on('close', () => {
clearInterval(timer);
console.log('客户端断开');
});
});
常见问题
HTTP 长连接和 WebSocket 的区别
HTTP/1.1 默认开启 Connection: keep-alive,即 HTTP 长连接。
- HTTP 长连接:仍然严格遵守一问一答。一个 Request 对应一个 Response;服务端不能主动推送,只能客户端发起请求。TCP 连接可以复用多次 HTTP 请求。
- WebSocket:握手完成之后脱离 HTTP,双向平等收发,服务端可随时主动下发消息。
本质区别:HTTP keep-alive 只是复用 TCP 连接,应用层依旧是 HTTP 请求-响应模型;WebSocket 是更换了应用层协议。
WebSocket 之前的实时通信备选方案
在 WebSocket 出现之前,要实现服务端推送,只能基于 HTTP 模拟:
- 短轮询:客户端定时器不断发起 HTTP 请求;没有新数据也返回。缺点:请求量大、实时性差、浪费流量与服务器资源。
- 长轮询:客户端发起请求,服务端阻塞挂起连接;有消息才返回响应;客户端收到后立刻再次发起请求。
- SSE(Server-Sent Events):单向通信,仅服务端向客户端推送,客户端不能发送消息。原生基于 HTTP,无需协议升级。适合日志、数据流等单向场景,不适合聊天这类双向交互业务。
SSE 与 EventSource
SSE(Server-Sent Events)是通信规范;EventSource 是浏览器实现该规范的客户端 API。
- SSE(Server-Sent Events):基于 HTTP 的服务端单向推送协议。定义响应头、消息帧格式、重连策略;连接始终为普通 HTTP 长流,不做协议升级。
- EventSource:浏览器原生 JS 对象,是 SSE 的客户端实现,封装 HTTP 请求、解析 SSE 数据流、内置自动重连。
关键特性:
- 通信方向:单向,仅服务端向客户端推送,客户端无法发送数据。
- 请求方式:仅支持 GET 请求。
- 重连:EventSource 内置断开自动重连,服务端可通过
retry:控制重连间隔。 - 同源:受同源策略限制,跨域需要服务端配置 CORS。
- 数据:仅文本,不支持二进制。
和 WebSocket 的核心差异:
- SSE:单向、普通 HTTP 流、自带重连、仅 GET、文本;适合日志、进度广播等单向场景。
- WebSocket:双向全双工、HTTP 握手后切换自定义协议、无内置重连、支持二进制;适合聊天、双向实时交互。
const es = new EventSource('/sse/stream');
es.onopen = () => {
console.log('连接建立');
};
es.onmessage = (e) => {
console.log('收到服务端推送', e.data);
};
es.onerror = (err) => {
console.error('出错/断开');
};
es.close();
WebSocket 生产环境注意事项
ws://明文协议在 HTTPS 页面中会被浏览器拦截,生产环境优先使用wss://。- WebSocket 连接容易因网络抖动、代理超时、设备休眠被断开,业务层必须实现心跳检测、断线自动重连。
- Nginx、反向代理需要配置 WebSocket Upgrade 相关请求头,否则握手失败、连接被切断。
bufferedAmount可以判断发送队列堆积,避免短时间大量发送二进制消息造成内存压力。