WebSocket入门及使用指南

最近在一个项目中,须要使用到websocket,因而就花了一点时间来熟悉websocket并总结写篇blog。javascript

为什么使用websocket

在浏览器与服务器通讯间,传统的 HTTP 请求在某些场景下并不理想,好比实时聊天、实时性的小游戏等等,html

其面临主要两个缺点:java

  • 没法作到消息的「实时性」;
  • 服务端没法主动推送信息;

其基于 HTTP 的主要解决方案有:nginx

  • 基于 ajax 的轮询:客户端定时或者动态相隔短期内不断向服务端请求接口,询问服务端是否有新信息;其缺点也很明显:多余的空请求(浪费资源)、数据获取有延时;
  • Long Poll:其采用的是阻塞性的方案,客户端向服务端发起 ajax 请求,服务端挂起该请求不返回数据直到有新的数据,客户端接收到数据以后再次执行 Long Poll;该方案中每一个请求都挂起了服务器资源,在大量链接的场景下是不可接受的;

能够看到,基于 HTTP 协议的方案都包含一个本质缺陷 —— 「被动性」,服务端没法下推消息,仅能由客户端发起请求不断询问是否有新的消息,同时对于客户端与服务端都存在性能消耗。web

WebSocket 是 HTML5 开始提供的一种浏览器与服务器间进行全双工通信的网络技术。 WebSocket 通讯协议于2011年被IETF定为标准RFC 6455,WebSocketAPI 被 W3C 定为标准。 在 WebSocket API 中,浏览器和服务器只须要要作一个握手的动做,而后,浏览器和服务器之间就造成了一条快速通道。二者之间就直接能够数据互相传送。ajax

WebSocket 是 HTML5 中提出的新的网络协议标准,其包含几个特色:算法

  • 创建于 TCP 协议之上的应用层;
  • 一旦创建链接(直到断开或者出错),服务端与客户端握手后则一直保持链接状态,是持久化链接;
  • 服务端可经过实时通道主动下发消息;
  • 数据接收的「实时性(相对)」与「时序性」;
  • 较少的控制开销。链接建立后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较小。在不包含头部的状况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,须要加上额外的4字节的掩码。而HTTP协议每次通讯都须要携带完整的头部。
  • 支持扩展。ws协议定义了扩展,用户能够扩展协议,或者实现自定义的子协议。(好比支持自定义压缩算法等)

实践

在浏览器中使用 Websocket 很是简单,在支持 Websocket 的浏览器中会提供了原生的 WebSocekt 对象,其中对于消息的接收与数据帧处理在浏览器中已经封装好了。
如下将用一个简单的例子解释如何使用 WebSocekt;
浏览器中提供了原生类 WebSocket ,使用 new 关键字实例化它:segmentfault

WebSocket WebSocket(String url,optional String | [] protocols); //let websocket = new WebSocket("ws://echo.websocket.org/");

 

接收两个参数:数组

  • url 表示须要链接的地址,好比:ws://localhost:8080浏览器

  • protocols 可选参数,能够是一个字符串或者一个数组,用来表示子协议,这样作可让一个服务器实现多种 WebSocket 子协议;
    实例化对象提供两个方法:

  • send 接收一个 String|ArrayBuffer|Blob 数据,做为数据发送到服务端;

  • close 接收一个(可选)的 code(关闭状态号,默认为 1000) 与一个(可选)的字符串(表示断开缘由),客户端主动断开链接;
    链接状态:

WebSocket 类提供了一些常量表示链接状态:

  • WebSocket.CONNECTING 0 链接还没开启;
  • WebSocket.OPEN 1 链接已开启并准备好进行通讯;
  • WebSocket.CLOSING 3 链接正在关闭的过程当中;
  • WebSocket.CLOSED 4 链接已经关闭,或者链接没法创建;
  • WebSocket 的实例对象中提供了 readyState 属性来判断当前状态;

实例化对象中能够监听到如下事件:

  • open 链接打开的回调事件,这时 readyState 变为 OPEN;
  • message 收到消息的回调事件,同时回调函数接收到一个 MessageEvent 数据;
  • close 链接关闭的回调事件,这时 readyState 变为 CLOSED;
  • error 创建与链接过程发生错误的回调事件;

