让前端飞Web前端之路深入挖掘CSS

H5数据实时方案「WebSocket」

2019-11-06  本文已影响0人  果汁凉茶丶

数据实时:即数据库中的数据得到更新,页面立刻就想得到更新并展示最新的数据状态。通常使用在大数据可视化分析,运营数据监控等场景。

# 数据实时方案

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协议,它具有以下优势:

# 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: websocketConnection: 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协议,双发可以互相发送数据了。

http、WebSocket及TCP的关系(图片摘自网络)

# 安全的WebSocket通讯

  与 HTTPS 类似,安全的ws连接使用的是wss://...开头的请求,它首先会通过https创建安全的连接,升级协议后,底层通信依然走的 SSL/TLS 协议

# 连接保持 - 心跳

  WebSocket为了保持客户端与服务端的实时双向通讯,需保持TCP通道链接没有断开。然而长时间没有数据往来的连接,会浪费一些连接资源,网络链路同样可能被网关关闭,毕竟网关不是我们能控制的。因此链路链接就需要提示说明还在使用周期内,这个提示就是心跳来实现的。

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

如果未传参数,会默认code1005,意为:无参数,未提供关闭原因状态码。查看 状态码详情。如果提供一个无效的状态码,会抛出异常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);

结束语

参考文献
WebSocket-菜鸟教程
WebSocket-MDN
WebSocket-廖雪峰的官方网站

上一篇下一篇

猜你喜欢

热点阅读