H5数据实时方案「WebSocket」

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

# 数据实时方案

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: 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通道链接没有断开。然而长时间没有数据往来的连接,会浪费一些连接资源,网络链路同样可能被网关关闭,毕竟网关不是我们能控制的。因此链路链接就需要提示说明还在使用周期内,这个提示就是心跳来实现的。

  • 发送方 --> 接收方: 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]])

如果未传参数,会默认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-廖雪峰的官方网站

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-WebSo...
    敢梦敢当阅读 8,877评论 0 50
  • WebSocket 机制 WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更...
    勇敢的_心_阅读 2,241评论 0 4
  • Socket并非是一个协议,而是为了方便使用TCP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。换句话...
    JunChow520阅读 3,334评论 0 4
  • 1 述 WebSocket是一种网络通信协议WebSocket 协议在2008年诞生,2011年成为国际标准。HT...
    凯玲之恋阅读 668评论 0 0
  • 此教程无需R语言 Step 1 确定需要检索的芯片 例如 GSE33006 Step 2 利用GEO2R分析差异基...
    Fobbite0579阅读 12,247评论 0 5