Websocket详谈

很多场景下的应用对数据实时更新要求很高。比如股票交易,数字资产交易,还有一些需要动态更新数据的大屏数据可视化应用等等。在html5面世前,动态更新数据的做法大都是使用ajax轮询来实现,但是轮询的效率低,而且非常浪费资源(因为必须不断建立连接)。到目前websocket已经很受大家喜爱,也逐步替代了轮询的做法,使用websocket的场景也越来越多。下面就来详细介绍:

WebSocket简介

WebSocket 是 HTML5 新增的一种在单个 TCP 连接上进行全双工通讯的协议。诞生于2008年,在2011年成为国际标准。现在新版的所有浏览器都已经支持,但不兼容低版本的浏览器。

WebSocket的最大特点是:允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端,是真正的双向平等对话,属于服务器推送技术的一种。

在RFC6455 中定义了它的通信标准。

为什么需要 WebSocket ?

了解HTTP协议的童鞋应该都知道HTTP协议有以下两个突出的特性:

其一:HTTP协议的通信只能由客户端发起,它无法实现服务器主动向客户端推送消息(单向请求)。

其二:HTTP协议是一种无状态的应用层协议,它采用的是请求/响应模型。每次通信都需要携带验证信息进行身份校验(耗时、耗资源、效率低)。

WebSocket可以说是在HTTP的基础上发明来的,改善了HTTP协议上面的两个特性。WebSocket只需要建立一次HTTP连接,就可以一直保持连接状态(如果两端长时间都没有通信也是会被关闭连接的 - 后面会讲到),此时已经是从HTTP协议升级到了WebSocket协议,后面的通信都是基于websocket协议。这相比于轮询方式的不停建立连接显然效率要大大提高。

WebSocket如何工作?

Web浏览器和服务器都必须支持 WebSocket 协议来建立和维护连接。由于 WebSocket 连接长期存在,与典型的 HTTP 连接不同,对服务器有重要的影响。

基于多线程或多进程的服务器无法适用于 WebSocket,因为它旨在打开连接,尽可能快地处理请求,然后关闭连接。

客户端简单示例:

var ws = new WebSocket("ws://echo.websocket.org");
或者加密协议:
var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function(evt) { 
  console.log("连接建立成功,可以开始通信了..."); 
  ws.send("Hello WebSocket!");
};

ws.onerror = function(evt) {
  console.log("连接出错 ...");
};   

ws.onmessage = function(evt) {
  console.log( "收到服务端消息: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("关闭连接 ...");
};      

Websocket客户端 API

1、WebSocket 构造函数:

WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例。
var webSocket = new WebSocket('ws://localhost:8080');
执行上面语句之后,客户端就会与服务器进行连接

2、webSocket.readyState

readyState属性返回实例对象的当前状态,共有四种:
CONNECTING:值为0,表示正在连接。
OPEN:值为1,表示连接成功,可以通信了。
CLOSING:值为2,表示连接正在关闭。
CLOSED:值为3,表示连接已经关闭,或者打开连接失败。

3、webSocket.bufferedAmount

bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束
var data = new ArrayBuffer(10000000);
webSocket.send(data);

if (webSocket.bufferedAmount === 0) {
// 发送完毕
} else {
// 发送还没结束
}

4、webSocket.onopen

onopen属性,用于指定连接成功后的回调函数
webSocket.onopen = function () {
webSocket.send('Hello Server!');
}
webSocket.addEventListener('open', function (event) {
webSocket.send('Hello Server!');
});

5、webSocket.onclose

onclose属性,用于指定连接关闭后的回调函数
webSocket.onclose = function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
};

webSocket.addEventListener("close", function(event) {
var code = event.code;
var reason = event.reason;
var wasClean = event.wasClean;
// handle close event
});

6、webSocket.onmessage

onmessage属性,用于指定收到服务器数据后的回调函数
webSocket.onmessage = function(event) {
var data = event.data;
// 处理数据
};

webSocket.addEventListener("message", function(event) {
var data = event.data;
// 处理数据
});

注意,服务器数据可能是文本,也可能是二进制数据(blob对象或Arraybuffer对象)
webSocket.onmessage = function(event){
if(typeof event.data === String) {
console.log("Received data string");
}

if(event.data instanceof ArrayBuffer){
var buffer = event.data;
console.log("Received arraybuffer");
}
}

除了动态判断收到的数据类型,也可以使用binaryType属性,显式指定收到的二进制数据类型。

// 收到的是 blob 数据
webSocket.binaryType = "blob";
webSocket.onmessage = function(e) {
console.log(e.data.size);
};

