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 实现有以下三种:

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、定时检测客户端是否已经断开连接,断开重连

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

推荐阅读更多精彩内容

  • 原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-WebSo...
    敢梦敢当阅读 8,882评论 0 50
  • Socket并非是一个协议,而是为了方便使用TCP而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。换句话...
    JunChow520阅读 3,334评论 0 4
  • WebSocket 机制 WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更...
    勇敢的_心_阅读 2,243评论 0 4
  • 一、Finalize方法的过程 一个对象真正宣告死亡,至少要经历两次标记过程:如果对象在进行可达性分析之后发现没有...
    兔子胡萝卜抱阅读 1,545评论 0 1
  • #幸福是需要修出来的~每天进步1%~幸福实修09班~张媛 20170727(11/30)09班 【幸福三朵玫瑰】 ...
    自在如我是阅读 221评论 1 1