WebSocket支持在客户端和服务器之间双向的、面向消息的文本和二进制数据流。它是浏览器中最接近原始网络套接字的API。除了WebSocket连接也不仅仅是一个网络套接字,因为浏览器在一个简单的API背后抽象了所有的复杂性,并提供了一些额外的服务:
连接协商和同源策略的实施
与现有HTTP基础设施的互操作性
面向消息的通信和有效的消息框架
子协议协商和可扩展性
WebSocket是浏览器中最通用、最灵活的传输方式之一。简单和最小的API使我们能够以流方式在客户机和服务器之间分层和交付任意的应用程序协议——从简单的JSON有效负载到定制的二进制消息格式的任何内容——其中任何一方都可以随时发送数据。
但是,定制协议的代价是它们是定制的。应用程序必须考虑到缺少状态管理、压缩、缓存和其他由浏览器提供的服务。设计约束和性能权衡总是存在的,利用WebSocket也不例外。简而言之,WebSocket不是HTTP、XHR或SSE的替代品,为了获得最佳性能,充分利用每种传输的优势至关重要。
WebSocket是由多个标准组成的:WebSocket API由W3C定义,WebSocket协议(RFC 6455)及其扩展由HyBi工作组(IETF)定义。
WebSocket API
浏览器提供的WebSocket API非常小和简单。同样,连接管理和消息处理的所有低级细节都由浏览器处理。为了启动一个新的连接,我们需要一个WebSocket资源的URL和一些应用程序回调:
var ws = new WebSocket('wss://example.com/socket'); ws.onerror = 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); } }
打开一个新的安全WebSocket连接(wss)
可选回调,在发生连接错误时调用
可选回调,在连接终止时调用
可选回调,在建立WebSocket连接时调用
客户端发起的消息到服务器
为来自服务器的每个新消息调用的回调函数
为接收到的消息调用二进制或文本处理逻辑
API不言自明。实际上,它应该与我们在前一章看到的EventSource API非常相似。这是有意为之,因为WebSocket提供了类似的扩展功能。话虽如此,两者之间也有一些重要的区别。让我们一个一个来看看。
效仿WebSocket
WebSocket协议已经经历了许多修订、实现回滚和安全调查。然而,好消息是RFC6455定义的最新版本(v13)现在被所有现代浏览器支持。唯一值得注意的遗漏是Android浏览器。有关最新状态,请参阅http://caniuse.com/websockets。
类似于SSE的polyfill策略(用定制的JavaScript模拟EventSource), WebSocket浏览器API可以通过一个可选的JavaScript库来模拟。然而,模拟WebSockets的难点不是API,而是传输!因此,polyfill库及其回退传输(XHR轮询、EventSource、iframe轮询等)的选择将对模拟WebSocket会话的性能产生重大影响。
为了简化跨浏览器的部署,流行的库(如SockJS)在浏览器中提供了类似WebSocket的对象的实现,而且更进一步地提供了一个定制服务器来实现对WebSocket的支持和各种替代传输。定制服务器和客户机的结合能够实现“无缝回退”:性能下降,但应用程序API保持不变。
其他库,比如Socket。除了多传输回退功能外,IO还实现了其他特性,如心跳、超时、支持自动重新连接等。
当考虑一个polyfill 库或“实时框架”,Socket.IO,密切关注客户端和服务器的底层实现和配置:始终利用本地WebSocket接口获得最佳性能,并确保回退传输满足您的性能目标。
WS和WSS URL方案
WebSocket资源URL使用自己的定制方案:ws用于明文通信(例如,ws://example.com/socket), wss用于需要加密通道(TCP+TLS)。为什么使用自定义模式,而不是熟悉的http?
WebSocket协议的主要用例是在浏览器和服务器上运行的应用程序之间提供一个优化的双向通信通道。然而,WebSocket有线协议可以在浏览器之外使用,并且可以通过非http交换进行协商。因此,HyBi工作组选择采用自定义URL方案。
尽管自定义方案启用了非http协商选项,但在实践中,还没有用于建立WebSocket会话的替代握手机制的现有标准。
接收文本和二进制数据
WebSocket通信由消息和应用程序代码组成,不需要担心缓冲、解析和重构接收到的数据。例如,如果服务器发送了1 MB的有效负载,应用程序的onmessage回调将只在客户机上整个消息可用时被调用。
此外,WebSocket协议对应用程序的有效负载没有任何假设和限制:文本和二进制数据都是公平的。在内部,该协议只跟踪关于消息的两部分信息:作为可变长度字段的有效负载的长度和区分UTF-8和二进制传输的有效负载类型。
当浏览器接收到新消息时,它会自动转换为DOMString对象(用于基于文本的数据)或Blob对象(用于二进制数据),然后直接传递给应用程序。唯一的另一个选项,作为性能提示和优化客户端,是告诉浏览器转换接收到的二进制数据到ArrayBuffer,而不是Blob:
var ws = new WebSocket('wss://example.com/socket'); ws.binaryType = "arraybuffer"; ws.onmessage = function(msg) { if(msg.data instanceof ArrayBuffer) { processArrayBuffer(msg.data); } else { processText(msg.data); } }
当收到二进制消息时,强制进行ArrayBuffer转换
用户代理可以将此作为如何处理传入的二进制数据的提示:如果属性设置为“blob”,则将其缓冲到磁盘是安全的,如果属性设置为“arraybuffer”,则可能更有效地将数据保存在内存中。自然,用户代理被鼓励使用更微妙的启发式来决定是否将传入数据保存在内存中……
WebSocket API, W3C候选推荐
Blob对象表示不变的原始数据的类文件对象。如果您不需要修改数据,也不需要将其分割成更小的块,那么它就是最佳格式——例如。,您可以将整个Blob对象传递给图像标记(请参阅使用XHR下载数据中的示例)。另一方面,如果需要对二进制数据执行额外的处理,那么ArrayBuffer可能更适合。
用JavaScript解码二进制数据
ArrayBuffer是一种通用的、固定长度的二进制数据缓冲区。但是,ArrayBuffer可以用来创建一个或多个ArrayBufferView对象,每个对象可以以特定的格式显示缓冲区的内容。例如,假设我们有以下类似c的二进制数据结构:
struct
someStruct
{
char
username
[
16
];
unsigned
short
id
;
float
scores
[
32
];
};
给定这种类型的ArrayBuffer对象,我们可以在同一个缓冲区中创建多个视图,每个视图都有自己的偏移量和数据类型:
var
buffer
=
msg
.
data
;
var
usernameView
=
new
Uint8Array
(
buffer
,
0
,
16
);
var
idView
=
new
Uint16Array
(
buffer
,
16
,
1
);
var
scoresView
=
new
Float32Array
(
buffer
,
18
,
32
);
console
.
log
(
"ID: "
+
idView
[
0
]
+
" username: "
+
usernameView
[
0
]);
for
(
var
j
=
0
;
j
<
32
;
j
++
)
{
console
.
log
(
scoresView
[
j
])
}
每个视图接受父缓冲区、起始字节偏移量和要处理的元素数量——偏移量是根据前面字段的大小计算的。因此,ArrayBuffer和WebSocket为我们的应用程序提供了所有必要的工具来在浏览器中流处理二进制数据。
发送文本和二进制数据
一旦WebSocket连接建立,客户端就可以随意发送和接收UTF-8和二进制消息。WebSocket提供了一个双向通信通道,它允许在相同的TCP连接上双向传递消息:
var ws = new WebSocket('wss://example.com/socket'); 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); }
发送UTF-8编码的文本消息
发送一个UTF-8编码的JSON有效负载
将ArrayBuffer内容作为二进制有效负载发送
将ArrayBufferView内容作为二进制有效负载发送
将Blob内容作为二进制有效负载发送
WebSocket API接受一个DOMString对象,该对象在网络上被编码为UTF-8,或者一个用于二进制传输的ArrayBuffer、ArrayBufferView或Blob对象。但是,请注意,后面的二进制选项只是为了API的方便:在网络上,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消息都是按照客户端排队的确切顺序传递的。结果,队列消息的大量积压,甚至单个大消息,都将延迟在it后排队的消息的传递——线头阻塞!
为了解决这个问题,应用程序可以将大消息分割成更小的块,仔细监视bufferedAmount值以避免在行头阻塞,甚至为挂起的消息实现自己的优先级队列,而不是盲目地将它们全部排在套接字上。
许多应用程序生成多种类型的消息:高优先级更新,如控制流量;低优先级更新,如后台传输。为了优化传递,应用程序应该密切关注每种类型的消息如何以及何时在套接字上排队!
Subprotocol谈判
WebSocket协议对每个消息的格式不做任何假设:一个位跟踪消息是否包含文本或二进制数据,这样它可以被客户端和服务器有效地解码,但其他消息的内容是不透明的。
此外,与HTTP或XHR请求不同的是,它们通过每个请求和响应的HTTP头来通信额外的元数据,对于WebSocket消息没有这种等效的机制。因此,如果需要关于消息的额外元数据,那么客户端和服务器必须同意实现它们自己的子协议来通信这些数据:
客户端和服务器可以预先就固定的消息格式达成一致。,所有通信都将通过json编码的消息或自定义二进制格式完成,必要的消息元数据将成为编码结构的一部分。
如果客户机和服务器需要传输不同的数据类型,那么它们可以就一致的消息头达成一致,该消息头可用于通信指令来解码剩余的有效负载。
可以使用文本和二进制消息的混合来传递有效负载和元数据信息。时,文本消息可以与等效的HTTP头通信,然后与应用程序有效负载通信二进制消息。
这个列表只是一个可能的策略的小样本。WebSocket消息的灵活性和低开销是以额外的应用逻辑为代价的。但是,消息序列化和元数据管理只是问题的一部分!一旦我们确定了消息的序列化格式,我们如何确保客户机和服务器相互理解,以及如何保持它们同步?
幸运的是,WebSocket提供了一个简单而方便的子协议协商API来解决第二个问题。作为初始连接握手的一部分,客户端可以向服务器发布它支持的协议:
var ws = new WebSocket('wss://example.com/socket', ['appProtocol', 'appProtocol-v2']); ws.onopen = function () { if (ws.protocol == 'appProtocol-v2') { ... } else { ... } }
在WebSocket握手期间发布的子协议数组
检查服务器选择的子协议
如前面的示例所示,WebSocket构造函数接受子协议名称的可选数组,它允许客户端发布它理解或愿意为这个连接使用的协议列表。指定的列表被发送到服务器,服务器被允许选择一个由客户机发布的协议。
如果子协议协商成功,那么onopen回调在客户端上被触发,并且应用程序可以查询WebSocket对象上的协议属性来确定所选择的协议。另一方面,如果服务器不支持任何客户端发布的客户端协议,那么WebSocket握手是不完整的:onerror回调被调用,连接被终止。
子协议名称由应用程序定义,并在初始HTTP握手期间按指定的方式发送到服务器。除此之外,指定的子协议对核心WebSocket API没有影响。