// 收到的是 ArrayBuffer 数据
webSocket.binaryType = "arraybuffer";
webSocket.onmessage = function(e) {
console.log(e.data.byteLength);
};

7、webSocket.onerror

onerror属性,用于指定报错时的回调函数
webSocket.onerror = function(event) {
// handle error event
};

webSocket.addEventListener("error", function(event) {
// handle error event
});

8、webSocket.send()

实例对象的send()方法用于向服务器发送数据

发送文本的例子
webSocket.send('your message');

发送 Blob 对象的例子。
var file = document.querySelector('input[type="file"]').files[0];
webSocket.send(file);

发送 ArrayBuffer 对象的例子。
// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
binary[i] = img.data[i];
}
webSocket.send(binary.buffer);

9、webSocket.close()

实例对象的close()方法用于向服务器关闭连接
webSocket.close()

服务端如何实现?

WebSocket 在服务端的实现非常丰富。Node.js、Java、C++、Python 等多种语言都有自己的解决方案
常用的 Node 实现有以下三种:

  • µWebSockets
  • Socket.IO
  • WebSocket-Node

WebSocket小结:

HTTP 和 WebSocket 有什么关系?

Websocket 其实是一个新的应用层协议,跟 HTTP 协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是 HTTP 协议上的一种补充。
首先Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手。

websocket握手阶段:

GET /chat HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
Sec-WebSocket-Protocol: chat, superchat

Connection: Upgrade:表示要升级协议
Upgrade: websocket:表示要升级到websocket协议
Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Version header,里面包含服务端支持的版本号
Sec-WebSocket-Key:是一个Base64 encode的值,这个是浏览器随机生成的,与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接
Sec-WebSocket-Protocol: 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。

然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~

Sec-WebSocket-Accept:这个则是经过服务器确认,根据客户端请求首部的Sec-WebSocket-Key计算出来的
计算公式为:
1、将Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。
2、通过SHA1计算出摘要,并转成base64字符串

toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )

Sec-WebSocket-Protocol:则是表示最终使用的协议。

至此,http已经完成它所有工作了,接下来就是完全按照Websocket协议进行通信了。

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优点:

1、支持双向通信,实时性更强。
2、不用频繁送HTTP请求,只需要发送一个HTTP请求进行websocket握手,接下来则可以利用该TCP连接通过websocket协议通讯,避免了传输多个HTTP Header的浪费
3、支持传输文本和二进制。
4、websocket数据传输是基于数据帧的,可以分片传输,不需要怕数据太大包容纳不下。
5、支持扩展。ws协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议。(比如支持自定义压缩算法等)

WebSocket客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)

websocket出现之前的一些持久连接操作:

1、长轮询:建立连接 -> 传输数据 -> 保持连接 -> 。。。-> 响应 -> 关闭连接
采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。需要有很高的并发,也就是说同时接待客户的能力。(场地大小)服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护

2、ajax轮询:建立连接 -> 传输数据 -> 响应 -> 关闭连接 -> 定时循环上面的过程
定时向后台发请求,需要服务器有很快的处理速度和资源。(速度)请求中有大半是无用,浪费带宽和服务器资源

3、长连接:建立连接 -> 传输数据 -> 保持连接 -> 传输数据 -> 。。。 -> 关闭连接
http1.0默认进行短连接,通过使用Connection: keep-alive进行长连接,http1.1默认进行持久连接。在一次 TCP 连接中可以完成多个 http 请求,但是对每个请求仍然要单独发 header,keep-alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Nginx\Apache)中设定这个时间。
启用keep-alive模式肯定更高效,性能更高。因为避免了建立/释放连接的开销

以上持久连接的缺点:
1、被动性 - 只能由客户端发送请求
2、在传统的方式上,要不断的建立和关闭连接,由于http是非状态性的,每次都要重新传输identity info(鉴别信息),来告诉服务端你是谁,解析耗时,耗资源,效率还低
3、http1.1串行单线程处理,响应是有顺序的,只有上一个请求完成后,下一个才能响应。一旦有任务处理超时等,后续任务只能被阻塞(线头阻塞)
4、keep-alive双方并没有建立正真的连接会话,服务端可以在任何一次请求完成后关闭

websocket长时间没有通信会自动断开的原因?

利用nginx代理websocket的时候,发现客户端和服务器握手成功后,如果在60s时间内没有数据交互,连接就会自动断开。
nginx.conf 文件里location 中的proxy_read_timeout 默认60s断开。
保持持久连接的做法:
1、把服务器的默认时间改大 + 发送心跳机制
2、定时检测客户端是否已经断开连接,断开重连

你可能感兴趣的:(Websocket详谈)