代码实现

<h1>Echo Test</h1>
    <input id="sendTxt" type="text">
    <button id="sendBtn">发送</button>
    <div id="recv"></div>
    <script type="text/javascript">
        var websocket = new WebSocket("ws://echo.websocket.org/"); // 引入websocket
        websocket.onopen = function(){ console.log('websocket open'); document.getElementById("recv").innerHTML = "Connected"; } // 结束websocket
        websocket.onclose = function(){ console.log('websocket close'); } // 接受到信息
        websocket.onmessage = function(e){ console.log(e.data); document.getElementById("recv").innerHTML = e.data; } // 点击发送webscoket
        document.getElementById("sendBtn").onclick = function(){ var txt = document.getElementById("sendTxt").value; websocket.send(txt); } </script>

 

首先触发 open 事件,以后每次发送数据服务端都会回复数据,所以触发了 message 事件,若是触发 close 事件;这里最后一次发送以后未收到服务端回复也是由于客户端当即断开了链接;websocket.send()是发送信息方法

 


事件与数据

对 WebSocket 实例监听事件有两种方式,这里以 message 事件为例:

  • 对 onmessage 属性直接赋值,正如以上:ws.onmessage = function () {};
  • 使用 addEventListener 监听事件,如:ws.addEventListener('message', function () {});

在 message 回调函数中获得 MessageEvent 类型参数 e ,咱们须要的数据能够经过 e.data 获取;

须要注意的一点是:不论服务端与客户端,其接受到的数据都是序列化后的字符串(固然也有 ArrayBuffer|Blob 类型数据),不少时候咱们须要解析处理数据,好比 JSON.parse(e.data)

链接稳定性

因为网络环境复杂,某些状况会出现断开链接或者链接出错,须要咱们在 close 或者 error 事件中监听非正常断开并重连;

因为一些缘由在 error 时浏览器并不会响应回调事件,所以稳妥的作法还须要在 open 以后开启一个定时任务去判断当前的链接状态 readyState ,在出现异常状况下尝试重连;

心跳

websocket规范定义了心跳机制,一方能够经过发送ping(opcode 0x9)消息给另外一方,另外一方收到ping后应该尽量快的返回pong(0xA)。

心跳机制是用于检测链接的对方在线状态,所以若是没有心跳,那么没法判断一方还在链接状态中,一些网络层好比 nginx 或者浏览器层会主动断开链接,

在 JavaScript 中,WebSocket 并无开放 ping/pong 的 API ,虽然浏览器自带了心跳处理,然而不一样厂商的实现也不尽相同,所以须要在咱们开发时候与服务端约定好一个自实现的心跳机制;

好比浏览器中,检测到 open 事件后,启动一个定时任务,每次发送数据 0x9 给服务端,而服务端返回 0xA 做为响应;

实践下来,心跳的定时任务通常是相隔 15-20 秒发送一次。

 

举例,WebSocket服务端向客户端发送ping,只须要以下代码(采用ws模块)

ws.ping('', false, true);

 

 

网络协议

前文说到,Websocket 是创建与 TCP 之上,那么其与 HTTP 协议有和关系呢?

Websocket 链接分为建连阶段与链接阶段,在创建链接阶段借助于 HTTP ,而在链接阶段则与 HTTP 无关。

建连阶段

从浏览器的 Network 中,找到 ws 链接,能够看到:

General
Request URL:ws://localhost:8080/
Request Method:GET
Status Code:101 Switching Protocols

Response Headers
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: py9bt3HbjicUUmFWJfI0nhGombo=

Request Headers
GET ws://localhost:8080/ HTTP/1.1
Host: localhost:8080
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://localhost:8080
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36
DNT: 1
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,la;q=0.6,ja;q=0.5
Sec-WebSocket-Key: 2idFk3+96Hs5hh+c9GOQCg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

 

这是一个标准的 HTTP 请求,相比于咱们常见的 HTTP 请求协议,请求头中多了几个字段:

