WebSocket⼊门及使⽤指南
最近在⼀个项⽬中,需要使⽤到websocket,于是就花了⼀点时间来熟悉websocket并总结写篇blog。
为何使⽤websocket
在浏览器与服务器通信间,传统的 HTTP 请求在某些场景下并不理想,⽐如实时聊天、实时性的⼩游戏等等,
其⾯临主要两个缺点:
⽆法做到消息的「实时性」;
服务端⽆法主动推送信息;
其基于 HTTP 的主要解决⽅案有:
基于 ajax 的轮询:客户端定时或者动态相隔短时间内不断向服务端请求接⼝,询问服务端是否有新信息;其缺点也很明显:多余的空请求(浪费资源)、数据获取有延时;
Long Poll:其采⽤的是阻塞性的⽅案,客户端向服务端发起 ajax 请求,服务端挂起该请求不返回数据直
到有新的数据,客户端接收到数据之后再次执⾏ Long Poll;该⽅案中每个请求都挂起了服务器资源,在⼤量连接的场景下是不可接受的;
可以看到,基于 HTTP 协议的⽅案都包含⼀个本质缺陷 —— 「被动性」,服务端⽆法下推消息,仅能由客户端发起请求不断询问是否有新的消息,同时对于客户端与服务端都存在性能消耗。
WebSocket 是 HTML5 开始提供的⼀种浏览器与服务器间进⾏全双⼯通讯的⽹络技术。 WebSocket 通信协议于2011年被IETF定为标准RFC 6455,WebSocketAPI 被 W3C 定为标准。在 WebSocket API 中,浏览器和服务器只需要要做⼀个握⼿的动作,然后,浏览器和服务器之间就形成了⼀条快速通道。两者之间就直接可以数据互相传送。
WebSocket 是 HTML5 中提出的新的⽹络协议标准,其包含⼏个特点:
建⽴于 TCP 协议之上的应⽤层;
⼀旦建⽴连接(直到断开或者出错),服务端与客户端握⼿后则⼀直保持连接状态,是持久化连接;
服务端可通过实时通道主动下发消息;
数据接收的「实时性(相对)」与「时序性」;
较少的控制开销。连接创建后,ws客户端、服务端进⾏数据交换时,协议控制的数据包头部较⼩。在不包含头部的情况下,服务端到客户端的包头只有2~10字节(取决于数据包长度),客户端到服务端的的话,需要加上额外的4字节的掩码。⽽HTTP协议每次通信都需要携带完整的头部。
⽀持扩展。ws协议定义了扩展,⽤户可以扩展协议,或者实现⾃定义的⼦协议。(⽐如⽀持⾃定义压缩算法等)
实践
在浏览器中使⽤ Websocket ⾮常简单,在⽀持 Websocket 的浏览器中会提供了原⽣的 WebSocekt 对象,其中对于消息的接收与数据帧处理在浏览器中已经封装好了。
以下将⽤⼀个简单的例⼦解释如何使⽤ WebSocekt;
浏览器中提供了原⽣类 WebSocket ,使⽤ new 关键字实例化它:
WebSocket WebSocket(String url,optional String | [] protocols);
//let websocket = new WebSocket("ws:///");
接收两个参数:
url 表⽰需要连接的地址,⽐如:;
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:///");
// 引⼊websocket
console.log('websocket open');
}
连接apple id服务器时出错// 结束websocket
console.log('websocket close');
}
// 接受到信息
console.log(e.data);
}
// 点击发送webscoket
var txt = ElementById("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.1101 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: 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.1101 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 ,⽐如。
使⽤的也是与 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)等问题。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论