WebSocket 教程
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它让浏览器和服务器之间能够建立持久化的双向通信通道。本教程将系统讲解 WebSocket 协议原理、浏览器客户端 API、以及多种语言的服务端实现。
什么是 WebSocket
WebSocket 协议于 2011 年由 IETF 标准化(RFC 6455),它解决了传统 HTTP 请求-响应模式在实时通信场景下的局限性。
HTTP 的局限性
在 WebSocket 出现之前,Web 应用实现实时通信主要依靠以下方式:
短轮询(Short Polling):客户端定时向服务器发送请求,询问是否有新数据。这种方式浪费带宽和服务器资源,因为大多数请求可能都没有新数据。
长轮询(Long Polling):客户端发送请求后,服务器保持连接打开,直到有数据可返回或超时。虽然比短轮询高效,但每次返回数据后都需要重新建立连接。
这两种方式都存在共同的问题:通信只能由客户端发起,服务器无法主动推送数据。
WebSocket 的优势
WebSocket 提供了真正的全双工通信能力:
- 全双工通信:客户端和服务器可以同时发送消息,不需要等待对方响应
- 持久连接:一次握手建立连接后,连接保持打开状态,无需重复建立
- 低延迟:消除了 HTTP 请求头的开销,消息实时传递
- 轻量级:数据帧头部只有 2-10 字节,远小于 HTTP 请求头
WebSocket 与 HTTP 的关系
WebSocket 虽然名字里带有 "Web",但它与 HTTP 是两种不同的协议。不过,WebSocket 的设计巧妙地利用了 HTTP 来完成初始握手。
握手过程
WebSocket 连接的建立始于一个 HTTP 请求,这个请求包含特殊的头部字段,表明客户端希望升级到 WebSocket 协议:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
服务器如果支持 WebSocket,会返回 101 Switching Protocols 响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
握手完成后,连接从 HTTP 协议切换到 WebSocket 协议,之后的通信都使用 WebSocket 的数据帧格式。
协议标识
WebSocket 使用与 HTTP 不同的 URL 方案:
ws://:非加密连接,默认端口 80wss://:加密连接(类似 HTTPS),默认端口 443
const socket = new WebSocket('ws://example.com/chat');
const secureSocket = new WebSocket('wss://example.com/chat');
适用场景
WebSocket 特别适合需要实时、双向通信的应用场景:
即时通讯
聊天应用是 WebSocket 最典型的应用场景。用户发送的消息需要实时传递给对方,同时还要接收其他用户的消息。
const socket = new WebSocket('wss://chat.example.com');
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
displayMessage(message);
};
function sendMessage(text) {
socket.send(JSON.stringify({
type: 'message',
content: text,
timestamp: Date.now()
}));
}
实时数据推送
股票行情、体育比分、实时监控等场景需要服务器持续推送最新数据。
const stockSocket = new WebSocket('wss://stocks.example.com/quotes');
stockSocket.onmessage = (event) => {
const quotes = JSON.parse(event.data);
updateStockDisplay(quotes);
};
协作编辑
在线文档编辑、白板协作等应用需要实时同步多个用户的操作。
const docSocket = new WebSocket('wss://docs.example.com/collab');
docSocket.onmessage = (event) => {
const change = JSON.parse(event.data);
applyRemoteChange(change);
};
function broadcastChange(change) {
docSocket.send(JSON.stringify({
type: 'edit',
...change
}));
}
在线游戏
多人在线游戏需要低延迟的双向通信来同步玩家状态和游戏事件。
物联网(IoT)
设备状态监控、远程控制等物联网应用需要实时接收设备数据并发送控制指令。
不适用场景
WebSocket 并非万能的解决方案,以下场景可能更适合其他技术:
RESTful API:传统的 CRUD 操作仍然适合使用 HTTP REST API,因为这类请求通常是独立的、无状态的。
文件上传/下载:大文件传输更适合使用 HTTP,因为 HTTP 有更好的缓存机制和断点续传支持。
简单的数据获取:如果只需要偶尔获取数据,使用 HTTP 请求更简单直接。
需要 HTTP 缓存的场景:WebSocket 连接是持久的,无法利用 HTTP 缓存机制。
WebSocket 与 HTTP/2 的关系
随着 HTTP/2 的普及,开发者经常问:WebSocket 是否会被 HTTP/2 取代?答案是否定的,两者可以共存并各有优势。
HTTP/2 带来的改进
HTTP/2 引入了多项重要改进:多路复用解决了队头阻塞问题,头部压缩减少了传输开销,服务器推送允许服务器主动发送资源。这些改进让 HTTP 在很多场景下效率大幅提升。
为什么 WebSocket 仍然重要
HTTP/2 的服务器推送只能将资源推送到浏览器缓存,应用程序无法直接接收这些推送数据。换句话说,HTTP/2 没有为应用层提供接收服务器推送的 API。这正是 WebSocket 的价值所在——它提供了真正的应用层双向通信能力。
技术选型建议
选择 WebSocket 的场景:
- 高吞吐量的双向数据交换(如多人在线游戏)
- 上下游数据流量接近对称的场景
- 需要最小化协议开销的场景
选择 HTTP/2 + SSE 的场景:
- 主要是服务器向客户端推送数据
- 希望利用现有 HTTP 基础设施
- 需要与代理、防火墙更好兼容
// HTTP/2 环境下 WebSocket 仍然独立工作
// WebSocket 不会自动享受 HTTP/2 的多路复用优势
const socket = new WebSocket('wss://example.com/ws');
// SSE 在 HTTP/2 下可以享受多路复用
const eventSource = new EventSource('/api/events');
WebSocket over HTTP/2 (RFC 8441)
传统上,WebSocket 使用 HTTP/1.1 的 Upgrade 机制建立连接。这种方式存在一个问题:WebSocket 连接与 HTTP/2 连接是独立的,无法共享同一个 TCP 连接。2018 年发布的 RFC 8441 标准解决了这个问题,允许 WebSocket 在 HTTP/2 连接上运行。
工作原理:RFC 8441 扩展了 HTTP/2 的 CONNECT 方法,引入了新的伪头部字段 :protocol。客户端通过发送带有 :protocol: websocket 的 CONNECT 请求来建立 WebSocket 连接,而不是使用传统的 GET 请求加 Upgrade 头部。
// 传统 WebSocket 握手 (HTTP/1.1)
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
// WebSocket over HTTP/2 握手
:method = CONNECT
:protocol = websocket
:scheme = https
:path = /chat
:authority = server.example.com
sec-websocket-version = 13
主要优势:
-
连接复用:WebSocket 可以与普通 HTTP/2 请求共享同一个 TCP 连接,减少连接开销。
-
多路复用:多个 WebSocket 连接可以在同一 HTTP/2 连接上并行运行,每个 WebSocket 使用独立的流(stream)。
-
更高效的握手:不需要完整的 HTTP/1.1 Upgrade 握手过程,减少了建立连接的延迟。
浏览器支持:Chrome 从版本 63 开始支持 WebSocket over HTTP/2。当页面通过 HTTP/2 加载时,浏览器会自动尝试使用 HTTP/2 建立 WebSocket 连接(如果服务器支持)。开发者无需修改代码,浏览器会自动处理协议协商。
客户端 服务器
| |
|-------- SETTINGS ----------->| (SETTINGS_ENABLE_CONNECT_PROTOCOL = 1)
| |
|------- CONNECT ------------>| (:protocol = websocket)
| (WebSocket headers) |
| |
|<------ 200 OK --------------| (WebSocket established)
| |
|<-----> WebSocket Data <----->| (在 HTTP/2 流上传输)
服务器配置要点:要支持 WebSocket over HTTP/2,服务器需要:
- 发送
SETTINGS_ENABLE_CONNECT_PROTOCOL设置参数(值为 1) - 支持扩展的 CONNECT 方法,能处理
:protocol伪头部
Nginx 从 1.25.0 版本开始支持此功能,需要在配置中启用:
server {
listen 443 ssl http2;
location /ws {
proxy_pass http://backend;
proxy_http_version 2.0;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
浏览器连接限制
理解浏览器的连接限制对于构建大规模 WebSocket 应用至关重要。
HTTP/1.1 的连接限制
在 HTTP/1.1 环境下,浏览器对每个域名有并发连接数限制。这是为了防止浏览器过度占用服务器资源。根据 HTTP/1.1 规范(RFC 2616),建议客户端对每个服务器最多保持 2 个持久连接,但现代浏览器通常放宽到 6 个左右。
| 浏览器 | 每域名最大连接数 |
|---|---|
| Chrome | 6 |
| Firefox | 6 |
| Safari | 6 |
| Edge | 6 |
WebSocket 的连接限制
好消息是:WebSocket 连接不受 HTTP/1.1 连接限制的约束。这意味着即使页面已经建立了 6 个 HTTP 连接,仍然可以创建新的 WebSocket 连接。
但是,浏览器对 WebSocket 连接仍有其他限制:
- 内存限制:每个连接都会占用内存,过多连接会影响页面性能
- 系统资源限制:操作系统对进程打开的文件描述符有限制
- 浏览器实现限制:部分浏览器可能对 WebSocket 连接总数有上限
实践建议
// 不推荐:为每个功能创建单独的 WebSocket 连接
const chatSocket = new WebSocket('wss://example.com/chat');
const notificationSocket = new WebSocket('wss://example.com/notifications');
const presenceSocket = new WebSocket('wss://example.com/presence');
// 推荐:使用单一连接多路复用
const socket = new WebSocket('wss://example.com/ws');
// 通过消息类型区分不同业务
socket.send(JSON.stringify({ channel: 'chat', data: message }));
socket.send(JSON.stringify({ channel: 'notification', subscribe: true }));
socket.send(JSON.stringify({ channel: 'presence', status: 'online' }));
// 统一处理不同类型的消息
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.channel) {
case 'chat':
handleChatMessage(msg.data);
break;
case 'notification':
showNotification(msg.data);
break;
case 'presence':
updatePresence(msg.data);
break;
}
};
移动端特殊考虑
在移动设备上使用 WebSocket 需要特别注意一些问题。
网络不稳定
移动网络环境不稳定,连接经常断开。需要实现健壮的重连机制:
class MobileWebSocket {
constructor(url, options = {}) {
this.url = url;
this.options = {
maxReconnectAttempts: 10,
baseDelay: 1000,
maxDelay: 30000,
...options
};
this.reconnectAttempts = 0;
this.connect();
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
this.reconnectAttempts = 0;
console.log('连接成功');
};
this.socket.onclose = (event) => {
// 非 1000 关闭码通常意味着需要重连
if (event.code !== 1000 && this.reconnectAttempts < this.options.maxReconnectAttempts) {
this.scheduleReconnect();
}
};
}
scheduleReconnect() {
// 指数退避 + 随机抖动,避免重连风暴
const delay = Math.min(
this.options.baseDelay * Math.pow(2, this.reconnectAttempts),
this.options.maxDelay
) + Math.random() * 1000;
this.reconnectAttempts++;
console.log(`${Math.round(delay / 1000)} 秒后第 ${this.reconnectAttempts} 次重连`);
setTimeout(() => this.connect(), delay);
}
}
应用切换到后台
移动应用切换到后台时,系统可能会暂停或断开 WebSocket 连接。需要处理页面可见性变化:
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
// 页面重新可见,检查连接状态
if (socket.readyState !== WebSocket.OPEN) {
console.log('页面恢复可见,重新连接...');
reconnect();
}
} else {
// 页面隐藏,可以选择暂停心跳或保持连接
// 取决于应用需求
}
});
电量消耗
持续活跃的 WebSocket 连接会增加电量消耗。可以考虑:
- 降低心跳频率:移动端可以使用更长的心跳间隔
- 智能休眠:页面不可见时降低活动频率
- 批量发送:合并多个小消息减少传输次数
// 根据页面可见性调整心跳策略
class AdaptiveHeartbeat {
constructor(socket) {
this.socket = socket;
this.activeInterval = 30000; // 前台:30 秒
this.backgroundInterval = 120000; // 后台:120 秒
this.currentInterval = this.activeInterval;
this.setupVisibilityHandler();
this.startHeartbeat();
}
setupVisibilityHandler() {
document.addEventListener('visibilitychange', () => {
this.currentInterval = document.visibilityState === 'visible'
? this.activeInterval
: this.backgroundInterval;
this.restartHeartbeat();
});
}
startHeartbeat() {
this.timer = setInterval(() => {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({ type: 'ping' }));
}
}, this.currentInterval);
}
restartHeartbeat() {
clearInterval(this.timer);
this.startHeartbeat();
}
}
浏览器支持
WebSocket API 在现代浏览器中得到广泛支持。根据 Can I Use 的数据,WebSocket 在全球浏览器中的支持率超过 98%。
所有主流浏览器的较新版本都支持 WebSocket:
- Chrome 4+
- Firefox 4+
- Safari 5+
- Edge 12+
- Opera 11+
特性检测与降级
对于不支持 WebSocket 的旧浏览器或特殊网络环境,应该实现特性检测和降级方案:
// 基本特性检测
if ('WebSocket' in window) {
// 浏览器支持 WebSocket
connectWebSocket();
} else {
// 降级到其他方案
console.log('浏览器不支持 WebSocket,使用替代方案');
connectLongPolling();
}
降级方案选择
| 降级方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 长轮询 | 通用降级 | 实现简单,兼容性好 | 延迟高,资源消耗大 |
| SSE | 服务器推送 | 原生支持自动重连 | 仅单向通信 |
| Socket.IO | 需要兼容性 | 自动降级,API 统一 | 库较大,协议非标准 |
| HTTP 流 | 大量数据推送 | 相对高效 | 实现复杂 |
使用 Socket.IO 实现自动降级:
// Socket.IO 会自动选择最佳传输方式
import { io } from 'socket.io-client';
const socket = io('http://localhost:8080', {
transports: ['websocket', 'polling'] // 优先使用 WebSocket
});
socket.on('connect', () => {
console.log('连接成功,传输方式:', socket.io.engine.transport.name);
});
手动实现降级:
class RealtimeConnection {
constructor(url) {
this.url = url;
this.socket = null;
this.pollInterval = null;
if ('WebSocket' in window) {
this.connectWebSocket();
} else {
this.connectLongPolling();
}
}
connectWebSocket() {
this.socket = new WebSocket(this.url);
this.socket.onmessage = (event) => {
this.onMessage(JSON.parse(event.data));
};
this.socket.onclose = () => {
// WebSocket 连接失败,尝试降级
if (this.pollInterval === null) {
console.log('WebSocket 失败,降级到长轮询');
this.connectLongPolling();
}
};
}
connectLongPolling() {
const poll = async () => {
try {
const response = await fetch(this.url.replace('ws', 'http'));
const data = await response.json();
this.onMessage(data);
} catch (error) {
console.error('轮询失败:', error);
}
};
poll();
this.pollInterval = setInterval(poll, 5000);
}
onMessage(data) {
// 由子类或使用者重写
console.log('收到消息:', data);
}
send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
} else {
// 长轮询模式下使用 HTTP POST
fetch(this.url.replace('ws', 'http'), {
method: 'POST',
body: JSON.stringify(data)
});
}
}
close() {
if (this.socket) {
this.socket.close();
}
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
}
}
代理和防火墙问题
某些企业网络或防火墙可能会阻止 WebSocket 连接。检测方法:
async function testWebSocketConnection(url, timeout = 5000) {
return new Promise((resolve) => {
const socket = new WebSocket(url);
const timer = setTimeout(() => {
socket.close();
resolve({ success: false, reason: 'timeout' });
}, timeout);
socket.onopen = () => {
clearTimeout(timer);
socket.close();
resolve({ success: true });
};
socket.onerror = () => {
clearTimeout(timer);
resolve({ success: false, reason: 'error' });
};
});
}
// 使用
testWebSocketConnection('wss://example.com/ws').then(result => {
if (!result.success) {
console.log('WebSocket 不可用,使用降级方案');
// 切换到长轮询
}
});
本教程内容
本教程将系统讲解 WebSocket 的各个方面:
- 协议原理:深入理解 WebSocket 协议的工作机制,包括握手过程、数据帧格式、连接状态管理等
- 浏览器客户端:学习使用 JavaScript WebSocket API,处理连接、消息、错误和关闭事件,以及新的 WebSocketStream API
- Node.js 服务端:使用 ws 库和 Socket.IO 构建高性能 WebSocket 服务器
- Python 服务端:使用 websockets 库和 FastAPI 实现 WebSocket 服务
- Java 服务端:使用 Jakarta WebSocket 和 Spring Boot 实现 WebSocket 服务
- Go 服务端:使用 gorilla/websocket 构建 Go 语言 WebSocket 服务
- 最佳实践:连接管理、心跳机制、错误处理、安全考虑、性能优化等
- 技术对比:WebSocket 与 SSE、长轮询、WebRTC 等实时技术的对比分析
- 故障排除:常见问题诊断和解决方案
快速开始
如果你迫不及待想体验 WebSocket,这里有一个最简单的示例:
服务端(Node.js):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
console.log('客户端已连接');
ws.on('message', (message) => {
console.log('收到消息:', message);
ws.send('服务器收到: ' + message);
});
ws.send('欢迎连接 WebSocket 服务器');
});
console.log('WebSocket 服务器运行在 ws://localhost:8080');
客户端(浏览器):
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = () => {
console.log('连接已建立');
socket.send('你好,服务器');
};
socket.onmessage = (event) => {
console.log('收到消息:', event.data);
};
socket.onerror = (error) => {
console.error('连接错误:', error);
};
socket.onclose = () => {
console.log('连接已关闭');
};
运行服务端代码后,在浏览器控制台执行客户端代码,你就能看到 WebSocket 的基本通信效果。
小结
本章介绍了 WebSocket 的基本概念、与 HTTP 的区别、适用场景以及浏览器支持情况。WebSocket 通过在单个 TCP 连接上提供全双工通信,解决了传统 HTTP 在实时通信场景下的局限性,成为现代 Web 应用实现实时功能的重要技术。
在接下来的章节中,我们将深入探讨 WebSocket 协议的工作原理,以及如何在不同编程语言中实现 WebSocket 客户端和服务端。