WebSocket 基础
WebSocket 是 HTML5 引入的全双工通信协议,允许浏览器和服务器之间建立持久连接,实现双向实时数据传输。与传统的 HTTP 请求-响应模式不同,WebSocket 连接建立后,双方可以随时主动发送数据,无需重复建立连接,极大提升了实时通信的效率。
前置知识
阅读本节前,建议先了解:Service Worker
基础概念
WebSocket vs HTTP
| 特性 | HTTP | WebSocket |
|---|---|---|
| 通信方向 | 单向(请求-响应) | 双向(全双工) |
| 连接方式 | 短连接,每次请求新建 | 持久连接,一次握手 |
| 实时性 | 需要轮询或长轮询 | 天然实时 |
| 开销 | 每次请求带完整 HTTP 头 | 建立后仅传输数据帧 |
| 协议 | http:// / https:// | ws:// / wss:// |
| 数据格式 | 文本(HTML/JSON等) | 文本或二进制 |
| 状态码 | HTTP 状态码 | 自定义 |
WebSocket 协议
| 协议 | 说明 | 端口 |
|---|---|---|
ws:// | 非加密 WebSocket | 默认 80 |
wss:// | 加密 WebSocket(TLS) | 默认 443 |
注意
生产环境必须使用 wss://(加密 WebSocket),ws:// 仅用于开发环境。
语法与 API
创建 WebSocket 连接
javascript
// 创建 WebSocket 连接
const ws = new WebSocket('wss://echo.websocket.org');
// 创建带子协议的连接
const ws2 = new WebSocket('wss://example.com/ws', ['soap', 'json']);WebSocket 就绪状态
javascript
// WebSocket 有四种就绪状态
const ws = new WebSocket('wss://example.com/ws');
// readyState 属性
switch (ws.readyState) {
case WebSocket.CONNECTING: // 0 - 正在连接
console.log('正在建立连接...');
break;
case WebSocket.OPEN: // 1 - 连接已建立
console.log('连接已建立,可以通信');
break;
case WebSocket.CLOSING: // 2 - 正在关闭
console.log('连接正在关闭...');
break;
case WebSocket.CLOSED: // 3 - 连接已关闭
console.log('连接已关闭');
break;
}
// 使用常量
ws.readyState === WebSocket.OPEN; // true 表示连接已建立| 状态 | 常量 | 值 | 说明 |
|---|---|---|---|
| CONNECTING | WebSocket.CONNECTING | 0 | 正在建立连接 |
| OPEN | WebSocket.OPEN | 1 | 连接已建立 |
| CLOSING | WebSocket.CLOSING | 2 | 正在关闭 |
| CLOSED | WebSocket.CLOSED | 3 | 连接已关闭 |
发送数据
javascript
const ws = new WebSocket('wss://example.com/ws');
ws.onopen = () => {
// 发送文本数据
ws.send('Hello Server');
ws.send('{"type":"greet","message":"你好"}');
// 发送二进制数据
const buffer = new ArrayBuffer(8);
const view = new Int32Array(buffer);
view[0] = 42;
ws.send(buffer);
// 发送 Blob
const blob = new Blob(['hello']);
ws.send(blob);
// 发送 ArrayBuffer(TypedArray)
const typedArray = new Uint8Array([1, 2, 3, 4]);
ws.send(typedArray.buffer);
};接收数据
javascript
ws.onmessage = (event) => {
// 判断数据类型
if (typeof event.data === 'string') {
console.log('收到文本:', event.data);
const data = JSON.parse(event.data);
} else if (event.data instanceof Blob) {
console.log('收到 Blob,大小:', event.data.size);
} else if (event.data instanceof ArrayBuffer) {
console.log('收到 ArrayBuffer,大小:', event.data.byteLength);
}
};关闭连接
javascript
// 关闭连接
ws.close(); // 默认关闭码 1000,无原因
ws.close(1000, '正常关闭'); // 自定义关闭码和原因
// 关闭码
ws.onclose = (event) => {
console.log('关闭码:', event.code);
console.log('关闭原因:', event.reason);
console.log('是否干净关闭:', event.wasClean);
};常用关闭码
| 关闭码 | 名称 | 说明 |
|---|---|---|
| 1000 | NORMAL_CLOSURE | 正常关闭 |
| 1001 | GOING_AWAY | 终端离开(如页面关闭) |
| 1002 | PROTOCOL_ERROR | 协议错误 |
| 1003 | UNSUPPORTED_DATA | 不支持的数据类型 |
| 1005 | NO_STATUS_RECEIVED | 没有收到关闭码 |
| 1006 | ABNORMAL_CLOSURE | 异常关闭(无关闭帧) |
| 1007 | INVALID_FRAME_PAYLOAD | 无效的数据负载 |
| 1008 | POLICY_VIOLATION | 策略违规 |
| 1009 | MESSAGE_TOO_BIG | 消息过大 |
| 1011 | INTERNAL_ERROR | 服务器内部错误 |
| 1012 | SERVICE_RESTART | 服务重启 |
| 1013 | TRY_AGAIN_LATER | 稍后重试 |
| 1014 | BAD_GATEWAY | 网关错误 |
详细说明
WebSocket 事件
| 事件 | 触发时机 | 说明 |
|---|---|---|
open | 连接建立成功 | 可以开始发送数据 |
message | 收到服务器消息 | 处理接收到的数据 |
error | 连接错误 | 通信中出现错误 |
close | 连接关闭 | 处理关闭逻辑 |
javascript
const ws = new WebSocket('wss://example.com/ws');
// 连接建立
ws.onopen = (event) => {
console.log('连接已建立');
ws.send('Hello');
};
// 收到消息
ws.onmessage = (event) => {
console.log('收到:', event.data);
};
// 连接错误
ws.onerror = (event) => {
console.error('WebSocket 错误:', event);
};
// 连接关闭
ws.onclose = (event) => {
console.log(`连接关闭: code=${event.code}, reason=${event.reason}`);
};WebSocket 属性
javascript
const ws = new WebSocket('wss://example.com/ws');
// readyState - 就绪状态
ws.readyState; // 0, 1, 2, 3
// bufferedAmount - 发送缓冲区中未发送的字节数
ws.send(largeData);
console.log(ws.bufferedAmount); // 查看缓冲区剩余数据量
// url - 连接 URL
ws.url; // 'wss://example.com/ws'
// protocol - 子协议
ws.protocol; // 如果服务器选择了 'json' 子协议
// extensions - 扩展
ws.extensions; // 服务器返回的扩展
// binaryType - 二进制数据类型
ws.binaryType = 'arraybuffer'; // 'arraybuffer' 或 'blob'
ws.binaryType = 'blob';实战示例
WebSocket 连接测试工具
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebSocket 测试工具</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; background: #f5f7fa; }
.ws-card {
max-width: 600px; margin: 0 auto; background: white;
border-radius: 12px; padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h2 { text-align: center; color: #1a1a2e; }
.connect-row {
display: flex; gap: 10px; margin-bottom: 16px;
}
.connect-row input {
flex: 1; padding: 10px; border: 2px solid #e0e0e0;
border-radius: 8px; font-size: 14px;
}
.btn {
padding: 10px 20px; border: none; border-radius: 8px;
cursor: pointer; font-size: 14px; font-weight: 600; color: white;
}
.btn-connect { background: #4CAF50; }
.btn-disconnect { background: #F44336; }
.btn-send { background: #1a73e8; }
.btn-clear { background: #9E9E9E; }
.status-bar {
display: flex; justify-content: space-between; align-items: center;
padding: 10px 16px; background: #f0f0f0; border-radius: 8px;
margin-bottom: 16px; font-size: 13px;
}
.status-dot {
width: 10px; height: 10px; border-radius: 50%;
display: inline-block; margin-right: 6px;
}
.status-dot.connected { background: #4CAF50; }
.status-dot.disconnected { background: #ccc; }
.message-log {
height: 300px; overflow-y: auto; padding: 12px;
background: #1a1a2e; border-radius: 8px;
font-family: monospace; font-size: 13px;
}
.log-entry {
padding: 3px 0;
}
.log-sent { color: #64b5f6; }
.log-received { color: #81c784; }
.log-system { color: #ffb74d; }
.log-error { color: #ef5350; }
.send-row {
display: flex; gap: 10px; margin-top: 12px;
}
.send-row textarea {
flex: 1; padding: 10px; border: 2px solid #e0e0e0;
border-radius: 8px; font-size: 14px; resize: vertical;
height: 60px; font-family: monospace;
}
</style>
</head>
<body>
<div class="ws-card">
<h2>WebSocket 测试工具</h2>
<div class="connect-row">
<input type="text" id="wsUrl" value="wss://echo.websocket.org" placeholder="ws:// 或 wss:// URL">
<button class="btn btn-connect" id="connectBtn" onclick="connectWS()">连接</button>
<button class="btn btn-disconnect" id="disconnectBtn" onclick="disconnectWS()" disabled>断开</button>
</div>
<div class="status-bar">
<span><span class="status-dot disconnected" id="statusDot"></span><span id="statusText">未连接</span></span>
<span id="readyState">readyState: --</span>
</div>
<div class="message-log" id="messageLog">
<div class="log-entry log-system">等待连接...</div>
</div>
<div class="send-row">
<textarea id="sendMsg" placeholder="输入要发送的消息"></textarea>
<button class="btn btn-send" id="sendBtn" onclick="sendMessage()" disabled>发送</button>
<button class="btn btn-clear" onclick="clearLog()">清空</button>
</div>
</div>
<script>
let ws = null;
function addLog(text, type = 'system') {
const log = document.getElementById('messageLog');
const time = new Date().toLocaleTimeString();
log.innerHTML += `<div class="log-entry log-${type}">[${time}] ${text}</div>`;
log.scrollTop = log.scrollHeight;
}
function updateStatus(connected) {
document.getElementById('statusDot').className = `status-dot ${connected ? 'connected' : 'disconnected'}`;
document.getElementById('statusText').textContent = connected ? '已连接' : '未连接';
document.getElementById('connectBtn').disabled = connected;
document.getElementById('disconnectBtn').disabled = !connected;
document.getElementById('sendBtn').disabled = !connected;
}
function updateReadyState() {
if (ws) {
const states = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
document.getElementById('readyState').textContent = `readyState: ${states[ws.readyState]}`;
}
}
function connectWS() {
const url = document.getElementById('wsUrl').value.trim();
if (!url) return;
addLog(`正在连接 ${url}...`);
updateReadyState();
ws = new WebSocket(url);
ws.onopen = () => {
addLog('连接已建立', 'system');
updateStatus(true);
updateReadyState();
};
ws.onmessage = (event) => {
addLog(`收到: ${event.data}`, 'received');
};
ws.onerror = (event) => {
addLog('连接错误', 'error');
updateReadyState();
};
ws.onclose = (event) => {
addLog(`连接关闭 (code: ${event.code}, reason: ${event.reason || '无'})`, 'system');
updateStatus(false);
updateReadyState();
};
}
function disconnectWS() {
if (ws) {
ws.close(1000, '用户主动断开');
ws = null;
}
}
function sendMessage() {
if (ws && ws.readyState === WebSocket.OPEN) {
const msg = document.getElementById('sendMsg').value;
if (msg) {
ws.send(msg);
addLog(`发送: ${msg}`, 'sent');
document.getElementById('sendMsg').value = '';
}
}
}
function clearLog() {
document.getElementById('messageLog').innerHTML = '';
}
</script>
</body>
</html>注意事项
- 必须使用 HTTPS/WSS:生产环境禁止使用未加密的
ws://协议 - 连接有开销:不要频繁创建和关闭连接
- 处理异常关闭:
close事件的code=1006表示异常关闭 - 检查
bufferedAmount:发送大量数据前检查缓冲区 - 页面关闭时断开连接:在
beforeunload事件中调用ws.close()
最佳实践
- 连接成功后再发送数据,检查
readyState - 使用 JSON 格式传递结构化消息
- 为消息添加类型字段,便于处理不同消息
- 设置二进制类型
binaryType根据需求选择
下一节
继续学习:连接管理与重连