XMLHttpRequest(XHR)是浏览器级别的API,使客户端可以通过JavaScript编写数据传输脚本。 XHR在Internet Explorer 5中首次亮相,成为异步JavaScript和XML(AJAX)革命背后的关键技术之一,并且现在已成为几乎所有现代Web应用程序的基本构建块。
XMLHTTP改变了一切。 它将“ D”放入DHTML。 它使我们能够异步地从服务器获取数据并保留客户端上的文档状态…Outlook Web Access(OWA)团队渴望在浏览器中构建丰富的Win32之类的应用程序,从而将该技术引入了IE,从而使AJAX成为现实。
Jim Van Eaton, Outlook Web Access: A catalyst for web evolution
在XHR之前,必须刷新网页才能发送或获取客户端和服务器之间任何的状态更新。 使用XHR,可以在应用程序JavaScript代码的完全控制下异步完成此工作流程。 XHR实现了从构建页面到在浏览器中构建交互式Web应用程序的飞跃。
但是,XHR的强大功能不仅在于它启用了浏览器内的异步通信,还在于使其变得简单。 XHR是浏览器提供的应用程序API,也就是说,浏览器会自动处理所有低级连接管理、协议协商(protocol negotiation)、格式化HTTP请求等等:
- 浏览器管理连接的建立,池化和终止。
- 浏览器确定最佳的HTTP(S)传输(HTTP / 1.0、1.1、2)。
- 浏览器处理HTTP缓存,重定向和内容类型协商(content-type negotiation)。
- 浏览器强制执行安全性,身份验证和隐私约束。
- …
我们的应用程序无需担心所有底层细节,而可以专注于业务逻辑,这些逻辑包括发起请求、管理请求的进度、以及处理服务器返回的数据。简单的API结合它在所有浏览器中无处不在的可用性,使XHR成为浏览器中联网的“瑞士军刀”。
因此,几乎每个网络用例(脚本下载,上传,流传输,甚至实时通知)都可以并且已经建立在XHR之上。 当然,这并不意味着XHR在每种情况下都是最有效的传输方式,实际上,正如我们将要看到的那样,它远非如此,但这仍然经常被用作老客户的后备传输方式,后者可能无法访问到更新的浏览器网络API。 考虑到这一点,让我们仔细研究XHR的最新功能,其用例以及性能的优缺点。
完整的XHR API及其功能的详尽分析超出了我们的讨论范围——我们的重点是性能!有关XMLHttpRequest API的概述,请参考正式的W3C标准。
XHR简史
尽管有XHR的名字,但它从未打算专门与XML绑定。 XML前缀显露了将第一个版本的XHR作为Internet Explorer 5中MSXML库的一部分发布的痕迹:
这是过去的美好日子,关键功能在发布前几天就塞满了……我意识到IE附带了MSXML库,并且我与XML团队有一些良好的联系,他们可能会帮上忙,我与当时负责该团队的Jean Paoli取得了联系,我们很快达成了一项协议,将其作为MSXML库的一部分进行发布。真正解释了XMLHTTP名称的来源是——它主要是关于HTTP的,撇开为了一起发布我因此需要将XML塞入名称中,它XML没有任何特定的关系。
Alex Hopmann, The story of XMLHTTP
Mozilla对照Microsoft建模了自己的XHR实现,并通过XMLHttpRequest接口公开了它。 随后Safari,Opera和其他浏览器的效仿,让XHR成为所有主流浏览器中事实上的标准,因此它的名称就此定型。 实际上,XHR的正式W3C工作草案规范只是在XHR广泛使用之后才于2006年发布的!
但是,尽管XHR早已流行并且在AJAX革命中发挥了关键作用,但其早期版本提供的功能有限:仅基于文本的数据传输、对上传的支持有限并且无法处理跨域请求。 为了解决这些缺点,2008年发布了“ XMLHttpRequest Level 2”草案,其中添加了许多新功能:
- 支持请求超时
- 支持二进制和基于文本的数据传输
- 支持媒体类型的应用程序覆盖和响应编码
- 支持监视每个请求的进度事件
- 支持有效的文件上传
- 支持安全的跨域请求
在2011年,“ XMLHttpRequest Level 2”规范与原始XMLHttpRequest工作草案合并。 因此,尽管您经常会找到对XHR版本或Level 1和Level 2的引用,但这些区别不再相关; 今天,只有一个统一的XHR规范。 实际上,所有新的XHR2特性和功能都是通过相同的XMLHttpRequest API提供的:相同的接口,更多的功能。
现在,所有现代浏览器都支持XHR2的新功能。参见caniuse.com/xhr2。 因此,无论何时提及XHR,我们都隐含地提及XHR2标准。
跨域资源共享(Cross-Origin Resource Sharing)
XHR是浏览器级别的API,可自动处理大量的底层细节,例如缓存、处理重定向、内容协商(content negotiation)、身份验证等等。 这具有双重目的。 首先,它使应用程序API更加易于使用,使我们能够专注于业务逻辑。 但是,其次,它允许浏览器对应用程序代码进行沙箱操作并强制实施一组安全性和策略约束。
XHR接口对每个请求强制执行严格的HTTP语义:应用程序提供数据和URL,浏览器格式化请求并处理每个连接的整个生命周期。 类似地,尽管XHR API允许应用程序添加自定义HTTP标头(通过setRequestHeader()方法),但仍有许多受保护的标头对应用程序代码不可用:
- Accept-Charset, Accept-Encoding, Access-Control-*
- Host, Upgrade, Connection, Referer, Origin
- Cookie,Sec-*,Proxy- *和其他十几个…
浏览器将拒绝覆盖任何不安全的标头,以确保应用程序无法模拟伪造的用户代理、用户或发出请求的来源。 实际上,保护源头(origin header)尤其重要,因为这是应用于所有XHR请求“同源策略”的关键部分。
“来源”(origin)定义为应用程序协议、域名和端口号的三元组,例如(http,example.com,80)和(https,example.com,443)被认为是不同的来源。 有关更多详细信息,请参见Web Origin概念。
同源策略的动机很简单:浏览器存储用户数据,例如身份验证令牌,cookie和其他私有元数据,这些数据不能在不同的应用程序之间泄漏—例如,如果没有相同的源沙箱,则example.com中的任意脚本可以访问和操纵thirdparty.com上的用户数据!
为了解决此特定问题,XHR的早期版本仅限于同源请求,其中请求的源(origin)必须与被请求资源的源(origin)匹配:从example.com启动的XHR只能请求另一相同 example.com源(origin)的资源。或者,如果相同的源这一前提条件不符合,则浏览器将简单地拒绝发起XHR请求并引发错误。
但是,在必要时,同源策略也对XHR的实用性施加了严格的限制:如果服务器想为运行在不同源的脚本提供资源怎么办?这就是“跨源资源共享”(CORS)出现的地方! CORS为客户端跨域请求提供了一种安全的选择加入机制(opt-in mechanism):
// script origin: (http, example.com, 80)
var xhr = new XMLHttpRequest();
xhr.open('GET', '/resource.js'); /**1**/
xhr.onload = function() { ... };
xhr.send();
var cors_xhr = new XMLHttpRequest();
cors_xhr.open('GET', 'http://thirdparty.com/resource.js'); /**2**/
cors_xhr.onload = function() { ... };
cors_xhr.send();
/**
1**
/同源XHR请求
/**
2**
/跨域XHR请求
CORS请求使用相同的XHR API,唯一的区别是所请求资源的URL与执行脚本的来源不同。在上一示例中,脚本是从(http,example.com, 80)执行的,第二个XHR请求从(http,thirdparty.com,80)访问resource.js。
针对CORS请求的选择加入身份验证机制(The opt-in authentication mechanism)在较低层进行处理:发出请求时,浏览器会自动附加受保护的Origin HTTP标头,该标头会通告发出请求的来源。 然后,远程服务器可以通过在响应中返回Access-Control-Allow-Origin标头来检查Origin标头并确定是否应允许该请求:
=> Request
GET /resource.js HTTP/1.1
Host: thirdparty.com
Origin: http://example.com /**1**/
...
<= Response
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://example.com /**2**/
...
/**
1**
/源标头(Origin header)由浏览器自动设置
/**
2**
/选择加入标头(Opt-in header)由服务器设置
在前面的示例中,thirdparty.com决定通过在响应中返回适当的访问控制标头来选择与example.com进行跨域资源共享。 另外,如果要禁止访问,可以简单地省略Access-Control-Allow-Origin标头,然后客户端的浏览器会自动让发送的请求失败。
如果第三方服务器不支持CORS,则客户端请求将失败,因为客户端始终会验证选择加入标头(the opt-in header)的存在。 作为一种特殊情况,CORS还允许服务器返回通配符(Access-Control-Allow-Origin:),以表示它允许从任何来源进行访问。 但是,在启用此策略之前,请三思!*
这样,我们都完成了,对吗? 事实并非如此,因为CORS采取了许多额外的安全预防措施来确保服务器支持CORS:
- CORS请求忽略了用户凭据,例如cookie和HTTP身份验证。
- 客户端只能发出“简单的跨域请求”,这对允许的请求方法(GET,POST,HEAD)以及对XHR可以发送和读取的HTTP标头的访问进行了限制。
若要启用cookie和HTTP身份验证,客户端在发出请求时必须在XHR对象上设置一个额外的属性(withCredentials),并且服务器还必须以适当的标头(Access-Control-Allow-Credentials)进行响应以表明它是故意允许该应用程序包含私人用户数据。 类似地,如果客户端需要编写或读取自定义HTTP标头,或者想要对该请求使用“非简单方法”,则它必须首先通过发出预检请求(Preflight request)来请求第三方服务器的许可:
=> Preflight request
OPTIONS /resource.js HTTP/1.1 /**1**/
Host: thirdparty.com
Origin: http://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: My-Custom-Header
...
<= Preflight response
HTTP/1.1 200 OK /**2**/
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: My-Custom-Header
...
(actual HTTP request) /**3**/
/**
1**
/预检请求OPTIONS以验证权限
/**
2**
/来自第三方源的成功预检响应
/**
3**
/实际的CORS请求
正式的W3C CORS规范定义了必须在何时何地使用预检请求:“简单”请求可以跳过它,但是有多种条件会触发该请求,并添加最少的往返网络延迟来验证权限。 好消息是,一旦发出预检请求,客户端就可以对其进行缓存,以避免对每个请求进行相同的验证。
所有现代浏览器均支持CORS;参见caniuse.com/cors。想要深入了解各种CORS策略和措施,请参考W3C官方标准。
用XHR下载数据(Downloading Data with XHR)
XHR可以传输基于文本的数据和二进制数据。 实际上,浏览器为各种本地数据类型提供自动编码和解码,这允许应用程序将这些类型直接传递给XHR以进行正确编码,反之亦然,以使浏览器自动解码这些类型:
ArrayBuffer
定长二进制数据缓冲区
Blob
不可变数据的二进制大对象
Document
解析HTML或XML文档
JSON
代表简单数据结构的JavaScript对象
Text
一个简单的文本字符串
浏览器可以依靠HTTP内容类型协商(content-type negotiation)来推断适当的数据类型(例如,将application/json响应解码为JSON对象),或者应用程序可以在发起XHR请求时显式覆盖数据类型:
var xhr = new XMLHttpRequest();
xhr.open('GET', '/images/photo.webp');
xhr.responseType = 'blob'; /**1**/
xhr.onload = function() {
if (this.status == 200) {
var img = document.createElement('img');
img.src = window.URL.createObjectURL(this.response); /**2**/
img.onload = function() {
window.URL.revokeObjectURL(this.src); /**3**/
}
document.body.appendChild(img);
}
};
xhr.send();
/**
1**
/设置返回数据类型为blob
/**
2**
/从Blob创建唯一的对象URI并将其设置为图像源
/**
3**
/加载图片后释放对象URI
请注意,我们正在以原始格式传输图像资源,而不依赖于base64编码,并且在不依赖数据URI的情况下向页面添加了图像元素。 在JavaScript中处理接收到的二进制数据时,没有网络传输开销或编码开销! XHR API使我们可以直接通过JavaScript编写高效、动态的应用程序脚本,而不管数据类型如何。
Blob接口是HTML5 File API的一部分,并充当任意数据块(二进制或文本)的不透明引用。 就其本身而言,blob引用的功能有限:您可以查询其大小,MIME类型,并将其拆分为较小的blob。 但是,它的真正作用是充当各种JavaScript API之间的有效交换机制。
用XHR上传数据(Uploading Data with XHR)
对于所有数据类型,通过XHR上传数据既简单又有效。 实际上,代码是相同的,唯一的区别是,在XHR请求上调用send()时,我们还传入一个数据对象。 其余的由浏览器处理:
var xhr = new XMLHttpRequest();
xhr.open('POST','/upload');
xhr.onload = function() { ... };
xhr.send("text string"); /**1**/
var formData = new FormData(); /**2**/
formData.append('id', 123456);
formData.append('topic', 'performance');
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };
xhr.send(formData); /**3**/
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };
var uInt8Array = new Uint8Array([1, 2, 3]); /**4**/
xhr.send(uInt8Array.buffer); /**5**/
/**
1**
/将简单的文本字符串上传到服务器
/**
2**
/通过FormData API创建动态表单
/**
3**
/将multipart/form-data对象上传到服务器
/**
4**
/创建一个无符号8位整数的定型数组(ArrayBuffer)
/**
5**
/将字节块上传到服务器
XHR send()方法接受DOMString,Document,FormData,Blob,File或ArrayBuffer对象,自动执行适当的编码,设置适当的HTTP内容类型,并分派请求。 是否需要发送二进制Blob或上传用户提供的文件? 简单:获取对该对象的引用,并将其传递给XHR。 实际上,通过一些额外的工作,我们还可以将大文件拆分为较小的块:
var blob = ...; /**1**/
const BYTES_PER_CHUNK = 1024 * 1024; /**2**/
const SIZE = blob.size;
var start = 0;
var end = BYTES_PER_CHUNK;
while(start < SIZE) { /**3**/
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
xhr.onload = function() { ... };
xhr.setRequestHeader('Content-Range', start+'-'+end+'/'+SIZE); /**4**/
xhr.send(blob.slice(start, end)); /**5**/
start = end;
end = start + BYTES_PER_CHUNK;
}
/**
1**
/任意数据块(二进制或文本)
/**
2**
/将块大小设置为1 MB
/**
3**
/以1MB为增量迭代提供的数据
/**
4**
/确定上传的数据范围(开始-结束/总计)
/**
5**
/通过XHR上传1 MB的数据片
XHR不支持请求流,这意味着我们在调用send()时必须提供完整的有效负载。 但是,此示例说明了一个简单的应用程序变通方法:通过多个XHR请求将文件拆分并分块上传。 这种实现模式绝不是真正的请求流API的替代,但是对于某些应用程序来说,它是可行的解决方案。
上传切片大文件是一种在连接不稳定或间歇性情况下提供更健壮的API的好技术,例如,如果某个块由于连接断开而失败,则应用程序可以重试,或者稍后再恢复上传,而不是从头开始重新启动完整传输 。
监控下载和上传进度(Monitoring Download and Upload Progress)
网络连接可以是间歇性的,并且延迟和带宽是高度可变的。 那么我们如何知道XHR请求是成功、超时还是失败? XHR对象提供了一个方便的API,用于监听进度事件(表15-1),这些事件显示了请求的当前状态。
事件类型 | 描述 | 触发次数 |
---|---|---|
loadstart | Transfer has begun | 1次 |
progress | Transfer is in progress | 0或1次 |
error | Transfer has failed | 0或1次 |
abort | Transfer is terminated | 0或1次 |
load | Transfer is successful | 0或1次 |
loadend | Transfer has finished | 1次 |
每次XHR传输均以loadstart开始,并以loadend事件结束,在此之间,将触发一个或多个其他事件以显示当前传输状态。 因此,要监视进度,应用程序可以在XHR对象上注册一组JavaScript事件侦听器:
var xhr = new XMLHttpRequest();
xhr.open('GET','/resource');
xhr.timeout = 5000; /**1**/
xhr.addEventListener('load', function() { ... }); /**2**/
xhr.addEventListener('error', function() { ... }); /**3**/
var onProgressHandler = function(event) {
if(event.lengthComputable) {
var progress = (event.loaded / event.total) * 100; /**4**/
...
}
}
xhr.upload.addEventListener('progress', onProgressHandler); /**5**/
xhr.addEventListener('progress', onProgressHandler); /**6**/
xhr.send();
/**
1**
/将请求超时设置为5,000毫秒(默认值:无超时)
/**
2**
/注册成功请求的回调
/**
3**
/注册失败请求的回调
/**
4**
/计算传输进度
/**
5**
/注册上传进度事件的回调
/**
6**
/注册下载进度事件的回调
load event或者error event将触发一次以显示XHR传输的最终状态,而进度事件(progress event)可以触发任意次,并提供了方便的API来跟踪传输状态:我们可以将事件的loaded属性与total属性进行比较以估算传输的数据量。
要估算传输的数据量,服务器必须在响应中提供内容长度:我们无法估算分块传输的进度,因为根据定义,响应的总大小是未知的。
同样,XHR请求没有默认超时,这意味着请求可以无限期“进行中”。 作为最佳实践,请始终为应用程序设置有意义的超时并处理错误!
用XHR流数据(Streaming Data with XHR)
在某些情况下,应用程序可能需要或希望以增量方式处理数据流,例如:数据流在客户端上可用时将其上传到服务器,或者处理从服务器下载的数据。 不幸的是,尽管这是一个重要的用例,但如今还没有用于XHR流的简单,高效,跨浏览器的API:
- send方法在上传的情况下期望完整的有效负载。
- response,responseText和responseXML属性不是为流(streaming)设计的。
在官方XHR规范中,流从来都不是官方用例。由于没有将上传文件手动拆分为较小的单个XHR,因此没有用于将数据流从客户端传输到服务器的API。 类似地,尽管XHR2规范确实提供了一些从服务器读取部分响应的能力,但实现效率低下并且非常有限。 那是个坏消息。
好消息是,地平线上有希望! 作为XHR的一流用例,缺乏流支持是一个公认的局限性,目前正在解决此问题:
Web应用程序应具有以多种形式获取和操作数据的能力,包括随着时间的推移提供的一系列数据。 该规范定义了Streams的基本表示形式,Streams引发的错误以及读取和创建Streams的编程方式。
W3C Streams API
XHR和Streams API的结合将在浏览器中实现高效的XHR流。 但是,Streams API仍在积极讨论中,尚未在任何浏览器中使用。 因此,我们陷入了困境,对吧? 好吧,不完全是。 正如我们前面提到的,不能选择使用XHR进行流式上传,但是我们对使用XHR进行流式下载提供了有限支持:
var xhr = new XMLHttpRequest();
xhr.open('GET', '/stream');
xhr.seenBytes = 0;
xhr.onreadystatechange = function() { /**1**/
if(xhr.readyState > 2) {
var newData = xhr.responseText.substr(xhr.seenBytes); /**2**/
// process newData
xhr.seenBytes = xhr.responseText.length; /**3**/
}
};
xhr.send();
/**
1**
/订阅状态和进度通知
/**
2**
/从部分响应中提取新数据
/**
3**
/更新已处理的字节偏移
该示例将在大多数现代浏览器中运行。 但是,性能不是很好,并且有很多实现方面的警告和陷阱:
- 请注意,我们正在手动跟踪可见字节的偏移量,然后手动切片数据:responseText正在缓冲完整的响应!对于较小的传输,这可能不是问题,但是对于较大的下载,尤其是在内存受限的设备(例如手机)上,这是一个问题。释放缓冲响应的唯一方法是完成请求并打开一个新请求。
- 只能从responseText属性中读取部分响应,这将我们限制为仅文本传输。无法读取二进制传输的部分响应。
- 读取部分数据后,我们必须确定消息边界:应用程序逻辑必须定义其自己的数据格式,然后对流进行缓冲和解析以提取单个消息。
- 浏览器在缓冲接收到的数据方面有不同的方式:某些浏览器可能会立即释放数据,而另一些浏览器可能会缓冲较小的响应并在较大的块中释放它们。
- 浏览器在允许增量式读取的内容类型方面有所不同,例如,某些浏览器允许使用“ text / html”,而其他浏览器只能使用“ application / x-javascript”。
简而言之,当前,XHR流既不高效也不方便,更糟糕的是,缺少通用规范也意味着不同浏览器的实现有所不同。 因此,至少在Streams API可用之前,XHR并不是流(Streaming)的理想选择。
无需绝望! 尽管XHR可能不符合该标准,但我们还有其他针对Streaming用例进行了优化的传输方式:服务器发送的事件提供了一种便捷的API,用于将基于文本的数据从服务器传输到客户端,而WebSocket为二进制和基于文本的数据都提供了有效和双向的Streaming。
XHR流的专有API和扩展
Firefox和Internet Explorer均提供自定义的“streaming XHR extensions”:
1.Firefox支持moz-chunked-text和moz-chunked-arraybuffer
2.Internet Explorer支持ms-stream
通过将XHR对象上的responseType属性设置为上述类型之一,两个浏览器都将避免缓冲完整响应,并且还将允许从
XHR对象以增量方式读取二进制响应。 不幸的是,Chrome,Opera或其他流行的浏览器中没有等效的API。因此,
对于跨浏览器应用程序,XHR streaming仍然是不切实际的传输。
实时通知和交付(Real-Time Notifications and Delivery)
XHR提供了一种简单有效的方式来将客户端与服务器同步:必要时,客户端会派发XHR请求来更新服务器上的适当数据。 但是,同样的问题反过来却要困难得多。 如果服务器上的数据已更新,服务器如何通知客户端?
HTTP不提供任何方式让服务器启动与客户端的新连接。 因此,要接收实时通知,客户端必须要么轮询服务器以获取更新,要么利用流传输来允许服务器在新通知可用时推送新通知。 不幸的是,正如我们在上一节中所看到的,对XHR流的支持是有限的,这使我们只能进行XHR轮询。
“实时”对于不同的应用程序具有不同的含义:某些应用程序需要毫秒级的开销,而另一些应用程序可能分钟级的延迟也无不可。 要确定最佳传输,请首先为应用程序定义明确的延迟和开销目标!
用XHR轮询(Polling with XHR)
从服务器获取更新的最简单策略之一是让客户端进行定期检查:客户端可以定期发起后台XHR请求(以轮询服务器)以检查更新。 如果服务器上有新数据可用,则在响应中返回它,否则响应为空。
轮询易于实现,但经常效率也很低。 轮询间隔的选择很关键:较长的轮询间隔会导致更新的延迟交付,而较短的间隔会导致不必要的流量以及客户端和服务器的高开销。 让我们考虑最简单的示例:
function checkUpdates(url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() { ... }; /**1**/
xhr.send();
}
setInterval(function() { checkUpdates('/updates') }, 60000); /**2**/
/**
1**
/处理从服务器收到的更新
/**
2**
/每60秒发出一个XHR请求
- 每个XHR请求都是一个独立的HTTP请求,平均而言,HTTP会为请求/响应标头带来约800字节的开销(没有HTTP cookie)。
- 当数据以可预测的间隔到达时,定期检查效果很好。 不幸的是,可预测的到达率是一个例外,不是正常现象。 因此,服务器上消息变为可用的与将消息传递到客户端之间,定期轮询将引入额外的延迟。
- 除非仔细考虑,否则轮询通常会成为在无线网络上代价高昂的性能反模式; 请参阅消除周期性和低效率的数据传输。 唤醒收音机会消耗大量电池电量!
最佳轮询间隔是多少? 没有准确答案。 频率取决于应用程序的需求,并且在效率和消息等待时间之间存在固有的权衡。 因此,轮询非常适合轮询间隔较长,新事件以可预测的速率到达且传输的有效负载较大的应用程序。 这种组合抵消了额外的HTTP开销,并使消息传递延迟最小化。
XHR轮询的性能建模
为了说明XHR轮询的延迟和开销之间的平衡,让我们考虑一个简单的电子邮件应用程序,该应用程序使用XHR轮询来
检查服务器上的邮件更新。 实现如下:
1.客户端每60秒发送一次XHR以检查更新。
2.每个XHR请求都包含客户端已知的最新消息ID。
3.服务器将客户端ID与它的消息列表进行比较。
4.服务器以新消息列表或空列表(无更新)作为响应。
平均消息延迟时间是多少?
如果在客户端检查更新之前刚好一条新消息到达服务器,则延迟是最小的--只是客户端和服务器之间的延迟。 相反
,相同的新消息可以在客户端检查之后的瞬间到达,在这种情况下,该消息将不得不等待下一次客户端检查(60秒)
。因此,如果消息到达速率是随机的,则消息将在客户端获得消息之前在服务器上平均等待30秒。
轮询的开销是多少?
平均HTTP 1.x请求会增加800字节的请求和响应开销(请参阅[测量和控制协议开销]
(https://hpbn.co/http1x/#measuring-and-controlling-protocol-overhead))。
因为客户端已登录,所以我们还将有一个额外的身份验证Cookie和消息ID; 假设这又增加了50个字节。 因此,不
返回任何新消息的请求将产生850个字节! 现在,假设我们有10,000个客户端,所有客户端的轮询间隔均为60秒:
每个客户端在每个请求上发送850字节的数据,这相当于每秒167个请求,并且服务器上的入口吞吐量的持续速率为
1.13 Mbps!这是一个恒定的比率,而实际上并未向任何客户端传递任何新消息。
30秒延迟是否太高? 我们可以减少轮询间隔,但是这样做会导致更高的吞吐量和开销:相同的10,000个客户端
(但间隔为1秒)将产生超过60 Mbps的吞吐量! 简而言之,除非轮询间隔长,否则轮询将很昂贵。
用XHR长轮询(Long-Polling with XHR)
定期轮询的挑战在于,有可能进行许多不必要的空检查。 考虑到这一点,如果我们对轮询工作流程进行了少许修改(图15-1):没有可用的更新时,不返回空响应,我们保持连接空闲直到有可用的更新,这会更有效吗?
利用长期保留的HTTP请求(“挂起的GET”)允许服务器将数据推送到浏览器的技术通常称为“ Comet”。 但是,您也可能以其他名称遇到它,例如“反向AJAX”,“ AJAX推送”和“ HTTP推送”。
通过保持连接开放直到有可用的更新(长轮询),在服务器上数据可用后就可以立即发送到客户端。 结果,长轮询提供了消息延迟的最佳方案,并且还消除了空检查,从而减少了XHR请求的数量和轮询的总体开销。 交付更新后,长轮询请求即告完成,客户端可以发出另一个长轮询请求,并等待下一条可用消息:
function checkUpdates(url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() { /**1**/
...
checkUpdates('/updates'); /**2**/
};
xhr.send();
}
checkUpdates('/updates'); /**3**/
/**
1**
/处理收到的更新并打开新的长轮询XHR
/**
2**
/发出长轮询请求以进行下一次更新(并永久循环)
/**
3**
/发出长轮询初始XHR请求
这样,长轮询相比定期轮询总是更好的选择吗? 除非知道消息到达速率并保持恒定,否则长轮询将始终提供更好的消息延迟。 如果这是主要标准,则长轮询将是赢家。
另一方面,开销讨论需要更细微的了解。 首先,请注意,每个传递的消息仍会产生相同的HTTP开销; 每个新消息都是一个独立的HTTP请求。 但是,如果消息到达率很高,则长轮询将发出比定期轮询更多的XHR请求!
长轮询通过最小化消息等待时间来动态地适应消息到达速率,这是您可能想要或可能不想要的行为。 如果对消息延迟有一定的容忍度,则轮询可能是一种更有效的传输方式,例如,如果更新速率较高,则轮询将提供简单的“消息聚合”机制,从而可以减少请求数量并延长手机的电池寿命。
实际上,并非所有消息都具有相同的优先级或延迟要求。 因此,您可能需要考虑多种策略:在服务器上聚合低优先级更新,并为高优先级更新触发立即交付; 请参阅Nagle和高效服务器推送。
通过XHR Long-Polling进行Facebook聊天
实际上,长轮询已成为通过XHR推送实时通知的最广泛使用的方法之一。尽管它可能不是最有效的传输方式,但它简
单,健壮,并且受任何兼容XHR的浏览器支持。在2008年这种方法首次部署在诸如Facebook的Chat之类的热门产品
“我们选择的一个用户从另一个用户处获得文本的方法包括在每个Facebook页面上加载一个iframe,并让该iframe
的JavaScript通过持久连接发出HTTP GET请求,直到服务器获得客户端数据后该请求才返回。如果请求被中断或
超时,则会重新建立该请求。 这绝不是一项新技术:它是Comet的一种变体,特殊的XHR长轮询、and/or BOSH。”
----Facebook Chat, Facebook Engineering Blog
今天,我们可以通过服务器发送事件和WebSocket更加有效地提供相同的功能。话虽如此,对于许多实时框架,
XHR仍然是通用的后备策略。 如果其他所有方法均失败,请进行长轮询以营救!
XHR用例和性能(XHR Use Cases and Performance)
XMLHttpRequest使我们实现了从构建页面到在浏览器中构建交互式Web应用程序的飞跃。 首先,它启用了浏览器内的异步通信,但同样重要的是,它还使过程变得简单。 调度和控制脚本化的HTTP请求仅需要几行JavaScript代码,浏览器将处理所有其余代码:
- 浏览器格式化HTTP请求并解析响应。
- 浏览器强制执行相关的安全性(同源)策略。
- 浏览器处理内容协商(例如gzip)。
- 浏览器处理请求和响应缓存。
- 浏览器处理身份验证,重定向等…
这样,对于遵循HTTP请求-响应周期的任何传输,XHR都是一种通用的高性能传输工具。是否需要获取需要身份验证的资源,应该在传输过程中对其进行压缩以及应该将其缓存以备将来使用?浏览器会处理所有这些以及更多的工作,使我们能够专注于应用程序逻辑!
但是,XHR也有其局限性。正如我们所看到的,流传输从来都不是XHR标准中的官方用例,而且支持也很有限:使用XHR进行流传输既不高效也不方便。不同的浏览器具有不同的行为,并且不可能进行有效的二进制流传输。简而言之,XHR不适合流式传输。
同样,通过XHR提供实时更新没有最佳的策略。定期轮询会导致高开销和消息延迟。长轮询提供了低延迟,但每条消息仍具有相同的开销;每个消息都是其自己的HTTP请求。为了同时具有低延迟和低开销,我们需要XHR流!
所以,尽管XHR是一种流行的“实时”交付机制,但它可能并不是工作表现最佳的运输方式。现代浏览器同时支持更简单和更有效的选项,例如服务器发送事件和WebSocket。因此,除非您有特定的原因要求进行XHR轮询,否则请使用它们。