跳到主要内容

故障排除与常见问题

开发 WebSocket 应用时,经常会遇到各种连接、通信和部署问题。本章汇总了常见的故障场景及其解决方案,帮助你快速定位和解决问题。

连接问题

无法建立连接

症状:调用 new WebSocket() 后连接立即失败,触发 errorclose 事件。

可能原因及解决方案

1. URL 格式错误

检查 URL 格式是否正确。WebSocket URL 必须以 ws://wss:// 开头:

// 正确
const socket = new WebSocket('wss://example.com/ws');

// 错误 - 缺少协议前缀
const socket = new WebSocket('example.com/ws');

// 错误 - 使用了 http 协议
const socket = new WebSocket('https://example.com/ws');

2. 服务器未运行或端口错误

确认 WebSocket 服务器正在运行,且监听的端口与客户端 URL 中的端口一致。可以通过以下方式检查:

# Linux/macOS 检查端口占用
lsof -i :8080
netstat -an | grep 8080

# Windows 检查端口占用
netstat -ano | findstr :8080

3. 协议不匹配

如果使用了非标准端口(非 80/443),某些浏览器可能会阻止连接。生产环境建议使用标准端口或配置 HTTPS/WSS。

4. 防火墙阻止

检查防火墙是否允许 WebSocket 连接。某些企业网络可能会阻止 WebSocket 协议。

连接超时

症状:连接长时间处于 CONNECTING 状态,最终超时断开。

解决方案

为客户端连接设置超时时间,超时后主动关闭并重试:

function connectWithTimeout(url, timeout = 5000) {
return new Promise((resolve, reject) => {
const socket = new WebSocket(url);
const timer = setTimeout(() => {
socket.close();
reject(new Error('连接超时'));
}, timeout);

socket.onopen = () => {
clearTimeout(timer);
resolve(socket);
};

socket.onerror = (error) => {
clearTimeout(timer);
reject(error);
};
});
}

// 使用
connectWithTimeout('wss://example.com/ws', 10000)
.then(socket => console.log('连接成功'))
.catch(error => console.error('连接失败:', error.message));

连接被拒绝(403 Forbidden)

症状:服务器返回 403 状态码,连接被拒绝。

可能原因

1. Origin 验证失败

服务器验证了请求的 Origin 头部,但客户端的来源不在允许列表中。需要在服务器端添加客户端的 origin 到白名单:

// Node.js ws 库
const wss = new WebSocket.Server({
port: 8080,
verifyClient: (info, callback) => {
const allowedOrigins = ['http://localhost:3000', 'https://example.com'];
const origin = info.origin;

if (allowedOrigins.includes(origin)) {
callback(true);
} else {
callback(false, 403, 'Forbidden');
}
}
});

2. 缺少认证信息

如果服务器需要认证,确保在连接时传递了正确的认证信息:

// 通过 URL 参数传递
const token = 'your-auth-token';
const socket = new WebSocket(`wss://example.com/ws?token=${token}`);

// 或通过 Cookie(如果同域)
// Cookie 会自动随请求发送

跨域问题

症状:从不同域名/端口访问 WebSocket 服务时被阻止。

解决方案

WebSocket 本身支持跨域,但服务器需要正确配置 Origin 验证。开发环境可以临时允许所有来源:

// 开发环境 - 允许所有来源
const wss = new WebSocket.Server({
port: 8080,
verifyClient: (info, callback) => callback(true)
});

// 生产环境 - 严格限制
const wss = new WebSocket.Server({
port: 8080,
verifyClient: (info, callback) => {
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(',');
callback(allowedOrigins.includes(info.origin));
}
});

代理和负载均衡问题

Nginx 代理配置错误

症状:通过 Nginx 代理后 WebSocket 连接失败或频繁断开。

解决方案

确保 Nginx 配置了正确的 WebSocket 代理参数:

upstream websocket {
server 127.0.0.1:8080;
}

server {
listen 80;
server_name example.com;

location /ws {
proxy_pass http://websocket;

# 必需的配置
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

# 其他有用的配置
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

# 超时设置 - 防止长时间连接被断开
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
}

关键配置说明

  • proxy_http_version 1.1:WebSocket 需要 HTTP/1.1
  • UpgradeConnection 头部:用于协议升级握手
  • proxy_read_timeout:设置较长的超时时间,防止空闲连接被断开

连接频繁断开

症状:连接建立后,一段时间无活动就断开。

解决方案

这通常是由于代理服务器或负载均衡器的超时设置导致的。解决方法:

1. 增加代理超时时间

# Nginx 增加超时时间
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;

2. 实现心跳机制

客户端和服务端定期发送心跳消息保持连接活跃:

// 客户端心跳
class HeartbeatWebSocket {
constructor(url) {
this.url = url;
this.socket = null;
this.heartbeatTimer = null;
this.heartbeatInterval = 30000; // 30 秒
this.connect();
}

connect() {
this.socket = new WebSocket(this.url);

this.socket.onopen = () => {
this.startHeartbeat();
};

this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
this.lastPong = Date.now();
}
};

this.socket.onclose = () => {
this.stopHeartbeat();
// 自动重连
setTimeout(() => this.connect(), 3000);
};
}

startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({ type: 'ping' }));
}
}, this.heartbeatInterval);
}

stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
}

SSL/TLS 证书问题

症状:使用 wss:// 连接时失败,提示证书错误。

解决方案

1. 证书过期或无效

确保证书有效且未过期。使用 Let's Encrypt 免费获取证书:

# 使用 certbot 获取证书
certbot certonly --standalone -d example.com

2. 自签名证书问题

开发环境使用自签名证书时,浏览器会阻止连接。需要:

  • 在浏览器中访问 https://example.com 并信任证书
  • 或使用正规的 CA 签发的证书

3. 混合内容问题

如果页面使用 HTTPS,WebSocket 必须使用 WSS:

// 页面是 HTTPS 时
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const socket = new WebSocket(`${protocol}//${location.host}/ws`);

消息传输问题

消息发送失败

症状:调用 send() 方法后消息未到达服务器。

诊断步骤

1. 检查连接状态

发送消息前检查连接是否已打开:

function safeSend(socket, data) {
if (socket.readyState !== WebSocket.OPEN) {
console.warn('连接未打开,当前状态:', socket.readyState);
return false;
}

try {
socket.send(data);
return true;
} catch (error) {
console.error('发送失败:', error);
return false;
}
}

2. 检查发送缓冲区

如果发送速度过快,缓冲区可能会溢出:

function sendWithBufferCheck(socket, data) {
const bufferSize = socket.bufferedAmount;

if (bufferSize > 1024 * 1024) { // 1MB
console.warn('发送缓冲区已满,等待...');
return false;
}

socket.send(data);
return true;
}

// 监控缓冲区
setInterval(() => {
console.log(`缓冲区大小: ${socket.bufferedAmount} 字节`);
}, 1000);

消息接收不完整

症状:接收到的消息不完整或格式错误。

解决方案

1. 处理消息分段

WebSocket 协议支持消息分段,但浏览器会自动重组。如果服务端发送的消息被分段,确保正确处理:

// 服务端发送大消息时可能会分段
// 浏览器端正常接收即可,无需特殊处理
socket.onmessage = (event) => {
console.log('收到完整消息:', event.data);
};

2. 二进制消息处理

确保正确设置了二进制消息的类型:

socket.binaryType = 'arraybuffer'; // 或 'blob'

socket.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new DataView(event.data);
// 处理 ArrayBuffer
} else if (event.data instanceof Blob) {
// 处理 Blob
} else {
// 处理文本
}
};

JSON 解析错误

症状:接收到消息后 JSON.parse() 报错。

解决方案

添加安全的 JSON 解析:

socket.onmessage = (event) => {
let data;

try {
data = JSON.parse(event.data);
} catch (error) {
console.error('JSON 解析失败:', error, '原始数据:', event.data);
return;
}

// 处理解析后的数据
handleMessage(data);
};

关闭码分析

当连接关闭时,通过关闭码可以快速定位问题:

关闭码常见原因解决方案
1000正常关闭无需处理,这是预期的关闭
1001端点离开通常是页面刷新或关闭,正常行为
1002协议错误检查发送的数据格式是否符合协议规范
1003不支持的数据类型检查是否发送了服务器不支持的二进制数据
1006异常关闭网络中断、服务器崩溃或防火墙阻止,检查网络和服务器日志
1008策略违规检查 Origin 验证或认证失败
1009消息过大减小单条消息的大小,考虑分批发送
1011服务器内部错误检查服务器日志,修复服务器端 bug
1015TLS 握手失败检查 SSL 证书配置

诊断关闭原因

socket.onclose = (event) => {
console.log('关闭码:', event.code);
console.log('关闭原因:', event.reason);
console.log('是否正常关闭:', event.wasClean);

switch (event.code) {
case 1000:
console.log('正常关闭');
break;
case 1006:
console.error('异常关闭,可能是网络问题或服务器崩溃');
// 尝试重连
reconnect();
break;
case 1008:
console.error('策略违规,可能是认证失败');
// 重新认证
reauthenticate();
break;
default:
console.error(`未知关闭码: ${event.code}`);
}
};

性能问题

消息延迟高

症状:消息从发送到接收有明显的延迟。

诊断和解决

