浏览器客户端 API
浏览器提供了原生的 WebSocket API,允许 JavaScript 代码与服务器建立 WebSocket 连接并进行双向通信。本章详细介绍浏览器 WebSocket API 的使用方法。
创建连接
构造函数
使用 new WebSocket() 构造函数创建 WebSocket 连接:
// 基本语法
const socket = new WebSocket(url, protocols);
参数说明:
- url(必需):WebSocket 服务器的 URL,格式为
ws://或wss:// - protocols(可选):单个协议字符串或协议数组
// 最简单的连接
const socket = new WebSocket('ws://localhost:8080');
// 使用加密连接
const secureSocket = new WebSocket('wss://example.com/chat');
// 指定子协议
const chatSocket = new WebSocket('ws://localhost:8080', 'chat-protocol');
// 指定多个子协议(按优先级排序)
const multiSocket = new WebSocket('ws://localhost:8080', ['chat', 'json']);
URL 格式
WebSocket URL 使用特定的协议标识符:
| 协议 | 说明 | 默认端口 |
|---|---|---|
ws:// | 非加密连接 | 80 |
wss:// | 加密连接(TLS/SSL) | 443 |
// 完整的 URL 示例
const socket = new WebSocket('wss://example.com:8080/path/to/endpoint?param=value');
URL 可以包含:
- 主机名和端口
- 路径
- 查询参数
子协议
子协议用于约定消息的格式和语义。客户端声明支持的协议列表,服务器从中选择一个返回:
// 客户端声明支持的协议
const socket = new WebSocket('ws://localhost:8080', ['json', 'protobuf']);
socket.onopen = () => {
// 检查服务器选择了哪个协议
console.log('选定的协议:', socket.protocol);
};
如果服务器不支持任何客户端声明的协议,连接会失败并触发 error 事件。
连接状态
readyState 属性
WebSocket 连接有四种状态,通过 readyState 属性获取:
const socket = new WebSocket('ws://localhost:8080');
console.log(socket.readyState);
| 状态常量 | 数值 | 说明 |
|---|---|---|
WebSocket.CONNECTING | 0 | 连接正在建立中 |
WebSocket.OPEN | 1 | 连接已建立,可以通信 |
WebSocket.CLOSING | 2 | 连接正在关闭中 |
WebSocket.CLOSED | 3 | 连接已关闭或无法建立 |
状态检查
在发送消息前检查连接状态是良好的实践:
function sendSafely(socket, data) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(data);
return true;
}
console.warn('连接未打开,无法发送消息');
return false;
}
// 使用常量进行状态检查
if (socket.readyState === WebSocket.OPEN) {
socket.send('Hello');
}
状态变化示例
const socket = new WebSocket('ws://localhost:8080');
console.log('初始状态:', socket.readyState);
socket.onopen = () => {
console.log('连接状态:', socket.readyState);
// 关闭连接
socket.close();
};
socket.onclose = () => {
console.log('关闭后状态:', socket.readyState);
};
实例属性
url
只读属性,返回构造函数传入的 URL:
const socket = new WebSocket('wss://example.com/chat');
console.log(socket.url);
protocol
只读属性,返回服务器选定的子协议:
const socket = new WebSocket('ws://localhost:8080', ['chat', 'json']);
socket.onopen = () => {
if (socket.protocol === 'chat') {
console.log('使用 chat 协议');
}
};
如果没有协商子协议,返回空字符串。
extensions
只读属性,返回服务器选择的扩展:
socket.onopen = () => {
console.log('扩展:', socket.extensions);
};
常见的扩展包括 permessage-deflate(消息压缩)。
bufferedAmount
只读属性,返回已排队但尚未发送的字节数。这个属性对于监控发送缓冲区很有用:
// 检查发送缓冲区
function sendLargeData(socket, data) {
// 如果缓冲区已经有大量数据等待发送,可能需要等待
if (socket.bufferedAmount > 1024 * 1024) {
console.warn('发送缓冲区已满,等待中...');
return false;
}
socket.send(data);
return true;
}
// 监控缓冲区状态
setInterval(() => {
console.log(`缓冲区: ${socket.bufferedAmount} 字节`);
}, 1000);
binaryType
控制接收二进制数据时的格式,可选值:
'blob'(默认):返回 Blob 对象'arraybuffer':返回 ArrayBuffer 对象
const socket = new WebSocket('ws://localhost:8080');
// 使用 ArrayBuffer 接收二进制数据
socket.binaryType = 'arraybuffer';
socket.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new DataView(event.data);
console.log('二进制数据:', view.getInt32(0));
}
};
// 使用 Blob 接收二进制数据
socket.binaryType = 'blob';
socket.onmessage = (event) => {
if (event.data instanceof Blob) {
console.log('Blob 大小:', event.data.size);
// 可以转换为其他格式
event.data.text().then(text => console.log(text));
}
};
发送消息
send() 方法
使用 send() 方法发送数据:
socket.send(data);
支持发送以下类型的数据:
// 发送文本
socket.send('Hello, WebSocket');
// 发送 JSON
socket.send(JSON.stringify({ type: 'chat', content: '你好' }));
// 发送 ArrayBuffer
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setInt32(0, 12345);
socket.send(buffer);
// 发送 TypedArray
const intArray = new Int32Array([1, 2, 3, 4, 5]);
socket.send(intArray.buffer);
// 发送 Blob
const blob = new Blob(['blob content'], { type: 'text/plain' });
socket.send(blob);
发送文本消息
文本消息是最常用的消息类型:
// 简单文本
socket.send('Hello World');
// JSON 格式
const message = {
type: 'chat',
from: 'user1',
content: '大家好',
timestamp: Date.now()
};
socket.send(JSON.stringify(message));
发送二进制消息
二进制消息适合传输图像、音频等数据:
// 发送 ArrayBuffer
const buffer = new ArrayBuffer(8);
const dataView = new DataView(buffer);
dataView.setFloat64(0, 3.14159);
socket.send(buffer);
// 发送 Uint8Array
const bytes = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
socket.send(bytes.buffer);
// 发送 Blob(如文件)
const file = document.querySelector('input[type="file"]').files[0];
socket.send(file);
安全发送
发送前应检查连接状态:
function safeSend(socket, data) {
if (socket.readyState !== WebSocket.OPEN) {
console.error('连接未打开');
return false;
}
try {
socket.send(data);
return true;
} catch (error) {
console.error('发送失败:', error);
return false;
}
}
消息队列
当连接尚未建立时,可以先将消息存入队列:
class MessageQueue {
constructor() {
this.queue = [];
}
add(message) {
this.queue.push(message);
}
flush(socket) {
while (this.queue.length > 0) {
const message = this.queue.shift();
socket.send(message);
}
}
}
const queue = new MessageQueue();
const socket = new WebSocket('ws://localhost:8080');
// 连接建立前可以添加消息
queue.add('第一条消息');
queue.add('第二条消息');
socket.onopen = () => {
// 连接建立后发送队列中的消息
queue.flush(socket);
};
接收消息
message 事件
通过监听 message 事件接收服务器发送的消息:
socket.onmessage = (event) => {
console.log('收到消息:', event.data);
};
// 或使用 addEventListener
socket.addEventListener('message', (event) => {
console.log('收到消息:', event.data);
});
处理文本消息
socket.onmessage = (event) => {
// 检查数据类型
if (typeof event.data === 'string') {
console.log('文本消息:', event.data);
// 尝试解析 JSON
try {
const data = JSON.parse(event.data);
console.log('JSON 数据:', data);
} catch (e) {
console.log('普通文本:', event.data);
}
}
};
处理二进制消息
// 设置二进制类型
socket.binaryType = 'arraybuffer';
socket.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
// 处理 ArrayBuffer
const view = new DataView(event.data);
const value = view.getInt32(0);
console.log('二进制数据:', value);
} else if (event.data instanceof Blob) {
// 处理 Blob
console.log('Blob 大小:', event.data.size);
}
};
消息类型判断
socket.onmessage = (event) => {
const data = event.data;
if (typeof data === 'string') {
// 文本消息
handleTextMessage(data);
} else if (data instanceof ArrayBuffer) {
// ArrayBuffer 二进制
handleArrayBuffer(data);
} else if (data instanceof Blob) {
// Blob 二进制
handleBlob(data);
}
};
关闭连接
close() 方法
使用 close() 方法关闭连接:
// 正常关闭
socket.close();
// 带关闭码关闭
socket.close(1000, '正常关闭');
// 自定义关闭码(3000-4999)
socket.close(4000, '自定义原因');
参数说明:
- code(可选):关闭码,默认 1000
- reason(可选):关闭原因,UTF-8 字符串,最大 123 字节
关闭码
常用关闭码:
| 代码 | 名称 | 说明 |
|---|---|---|
| 1000 | Normal Closure | 正常关闭 |
| 1001 | Going Away | 端点离开(页面关闭、导航等) |
| 1002 | Protocol Error | 协议错误 |
| 1003 | Unsupported Data | 不支持的数据类型 |
| 1005 | No Status Received | 保留,不应发送 |
| 1006 | Abnormal Closure | 异常关闭(无关闭帧) |
| 1007 | Invalid Frame Payload Data | 无效的负载数据 |
| 1008 | Policy Violation | 策略违规 |
| 1009 | Message Too Big | 消息过大 |
| 1010 | Mandatory Ext | 缺少必要扩展 |
| 1011 | Internal Error | 服务器内部错误 |
| 1012 | Service Restart | 服务重启 |
| 1013 | Try Again Later | 稍后重试 |
| 1015 | TLS Handshake | TLS 握手失败 |
自定义关闭码范围:
- 3000-3999:供库和框架使用
- 4000-4999:供应用程序使用
close 事件
监听 close 事件处理连接关闭:
socket.onclose = (event) => {
console.log('关闭码:', event.code);
console.log('关闭原因:', event.reason);
console.log('是否正常关闭:', event.wasClean);
};
CloseEvent 属性:
- code:关闭码
- reason:关闭原因字符串
- wasClean:是否正常关闭(布尔值)
socket.onclose = (event) => {
if (event.wasClean) {
console.log(`连接正常关闭: ${event.code} - ${event.reason}`);
} else {
console.log('连接异常断开');
if (event.code === 1006) {
console.log('网络可能中断');
}
}
};
错误处理
error 事件
监听 error 事件处理错误:
socket.onerror = (event) => {
console.error('WebSocket 错误:', event);
};
// 或使用 addEventListener
socket.addEventListener('error', (event) => {
console.error('连接错误:', event);
});
注意:error 事件的 event 对象不包含详细的错误信息。通常需要结合 close 事件一起处理:
socket.onerror = (event) => {
console.error('发生错误');
};
socket.onclose = (event) => {
if (!event.wasClean) {
console.error(`连接异常关闭: ${event.code}`);
switch (event.code) {
case 1006:
console.error('无法建立连接或连接中断');
break;
case 1008:
console.error('策略违规,可能需要认证');
break;
case 1011:
console.error('服务器内部错误');
break;
}
}
};
常见错误场景
socket.onerror = () => {
console.error('连接错误');
};
socket.onclose = (event) => {
// 连接失败
if (event.code === 1006) {
// 可能的原因:
// 1. 服务器未运行
// 2. URL 错误
// 3. 网络问题
// 4. CORS 问题
console.error('无法连接到服务器');
}
};
事件处理
事件类型
WebSocket 有四个主要事件:
| 事件 | 说明 | 触发时机 |
|---|---|---|
open | 连接打开 | 握手成功完成 |
message | 收到消息 | 接收到服务器消息 |
error | 发生错误 | 连接或通信错误 |
close | 连接关闭 | 连接关闭(正常或异常) |
事件监听方式
方式一:on 属性赋值
socket.onopen = (event) => {
console.log('连接已建立');
};
socket.onmessage = (event) => {
console.log('收到消息:', event.data);
};
socket.onerror = (event) => {
console.error('发生错误');
};
socket.onclose = (event) => {
console.log('连接已关闭');
};
方式二:addEventListener
socket.addEventListener('open', (event) => {
console.log('连接已建立');
});
socket.addEventListener('message', (event) => {
console.log('收到消息:', event.data);
});
socket.addEventListener('error', (event) => {
console.error('发生错误');
});
socket.addEventListener('close', (event) => {
console.log('连接已关闭');
});
addEventListener 的优势是可以添加多个监听器:
// 多个监听器
socket.addEventListener('message', logger);
socket.addEventListener('message', handler);
socket.addEventListener('message', analytics);
function logger(event) {
console.log('日志:', event.data);
}
function handler(event) {
// 处理消息
}
function analytics(event) {
// 统计分析
}
完整示例
基础聊天客户端
class ChatClient {
constructor(url, username) {
this.url = url;
this.username = username;
this.socket = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.messageHandlers = [];
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log('已连接到服务器');
this.reconnectAttempts = 0;
// 发送加入消息
this.send({
type: 'join',
username: this.username
});
};
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
this.messageHandlers.forEach(handler => handler(data));
};
this.socket.onerror = () => {
console.error('连接错误');
};
this.socket.onclose = (event) => {
console.log('连接关闭:', event.code, event.reason);
// 自动重连
if (!event.wasClean && this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
console.log(`${delay / 1000} 秒后重连...`);
setTimeout(() => this.connect(), delay);
}
};
}
send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
}
}
sendMessage(content) {
this.send({
type: 'message',
username: this.username,
content: content,
timestamp: Date.now()
});
}
onMessage(handler) {
this.messageHandlers.push(handler);
}
disconnect() {
if (this.socket) {
this.socket.close(1000, '用户断开');
}
}
}
// 使用示例
const client = new ChatClient('ws://localhost:8080/chat', '张三');
client.onMessage((data) => {
if (data.type === 'message') {
console.log(`${data.username}: ${data.content}`);
} else if (data.type === 'system') {
console.log(`[系统] ${data.content}`);
}
});
client.connect();
// 发送消息
client.sendMessage('大家好!');
实时数据订阅
class RealtimeDataClient {
constructor(url) {
this.url = url;
this.socket = null;
this.subscriptions = new Map();
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log('已连接');
// 重新订阅
this.subscriptions.forEach((callback, topic) => {
this.socket.send(JSON.stringify({
action: 'subscribe',
topic: topic
}));
});
};
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
const callback = this.subscriptions.get(data.topic);
if (callback) {
callback(data);
}
};
this.socket.binaryType = 'arraybuffer';
}
subscribe(topic, callback) {
this.subscriptions.set(topic, callback);
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({
action: 'subscribe',
topic: topic
}));
}
}
unsubscribe(topic) {
this.subscriptions.delete(topic);
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({
action: 'unsubscribe',
topic: topic
}));
}
}
}
// 使用示例
const client = new RealtimeDataClient('wss://api.example.com/realtime');
client.connect();
// 订阅股票价格
client.subscribe('stock:AAPL', (data) => {
console.log('AAPL 价格:', data.price);
});
// 订阅天气数据
client.subscribe('weather:beijing', (data) => {
console.log('北京天气:', data.temperature);
});
带心跳的客户端
class WebSocketWithHeartbeat {
constructor(url, options = {}) {
this.url = url;
this.options = {
heartbeatInterval: 30000,
heartbeatTimeout: 5000,
maxMissedHeartbeats: 3,
...options
};
this.socket = null;
this.heartbeatTimer = null;
this.missedHeartbeats = 0;
this.connect();
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log('连接已建立');
this.missedHeartbeats = 0;
this.startHeartbeat();
this.onOpen();
};
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
this.missedHeartbeats = 0;
return;
}
this.onMessage(data);
};
this.socket.onclose = (event) => {
this.stopHeartbeat();
this.onClose(event);
};
this.socket.onerror = (error) => {
this.onError(error);
};
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.socket.readyState !== WebSocket.OPEN) {
return;
}
this.missedHeartbeats++;
if (this.missedHeartbeats > this.options.maxMissedHeartbeats) {
console.log('心跳超时,关闭连接');
this.socket.close();
return;
}
this.socket.send(JSON.stringify({ type: 'ping' }));
}, this.options.heartbeatInterval);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
}
}
close() {
this.stopHeartbeat();
if (this.socket) {
this.socket.close(1000, '用户关闭');
}
}
// 子类可以重写这些方法
onOpen() {}
onMessage(data) {}
onClose(event) {}
onError(error) {}
}
浏览器兼容性
WebSocket API 在现代浏览器中得到广泛支持:
| 浏览器 | 最低版本 |
|---|---|
| Chrome | 4+ |
| Firefox | 4+ |
| Safari | 5+ |
| Edge | 12+ |
| Opera | 11+ |
| IE | 10+ |
特性检测
在使用 WebSocket 前检测浏览器支持:
if ('WebSocket' in window) {
const socket = new WebSocket('ws://localhost:8080');
} else {
console.error('浏览器不支持 WebSocket');
// 可以降级到轮询方式
}
Web Worker 支持
WebSocket API 在 Web Worker 中也可用:
// worker.js
self.onmessage = function(e) {
if (e.data === 'start') {
const socket = new WebSocket('ws://localhost:8080');
socket.onmessage = function(event) {
self.postMessage(event.data);
};
}
};
调试技巧
Chrome DevTools
- 打开开发者工具(F12)
- 切换到 Network 标签
- 筛选 WS(WebSocket)
- 点击连接查看详情
可以查看:
- 连接请求和响应头
- 发送和接收的消息
- 时间线
日志包装
创建带日志的 WebSocket 实例:
function createLoggedWebSocket(url) {
const socket = new WebSocket(url);
const originalSend = socket.send.bind(socket);
socket.send = (data) => {
console.log('发送:', data);
return originalSend(data);
};
socket.addEventListener('message', (event) => {
console.log('接收:', event.data);
});
socket.addEventListener('open', () => {
console.log('连接已打开');
});
socket.addEventListener('close', (event) => {
console.log('连接已关闭:', event.code, event.reason);
});
socket.addEventListener('error', () => {
console.error('连接错误');
});
return socket;
}
// 使用
const socket = createLoggedWebSocket('ws://localhost:8080');
小结
本章详细介绍了浏览器 WebSocket API:
- 创建连接:使用
new WebSocket(url, protocols)构造函数 - 连接状态:通过
readyState属性检查连接状态 - 实例属性:
url、protocol、extensions、bufferedAmount、binaryType - 发送消息:使用
send()方法发送文本和二进制数据 - 接收消息:监听
message事件 - 关闭连接:使用
close()方法和关闭码 - 错误处理:监听
error和close事件 - 完整示例:聊天客户端、数据订阅、心跳机制
浏览器 WebSocket API 简洁但功能完整,适合构建各类实时 Web 应用。在实际开发中,建议结合重连机制、心跳检测和错误处理,确保应用的健壮性。