前言

众所周知,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的实例,监听端口8080wss.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/websocketHTTP/1.1\r\nHost: xxx.xxx.xxx.xxx:xx\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nUpgrade: websocket\r\nOrigin: http://xxx.xxx.xxx.xxx:xx\r\nSec-WebSocket-Version: 13\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36\r\nAccept-Encoding: gzip, deflate, sdch\r\nAccept-Language: zh-CN,zh;q=0.8,en;q=0.6\r\nSec-WebSocket-Key: N+GWswsViw18TfSpryLcVw==\r\nSec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n

服务端如果也设置了启用压缩,则在响应中会有Sec-WebSocket-Extensions首部,这样就完成了协商,之后的通讯将启用压缩

  HTTP/1.1 101 \r\nServer: Apache-Coyote/1.1\r\nUpgrade: websocket\r\nConnection: upgrade\r\nSec-WebSocket-Accept: xwLDQrb5kzxpZDdeTcUd+7diXXU=\r\nSec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15\r\nDate: \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

Node.js websocket/ws 详解相关推荐

  1. 《Node.js开发实战详解》学习笔记

    <Node.js开发实战详解>学习笔记 --持续更新中 一.NodeJS设计模式 1 . 单例模式 顾名思义,单例就是保证一个类只有一个实例,实现的方法是,先判断实例是否存在,如果存在则直 ...

  2. Node.js中Async详解

    Node.js中Async详解:流程控制 安装 npm install async --save 地址 https://github.com/caolan/async Async的内容主要分为三部分 ...

  3. Node.js和npm详解(直接上手演示)

    我们先直接操作一下,让你们看效果,刚开始文字说的再多也比不上看一遍效果! 我们用java语言来比较一下 首先java语言是如何在桌面进行编译的呢? 第一步:编写java文件 第二步:在cmd命令框中输 ...

  4. Node.js 从门详解 (二)

    目录 1. 模块化的基本概念 1.1 什么是模块化 1.2 模块化规范 2. Node.js 中模块化 2.1 Node.js 中模块的分类 2.2 加载模块 2.3 Node.js中的模块作用域 2 ...

  5. Node.js HTTP 使用详解

    对于初学者有没有发觉在查看Node.js官方API的时候非常简单,只有几个洋文描述两下子,没了,我第一次一口气看完所以API后,对于第一个示例都有些懵,特别是参数里的request和response, ...

  6. 【Node.js】关于Node.js接口的详解和案例--restful风格接口。案例:添加商品接口,添加员工接口,删除员工接口

    1.首先我们需要知道,接口是什么? 接口是后端为前端提供的数据--动态资源:Node.js通过每一个路由就可以实现接口 2.RESTful接口:是一种接口的架构风格 1.请求的URL(资源) 在资源前 ...

  7. Node.js 应用开发详解04 3 大主流系统框架:由浅入深分析 Express、Koa 和 Egg.js

    上一讲我们没有应用任何框架实现了一个简单后台服务,以及一个简单版本的 MSVC 框架.本讲将介绍一些目前主流框架的设计思想,同时介绍其核心代码部分的实现,为后续使用框架优化我们上一讲实现的 MSVC ...

  8. 阿里云ECS服务器部署Node.js项目全过程详解

    本文详细介绍如何部署NodeJS项目到阿里云ECS上,以及本人在部署过程中所遇到的问题.坑点和解决办法,可以说是全网最全最详细的教程了.同时讲解了如何申请阿里云免费SSL证书,以及一台ECS服务器配置 ...

  9. node.js中ws模块创建服务端和客户端,网页WebSocket客户端

    首先下载websocket模块,命令行输入 npm install ws 1.node.js中ws模块创建服务端 // 加载node上websocket模块 ws; var ws = require( ...

最新文章

  1. 英文首字母排序mysql_利用MySQL数据库来处理中英文取首字母排序
  2. UIScrollView控件常用属性
  3. 当AI渗透到财务管理 未来人机协作机器人有望“独当一面”
  4. android 圆角按钮渐变,Android实现圆形渐变加载进度条
  5. 携程在港挂牌:两次疫情两次上市 穿越周期初心不灭
  6. 看完此文章若你还不能完美的入门Python,我将永远退出IT界
  7. 孤读Paper——《Deep Snake for Real-Time Instance Segmentation》
  8. Spring连接数据库的几种常用的方式
  9. 机器学习(二)——xgboost(实战篇)Pima印第安人数据集上的机器学习-分类算法(根据诊断措施预测糖尿病的发病)
  10. js配合css3开发流畅的web拾色器功能
  11. 花开蝶自来——回到梦开始的地方
  12. C语言字母an,易错题之大一C语言英语
  13. python plc fx5u_三菱PLC FX5U系列模块型号对照一览表
  14. Python爬虫教程——入门一之爬虫基础了解
  15. Egret微信游戏接入
  16. matlab 固态硬盘,电脑内存和固态硬盘有什么区别 电脑内存和固态硬盘对比【详解】...
  17. svn没有对号等符号的问题
  18. node.js 微信小程序 部署服务器_微信小程序开发入门(一),Nodejs搭建本地服务器...
  19. html图片怎么装修到店铺,PS店铺装修和HTML基本操作
  20. php通用图像处理库imagine使用

热门文章

  1. 【记录3】小程序账号冻结之十分钟内解决(忘记原始ID或者公众号名称的解决方法)
  2. HTPC打造本地Arch Server
  3. 自顶向下,逐步求精的案例
  4. limt和(pagehelper或者page分页插件)的区别
  5. 嵌入式开发不用写文档?
  6. 微信公众平台开发需要懂哪些技术
  7. 库函数strcpy、memcpy和memset
  8. 7-5 输出大写英文字母
  9. CTF pwn/re手在学习过程中的零碎操作积累
  10. 爬取链家二手房首页和详情页信息