1. 检查网络延迟

// 测量往返时间
function measureRTT(socket) {
const start = Date.now();
socket.send(JSON.stringify({ type: 'ping', timestamp: start }));

socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
const rtt = Date.now() - start;
console.log(`RTT: ${rtt}ms`);
}
};
}

2. 减少消息大小

  • 使用简短的字段名
  • 考虑使用二进制格式(如 MessagePack)代替 JSON
  • 启用压缩扩展
// 启用压缩(服务端 Node.js)
const wss = new WebSocket.Server({
port: 8080,
perMessageDeflate: {
zlibDeflateOptions: { level: 3 }
}
});

3. 批量发送消息

class MessageBatcher {
constructor(sendCallback, batchSize = 10, flushInterval = 100) {
this.sendCallback = sendCallback;
this.batchSize = batchSize;
this.flushInterval = flushInterval;
this.batch = [];
this.timer = null;
}

add(message) {
this.batch.push(message);

if (this.batch.length >= this.batchSize) {
this.flush();
} else if (!this.timer) {
this.timer = setTimeout(() => this.flush(), this.flushInterval);
}
}

flush() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}

if (this.batch.length === 0) return;

this.sendCallback(this.batch);
this.batch = [];
}
}

// 使用
const batcher = new MessageBatcher((batch) => {
socket.send(JSON.stringify({ type: 'batch', messages: batch }));
});

batcher.add({ id: 1, data: 'message 1' });
batcher.add({ id: 2, data: 'message 2' });

内存占用过高

症状:长时间运行后内存持续增长。

解决方案

1. 清理断开的连接

确保服务端正确清理断开连接的资源:

const connections = new Map();

wss.on('connection', (ws, req) => {
const id = generateId();
connections.set(id, ws);

ws.on('close', () => {
connections.delete(id);
// 清理其他关联资源
cleanupResources(id);
});

ws.on('error', (error) => {
console.error('连接错误:', error);
connections.delete(id);
});
});

2. 限制消息历史

不要无限保存消息历史:

class MessageHistory {
constructor(maxSize = 100) {
this.messages = [];
this.maxSize = maxSize;
}

add(message) {
this.messages.push(message);
if (this.messages.length > this.maxSize) {
this.messages.shift();
}
}
}

3. 检查内存泄漏

使用 Node.js 的内存分析工具:

# 生成堆快照
node --inspect server.js
# 然后在 Chrome DevTools 中分析

# 或使用 heapdump 模块
npm install heapdump

调试技巧

浏览器调试

Chrome DevTools

  1. 打开 DevTools(F12)
  2. 切换到 Network 标签
  3. 点击 WS 过滤器
  4. 选择 WebSocket 连接查看详情

可以查看:

  • 握手请求和响应头
  • 发送和接收的消息(Messages 标签)
  • 时间线(Timing 标签)

实时监控 WebSocket 状态

// 在控制台中执行,实时监控所有 WebSocket 活动
(function() {
const originalWebSocket = window.WebSocket;

window.WebSocket = function(...args) {
const ws = new originalWebSocket(...args);

const originalSend = ws.send.bind(ws);
ws.send = function(data) {
console.log('%c[WS SEND]', 'color: green; font-weight: bold',
typeof data === 'string' ? data : `Binary ${data.byteLength || data.size} bytes`);
return originalSend(data);
};

ws.addEventListener('message', (event) => {
console.log('%c[WS RECV]', 'color: blue; font-weight: bold',
typeof event.data === 'string' ? event.data : `Binary ${event.data.byteLength || event.data.size} bytes`);
});

ws.addEventListener('open', () => {
console.log('%c[WS OPEN]', 'color: green; font-weight: bold', 'Connection established');
});

ws.addEventListener('close', (event) => {
console.log('%c[WS CLOSE]', 'color: red; font-weight: bold',
`Code: ${event.code}, Reason: ${event.reason}, Clean: ${event.wasClean}`);
});

ws.addEventListener('error', () => {
console.log('%c[WS ERROR]', 'color: red; font-weight: bold', 'Connection error');
});

return ws;
};

window.WebSocket.prototype = originalWebSocket.prototype;
console.log('WebSocket 监控已启用');
})();

网络抓包分析

使用 Wireshark

对于更深入的网络层分析,可以使用 Wireshark 抓包:

  1. 选择正确的网络接口
  2. 设置过滤器:tcp.port == 8080websocket
  3. 右键 WebSocket 握手包 → Follow → TCP Stream 查看完整流

握手包分析要点

// 正常握手请求应包含
GET /path HTTP/1.1
Upgrade: websocket // 必需
Connection: Upgrade // 必需
Sec-WebSocket-Key: xxx // 必需,Base64 编码的随机值
Sec-WebSocket-Version: 13 // 必需,版本号

