Node.js websocket/ws 详解

  • Post author:
  • Post category:其他


前言

众所周知,HTTP协议是一种无状态、无连接、单向的应用层协议,只能由客户端发起请求,服务端响应请求。

这就显示了一个明显的弊端:服务端无法主动向客户端发起消息,一旦客户端需要知道服务端的频繁状态变化,就要由客户端盲目地多次请求以获得最新地状态,这就是

长轮询

而长轮询有显著地缺点:效率低、非常耗费资源,就在这个时候WebSocket出现了。

WebSocket是一个长连接,客户端可以给服务端发送消息,服务端也可以给客户端发送消息,这便是

全双工通信

而node并没有提供Websocket的API,我们需要对Node.js提供的HTTPServer做额外的开发,好在npm上已经有许多的实现,其中使用最为广泛的就是本文主角——ws模块

WebSocket 串讲

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协议。

初识ws模块

先创建一个服务端程序

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连接就完成了!

WebSocketServer Class

继承自EventEmitter,存在于客户端的一个WebsocketServer

constructor (options, callback)

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 要监听的端口

perMessageDeflate {Boolean|Object} 详解

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.
  }
});

handleProtocols(protocol, req)

对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

获取,

针对自己定义的不同的协议作不同的处理

verifyClient()

如果没有设置这个方法,则默认会接收所有连接Websocket

的请求

有两种形参形式:

verifyClient(info), verifyClient(info, callback)


info有如下属性:

  • origin 字符串 即websocket的origin
  • secure Boolean 仅当req.connection.authorized 或req.connection.encrypted 不为null时为true.
  • req 即这个客户端请求连接的GET请求.

对于单形参的形式,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 host path详解


注意

: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)

connection事件

var wss = new ws.Server({port: 3000});
wss.on("connection", (socket, request)=>{});

当握手完成后会发出,socket参数为WebSocket类型,request为http.IncomingMessage类型

一般在这个事件中通过socket.on注册socket的事件

error事件

var wss = new ws.Server({port: 3000});
wss.on("connection", (error)=>{});

当依赖的httpServer出现错误时发出,error为Error类型

headers事件

var wss = new ws.Server({port: 3000});
wss.on("connection", (headers, request)=>{});

握手事件中,服务器即将响应请求时会发出这个事件,可以在方法中对headers进行修改

headers为数组类型,request为http.IncomingMessage类型

listening事件

var wss = new ws.Server({port: 3000});
wss.on("connection", ()=>{});

当绑定依赖的httoServer时发出

其它属性、方法

server.clients

如上文constructor处所提,仅当clientTracking为true时这个属性有实例,为set类型,储存着所有websocket连接

server.address()

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.

server.close([callback])

关闭这个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();
  }

WebSocket Class

继承自EventEmitter


这个类的实例有两种,一种是客户端的实例,一种是服务端的实例

constructor

new WebSocket(address[, protocols][, options])

  • address {String|url.Url|url.URL}

    *必填

    -,要连接的url
  • protocols {String|Array}

    *可选

    -,要使用的协议,即Sec-WebSocket-Protocol首部
  • options {Object}

    可选

    • handshakeTimeout {Number} Timeout in milliseconds for the handshake request.
    • perMessageDeflate {Boolean|Object} 与WebSocketServer的类似,

      但默认值为true
    • protocolVersion {Number} 即Sec-WebSocket-Version首部.
    • origin {String} 即Origin或Sec-WebSocket-Origin首部,具体是哪一个由protocolVersion决定.
    • maxPayload {Number} message最大负载,单位:字节


一般只有客户端才通过这个方法创建实例,服务端的实例是由WebsocketServer自动创建的

监听事件

一般语法: websocket.on(“event”, Function())


无论是客户端还是服务端的实例都需要监听事件

message 事件

websocket.on("message", (data)=>{});

当收到消息时发出,data 类型为 String|Buffer|ArrayBuffer|Buffer[]

close 事件

websocket.on("close", (code, reason)=>{});

当连接断开时发出

error 事件

websocket.on("error", (error)=>{});

open 事件

websocket.on("open", ()=>{});

连接建立成功时发出

ping 事件

websocket.on("ping", (data)=>{});

收到ping消息时发出,data为Buffer类型

pong 事件

websocket.on("pong", (data)=>{});

收到pong消息时发出,data为Buffer类型


注:ping,pong事件通常用来检测连接是否仍联通,由客户端(服务端)发出一个ping事件,服务端(客户端)收到后回复一个pong事件,客户端(服务端)收到后就知道连接仍然联通

unexpected-response 事件

websocket.on("unexpected-response", (request, response)=>{});

request {http.ClientRequest} response {http.IncomingMessage}

当服务端返回的报文不是期待的结果,例如401,则会发出这个事件,如果这个事件没有被监听,则会抛出一个错误

upgrade事件

websocket.on("upgrade", (response)=>{});

response {http.IncomingMessage}

握手过程中,当收到服务端回复时发出该事件,你可以在response中查看cookie等header

其它属性、方法

websocket.readyState

客户端、服务端实例都可调用

-{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.

websocket.protocol

{String} 类型

客户端、服务端实例都可调用

返回服务器选择使用的协议

websocket.send(data[, options][, callback])

客户端、服务端实例都可调用

  • data 要发送的信息
  • options {Object}

    • compress {Boolean} 当permessage-deflate启用时默认为true
    • binary {Boolean} 是否采用二进制传输,默认为false,自动检测
    • mask {Boolean} 当时客户端的实例时为true
    • fin {Boolean} 当前data是否是message的最后一个fragment,默认为true
  • callback 信息发送完成后的回调函数

websocket.url

{String}类型

仅客户端实例可调用

返回服务器的url,如果是服务器的Client则没有这个属性

websocket.bufferedAmount

{Number} 类型

客户端、服务端实例都可调用

返回已经被send()加入发送队列,但仍未发送到网络的message的数量

websocket.ping([data[, mask]][, callback])

客户端、服务端实例都可调用

  • data {Any} 携带的信息
  • mask {Boolean} Specifies whether data should be masked or not. Defaults to true when websocket is not a server client.
  • callback {Function} ping消息发送后的回调事件

    发送一个ping消息

websocket.pong([data[, mask]][, callback])

客户端、服务端实例都可调用

  • data {Any} 携带的信息
  • mask {Boolean} Specifies whether data should be masked or not. Defaults to true when websocket is not a server client.
  • callback {Function} pong消息发送后的回调事件

    发送一个pong消息

websocket.close([code[, reason]])

开始发出断开连接的请求

原文:Initiate a closing handshake.

websocket.terminate()

客户端、服务端实例都可调用

强制关闭连接

websocket.binaryType

-{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.

websocket.addEventListener(type, listener)

  • type {String} A string representing the event type to listen for.
  • listener {Function} The listener to add.

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);


小结:

  • 服务端的创建有port,server,noserver等多种方式
  • 服务端监听的connection事件中,方法中的参数ws就是前文提到的**服务端的**Websocket实例

创建一个简单的客户端程序

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);
});


小结:

  • 通过new Websocket()形式创建的就是前文提到的客户端的Websocket实例
  • ws.send() 方法可以发送字符串、二进制数据等多种形式

如何获得客户端的ip

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];
});

通过ping, pong事件检测连接是否仍可用

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事件会自动返回,不需要监听

资料参考

廖雪峰大神的node教程

https://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/001434501549492cdf5d4013db14fa9ad8ca172f0664345000

ws的github仓库

https://github.com/websockets/ws



版权声明:本文为LiMubai_CN原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。