为了账号安全,请及时绑定邮箱和手机立即绑定

【腾云阁】WebSocket 浅析

标签:
webpack

在WebSocket API尚未被众多浏览器实现和发布的时期,开发者在开发需要接收来自服务器的实时通知应用程序时,不得不求助于一些“hacks”来模拟实时连接以实现实时通信,最流行的一种方式是长轮询 。 长轮询主要是发出一个HTTP请求到服务器,然后保持连接打开以允许服务器在稍后的时间响应(由服务器确定)。为了这个连接有效地工作,许多技术需要被用于确保消息不错过,如需要在服务器端缓存和记录多个的连接信息(每个客户)。虽然长轮询是可以解决这一问题的,但它会耗费更多的资源,如CPU、内存和带宽等,要想很好的解决实时通信问题就需要设计和发布一种新的协议。

WebSocket 是伴随HTML5发布的一种新协议。它实现了浏览器与服务器全双工通信(full-duplex),可以传输基于消息的文本和二进制数据。WebSocket 是浏览器中最靠近套接字的API,除最初建立连接时需要借助于现有的HTTP协议,其他时候直接基于TCP完成通信。它是浏览器中最通用、最灵活的一个传输机制,其极简的API 可以让我们在客户端和服务器之间以数据流的形式实现各种应用数据交换(包括JSON 及自定义的二进制消息格式),而且两端都可以随时向另一端发送数据。在这个简单的API 之后隐藏了很多的复杂性,而且还提供了更多服务,如:

  • 连接协商和同源策略;

  • 与既有 HTTP 基础设施的互操作;

  • 基于消息的通信和高效消息分帧;

  • 子协议协商及可扩展能力。

所幸,浏览器替我们完成了上述工作,我们只需要简单的调用即可。任何事物都不是完美的,设计限制和性能权衡始终会有,利用WebSocket 也不例外,在提供自定义数据交换协议同时,也不再享有在一些本由浏览器提供的服务和优化,如状态管理、压缩、缓存等。

随着HTML5的发布,越来越多的浏览器开始支持WebSocket,如果你的应用还在使用长轮询,那就可以考虑切换了。下面的图表显示了在一种常见的使用案例下,WebSocket和长轮询之间的带宽消耗差异:


image.gif

1.WebSocket API

WebSocket 对象提供了一组 API,用于创建和管理 WebSocket 连接,以及通过连接发送和接收数据。浏览器提供的WebSocket API很简洁,调用示例如下:

var ws = new WebSocket('wss://example.com/socket'); // 创建安全WebSocket 连接(wss)ws. = function (error) { ... } // 错误处理ws.onclose = function () { ... } // 关闭时调用ws.onopen = function () { // 连接建立时调用ws.send("Connection established. Hello server!"); // 向服务端发送消息}

ws.onmessage = function(msg) { // 接收服务端发送的消息if(msg.data instanceof Blob) { // 处理二进制信息processBlob(msg.data);
} else {
processText(msg.data); // 处理文本信息}
}

1.1.接收和发送数据

WebSocket提供了极简的API,开发者可以轻松的调用,浏览器会为我们完成缓冲、解析、重建接收到的数据等工作。应用只需监听onmessage事件,用回调处理返回数据即可。 WebSocket支持文本和二进制数据传输,浏览器如果接收到文本数据,会将其转换为DOMString 对象,如果是二进制数据或Blob 对象,可直接将其转交给应用或将其转化为ArrayBuffer,由应用对其进行进一步处理。从内部看,协议只关注消息的两个信息:净荷长度和类型(前者是一个可变长度字段),据以区别UTF-8 数据和二进制数据。示例如下:

var wss = new WebSocket('wss://example.com/socket');
ws.binaryType = "arraybuffer"; 
// 接收数据wss.onmessage = function(msg) {if(msg.data instanceof ArrayBuffer) {
processArrayBuffer(msg.data);
} else {
processText(msg.data);
}
}// 发送数据ws.onopen = function () {
socket.send("Hello server!"); 
socket.send(JSON.stringify({'msg': 'payload'}));var buffer = new ArrayBuffer(128);
socket.send(buffer);var intview = new Uint32Array(buffer);
socket.send(intview);var blob = new Blob([buffer]);
socket.send(blob); 
}