// 正常握手响应应包含
HTTP/1.1 101 Switching Protocols // 必须是 101
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: xxx // 必需,由 Sec-WebSocket-Key 计算得出

服务端日志

添加详细的日志记录:

// 详细的连接日志
wss.on('connection', (ws, req) => {
const ip = req.socket.remoteAddress;
const url = req.url;
const headers = req.headers;

console.log('新连接:', {
time: new Date().toISOString(),
ip,
url,
origin: headers.origin,
userAgent: headers['user-agent']
});

ws.on('message', (data) => {
console.log('收到消息:', {
time: new Date().toISOString(),
size: data.length,
preview: data.toString().substring(0, 100)
});
});

ws.on('close', (code, reason) => {
console.log('连接关闭:', {
time: new Date().toISOString(),
code,
reason: reason.toString()
});
});

ws.on('error', (error) => {
console.error('连接错误:', {
time: new Date().toISOString(),
error: error.message,
stack: error.stack
});
});
});

使用测试工具

wscat:命令行 WebSocket 客户端

# 安装
npm install -g wscat

# 连接
wscat -c ws://localhost:8080

# 发送消息
> {"type": "ping"}

# 查看帮助
wscat --help

Postman:支持 WebSocket 测试

  1. 创建新的 WebSocket 请求
  2. 输入 WebSocket URL
  3. 连接并发送测试消息

常见问题 FAQ

Q: WebSocket 和 HTTP 长轮询如何选择?

WebSocket 适合:

  • 需要真正的双向实时通信
  • 消息频率高
  • 需要低延迟

HTTP 长轮询适合:

  • 消息频率低
  • 需要简单的实现
  • 客户端环境不支持 WebSocket

Q: 如何处理断线重连?

实现自动重连机制,使用指数退避算法避免频繁重连:

class ReconnectingWebSocket {
constructor(url) {
this.url = url;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.connect();
}

connect() {
this.socket = new WebSocket(this.url);

this.socket.onopen = () => {
this.reconnectAttempts = 0;
console.log('连接成功');
};

this.socket.onclose = (event) => {
if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
this.reconnectAttempts++;
console.log(`${delay / 1000} 秒后重连,第 ${this.reconnectAttempts} 次尝试`);
setTimeout(() => this.connect(), delay);
}
};
}
}

Q: 如何实现 WebSocket 认证?

推荐方式:

  1. 首次连接时通过 URL 参数传递 token(适合简单场景)
  2. 在握手时验证 Cookie 或 Authorization 头部(更安全)
  3. 连接建立后发送认证消息(适合需要更复杂认证流程的场景)
// 方式 1:URL 参数
const token = localStorage.getItem('token');
const socket = new WebSocket(`wss://api.example.com/ws?token=${token}`);

// 方式 3:连接后认证
socket.onopen = () => {
socket.send(JSON.stringify({
type: 'auth',
token: localStorage.getItem('token')
}));
};

socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'auth_result') {
if (data.success) {
console.log('认证成功');
} else {
console.log('认证失败,跳转到登录页');
}
}
};

Q: 如何测试 WebSocket 服务?

使用专业的 WebSocket 测试工具:

  1. wscat:命令行工具,适合快速测试
  2. Postman:图形界面,支持保存测试用例
  3. WebSocket King Client:Chrome 扩展,功能丰富
  4. 自己编写测试脚本:使用测试框架(如 Jest)编写自动化测试
// Jest 测试示例
const WebSocket = require('ws');

describe('WebSocket Server', () => {
let ws;

beforeEach((done) => {
ws = new WebSocket('ws://localhost:8080');
ws.on('open', done);
});

afterEach(() => {
ws.close();
});

test('should echo message', (done) => {
ws.on('message', (data) => {
expect(data.toString()).toBe('Echo: hello');
done();
});

ws.send('hello');
});
});

小结

本章介绍了 WebSocket 开发中常见的问题及其解决方案:

  1. 连接问题:URL 格式、服务器配置、跨域、超时
  2. 代理问题:Nginx 配置、SSL 证书
  3. 消息传输问题:发送失败、接收不完整、JSON 解析
  4. 关闭码分析:通过关闭码快速定位问题
  5. 性能问题:延迟、内存占用
  6. 调试技巧:浏览器工具、服务端日志、测试工具
  7. 常见问题 FAQ:实际开发中的常见疑问

遇到问题时,建议按照以下步骤排查:

  1. 检查浏览器控制台和 Network 标签
  2. 检查服务端日志
  3. 使用简单的测试工具验证服务端是否正常
  4. 逐步缩小问题范围
  5. 查阅官方文档和规范