跳到主要内容

浏览器客户端 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.CONNECTING0连接正在建立中
WebSocket.OPEN1连接已建立,可以通信
WebSocket.CLOSING2连接正在关闭中
WebSocket.CLOSED3连接已关闭或无法建立

状态检查

在发送消息前检查连接状态是良好的实践:

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 字节

关闭码

常用关闭码:

代码名称说明
1000Normal Closure正常关闭
1001Going Away端点离开(页面关闭、导航等)
1002Protocol Error协议错误
1003Unsupported Data不支持的数据类型
1005No Status Received保留,不应发送
1006Abnormal Closure异常关闭(无关闭帧)
1007Invalid Frame Payload Data无效的负载数据
1008Policy Violation策略违规
1009Message Too Big消息过大
1010Mandatory Ext缺少必要扩展
1011Internal Error服务器内部错误
1012Service Restart服务重启
1013Try Again Later稍后重试
1015TLS HandshakeTLS 握手失败

自定义关闭码范围:

  • 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 在现代浏览器中得到广泛支持:

浏览器最低版本
Chrome4+
Firefox4+
Safari5+
Edge12+
Opera11+
IE10+

特性检测

在使用 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

  1. 打开开发者工具(F12)
  2. 切换到 Network 标签
  3. 筛选 WS(WebSocket)
  4. 点击连接查看详情

可以查看:

  • 连接请求和响应头
  • 发送和接收的消息
  • 时间线

日志包装

创建带日志的 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:

  1. 创建连接:使用 new WebSocket(url, protocols) 构造函数
  2. 连接状态:通过 readyState 属性检查连接状态
  3. 实例属性urlprotocolextensionsbufferedAmountbinaryType
  4. 发送消息:使用 send() 方法发送文本和二进制数据
  5. 接收消息:监听 message 事件
  6. 关闭连接:使用 close() 方法和关闭码
  7. 错误处理:监听 errorclose 事件
  8. 完整示例:聊天客户端、数据订阅、心跳机制

浏览器 WebSocket API 简洁但功能完整,适合构建各类实时 Web 应用。在实际开发中,建议结合重连机制、心跳检测和错误处理,确保应用的健壮性。