本文将详细介绍如何使用 ws 库来实现 WebSocket 服务器和客户端,包括安装、性能优化、压缩、示例代码以及一些常见问题的解决方案。
协议支持
- HyBi 草案 07-12:可以通过设置
protocolVersion: 8来使用。 - HyBi 草案 13-17:这是当前默认的协议版本,可以通过设置
protocolVersion: 13来使用。
安装
使用 npm 安装 ws 库:
npm install ws
性能优化
可以选择安装 bufferutil 和 utf-8-validate 模块来提高性能:
npm install --save-optional bufferutil
npm install --save-optional utf-8-validate
bufferutil:用于高效地执行 WebSocket 帧的数据有效负载的掩码和解除掩码操作。utf-8-validate:用于高效地验证消息是否包含有效的 UTF-8。
如果不希望尝试并使用这些模块,可以使用 WS_NO_BUFFER_UTIL 和 WS_NO_UTF_8_VALIDATE 环境变量来禁用它们。即使安装了 utf-8-validate 模块,如果 buffer.isUtf8() 方法可用,也不需要使用 utf-8-validate 模块。
WebSocket 压缩
ws 支持 permessage-deflate 扩展,可以在客户端和服务器之间协商压缩算法及其参数。默## 协议支持
- HyBi草案07-12(使用选项
protocolVersion: 8) - HyBi草案13-17(当前默认值,也可以使用选项
protocolVersion: 13)
安装
npm install ws
提高性能
有两个可选的模块可以与ws模块一起安装。这些模块是二进制插件,可以提高某些操作的性能。预构建的二进制文件适用于最受欢迎的平台,因此您不一定需要在计算机上安装C ++编译器。
npm install --save-optional bufferutil:允许有效地执行WebSocket帧的数据有效负载的掩码和解除掩码等操作。npm install --save-optional utf-8-validate:允许有效地检查消息是否包含有效的UTF-8。
为了甚至都不要尝试要求并使用这些模块,请使用WS_NO_BUFFER_UTIL和WS_NO_UTF_8_VALIDATE环境变量。这些可能在系统中增强安全性,其中用户可以将软件包放入另一个用户的应用程序的软件包搜索路径中,由于Node.js解析器算法的工作方式,这可能很有用。
即使已安装utf-8-validate模块,在WS_NO_UTF_8_VALIDATE环境变量的值如何,如果buffer.isUtf8()可用,则不需要使用utf-8-validate模块。
WebSocket压缩
ws支持permessage-deflate扩展,它使客户端和服务器可以协商压缩算法及其参数,然后选择性地将其应用于每个WebSocket消息的数据有效负载。
服务器默认情况下禁用该扩展,在客户端上默认启用。它在性能和内存消耗方面增加了显着的开销,因此我们建议仅在确实需要时启用它。
请注意,Node.js对高性能压缩有各种问题,特别是在Linux上增加并发性可能会导致灾难性的内存碎片化和性能下降。如果您打算在生产中使用permessage-deflate,则值得设置一个代表您工作负载的测试,并确保Node.js / zlib将其处理为可接受的性能和内存使用。
通过下面定义的选项可以调整permessage-deflate的设置。您还可以使用zlibDeflateOptions和zlibInflateOptions,这些选项被直接传递到原始deflate / inflate流的创建中。
有关更多选项,请参见文档。
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({
port: 8080,
perMessageDeflate: {
zlibDeflateOptions: {
// 查看zlib默认值
chunkSize: 1024, // 块大小
memLevel: 7, // 内存级别,1-9之间
level: 3 // 压缩级别,0-9之间
},
zlibInflateOptions: {
chunkSize: 10 * 1024 // 块大小
},
// 其他可设置选项:
clientNoContextTakeover: true, // 默认为协商的值。
serverNoContextTakeover: true, // 默认为协商的值。
serverMaxWindowBits: 10, // 默认为协商的值。
// 下面这些选项指定为默认值。
concurrencyLimit: 10, // 用于性能的zlib并发限制。
threshold: 1024 // 如果上下文接管被禁用,则消息大小(以字节为单位)低于此大小时不应该压缩。
}
});
只有在服务器支持并启用该扩展时,客户端才会使用该扩展。要始终在客户端上禁用该扩展,请将perMessageDeflate选项设置为false。
import WebSocket from 'ws';
const ws = new WebSocket('ws://www.host.com/path', {
perMessageDeflate: false
});
使用示例
发送和接收文本数据
import WebSocket from 'ws';
const ws = new WebSocket('ws://www.host.com/path');
ws.on('error', console.error);
ws.on('open', function open() {
ws.send('something');
});
ws.on('message', function message(data) {
console.log('received: %s', data);
});
发送二进制数据
import WebSocket from 'ws';
const ws = new WebSocket('ws://www.host.com/path');
ws.on('error', console.error);
ws.on('open', function open() {
const array = new Float32Array(5);
for (var i = 0; i < array.length; ++i) {
array[i] = i / 2;
}
ws.send(array);
});
简单服务器
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data) {
console.log('received: %s', data);
});
ws.send('something');
});
外部HTTP/S服务器
import { createServer } from 'https';
import { readFileSync } from 'fs';
import { WebSocketServer } from 'ws';
const server = createServer({
cert: readFileSync('/path/to/cert.pem'),
key: readFileSync('/path/to/key.pem')
});
const wss = new WebSocketServer({ server });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data) {
console.log('received: %s', data);
});
ws.send('something');
});
server.listen(8080);
多个服务器共享单个HTTP / S服务器
import { createServer } from 'http';
import { parse } from 'url';
import { WebSocketServer } from 'ws';
const server = createServer();
const wss1 = new WebSocketServer({ noServer: true });
const wss2 = new WebSocketServer({ noServer: true });
wss1.on('connection', function connection(ws) {
ws.on('error', console.error);
// ...
});
wss2.on('connection', function connection(ws) {
ws.on('error', console.error);
// ...
});
server.on('upgrade', function upgrade(request, socket, head) {
const { pathname } = parse(request.url);
if (pathname === '/foo') {
wss1.handleUpgrade(request, socket, head, function done(ws) {
wss1.emit('connection', ws, request);
});
} else if (pathname === '/bar') {
wss2.handleUpgrade(request, socket, head, function done(ws) {
wss2.emit('connection', ws, request);
});
} else {
socket.destroy();
}
});
server.listen(8080);
客户端认证
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
function onSocketError(err) {
console.error(err);
}
const server = createServer();
const wss = new WebSocketServer({ noServer: true });
wss.on('connection', function connection(ws, request, client) {
ws.on('error', console.error);
ws.on('message', function message(data) {
console.log(`Received message ${data} from user ${client}`);
});
});
server.on('upgrade', function upgrade(request, socket, head) {
socket.on('error', onSocketError);
// This function is not defined on purpose. Implement it with your own logic.
authenticate(request, function next(err, client) {
if (err || !client) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
socket.removeListener('error', onSocketError);
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request, client);
});
});
});
server.listen(8080);
还可以查看提供的示例,使用express-session。
服务器广播
客户端WebSocket向所有已连接的WebSocket客户端(包括自身)广播。
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data, isBinary) {
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary });
}
});
});
});
客户端WebSocket向每个其他已连接的WebSocket客户端(不包括自身)广播。
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data, isBinary) {
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary });
}
});
});
});
Round-trip time(通信延迟)
import WebSocket from 'ws';
const ws = new WebSocket('wss://websocket-echo.com/');
ws.on('error', console.error);
ws.on('open', function open() {
console.log('connected');
ws.send(Date.now());
});
ws.on('close', function close() {
console.log('disconnected');
});
ws.on('message', function message(data) {
console.log(`Round-trip time: ${Date.now() - data} ms`);
setTimeout(function timeout() {
ws.send(Date.now());
}, 500);
});
使用 Node.js streams API
import WebSocket, { createWebSocketStream } from 'ws';
const ws = new WebSocket('wss://websocket-echo.com/');
const duplex = createWebSocketStream(ws, { encoding: 'utf8' });
duplex.on('error', console.error);
duplex.pipe(process.stdout);
process.stdin.pipe(duplex);
其他示例
对于使用ws服务器与浏览器客户端通信的完整示例,请参见examples文件夹。
否则,请参见测试用例。
常问问题
如何获取客户端的IP地址?
可以从原始套接字中获取远程IP地址。
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws, req) {
const ip = req.socket.remoteAddress;
ws.on('error', console.error);
});
当服务器在像NGINX这样的代理后运行时,事实上的标准是使用X-Forwarded-For头。
wss.on('connection', function connection(ws, req) {
const ip = req.headers['x-forwarded-for'].split(',')[0].trim();
ws.on('error', console.error);
});
如何检测并关闭中断连接?
有时,服务器和客户端之间的链接会以使服务器和客户端都不知道连接中断的方式中断(例如,拔出插头)。
在这些情况下,可以使用ping消息作为验证远程终点是否仍然响应的手段。
import { WebSocketServer } from 'ws';
function heartbeat() {
this.isAlive = true;
}
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.isAlive = true;
ws.on('error', console.error);
ws.on('pong', heartbeat);
});
const interval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('close', function close() {
clearInterval(interval);
});
根据规范要求,在需要的情况下会自动发送pong消息作为响应ping消息。
就像上面的服务器示例一样,您的客户端也可能失去连接而不知道它。您可能需要在客户端上添加一个ping侦听器来防止这种情况发生。一个简单的实现是:
import WebSocket from 'ws';
function heartbeat() {
clearTimeout(this.pingTimeout);
// Use `WebSocket#terminate()`, which immediately destroys the connection,
// instead of `WebSocket#close()`, which waits for the close timer.
// Delay should be equal to the interval at which your server
// sends out pings plus a conservative assumption of the latency.
this.pingTimeout = setTimeout(() => {
this.terminate();
}, 30000 + 1000);
}
const client = new WebSocket('wss://websocket-echo.com/');
client.on('error', console.error);
client.on('open', heartbeat);
client.on('ping', heartbeat);
client.on('close', function clear() {
clearTimeout(this.pingTimeout);
});
如何通过代理进行连接?
使用自定义的http.Agent实现,例如https-proxy-agent或socks-proxy-agent。认情况下,服务器禁用该扩展,而客户端启用。
启用压缩的示例代码如下:
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({
port: 8080,
perMessageDeflate: {
zlibDeflateOptions: {
chunkSize: 1024,
memLevel: 7,
level: 3
},
zlibInflateOptions: {
chunkSize: 10 * 1024
},
clientNoContextTakeover: true,
serverNoContextTakeover: true,
serverMaxWindowBits: 10,
concurrencyLimit: 10,
threshold: 1024
}
});
如果不需要启用压缩,可以在客户端上禁用该扩展:
import WebSocket from 'ws';
const ws = new WebSocket('ws://www.host.com/path', {
perMessageDeflate: false
});
使用示例
发送和接收文本数据
import WebSocket from 'ws';
const ws = new WebSocket('ws://www.host.com/path');
ws.on('error', console.error);
ws.on('open', function open() {
ws.send('something');
});
ws.on('message', function message(data) {
console.log('received: %s', data);
});
发送二进制数据
import WebSocket from 'ws';
const ws = new WebSocket('ws://www.host.com/path');
ws.on('error', console.error);
ws.on('open', function open() {
const array = new Float32Array(5);
for (var i = 0; i < array.length; ++i) {
array[i] = i / 2;
}
ws.send(array);
});
简单服务器
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data) {
console.log('received: %s', data);
});
ws.send('something');
});
使用外部 HTTP/S 服务器
import { createServer } from 'https';
import { readFileSync } from 'fs';
import { WebSocketServer } from 'ws';
const server = createServer({
cert: readFileSync('/path/to/cert.pem'),
key: readFileSync('/path/to/key.pem')
});
const wss = new WebSocketServer({ server });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data) {
console.log('received: %s', data);
});
ws.send('something');
});
server.listen(8080);
多个 WebSocket 服务器共享一个 HTTP/S 服务器
import { createServer } from 'http';
import { parse } from 'url';
import { WebSocketServer } from 'ws';
const server = createServer();
const wss1 = new WebSocketServer({ noServer: true });
const wss2 = new WebSocketServer({ noServer: true });
wss1.on('connection', function connection(ws) {
ws.on('error', console.error);
// ...
});
wss2.on('connection', function connection(ws) {
ws.on('error', console.error);
// ...
});
server.on('upgrade', function upgrade(request, socket, head) {
const { pathname } = parse(request.url);
if (pathname === '/foo') {
wss1.handleUpgrade(request, socket, head, function done(ws) {
wss1.emit('connection', ws, request);
});
} else if (pathname === '/bar') {
wss2.handleUpgrade(request, socket, head, function done(ws) {
wss2.emit('connection', ws, request);
});
} else {
socket.destroy();
}
});
server.listen(8080);
客户端认证
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
function onSocketError(err) {
console.error(err);
}
const server = createServer();
const wss = new WebSocketServer({ noServer: true });
wss.on('connection', function connection(ws, request, client) {
ws.on('error', console.error);
ws.on('message', function message(data) {
console.log(`Received message ${data} from user ${client}`);
});
});
server.on('upgrade', function upgrade(request, socket, head) {
socket.on('error', onSocketError);
// 这里的 authenticate 方法需自行实现
authenticate(request, function next(err, client) {
if (err || !client) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
socket.removeListener('error', onSocketError);
wss.handleUpgrade(request, socket, head, function done(ws) {
wss.emit('connection', ws, request, client);
});
});
});
server.listen(8080);
服务器广播
向所有已连接的客户端(包括自身)广播消息
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data, isBinary) {
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary });
}
});
});
});
向所有其他已连接的客户端(不包括自身)广播消息
import WebSocket, { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('error', console.error);
ws.on('message', function message(data, isBinary) {
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data, { binary: isBinary });
}
});
});
});
通信延迟测量
import WebSocket from 'ws';
const ws = new WebSocket('wss://websocket-echo.com/');
ws.on('error', console.error);
ws.on('open', function open() {
console.log('connected');
ws.send(Date.now());
});
ws.on('close', function close() {
console.log('disconnected');
});
ws.on('message', function message(data) {
console.log(`Round-trip time: ${Date.now() - data} ms`);
setTimeout(function timeout() {
ws.send(Date.now());
}, 500);
});
使用 Node.js streams API
import WebSocket, { createWebSocketStream } from 'ws';
const ws = new WebSocket('wss://websocket-echo.com/');
const duplex = createWebSocketStream(ws, { encoding: 'utf8' });
duplex.on('error', console.error);
duplex.pipe(process.stdout);
process.stdin.pipe(duplex);
其他示例
有关使用 ws 服务器与浏览器客户端通信的完整示例,请参见 examples 文件夹。更多示例可以参考测试用例。
常见问题
如何获取客户端的 IP 地址?
可以从原始套接字中获取远程 IP 地址:
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws, req) {
const ip = req.socket.remoteAddress;
ws.on('error', console.error);
});
如果服务器在像 NGINX 这样的代理后运行,可以使用 X-Forwarded-For 头获取客户端 IP 地址:
wss.on('connection', function connection(ws, req) {
const ip = req.headers['x-forwarded-for'].split(',')[0].trim();
ws.on('error', console.error);
});
如何检测并关闭中断连接?
使用 ping 消息来验证远程终点是否仍然响应:
import { WebSocketServer } from 'ws';
function heartbeat() {
this.isAlive = true;
}
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.isAlive = true;
ws.on('error', console.error);
ws.on('pong', heartbeat);
});
const interval = setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('close', function close() {
clearInterval(interval);
});
根据规范要求,在需要的情况下会自动发送 pong 消息作为响应 ping 消息。
客户端也可能失去连接而不知道它。可以在客户端上添加一个 ping 侦听器来防止这种情况发生:
import WebSocket from 'ws';
function heartbeat() {
clearTimeout(this.pingTimeout);
this.pingTimeout = setTimeout(() => {
this.terminate();
}, 30000 + 1000);
}
const client = new WebSocket('wss://websocket-echo.com/');
client.on('error', console.error);
client.on('open', heartbeat);
client.on('ping', heartbeat);
client.on('close', function clear() {
clearTimeout(this.pingTimeout);
});
如何通过代理进行连接?
可以使用自定义的 http.Agent 实现,例如 https-proxy-agent 或 socks-proxy-agent。