Blob 对象是包含有只读原始数据的类文件对象,可存储二进制数据,它会被写入磁盘;ArrayBuffer (缓冲数组)是一种用于呈现通用、固定长度的二进制数据的类型,作为内存区域可以存放多种类型的数据。

对于将要传输的二进制数据,开发者可以决定以何种方式处理,可以更好的处理数据流,Blob 对象一般用来表示一个不可变文件对象或原始数据,如果你不需要修改它或者不需要把它切分成更小的块,那这种格式是理想的;如果你还需要再处理接收到的二进制数据,那么选择ArrayBuffer 应该更合适。

WebSocket 提供的信道是全双工的,在同一个TCP 连接上,可以双向传输文本信息和二进制数据,通过数据帧中的一位(bit)来区分二进制或者文本。WebSocket 只提供了最基础的文本和二进制数据传输功能,如果需要传输其他类型的数据,就需要通过额外的机制进行协商。WebSocket 中的send( ) 方法是异步的:提供的数据会在客户端排队,而函数则立即返回。在传输大文件时,不要因为回调已经执行,就错误地以为数据已经发送出去了,数据很可能还在排队。要监控在浏览器中排队的数据量,可以查询套接字的bufferedAmount 属性:

var ws = new WebSocket('wss://example.com/socket');

ws.onopen = function () {
subscribeToApplicationUpdates(function(evt) { 
if (ws.bufferedAmount == 0) 
ws.send(evt.data); 
});
};

前面的例子是向服务器发送应用数据,所有WebSocket 消息都会按照它们在客户端排队的次序逐个发送。因此,大量排队的消息,甚至一个大消息,都可能导致排在它后面的消息延迟——队首阻塞!为解决这个问题,应用可以将大消息切分成小块,通过监控bufferedAmount 的值来避免队首阻塞。甚至还可以实现自己的优先队列,而不是盲目都把它们送到套接字上排队。要实现最优化传输,应用必须关心任意时刻在套接字上排队的是什么消息!

1.2.子协议协商

在以往使用HTTP 或XHR 协议来传输数据时,它们可以通过每次请求和响应的HTTP 首部来沟通元数据,以进一步确定传输的数据格式,而WebSocket 并没有提供等价的机制。上文已经提到WebSocket只提供最基础的文本和二进制数据传输,对消息的具体内容格式是未知的。因此,如果WebSocket需要沟通关于消息的元数据,客户端和服务器必须达成沟通这一数据的子协议,进而间接地实现其他格式数据的传输。下面是一些可能策略的介绍:

  • 客户端和服务器可以提前确定一种固定的消息格式,比如所有通信都通过 JSON编码的消息或者某种自定义的二进制格式进行,而必要的元数据作为这种数据结构的一个部分;

  • 如果客户端和服务器要发送不同的数据类型,那它们可以确定一个双方都知道的消息首部,利用它来沟通说明信息或有关净荷的其他解码信息;

  • 混合使用文本和二进制消息可以沟通净荷和元数据,比如用文本消息实现 HTTP首部的功能,后跟包含应用净荷的二进制消息。

上面介绍了一些可能的策略来实现其他格式数据的传输,确定了消息的串行格式化,但怎么确保客户端和服务端是按照约定发送和处理数据,这个约定客户端和服务端是如何协商的呢?这就需要WebSocket 提供一个机制来协商,这时WebSocket构造器方法的第二个可选参数就派上用场了,通过这个参数客户端和服务端就可以根据约定好的方式处理发送及接收到的数据。
WebSocket构造器方法如下所示:

WebSocket WebSocket(in DOMString url, // 表示要连接的URL。这个URL应该为响应WebSocket的地址。in optional DOMString protocols // 可以是一个单个的协议名字字符串或者包含多个协议名字字符串的数组。默认设为一个空字符串。);

通过上述WebSocket构造器方法的第二个参数,客户端可以在初次连接握手时,可以告知服务器自己支持哪种协议。如下所示:

var ws = new WebSocket('wss://example.com/socket',['appProtocol', 'appProtocol-v2']);

ws.onopen = function () {if (ws.protocol == 'appProtocol-v2') { 
...
} else {
...
}
}

如上所示,WebSocket 构造函数接受了一个可选的子协议名字的数组,通过这个数组,客户端可以向服务器通告自己能够理解或希望服务器接受的协议。当服务器接收到该请求后,会根据自身的支持情况,返回相应信息。

  • 有支持的协议,则子协议协商成功,触发客户端的onopen回调,应用可以查询WebSocket 对象上的protocol 属性,从而得知服务器选定的协议;

  • 没有支持的协议,则协商失败,触发 回调,连接断开。

1.3.WS与WSS

WebSocket 资源URI采用了自定义模式:ws 表示纯文本通信( 如ws://example.com/socket),wss 表示使用加密信道通信(TCP+TLS)。为什么不使用http而要自定义呢?
WebSocket 的主要目的,是在浏览器中的应用与服务器之间提供优化的、双向通信机制。可是,WebSocket 的连接协议也可以用于浏览器之外的场景,可以通过非HTTP协商机制交换数据。考虑到这一点,HyBi Working Group 就选择采用了自定义的URI模式:

  • ws协议:普通请求,占用与http相同的80端口;

  • wss协议:基于SSL的安全传输,占用与tls相同的443端口。

各自的URI如下:

ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]

很多现有的HTTP 中间设备可能不理解新的WebSocket 协议,而这可能导致各种问题:盲目的连接升级、意外缓冲WebSocket 帧、不明就里地修改内容、把WebSocket 流量误当作不完整的HTTP 通信,等等。这时WSS就提供了一种不错的解决方案,它建立一条端到端的安全通道,这个端到端的加密隧道对中间设备模糊了数据,因此中间设备就不能再感知到数据内容,也就无法再对请求做特殊处理

2. WebSocket协议

HyBi Working Group 制定的WebSocket 通信协议(RFC 6455)包含两个高层组件:开放性HTTP 握手用于协商连接参数,二进制消息分帧机制用于支持低开销的基于消息的文本和二进制数据传输。WebSocket 协议尝试在既有HTTP 基础设施中实现双向HTTP 通信,因此也使用HTTP 的80 和443 端口。不过,这个设计不限于通过HTTP 实现WebSocket 通信,未来的实现可以在某个专用端口上使用更简单的握手,而不必重新定义一个协议。WebSocket 协议是一个独立完善的协议,可以在浏览器之外实现。不过,它的主要应用目标还是实现浏览器应用的双向通信。

2.1.数据成帧

WebSocket 使用了自定义的二进制分帧格式,把每个应用消息切分成一或多个帧,发送到目的地之后再组装起来,等到接收到完整的消息后再通知接收端。基本的成帧协议定义了帧类型有操作码、有效载荷的长度,指定位置的Extension data和Application data,统称为Payload data,保留了一些特殊位和操作码供后期扩展。在打开握手完成后,终端发送一个关闭帧之前的任何时间里,数据帧可能由客户端或服务器的任何一方发送。具体的帧格式如下所示:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1+-+-+-+-+-------+-+-------------+-------------------------------+|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           ||N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +|     Extended payload length continued, if payload len == 127  |+ - - - - - - - - - - - - - - - +-------------------------------+|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +|                     Payload Data continued ...                |+---------------------------------------------------------------+
  • FIN: 1 bit 。表示此帧是否是消息的最后帧,第一帧也可能是最后帧。

  • RSV1,RSV2,RSV3: 各1 bit 。必须是0,除非协商了扩展定义了非0的意义。

  • opcode:4 bit。表示被传输帧的类型:x0 表示一个后续帧;x1 表示一个文本帧;x2 表示一个二进制帧;x3-7 为以后的非控制帧保留;x8 表示一个连接关闭;x9 表示一个ping;xA 表示一个pong;xB-F 为以后的控制帧保留。

  • Mask: 1 bit。表示净荷是否有掩码(只适用于客户端发送给服务器的消息)。

  • Payload length: 7 bit, 7 + 16 bit, 7 + 64 bit。 净荷长度由可变长度字段表示: 如果是 0~125,就是净荷长度;如果是 126,则接下来 2 字节表示的 16 位无符号整数才是这一帧的长度; 如果是 127,则接下来 8 字节表示的 64 位无符号整数才是这一帧的长度。

  • Masking-key:0或4 Byte。 用于给净荷加掩护,客户端到服务器标记。

  • Extension data: x Byte。默认为0 Byte,除非协商了扩展。

  • Application data: y Byte。 在"Extension data"之后,占据了帧的剩余部分。

  • Payload data: (x + y) Byte。"extension data" 后接 "application data"。



作者:腾讯云技术社区_腾云阁
链接:https://www.jianshu.com/p/bc7d1a260c0c


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消