《Web性能权威指南》-浏览器API与协议-读书笔记
本文是《Web性能权威指南》第四部分——浏览器API与协议的读书笔记。
第一部分——网络技术概览,请参考网络技术概览;
第二部分——无线网络性能,请参考无线网络性能;
第三部分——HTTP,请参考HTTP。
浏览器网络概述
现代浏览器是一个囊括数百个组件的操作系统,包括进程管理、安全沙箱、分层优化缓存、JS虚拟机、图形渲染和GPU管道、存储系统、传感器、音频与视频、网络机制等。
浏览器乃至运行在其中的应用的性能,取决于若干组件:解析、布局、HTML与CSS的样式计算、JS执行速度、渲染管道、网络相关各层协议的配合。
连接管理与优化
浏览器完成:套接字重用、请求优先级排定、晚绑定、协议协商、施加连接数限制;把请求管理生命周期与套接字管理分开。
套接字是以池的形式进行管理的,即按照来源,每个池都有自己的连接限制和安全约束。挂起的请求是排好队的、有优先次序的,然后再适时把它们绑定到池中个别的套接字上。除非服务器有意关闭连接,否则同一个套接字可以自动用于多个请求!
解读:
- 来源:由应用协议、域名和端口三个要件构成;
- 套接字池:属于同一个来源的一组套接字。实践中,所有主流浏览器的最大池规模都是6个套接字。
浏览器的套接字池管理的好处:
- 可自动重用TCP连接,从而有效保障性能;
- 可按照优先次序发送排队的请求;
- 可重用套接字以最小化延迟并提升吞吐量;
- 可预测请求提前打开套接字;
- 可优化何时关闭空闲套接字;
- 可优化分配给所有套接字的带宽。
网络安全与沙箱
浏览器沙箱机制:对不受信任的应用代码采取一致的安全与策略限制。
浏览器提供的安全策略:
- 连接限制:浏览器管理所有打开的套接字池并强制施加连接数限制,保护客户端和服务器的资源不会被耗尽;
- 请求格式化与响应处理:浏览器格式化所有外发请求以保证格式一致和符合协议的语义,从而保护服务器。类似地,响应解码也会自动完成,以保护用户;
- TLS协商:浏览器执行TLS握手和必要的证书检查。任何证书有问题(比如服务器正在使用自已签发的证书),用户都会收到通知;
- 同源策略:浏览器会限制应用只能向哪个来源发送请求;
- 还有其他很多。
充分体现最低特权(Least Privilege)原则。浏览器只向应用代码公开那些必要的API和资源:应用提供数据和URL,浏览器执行请求并负责管理每个连接的整个生命周期。
另外,同源策略实际上是一组相关机制,涉及对DOM访问、cookie和会话状态管理、网络及其他浏览器组件的限制。
资源与客户端状态缓存
浏览器缓存管理:
- 浏览器针对每个资源自动执行缓存指令;
- 浏览器会尽可能恢复失效资源的有效性;
- 浏览器会自动管理缓存大小及资源回收。
浏览器还提供会话认证和cookie管理。认证的会话可以在多个标签页或浏览器口间共享,反之亦然;如果用户在某个标签页中退出,那么其他所有打开窗口中的会话都将失效。
应用API与协议
不存在哪个协议或API最好的问题。每个稍微复杂点的应用都会基于不同的需求用到各种传输机制,包括读写浏览器缓存、协议开销、消息延迟、可靠性、数据传输类型。
XHR、SSE和WebSocket的高级特性
项目 | XMLHttpRequest | Server-Sent Event | WebSocket |
---|---|---|---|
请求流 | 否 | 否 | 是 |
响应流 | 受限 | 是 | 是 |
分帧机制 | HTTP | 事件流 | 二进制分帧 |
二进制数据传输 | 是 | 否(base64) | 是 |
压缩 | 是 | 是 | 受限 |
应用传输协议 | HTTP | HTTP | WebSocket |
网络传输协议 | TCP | TCP | TCP |
XMLHttpRequest
XHR,是浏览器层面的API,可以让开发人员通过JS实现数据传输。XHR是在IE5中首次亮相的,后来成为AJAX(Asynchronous JavaScript and XML)核心技术,是几乎所有Web应用必不可少的基本构件。
XHR诞生前,网页要获取客户端和服务器的任何状态更新,都必须刷新一次。XHR可实现异步,且完全通过应用的JS代码完成。XHR是从制作网页转换为开发交互应用的根本技术。
XHR是浏览器提供的应用API,即浏览器会自动完成所有底层工作:
- 连接管理;
- 协议协商;
- HTTP请求格式化;
- 连接建立、套接字池和连接终止;
- 选择最佳的HTTP传输协议(HTTP 1.0、1.x和2.0);
- 处理HTTP缓存、重定向和内容类型协商;
- 保障安全、验证和隐私;
- 其他很多。
这并不是说XHR在任何场景中都是最有效的传输方式。
XHR简史
XHR的早期版本能力有限:只能传输文本,处理上传的能力不足,不能处理跨域请求。W3C于2008年发布XHR Level 2草案,新增支持如下新功能:
- 请求超时;
- 传输二进制和文本数据;
- 应用重写媒体类型和编码响应;
- 监控每个请求的进度事件;
- 有效的文件上传;
- 安全的跨来源请求。
所有新的XHR2功能,都是通过同一个XMLHttpRequest API提供的:接口不变,功能增强。
CORS
跨源资源共享
一个源由应用协议、域名和端口这三个要件共同定义。
要启用cookie和HTTP认证,客户端必须在发送请求时通过XHR对象发送额外的属性(withCredentials),而服务器也必须以适当的首部(Access-Control-Allow-Credentials)响应,表示它允许应用发送用户的隐私数据。如果客户端需要写或者读自定义的HTTP首部,或者想要使用“不简单的方法”发送请求,那么它必须首先要获得第三方服务器的许可,即向第三方服务器发送一个预备(preflight)请求:
XHR下载
XHR可传输文本数据、二进制数据。浏览器会自动为各种原生数据类型提供编码和解码服务,应用在直接将这些数据传给XHR时就已经编码/解码好了,反之亦然。浏览器可自动解码的数据类型如下:
- ArrayBuffer:固定长度的二进制数据缓冲区;
- Blob:二进制大对象或不可变数据;
- Document:解析后得到的HTML或XML文档;
- JSON:表示简单数据结构的JS对象;
- Text:简单的文本字符串。
浏览器可依靠HTTP的content-type
首部来推断数据类型,应用也可在发起XHR请求时显式重写数据类型。
Blob是HTML5的File API,就像一个不透明的引用,可指向任何数据块(二进制或文本)。只能查询其大小、MIME类型,或将它切分成更小的块,用于提供一种JS API之间的高效的互操作机制。
XHR上传
通过XHR上传任何类型的数据都很简单且高效:
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };
xhr.send("text string");// 发送字符串
XHR对象的send()方法可接受DOMString、Document、FormData、Blob、File及ArrayBuffer对象,并自动完成相应的编码,设置适当的HTTP内容类型(content-type),然后再分派请求。
XHR不支持请求流,这意味着在调用send()时必须提供完整的文件。一种解决方案:切分文件,然后通过多个XHR请求分段上传。
监控下载和上传进度
网络连接可能会间歇性中断,而延迟和带宽也高度不稳定。XHR对象提供一个方便的API,用于监控进度事件,可代表请求的当前状态。
事件类型 | 说明 | 触发次数 |
---|---|---|
loadstart | 传输已开始 | 一次 |
progress | 正在传输 | 零或多次 |
error | 传输出错 | 零或多次 |
abort | 传输终止 | 零或多次 |
load | 传输成功 | 零或多次 |
loadend | 传输完成 | 一次 |
每个XHR请求开始时都会触发loadstart事件,而结束时都会触发loadend事件。要监控进度,可以在XHR对象上注册一系列JavaScript事件监听器
var xhr = new XMLHttpRequest();
xhr.open('GET', '/resource');
xhr.timeout = 5000; // 设置请求的超时时间为5000ms,默认无超时限制
xhr.addEventListener('load', function() { ... }); // 为请求成功注册回调
xhr.addEventListener('error', function() { ... });
var onProgressHandler = function(event) {
if (event.lengthComputable) {
var progress = (event.loaded / event.total) * 100;
}
}
xhr.upload.addEventListener('progress', onProgressHandler); // 为上传进度事件注册回调
xhr.download.addEventListener('progress', onProgressHandler);
xhr.send();
解读:load和error被触发,代表XHR传输的最终状态;progress事件则可能触发任意多次,这就为监控传输状态提供便利:可比较loaded与total属性,估算传输完成的数据比例。
要估算传输完成的数据量,服务器必须在其响应中提供内容长度(Content-Length)首部。分块数据则无法估计进度。
流式数据传输
XHR2规范提供读取服务器部分响应的能力,但效率却很低,且有诸多限制。
XHR加Streams API可让浏览器支持高效的XHR流。通过XHR实现流式上传还不行,通过XHR实现流式下载却得到浏览器有限支持。
SSE提供方便的流API,用于从服务器向客户端发送文本数据;WebSocket提供高效双向的流机制,同时支持二进制和文本数据。
Firefox和IE都提供定制的XHR流扩展:
- Firefox支持
moz-chunked-text
和moz-chunked-arraybuffer
; - IE支持
ms-stream
。
通过将XHR对象上的responseType属性设置为前述数据类型,这两个浏览器就可以不缓冲整个响应,同时允许通过XHR对象递增地读取二进制响应。
实时通知与交付
客户端和服务端交互:
- 必要时,客户端可向服务器发送一个XHR请求,以更新服务器上的相应数据;
- 服务器的数据更新,主动通知客户端,不容易。
HTTP没有提供服务器向客户端发起连接的方式。
解决方法:
- 客户端轮询服务端;
- 利用流式传输让服务器推送通知。
XHR轮询
轮询间隔:长轮询间隔意味着延迟交付,而短轮询间隔会导致客户端与服务器间不必要的流量和协议开销。
最佳的轮询间隔取决于应用,而且始终都会存在关于效率和消息延迟的权衡。
轮询最适合间隔时间长,新事件到达时间有规律,且传输数据量大的场景。这个组合可以抵消多余的HTTP开销,并将消息交付的延迟最小化。
XHR长轮询
长轮询:在没更新时不再返回空响应,而是把连接保持到有更新时。
Comet:利用长时间保留的HTTP请求(挂起的GET)来让服务器向浏览器推送数据的技术,也叫保留AJAX、AJAX推送、HTTP推送。
长轮询解决消息交付延迟的问题,消灭空检查,减少XHR请求次数和轮询的整体开销。
长轮询不一定总是比轮询更好。
实践中,并不是所有更新都具有相同的优先级或者延迟要求。可考虑采取混合策略:在服务器上累积低优先级的更新,同时对高优先级消息则立即触发更新。
使用场景及性能
XHR实现异步通信,分派和控制HTTP请求只要几行JS代码,浏览器包办各种复杂任务:
- 浏览器格式化HTTP请求并解析响应;
- 浏览器强制施加相关的安全(同源)策略;
- 浏览器处理内容协商(如gzip压缩);
- 浏览器处理请求和响应的缓存;
- 浏览器处理认证、重定向;
- 等
局限:
- XHR不适合流式数据处理;
- XHR实时交付更新有延迟或高开销。
服务器发送事件
Server-Sent Events,SSE。SSE设计两个组件,使得服务器可向客户端流式发送文本消息(比如服务器上生成的实时通知或更新):
- 浏览器中的EventSource:可让客户端以DOM事件的形式接收到服务器推送的通知
- 新的事件流数据格式:用于交付每一次更新。
这两者使SSE成为在浏览器中处理实时数据的高效而不可或缺的工具:
- 通过一个长连接低延迟交付;
- 高效的浏览器消息解析,不会出现无限缓冲;
- 自动跟踪最后看到的消息及自动重新连接;
- 消息通知在客户端以DOM事件形式呈现。
实际上,SSE提供的是一个高效、跨浏览器的XHR流实现,消息交付只使用一个长HTTP连接。与自己实现XHR流不同,浏览器会管理连接、解析消息,从而让开发者只关注业务逻辑。
API
使用实例:
var source = new EventSource("/path/to/stream-url"); // 打开到流终点的SSE连接
source.onopen = function () { ... }; // 可选回调,建立连接时调用
source.onerror = function () { ... }; // 可选回调,连接失败时调用
source.addEventListener("foo", function (event) {
processFoo(event.data);
});
// 监听所有事件,不明确指定事件类型
source.onmessage = function (event) {
log_message(event.id, event.data);
if (event.id== "CLOSE") {
// 如果服务器发送"CLOSE"消息ID,关闭SSE连接
source.close();
}
}
EventSource可以像常规XHR一样利用CORS许可及选择同意机制,实现客户端到远程服务器的流式事件数据传输。
SSE实现节省内存的XHR流。与原始的XHR流在连接关闭前会缓冲接收到的所有响应不同,SSE连接会丢弃已经处理过的消息,而不会在内存中累积。
EventSource接口还能自动重新连接并跟踪最近接收的消息:如果连接断开,EventSource会自动重新连接到服务器,还可以向服务器发送上一次接收到的消息ID(基于事件流协议),以便服务器重传丢失的消息并恢复流。
Event Stream协议
SSE事件流是以流式HTTP响应形式交付的:客户端发起常规HTTP请求,服务器以自定义的text/event-stream
内容类型响应,然后交付UTF-8编码的事件数据。
事件流协议:
- 事件载荷就是一或多个相邻data字段的值;
- 事件可以带ID和event表示事件类型;
- 事件边界用换行符标识。
在接收端,EventSource接口通过检查换行分隔符来解析到来的数据流,从data字段中提取有效载荷,检查可选的ID和类型,最后再分派一个DOM事件告知应用。如果存在某个类型,那么就会触发自定义的DOM事件处理程序;否则,就会调用通用的onmessage回调。
不支持二进制传输是有意为之的。SSE的设计目标是简单、高效,作为一种服务器向客户端传送文本数据的机制。如果你想传输二进制数据,WebSocket才是更合适的选择。
除了自动解析事件数据,SSE还内置支持断线重连,恢复客户端因断线而丢失的消息。默认情况下,如果连接中断,浏览器会自动重新连接。SSE规范建议的间隔时间是2~3秒,这也是大多数浏览器采用的默认值。服务器也可设置一个自定义间隔时间,只要在推送任何消息时向客户端发送一个retry命令即可。
根据应用的要求和数据流,服务器可以采取不同的实现策略:
- 如果丢失消息可接受,就不需要事件ID或特殊逻辑,只要让客户端重连并恢复数据流即可;
- 如果必须恢复消息,则服务器就需要指定相关事件的ID,以便客户端在重连时报告最后接收到的ID。同样,服务器也需要实现某种形式的本地缓存,以便恢复并向客户端重传错过的消息。
使用场景及性能
SSE是服务器向客户端发送实时文本消息的高性能机制:服务器可以在消息刚刚生成就将其推送到客户端(低延迟),使用长连接的事件流协议,还可gzip压缩(低开销),浏览器负责解析消息,也没有无限缓冲。还有超级简单的EventSource API能自动重新连接和把消息通知作为DOM事件。
SSE的两个局限:
- 只能从服务器向客户端发送数据,不能满足需要请求流的场景(比如向服务器流式上传大文件);
- 事件流协议设计为只能传输UTF-8数据,即使可以传输二进制流,效率也不高。
UTF-8的限制可在应用层克服。
实时推送就像轮询一样,可能会极大影响电池的待机时间。首先,可以考虑批量处理消息,尽量少唤醒无线电模块。其次,避免不必要的长连接,SSE连接在无线电空闲时不会断开。
WebSocket
WebSocket可实现客户端与服务器间双向的基于消息的文本或二进制数据传输。它是浏览器中最靠近套接字的API。浏览器基于WebSocket提供的服务:
- 连接协商和同源策略;
- 与既有HTTP基础设施的互操作;
- 基于消息的通信和高效消息分帧;
- 子协议协商及可扩展能力。
自定义数据交换协议的问题通常也在于自定义。因为应用必须考虑状态管理、压缩、缓存及其他原来由浏览器提供的服务。设计限制和性能权衡始终会有,利用WebSocket也不例外。WebSocket并不能取代HTTP、XHR或SSE,而为了追求最佳性能,关键还是要利用这些机制的长处。
WebSocket由多个标准构成:WebSocket API是W3C定义的,而WebSocket协议(RFC 6455)及其扩展则由HyBi Working Group(IETF)定义。
API
WebSocket API使用示例:
// 打开新的安全WebSocket连接(wss)
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);
}
}
WS与WSS
WebSocket资源URL采用自定义模式:ws表示纯文本通信,wss表示使用加密信道通信(TCP+TLS)。
WebSocket的连接协议也可用于浏览器之外的场景,可通过非HTTP协商机制交换数据。所以自定义URL协议。
接收文本和二进制数据
ArrayBuffer表示一个普通的、固定长度的二进制数据缓冲。可用ArrayBuffer创建一或多个ArrayBufferView对象,每一个都可通过特定格式来展示缓冲中的内容。在取得这个类型的ArrayBuffer对象后,可以对同一个缓冲创建多个不同的视图,每个视图的偏移量和数据类型都可以不一样。每个视图都以父缓冲、开始字节偏移量和要处理的元素数作为参数,其中偏移量根据之前字段的大小计算。ArrayBuffer和WebSocket实际上为开发者在浏览器中处理二进制数据提供所有必要的工具。
发送文本和二进制数据
WebSocket提供双向通信信道,即在同一个TCP连接上,可双向传输数据。
send()方法是异步的,查询套接字的bufferedAmount属性,监控在浏览器中排队的数据量,来获取数据发送状态。
所有WebSocket消息都会按照它们在客户端排队的次序逐个发送。大量排队的消息,甚至一个大消息,都可能导致排在它后面的消息延迟:队首阻塞。
为解决这个问题,应用可以将大消息切分成小块,通过监控bufferedAmount的值来避免队首阻塞。甚至还可以实现自己的优先队列,而不是盲目都把它们送到套接字上排队。
子协议协商
WebSocket协议对每条消息的格式事先不作任何假设:仅用一位标记消息是文本还是二进制,以便客户端和服务器有效地解码数据,而除此之外的消息内容就是未知的。
如果需要沟通关于消息的元数据,客户端和服务器必须达成沟通这一数据的子协议。
与WebSocket消息的灵活性和低延迟对应的,就是应用逻辑必须复杂一点。
如果子协议协商成功,就会触发客户端的onopen回调,应用可查询WebSocket对象上的protocol属性,从而得知服务器选定的协议。反之,服务器如果不支持客户端声明的任何一个协议,则WebSocket握手是不完整的,此时会触发onerror回调,连接断开。
协议
HyBi Working Group制定的WebSocket通信协议(RFC 6455)包含两个高层组件:
- 开放性HTTP握手用于协商连接参数;
- 二进制消息分帧机制用于支持低开销的基于消息的文本和二进制数据传输。
WebSocket协议是一个独立完善的协议,可在浏览器之外实现。
二进制分帧层
客户端和服务器WebSocket应用通过基于消息的API通信:发送端提供任意UTF-8或二进制的净荷,接收端在整个消息可用时收到通知。使用自定义二进制分帧格式,把每个消息切分成多个帧,发送到目的地之后再组装起来,等到接收到完整消息后再通知接收端。
两个概念:
- 帧:最小的通信单位,包含可变长度的帧首部和净荷部分,净荷可能包含完整或部分应用消息;
- 消息:一系列帧,与应用消息对等。
是否把消息分帧由客户端和服务器实现决定,应用层可以不关心。但提到性能优化,得理解帧:
- 每一帧的第一位(FIN)表示当前帧是不是消息的最后一帧。一条消息有可能只对应一帧。
- 操作码(4位)表示被传输帧的类型:传输应用数据时,是文本(1)还是二进制(2);连接有效性检查时,是关闭(8)、呼叫(ping,9)还是回应(pong,10)。
- 掩码位表示净荷是否有掩码(只适用于客户端发送给服务器的消息)。
- 净荷长度由可变长度字段表示:
- 如果是0~125,就是净荷长度;
- 如果是126,则接下来2字节表示的16位无符号整数才是这一帧的长度;
- 如果是127,则接下来8字节表示的64位无符号整数才是这一帧的长度。
- 掩码键包含32位值,用于给净荷加掩护。
- 净荷包含应用数据,如果客户端和服务器在建立连接时协商过,也可以包含自定义的扩展数据。
所有客户端发送帧的净荷都要使用帧首部中指定的值加掩码,这样可以防止客户端中运行的恶意脚本对不支持WebSocket的中间设备进行缓存投毒攻击(cache poisoning attack,参考DNS-cache-poisoning/)。
服务器发送的每个WebSocket帧会产生2~10
字节分帧开销,客户端必须发送掩码键,会增加4字节,结果就是6~14字节
开销。此外,没有其他元数据(如首部字段或其他关于净荷的信息):所有WebSocket通信都是通过交换帧实现的,而帧将净荷视为不透明的应用数据块。
队首阻塞:WebSocket消息可能会被分成一或多个帧,但不同消息的帧不能交错发送,因为没有与HTTP 2.0分帧机制中流ID对等的字段。
WebSocket不支持多路复用,每个WebSocket连接都需要一个专门的TCP连接。
协议扩展
数据格式和WebSocket协议的语义可以通过新的操作码和数据字段扩展。
HyBi Working Group进行两项扩展:
- 多路复用扩展(Multiplexing Extension for WebSockets)可将WebSocket的逻辑连接独立出来,实现共享底层的TCP连接;使用
信道ID
扩展每个WebSocket帧,从而实现多个虚拟WebSocket信道
共享一个TCP连接。 - 压缩扩展(Compression Extensions for WebSocket)增加压缩功能,相当于HTTP的传输编码协商。
压缩扩展有两种方式:
- 逐帧压缩:以帧为单位,对被分成多个帧的大消息不友好;
- 消息压缩:以消息为单位。
要使用扩展,客户端必须在第一次的Upgrade握手中通知服务器,服务器必须选择并确认要在商定连接中使用的扩展。
HTTP升级协商
WebSocket协议提供很多强大的特性:基于消息的通信、自定义二进制分帧层、子协议协商、可选的协议扩展等。
WebSocket可利用HTTP完成握手;可重用并扩展HTTP的Upgrade流,为其添加自定义WebSocket首部以完成协商:
Sec-WebSocket-Version
:客户端发送,表示想使用的WebSocket协议版本(RFC 6455)。如果服务器不支持这个版本,必须回应自己支持的版本;Sec-WebSocket-Key
:客户端发送,自动生成的一个键,以验证服务器支持请求的协议版本;Sec-WebSocket-Accept
:服务器响应,包含Sec-WebSocket-Key
的签名值,证明它支持请求的协议版本;Sec-WebSocket-Protocol
:用于协商应用子协议:客户端发送支持的协议列表,服务器必须只回应一个协议名;Sec-WebSocket-Extensions
:用于协商本次连接要使用的WebSocket扩展:客户端发送支持的扩展,服务器通过返回相同的首部确认自己支持一或多个扩展。
所有兼容RFC 6455的WebSocket服务器都使用相同的算法计算Sec-WebSocket-Accept
:将Sec-WebSocket-Key
内容与标准定义的唯一GUID字符串拼接起来,计算出SHA1散列值,结果是一个base64编码字符串,把这个字符串发给客户端即可。
一个成功的WebSocket握手必须是客户端发送协议版本和自动生成的Sec-WebSocket-Key
,服务器返回101HTTP响应码(Switching Protocols)和散列形式的Sec-WebSocket-Accept
,确认选择的协议版本:
- 客户端必须发送
Sec-WebSocket-Version
和Sec-WebSocket-Key
; - 服务器必须返回
Sec-WebSocket-Accept
确认协议; - 客户端可以通过
Sec-WebSocket-Protocol
发送应用子协议列表; - 服务器必须选择一个子协议并通过
Sec-WebSocket-Protocol
返回协议名;如果服务器不支持任何一个协议,连接断开。 - 客户端可以通过
Sec-WebSocket-Extensions
发送协议扩展; - 服务器可以通过
Sec-WebSocket-Extensions
确认一或多个扩展;如果服务器没有返回扩展,则连接不支持扩展。
如果HTTP中间设备不理解WebSocket协议,则可能导致各种问题:盲目的连接升级、意外缓冲WebSocket帧、不明就里地修改内容、把WebSocket流量误当作不完整的HTTP通信。
解决方法:在执行HTTP Upgrade握手之前,先协商一次TLS会话,使用WSS建立一条端到端的安全通道。
使用场景及性能
WebSocket不能取代XHR或SSE。
请求和响应流
WebSocket是唯一一个能通过同一个TCP连接实现双向通信的机制,客户端和服务器随时可以交换数据。WebSocket在两个方向上都能保证文本和二进制应用数据的低延迟交付。
解读:
- XHR是专门为“事务型”请求/响应通信而优化的:客户端向服务器发送完整的、格式良好的HTTP请求,服务器返回完整的响应。这里不支持请求流,在Streams API可用之前,没有可靠的跨浏览器响应流API;
- SSE可以实现服务器到客户端的高效、低延迟的文本数据流:客户端发起SSE连接,服务器使用事件源协议将更新流式发送给客户端。客户端在初次握手后,不能向服务器发送任何数据。
把传输机制从XHR切换为SSE或WebSocket并不会减少客户端与服务器间的往返次数!不管什么传输机制,数据包的传播延迟都一样。
排队延迟:消息在被发送给另一端之前必须在客户端或服务器上等待的时间。
XHR轮询:排队延迟就是客户端轮询间隔,服务器上的消息可用之后,必须等到下一次客户端XHR请求才能发送;
SSE和WebSocket使用持久连接,服务器(和客户端,如果是WebSocket)就可在消息可用时立即发送它。
消息开销
建立WebSocket连接后,应用消息会被拆分为一或多个帧,每个帧会添加2~14
字节开销。分帧按照自定义二进制格式完成,UTF-8和二进制应用数据可有效地通过相同的机制编码。不包括IP、TCP和TLS分帧开销;无论使用什么协议,TLS一共会给每个消息增加60~100
字节。
对比:
- SSE:会给每个消息添加5字节,但仅限于UTF-8内容;
- HTTP 1.x:包括XHR及其他常规请求,会携带500~800字节的HTTP元数据,加上cookie;
- HTTP 2.0:可压缩HTTP元数据。如果请求都不修改首部,开销可低至8字节。
数据效率及压缩
XHR请求可协商最优的传输编码格式(如对文本数据采用gzip压缩)。SSE局限于UTF-8文本数据,事件流数据可在整个会话期间使用gzip压缩。
WebSocket可传输文本和二进制数据,不同类型的数据得分开压缩;二进制的净荷也可能已压缩过。因此,WebSocket必须实现自己的压缩机制,并针对不同的消息类型选择不同的压缩机制。
HyBi工作组正在为WebSocket协议制定以消息为单位的压缩扩展。
Chrome和某些WebKit浏览器支持WebSocket协议压缩扩展的老版本(以帧为单位压缩)。
自定义应用协议
部署WebSocket基础设施
三个方面:
- 位于各自网络中的路由器、负载均衡器和代理;
- 外部网络中透明、确定的代理服务器(如ISP和运营商的代理);
- 客户网络中的路由器、防火墙和代理。
性能检查表
部署高性能的WebSocket服务要求细致地调优和考量,无论在客户端还是在服务器上。可参考下列要点:
- 使用安全WebSocket(基于TLS的WSS)实现可靠的部署;
- 密切关注腻子脚本的性能(如果使用腻子脚本);
- 利用子协议协商确定应用协议;
- 优化二进制净荷以最小化传输数据;
- 考虑压缩UTF-8内容以最小化传输数据;
- 设置正确的二进制类型以接收二进制净荷;
- 监控客户端缓冲数据的量;
- 切分应用消息以避免队首阻塞;
- 合用的情况下利用其他传输机制。
在移动端使用WebSocket时,要注意以下问题:
- 节约用电;
- 消除周期性及无效的数据传输;
- 内格尔及有效的服务器推送;
- 消除不必要的长连接。