数据实时: 即数据库中的数据得到更新, 页面立刻就想得到更新并展示最新的数据状态. 通常使用在大数据可视化分析, 运营数据监控等场景.
# 数据实时方案
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)其发起的请求, 不再是 http://... 而是换成了 ws://... 开头的地址, 且只支持 GET
(2)请求头 Upgrade: websocket 和 Connection: Upgrade 表示该连接将要被升级为 WebSocket 连接;
(3)Sec-WebSocket-Key 是用于标识这个连接, 并非用于加密数据;
(4)Sec-WebSocket-Version 指定了 WebSocket 的协议版本.
如果服务器接收这个请求, 就会响应如下:
- HTTP/1.1 101 Switching Protocols
- Upgrade: websocket
- Connection: Upgrade
- Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU= // 服务端随机串
服务端随机串是根据客户端随机串计算出来的, 计算规则为:(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 协议, 双发可以互相发送数据.
http,WebSocket 及 TCP 的关系
# 安全的 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, 其值即含义如下
值 | 状态含义 |
---|---|
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')
- }
- // 连接失败回调
- 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.send('hello websocket')
- # 接受服务端消息回调 onmessage
当服务器向客户端发来消息时, WebSocket 对象会触发 message 事件. 这个 message 事件与其他传递消息的协议类似, 也是把返回的数据保存在 event.data 属性中
- socket.onmessage = function(event) {
- var data = event.data;
- // 处理数据
- };
- # 关闭回调 onclose
- socket.onlcose = function(event) {
- // ...
- }
- # 当前剩余未发送数据 bufferedAmount
- if(ws.bufferedAmount === 0){
- console.log("发送完毕");
- }else{
- console.log("还有", ws.bufferedAmount, "数据没有发送");
- }
- # 一个实例
1. 客户端实现
- let ws = new WebSocket("ws://localhost:8080");
- ws.onopen = function() {
- console.log("client: 打开连接");
- ws.send("client:hello, 服务端");
- };
- ws.onmessage = function(e) {
- console.log("client: 接收到服务端的消息" + e.data);
- setTimeout(() => {
- ws.close();
- }, 5000);
- };
- ws.onclose = function(params) {
- console.log("client: 关闭连接");
- };
2. 服务端实现, 使用 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);
来源: http://www.jianshu.com/p/d271bb8c0db6