众所周知,HTTP协议是一种无状态、无连接、单向的应用层协议,只能由客户端发起请求,服务端响应请求。
这就显示了一个明显的弊端:服务端无法主动向客户端发起消息,一旦客户端需要知道服务端的频繁状态变化,就要由客户端盲目地多次请求以获得最新地状态,这就是长轮询
而长轮询有显著地缺点:效率低、非常耗费资源,就在这个时候WebSocket出现了。
WebSocket是一个长连接,客户端可以给服务端发送消息,服务端也可以给客户端发送消息,这便是全双工通信
而node并没有提供Websocket的API,我们需要对Node.js提供的HTTPServer做额外的开发,好在npm上已经有许多的实现,其中使用最为广泛的就是本文主角——ws模块
WebSocket连接也是由一个标准的HTTP请求发起,格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13
支持Websocket的服务器在收到请求后会返回一个响应,格式如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议,之后的数据传输将不再通过http协议,而是使用全双工通信的Websocket协议。
先创建一个服务端程序
const WebSocket = require('ws');//引入模块
const wss = new WebSocket.Server({ port: 8080 });//创建一个WebSocketServer的实例,监听端口8080
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('received: %s', message);
ws.send('Hi Client');
});//当收到消息时,在控制台打印出来,并回复一条信息
});
再创建一个客户端程序
const WebSocket = require('ws');
const ws = new WebSocket('ws://localhost:8080');
ws.on('open', function open() {
ws.send('Hi Server');
});//在连接创建完成后发送一条信息
ws.on('message', function incoming(data) {
console.log(data);
});//当收到消息时,在控制台打印出来
在node环境中先运行服务端程序,再运行客户端程序,我们就可以在控制台分别看到两个端的打印信息了
至此,通过ws模块创建的一个最简单的Websocket连接就完成了!
继承自EventEmitter,存在于客户端的一个WebsocketServer
options有以下属性、方法:
名称 | 默认值 | 描述 |
---|---|---|
maxPayload | 100 *1024 *1024 | 每条message的最大载荷,单位:字节 |
perMessageDeflate | false | 见详解 |
handleProtocols(protocol, req) | null | 见详解 |
clientTracking | true | 会在内部创建一个set,存下所有的连接,即.clients属性,源码:if (options.clientTracking) this.clients = new Set(); |
verifyClient | null | verifyClient(info, (verified, code, message, headers)) |
noServer | false | 是否启用无Server模式 |
backlog | null | use default (511 as implemented in net.js) |
server | null | 在一个已有的HTTP/S Server的基础上创建 |
host | null | 服务器host |
path | null | 只接收这个path的Websocket访问,不指定则接收所有 |
port | null | 要监听的端口 |
Websocket 协议的message传输有直接传输和先压缩再传输两种形式,而压缩算法是开放的。客户端和服务端会协商是否启用压缩
客户端如果设置了启用压缩,则在发起WebSocket通信时会添加Sec-WebSocket-Extensions: permessage-deflate首部
GET /examples/websocket
HTTP/1.1\r\n
Host: xxx.xxx.xxx.xxx:xx\r\n
Connection: Upgrade\r\n
Pragma: no-cache\r\n
Cache-Control: no-cache\r\n
Upgrade: websocket\r\n
Origin: http://xxx.xxx.xxx.xxx:xx\r\n
Sec-WebSocket-Version: 13\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36\r\n
Accept-Encoding: gzip, deflate, sdch\r\n
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6\r\n
Sec-WebSocket-Key: N+GWswsViw18TfSpryLcVw==\r\n
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n
\r\n
服务端如果也设置了启用压缩,则在响应中会有Sec-WebSocket-Extensions首部,这样就完成了协商,之后的通讯将启用压缩
HTTP/1.1 101 \r\n
Server: Apache-Coyote/1.1\r\n
Upgrade: websocket\r\n
Connection: upgrade\r\n
Sec-WebSocket-Accept: xwLDQrb5kzxpZDdeTcUd+7diXXU=\r\n
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15\r\n
Date: \r\n
ws模块中perMessageDeflate这个属性默认为false,即不开启压缩,true则为开启压缩,也可以传入一个Object自定义一些配置,此处略,官方例子如下:
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port: 8080,
perMessageDeflate: {
zlibDeflateOptions: { // See zlib defaults.
chunkSize: 1024,
memLevel: 7,
level: 3,
},
zlibInflateOptions: {
chunkSize: 10 -1024
},
// Other options settable:
clientNoContextTakeover: true, // Defaults to negotiated value.
serverNoContextTakeover: true, // Defaults to negotiated value.
clientMaxWindowBits: 10, // Defaults to negotiated value.
serverMaxWindowBits: 10, // Defaults to negotiated value.
// Below options specified as default values.
concurrencyLimit: 10, // Limits zlib concurrency for perf.
threshold: 1024, // Size (in bytes) below which messages
// should not be compressed.
}
});
对sec-websocket-protocol的协议进行一个选择,默认会选择第一个协议,这些协议是用户自定义的字符串,比如可以用chat代表即时聊天,就可以写成sec-websocket-protocol:chat,…
部分源码如下:
var protocol = req.headers['sec-websocket-protocol'];
...
if (this.options.handleProtocols) {
protocol = this.options.handleProtocols(protocol, req);
} else {
protocol = protocol[0];
}
故该方法最后应返回一个字符串,为选中的协议,之后可以通过ws.protocal获取,针对自己定义的不同的协议作不同的处理
如果没有设置这个方法,则默认会接收所有连接Websocket
的请求
有两种形参形式: verifyClient(info), verifyClient(info, callback)
info有如下属性:
对于单形参的形式,return true代表通过,return false代表不通过,将自动返回一个401响应
对于双形参的形式,调用callback(true, null, null, null)代表通过,调用calback(false, 401, “unauthorized”,null)代表不通过
一般来说,双形参的形式仅当需要自定义错误响应的信息时使用
源码如下:
if (this.options.verifyClient) {
const info = {
origin: req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
secure: !!(req.connection.authorized || req.connection.encrypted),
req
};
if (this.options.verifyClient.length === 2) {
this.options.verifyClient(info, (verified, code, message, headers) => {
if (!verified) {
return abortHandshake(socket, code || 401, message, headers);
}
this.completeUpgrade(extensions, req, socket, head, cb);
});
return;
}
if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
}
注意:port、server、noserver = true是互斥的,这三者必须设置且只能设置一个
当设置了port时,server和noserver将不起效果,会创建一个httpserver去监听这个端口
当没有设置port且设置了server时,将使用这个以有的server
部分源码如下:
if (options.port != null) {
this._server = http.createServer((req, res) => {
const body = http.STATUS_CODES[426];
res.writeHead(426, {
'Content-Length': body.length,
'Content-Type': 'text/plain'
});
res.end(body);
});
this._server.listen(options.port, options.host, options.backlog, callback);
} else if (options.server) {
this._server = options.server;
}
if (this._server) {
this._removeListeners = addListeners(this._server, {
listening: this.emit.bind(this, 'listening'),
error: this.emit.bind(this, 'error'),
upgrade: (req, socket, head) => {
this.handleUpgrade(req, socket, head, (ws) => {
this.emit('connection', ws, req);
});
}
});
}
由上面的源码也可以看出,host属性仅在指定port新建http server时有效
path属性未指定时将接收所有的url,指定后将仅接收指定的url,部分源码如下
/**
*See if a given request should be handled by this server instance.
*
*@param {http.IncomingMessage} req Request object to inspect
*@return {Boolean} `true` if the request is valid, else `false`
*@public
*/
shouldHandle (req) {
if (this.options.path && url.parse(req.url).pathname !== this.options.path) {
return false;
}
return true;
}
一般语法:.on(“event”, funcion)
var wss = new ws.Server({port: 3000});
wss.on("connection", (socket, request)=>{});
当握手完成后会发出,socket参数为WebSocket类型,request为http.IncomingMessage类型
一般在这个事件中通过socket.on注册socket的事件
var wss = new ws.Server({port: 3000});
wss.on("connection", (error)=>{});
当依赖的httpServer出现错误时发出,error为Error类型
var wss = new ws.Server({port: 3000});
wss.on("connection", (headers, request)=>{});
握手事件中,服务器即将响应请求时会发出这个事件,可以在方法中对headers进行修改
headers为数组类型,request为http.IncomingMessage类型
var wss = new ws.Server({port: 3000});
wss.on("connection", ()=>{});
当绑定依赖的httoServer时发出
如上文constructor处所提,仅当clientTracking为true时这个属性有实例,为set类型,储存着所有websocket连接
Returns an object with port, family, and address properties specifying the bound address, the address family name, and port of the server as reported by the operating system if listening on an IP socket. If the server is listening on a pipe or UNIX domain socket, the name is returned as a string.
关闭这个WebsocketServer所有的websocket连接,并且如果所依赖的httpServer是它创建的的话(即指定了port),这个httpServer
会被关闭,源码如下:
/**
*Close the server.
*
*@param {Function} cb Callback
*@public
*/
close (cb) {
//
// Terminate all associated clients.
//
if (this.clients) {
for (const client of this.clients) client.terminate();
}
const server = this._server;
if (server) {
this._removeListeners();
this._removeListeners = this._server = null;
//
// Close the http server if it was internally created.
//
if (this.options.port != null) return server.close(cb);
}
if (cb) cb();
}
继承自EventEmitter
这个类的实例有两种,一种是客户端的实例,一种是服务端的实例
new WebSocket(address[, protocols][, options])
一般只有客户端才通过这个方法创建实例,服务端的实例是由WebsocketServer自动创建的
一般语法: websocket.on(“event”, Function())
无论是客户端还是服务端的实例都需要监听事件
websocket.on("message", (data)=>{});
当收到消息时发出,data 类型为 String|Buffer|ArrayBuffer|Buffer[]
websocket.on("close", (code, reason)=>{});
当连接断开时发出
websocket.on("error", (error)=>{});
websocket.on("open", ()=>{});
连接建立成功时发出
websocket.on("ping", (data)=>{});
收到ping消息时发出,data为Buffer类型
websocket.on("pong", (data)=>{});
收到pong消息时发出,data为Buffer类型
注:ping,pong事件通常用来检测连接是否仍联通,由客户端(服务端)发出一个ping事件,服务端(客户端)收到后回复一个pong事件,客户端(服务端)收到后就知道连接仍然联通
websocket.on("unexpected-response", (request, response)=>{});
request {http.ClientRequest} response {http.IncomingMessage}
当服务端返回的报文不是期待的结果,例如401,则会发出这个事件,如果这个事件没有被监听,则会抛出一个错误
websocket.on("upgrade", (response)=>{});
response {http.IncomingMessage}
握手过程中,当收到服务端回复时发出该事件,你可以在response中查看cookie等header
客户端、服务端实例都可调用
-{Number}
返回当前连接的状态码
Constant | Value | Description |
---|---|---|
CONNECTING | 0 | The connection is not yet open. |
OPEN | 1 | The connection is open and ready to communicate. |
CLOSING | 2 | The connection is in the process of closing. |
CLOSED | 3 | The connection is closed. |
{String} 类型
客户端、服务端实例都可调用
返回服务器选择使用的协议
客户端、服务端实例都可调用
{String}类型
仅客户端实例可调用
返回服务器的url,如果是服务器的Client则没有这个属性
{Number} 类型
客户端、服务端实例都可调用
返回已经被send()加入发送队列,但仍未发送到网络的message的数量
客户端、服务端实例都可调用
客户端、服务端实例都可调用
开始发出断开连接的请求
原文:Initiate a closing handshake.
客户端、服务端实例都可调用
强制关闭连接
-{String}
A string indicating the type of binary data being transmitted by the connection. This should be one of “nodebuffer”, “arraybuffer” or “fragments”. Defaults to “nodebuffer”. Type “fragments” will emit the array of fragments as received from the sender, without copyfull concatenation, which is useful for the performance of binary protocols transferring large messages with multiple fragments.
Register an event listener emulating the EventTarget interface.
1.直接指定port创建
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
ws.send('something');
});
2.从一个以有的httpServer的实例创建
const fs = require('fs');
const https = require('https');
const WebSocket = require('ws');
const server = new https.createServer({
cert: fs.readFileSync('/path/to/cert.pem'),
key: fs.readFileSync('/path/to/key.pem')
});
const wss = new WebSocket.Server({ server });
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
ws.send('something');
});
server.listen(8080);
3.与koa框架的结合使用
// koa app的listen()方法返回http.Server:
let server = app.listen(3000);
// 创建WebSocketServer:
let wss = new WebSocketServer({
server: server
});
4.多个WebsocketServer依赖相同的httpServer
const http = require('http');
const WebSocket = require('ws');
const server = http.createServer();
const wss1 = new WebSocket.Server({ noServer: true });
const wss2 = new WebSocket.Server({ noServer: true });
wss1.on('connection', function connection(ws) {
// ...
});
wss2.on('connection', function connection(ws) {
// ...
});
server.on('upgrade', function upgrade(request, socket, head) {
const pathname = url.parse(request.url).pathname;
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);
小结:
ws模块不仅包含服务端模块,还包含客户端模块
1.直接创建一个Websocket连接
const WebSocket = require('ws');
const ws = new WebSocket('ws://www.host.com/path');
ws.on('open', function open() {
ws.send('something');
});
ws.on('message', function incoming(data) {
console.log(data);
});
2.发送二进制数据
const WebSocket = require('ws');
const ws = new WebSocket('ws://www.host.com/path');
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);
});
小结:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
//直接连接的情况
wss.on('connection', function connection(ws, req) {
const ip = req.connection.remoteAddress;
});
//存在代理的情况
wss.on('connection', function connection(ws, req) {
const ip = req.headers['x-forwarded-for'].split(/\s*,\s*/)[0];
});
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
function noop() {}
function heartbeat() {
this.isAlive = true;
}
wss.on('connection', function connection(ws) {
ws.isAlive = true;
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(noop);
});
}, 30000);
注:Websocket客户端在收到ping事件会自动返回,不需要监听
ws的github仓库 https://github.com/websockets/ws