Connection: Upgrade Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: 2idFk3+96Hs5hh+c9GOQCg==

重点请求首部意义以下:

  • Connection: Upgrade:表示要升级协议
  • Upgrade: websocket:表示要升级到websocket协议。
  • Sec-WebSocket-Version: 13:表示websocket的版本。若是服务端不支持该版本,须要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。
  • Sec-WebSocket-Key :是一个 Base64 encode 的值,由浏览器随机生成的,用于验证服务器链接的正确性;与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防御,好比恶意的链接,或者无心的链接。
  • Connection 为 Upgrade ,Upgrade 为 websocket ,表示告知 Nginx 与 Apache 等服务器该次链接并不是为 HTTP 链接,实质上是一个 websocket ,所以服务器会转发到相应的 websocket 任务处理;
  • Sec-WebSocket-Versio 表示为使用的 websocket 服务版本;

 

响应头中:

HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: py9bt3HbjicUUmFWJfI0nhGombo=

 

能够看到其返回状态码为 101 ,表示切换协议;
Upgrade 与 Connection 用于回复客户端表示已经切换协议成功;
Sec-WebSocket-Accept 字段与 Sec-WebSocket-Key 相对应,用于验证服务的正确性;

链接阶段

当经过 HTTP 创建链接握手后,接下来则是真正的 Websocket 链接了,其基于 TCP 收发数据,Websocket 封装并开放接口。

WSS

在 HTTP 协议中,不少时候为了加密与安全须要使用 HTTPS 请求(HTTP + TCL);
相应的,在 Websocket 协议中,也是可使用加密传输的 —— wss ,好比 wss://localhost:8080

使用的也是与 HTTPS 同样的证书,在这里通常是交由 Nginx 等服务层去作证书处理。

 

Sec-WebSocket-Key/Accept的做用

前面提到了,Sec-WebSocket-Key/Sec-WebSocket-Accept在主要做用在于提供基础的防御,减小恶意链接、意外链接。

做用大体概括以下:

  1. 避免服务端收到非法的websocket链接(好比http客户端不当心请求链接websocket服务,此时服务端能够直接拒绝链接)
  2. 确保服务端理解websocket链接。由于ws握手阶段采用的是http协议,所以可能ws链接是被一个http服务器处理并返回的,此时客户端能够经过Sec-WebSocket-Key来确保服务端认识ws协议。(并不是百分百保险,好比老是存在那么些无聊的http服务器,光处理Sec-WebSocket-Key,但并无实现ws协议。。。)
  3. 用浏览器里发起ajax请求,设置header时,Sec-WebSocket-Key以及其余相关的header是被禁止的。这样能够避免客户端发送ajax请求时,意外请求协议升级(websocket upgrade)
  4. 能够防止反向代理(不理解ws协议)返回错误的数据。好比反向代理先后收到两次ws链接的升级请求,反向代理把第一次请求的返回给cache住,而后第二次请求到来时直接把cache住的请求给返回(无心义的返回)。
  5. Sec-WebSocket-Key主要目的并非确保数据的安全性,由于Sec-WebSocket-Key、Sec-WebSocket-Accept的转换计算公式是公开的,并且很是简单,最主要的做用是预防一些常见的意外状况(非故意的)。
强调:Sec-WebSocket-Key/Sec-WebSocket-Accept 的换算,只能带来基本的保障,但链接是否安全、数据是否安全、客户端/服务端是否合法的 ws客户端、ws服务端,其实并无实际性的保证。

 

数据掩码的做用

WebSocket协议中,数据掩码的做用是加强协议的安全性。但数据掩码并非为了保护数据自己,由于算法自己是公开的,运算也不复杂。除了加密通道自己,彷佛没有太多有效的保护通讯安全的办法。

那么为何还要引入掩码计算呢,除了增长计算机器的运算量外彷佛并无太多的收益。

答案仍是两个字:安全。但并非为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。

 

 

 

本文参考文章: https://qiutc.me/post/websocket-guide.html 、https://segmentfault.com/a/1190000012709475

相关文章
相关标签/搜索