数据实时:即数据库中的数据得到更新,页面立刻就想得到更新并展示最新的数据状态。通常使用在大数据可视化分析,运营数据监控等场景。
# 数据实时方案
Web想要更新页面,通常都是客户端发起Http异步请求,主动向服务端索取数据
,方案有:
(1)Ajax轮询,又称 Ajax短连接:即启动一个定时器隔一定时间(如1s
)发送一个请求,服务端收到请求无论如何都直接返回当前数据库状态数据。缺点是实时性不够,产生很多不必要的请求。可用于刷新频率不是很高的场景。
(2)Ajax长连接:客户端发起Http请求,并设置一个长超时时间,服务端收到请求后,检查数据库如果没有更新则阻塞请求,直到有更新或超时为止。客户端每次收到响应后,立即再发一个请求,Comet就是这种方式。缺点是服务器的处理线程长时间挂起,极大浪费资源,且网络链路可能被网关关闭,需要如ping
数据来维持链接。
以上两种机制都治标不治本,是否能有一种机制,由服务端自己检测数据状态,有更新主动告知客户端
。好在,HTML5推出了 WebSocket
协议,解决了这个问题
# WebSocket是什么
WebSocket(以下简称 ws
)是HTML5提供的一种在单个 TCP 连接上进行全双工通讯的网络技术,目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,让双方都可以主动给对方发消息。
虽说ws是H5下新的协议,但其实也不是全新的。它属于应用层协议,复用了HTTP的握手通道。ws协议与HTTP协议都是基于TCP的,因此都是可靠的协议。ws客户端和服务器只需要做一个握手的动作,两者之间就形成了一条快速通道。在建立握手连接时,数据是通过http进行传输的,但建立之后,真正的数据传输阶段就不需要http参与了
# WebSocket的优点
ws协议相比于HTTP协议,它具有以下优势:
- 全双工通信能力:支持客户端和服务端主动给对方发送消息
- 高实时性:Ajax轮询只是不断的请求,而服务端检测到更新主动推送才是真正意义上的实时。
- 高效节能:HTTP协议请求一般都会有较长的头部,而需要实时更新的数据可能就一点点,这就造成了带宽很多不必要的消耗。而ws协议控制数据包的头部比较小,一般只有十个字节左右。
- 支持扩展: ws协议定义了扩展,用户可以扩展协议,或实现自定义子协议。
-
没有跨域限制:不是xhr请求,没有同源策略的限制
# WebSocket的第一次握手
虽说ws支持双向通讯能力,但请求必须是由客户发起。由于发起时是一个http握手,因此格式如下
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw== // 客户端随机串
Sec-WebSocket-Version: 13
值得注意的是:
(1)其只能发GET请求,且不再是 http://...
而是换成了 ws://...
开头的地址
(2)请求头Upgrade: websocket
和Connection: Upgrade
表示该连接将要被升级为WebSocket连接;
(3)Sec-WebSocket-Key
标识连接的Key
串(下方有更多解释)
(4)Sec-WebSocket-Version
指定了WebSocket的协议版本。
如果服务器识别key
正确,会接收这个请求,就会响应如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU= // 服务端随机串
服务端Accept
串是根据客户端随机串计算出来的,计算规则为:(1)与固定串拼接,(2)执行sha1算法,(3)转为base64字符串。这对Key/Accept
需ws客户端和服务端提前约定,目的是为了避免非法ws请求等一些常见的意外情况。并不能确保数据安全性,毕竟算法公开且简单。公式如下:
toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
响应码101
表示将切换协议,更改后的协议就是Upgrade: websocket
指定的WebSocket协议。当连接建立成功后,双方就可以自由通讯消息了。消息一般分两种:(1)文本,(2)二进制数据。开发中会使用JSON文本数据比较直观。
# ws为什么能实现全双工通讯
前文多次遇到 全双工通信
字眼,意思就是客户端和服务端能随时给对方发送消息。好像理解了但又朦朦胧胧。这里解释一下:
- 单工: 数据传输只支持在一个方向上的传输,同时只能有一方发送或接收消息。
- 半双工:数据允许在两个方向上传输,但任一时刻,只允许有一方在传输,是一种切换方向的单工通信
- 全双工:任何时刻都允许两个方向进行数据传输,不受对方限制。
HTTP 和WebSocket 都是基于TCP传输协议的,其实TCP本身是支持全双工通讯
的,而HTTP协议的请求,因为其应答机制限制了全双工通信。当第一次握手完成后,协议由HTTP切换成了WebSocket,ws连接建立,其实只是简单规定了下:后续通讯不再使用http协议,双发可以互相发送数据了。
# 安全的WebSocket通讯
与 HTTPS 类似,安全的ws连接使用的是wss://...
开头的请求,它首先会通过https创建安全的连接,升级协议后,底层通信依然走的 SSL/TLS 协议
# 连接保持 - 心跳
WebSocket为了保持客户端与服务端的实时双向通讯,需保持TCP通道链接没有断开。然而长时间没有数据往来的连接,会浪费一些连接资源,网络链路同样可能被网关关闭,毕竟网关不是我们能控制的。因此链路链接就需要提示说明还在使用周期内,这个提示就是心跳来实现的。
- 发送方 --> 接收方: ping
- 接收方 --> 发送方: pong
举例,ws服务端向客户端发送ping
,代码如下
ws.ping('', false, true)
$ WebSocket API
理解了WebSocket的概念及相应的特征后,来看看怎么上手编写
# 创建WebSocket实例
ws提供了WebSocket(url[, protocals])
构造函数来返回实例化ws对象。参数一表示要连接的URL,参数二表示可接受的子协议。
let socket= new WebSocket('http://localhost:8080')
执行以上代码,浏览器就开始尝试创建连接,与 xhr 的readystatechange
类似的是,ws连接也有一个表示当前状态的属性readyState
,
# 连接状态-readyState 只读
用于返回当前WebSocket连接的状态,其值即含义如下
值 | 状态含义 |
---|---|
0 | WebSocket.CONNECTING |
1 | WebSocket.OPEN |
2 | WebSocket.CLOSING |
3 | WebSocket.CLOSED |
一个ws连接各个状态的执行时刻如下
let socket = new WebSocket('http://localhost:8080')
// 正在创建连接
console.log('[readyState]:', socket.readyState) // 0
// 连接建立成功后触发onopen回调
socket.onopen = function() {
console.log('connected,[readyState]:', socket.readyState) // 1
// 发送消息
socket.send('from client: Hello')
}
// 从服务端收到信息触发onmessage回调
socket.onmessage = function() {
console.log('received,[readyState]:', socket.readyState) // 1
// 发送消息
socket.send('from client: Hello')
}
// 连接失败触发onerror回调
socket.onerror = function() {
console.log('connect error, [readyState]:', socket.readyState) // 3
}
// 调用关闭连接,状态立刻变成2(正在关闭)。关闭成功触发onclose变成3
socket.close()
// 连接关闭触发onclose回调,有回调参数
socket.onclose = function(event) {
const { code, reason, wasClean } = event
console.log('connect closed, [readyState]:', socket.readyState) // 3
console.log(code, reason, wasClean) // wasClean表示连接是否已经关闭。boolean
}
当readyState
的值从 0 变成 1 后,客户端和服务端就可以通讯了。
# 方法
- 发送数据 send()
发送数据一定是伴随在连接已经打开的情况下
socket.addEventListener('open', function(event) {
sokcet.send('hello server')
})
- 关闭连接 close()
关闭当前连接。可以传 0/1/2
个参数。code
解释关闭原因的状态码。reason
解释关闭原因的描述(限制123个字节)。
sokcet.close([code[, reason]])
如果未传参数,会默认code
为1005
,意为:无参数,未提供关闭原因状态码。查看 状态码详情。如果提供一个无效的状态码,会抛出异常INVALID_ACCESS_ERR
。
# 事件
- 连接已建立 onopen
socket.addEventListener('open', function(event) {
// TODO: send message
});
- 接收服务端消息回调 onmessage
当服务器向客户端发来消息时,WebSocket对象会触发message
事件。这个message
事件与其他传递消息的协议类似,也是把返回的数据保存在event.data
属性中
socket.addEventListener('message', function(event) {
var data = event.data;
// TODO:
});
- 关闭连接的回调 onclose
socket.addEventListener('close', function(event) {
const { code, reason, wasClean } = event
// TODO:
});
- 连接失败的回调 onerror
socket.addEventListener('error', function(event) {
console.error("WebSocket error observed:", event)
});
# 属性
- 当前剩余未发送数据 bufferedAmount 只读
用于返回已经被send()
方法放入队列但还没有被发送到网络中的数据的字节数,只有发送完成它才会被重置为0。如果发送过程中连接被关闭不会重置,不断的调用send()
该值会不断增长。
if (ws.bufferedAmount === 0){
console.log("发送已完成");
} else {
console.log("还有", ws.bufferedAmount, "数据没有发送");
}
- 连接二进制类型 binaryType 只读
返回websocket连接所传输二进制数据的类型
const binaryType = socket.binaryType
- 已选择的扩展值 extensions 只读
返回服务器已选择的扩展值
const extensions = socket.extensions
- 子协议 protocol 只读
返回服务器端选中的子协议的名字;也就是在实例化WebSocket
对象时,在参数protocols
中指定的字符串
const protocol = socket.protocol
- 子协议 url 只读
返回值为当构造函数创建WebSocket
实例对象时URL的绝对路径。
const url = socket.url
$ 一个服务端实例
这里提供一个简单的例子,引入了ws
库实现。也可以使用socket.io
var app = require('express')();
var server = require('http').Server(app);
var WebSocket = require('ws');
var wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
console.log('server: receive connection.');
ws.on('message', function incoming(message) {
console.log('server: received: %s', message);
});
ws.send('world');
});
app.get('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});
app.listen(3000);