JS红皮书读书笔记-21-Ajax与Comet

TOC
  1. 1. XMLHttpRequest对象
    1. 1.1. XHR的用法
    2. 1.2. Http头部信息
    3. 1.3. Get请求
    4. 1.4. Post请求
  2. 2. XMLHttpRequest2级
    1. 2.1. FormData
    2. 2.2. 超时设定
    3. 2.3. overrideMimeType方法
  3. 3. 进度事件
    1. 3.1. load事件
    2. 3.2. progress事件
  4. 4. 跨域资源共享CORS
    1. 4.1. IE对CORS的实现
    2. 4.2. 其他浏览器对CORS的实现
    3. 4.3. Preflighted Request
    4. 4.4. 带凭据的请求
    5. 4.5. 跨浏览器的CORS
  5. 5. 其他跨域技术
    1. 5.1. 图像ping
    2. 5.2. JSONP
    3. 5.3. Comet
    4. 5.4. 服务器发送事件
    5. 5.5. WebSockets
    6. 5.6. SSE 与WebSockets
  6. 6. 安全

Ajax,是对 Asynchronous JavaScript + XML 的简写。

Ajax 技术的核心是 XMLHttpRequest 对象(简称 XHR),这是由微软首先引入的一个特性,其他浏览器提供商后来都提供了相同的实现。在 XHR 出现之前,Ajax 式的通信必须借助一些 hack 手段来实现,大多数是使用隐藏的框架或内嵌框架。XHR 为向服务器发送请求和解析服务器响应提供了流畅的接口。能够以异步方式从服务器取得更多信息,意味若用户单击后,可以不必刷新页面也能取得新数据。也就是说,可以使用 XHR 对象取得新数据,然后再通过 DOM 将新数据插入到页面中。

另外,虽然名字中包含 XML 的成分,但 Ajax 通信与数据格式无关;这种技术就是无须刷新页面即可从服务器取得数据,但不一定是 XML 数据。

XMLHttpRequest对象

IE5 是第一款引入 XHR 对象的浏览器。在 IE5 中,XHR 对象是通过 MSXML 库中的一个 ActiveX对象实现的。因此,在 IE 中可能会遇到三种不同版本的 XHR 对象,即 MSXML2.XMLHttp 、MSXML2.XMLHttp.3.0 和 MXSML2.XMLHttp.6.0。

针对旧版IE:

//适用于 IE7 之前的版本
function createXHR(){
if (typeof arguments.callee.activeXString != "string"){
var versions = [
"MSXML2.XMLHttp.6.0",
"MSXML2.XMLHttp.3.0",
"MSXML2.XMLHttp"
], i, len;

for (i=0,len=versions.length; i < len; i++){
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex){

}
}
}
return new ActiveXObject(arguments.callee.activeXString);
}

主流浏览器:

var xhr = new XMLHttpRequest();

创建好了这个xhr对象, 才能进行后续的操作.

XHR的用法

在使用 XHR 对象时,要调用的第一个方法是 open(),它接受 3 个参数:要发送的请求的类型(”get”、”post”等)、请求的 URL 和表示是否异步发送请求的布尔值。下面就是调用这个方法的例子。

xhr.open("get", "example.php", false);

这行代码会启动一个针对 example.php 的 GET 请求。有关这行代码,需要说明两点:一是 URL 相对于执行代码的当前页面(当然也可以使用绝对路径);二是调用 open()方法并不会真正发送请求,而只是启动一个请求以备发送。


同步请求

要发送特定的请求,还必须像下面这样调用 send()方法:

xhr.open("get", "example.txt", false); //主要这个是同步请求
xhr.send(null); //如不需要发送数据则不用传参数, 但是有些浏览器会报错, 故统一传null做兼容

由于这次请求是同步的,JavaScript 代码会等到服务器响应之后再继续执行。在收到响应后,响应的数据会自动填充 XHR 对象的属性,相关的属性简介如下:

  • responseText:作为响应主体被返回的文本。
  • responseXML:如果响应的内容类型是”text/xml”或”application/xml”,这个属性中将保存包含若响应数据的 XML DOM 文档。
  • status:响应的 HTTP 状态。
  • statusText:HTTP 状态的说明。

同步请求常规用法:

var xhr  = new XMLHttpRequest();
xhr.open("GET","api.txt",false); // 同步请求
xhr.send(null);
if ( (xhr.status >=200 && xhr.status<300) || xhr.status == 304) { // 注意文明常用 xhr.status做检测, 比较靠谱
console.log(xhr.statusText)
}else{
console.log(xhr.status)
}
console.log('api.txt get') // 验证同步执行

除非你硬要这么做, 否则开发中是几乎用不到ajax的同步请求.


异步请求

多数情况下,我们还是要发送异步请求,才能让JavaScript 继续执行而不必等待响应。此时,可以检测 XHR 对象的 readyState 属性,该属性表示请求/响应过程的当前活动阶段。这个属性可取的值如下:

  • 0:未初始化。尚未调用 open()方法。
  • 1:启动。已经调用 open()方法,但尚未调用 send()方法。
  • 2:发送。已经调用 send()方法,但尚未接收到响应。
  • 3:接收。已经接收到部分响应数据。
  • 4:完成。已经接收到全部响应数据,而且已经可以在客户端使用了。

只要 readyState 属性的值由一个值变成另一个值,都会触发一次 readystatechange 事件。可以利用这个事件来检测每次状态变化后 readyState 的值。通常,我们只对 readyState 值为 4 的阶段感兴趣,因为这时所有数据都已经就绪。

不过,必须在调用 open()之前指定 onreadystatechange 事件处理程序才能确保跨浏览器兼容性。

function asyncFn(){
var xhr = new XMLHttpRequest();

// xhr.onreadystatechange为异步方法, 原本放在前面或者后面影响不大, 但是因为浏览器兼容, 建议放xhr.open前面
xhr.onreadystatechange = function(){ //注意这里采用的是DOM0的写法, 保证最大兼容性
if (xhr.readyState == 4){ // 没有用this代替xhr对象是因为兼容浏览器
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
console.log(xhr.responseText);
} else {
console.log("Request was unsuccessful: " + xhr.status);
}
}
};

xhr.open("get", "api.txt", true);
xhr.send(null);
//xhr.abort(); //如果有必要, 是可以再响应接受到之前取消这次异步请求.
}
asyncFn();

console.log('api.txt get') //证明它先输出,说明异步

Http头部信息

每个 HTTP 请求和响应都会带有相应的头部信息,其中有的对开发人员有用,有的也没有什么用。

XHR 对象也提供了操作这两种头部(即请求头部和响应头部)信息的方法。默认情况下,在发送 XHR 请求的同时,还会发送下列头部信息。

  • Accept:浏览器能够处理的内容类型。
  • Accept-Charset:浏览器能够显示的字符集。
  • Accept-Encoding:浏览器能够处理的压缩编码。
  • Accept-Language:浏览器当前设置的语言。
  • Connection:浏览器与服务器之间连接的类型。
  • Cookie:当前页面设置的任何 Cookie。
  • Host:发出请求的页面所在的域 。
  • Referer:发出请求的页面的 URI。注意,HTTP 规范将这个头部字段拼写错了,而为保证与规范一致,也只能将错就错了。(这个英文单词的正确拼法应该是 referrer。)
  • User-Agent:浏览器的用户代理字符串。

虽然不同浏览器实际发送的头部信息会有所不同,但以上列出的基本上是所有浏览器都会发送的。使用 xhr.setRequestHeader()方法可以设置自定义的请求头部信息。这个方法接受两个参数:头部字段的名称和头部字段的值。并且这个方法要在xhr.open和xhr.send之间使用:

xhr.onreadystatechange = function(){
if (xhr.readyState == 4){ // 没有用this代替xhr对象是因为兼容浏览器
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
console.log(xhr.responseText);
// 获取响应头
var hd = xhr.getResponseHeader('userName');
var hds = xhr.getAllResponseHeaders();
console.log(hds)
} else {
console.log("Request was unsuccessful: " + xhr.status);
}
}
};

xhr.open("get", "api.txt", true);
xhr.setRequestHeader('userName', 'testdog'); //注意必须在中间, 但是建议开发者不要使用已有的字段名称, 防止后端获取失败, 因为有些浏览器禁止这么做
xhr.send(null);

Get请求

// get请求要注意的就是URL后面参数的问题, 保险起见, 参数值和参数名最好要用encodeURIComponent编码再传参
xhr.open("get", "URL?v1=1&v2=2", true);

Post请求

与GET请求差异的是, POST请求更被期望于把数据作为请求的主体提交到后端.那么xhr.open的第一个参数自然就要改成”post”.

它可以传送任何你想发送到服务器的字符串.

默认情况下,服务器对 POST 请求和提交 Web 表单的请求并不会一视同仁。因此,服务器端必须有程序来读取发送过来的原始数据,并从中解析出有用的部分。不过,我们可以使用 XHR 来模仿表单提交:首先将 Content-Type 头部信息设置为 application/x-www-form-urlencoded,也就是表单提交时的内容类型,其次是以适当的格式创建一个字符串。

xhr.open("post",url,true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(formData);// formData必须经过序列化

这样就实现了ajax模拟表单提交.

XMLHttpRequest2级

鉴于 XHR 已经得到广泛接受,成为了事实标准,W3C 也若手制定相应的标准以规范其行为。XMLHttpRequest 1 级只是把已有的 XHR 对象的实现细节描述了出来。而 XMLHttpRequest 2 级则进一步发展了 XHR。并非所有浏览器都完整地实现了 XMLHttpRequest 2 级规范,但所有浏览器都实现了它规定的部分内容。

FormData

现代 Web 应用中频繁使用的一项功能就是表单数据的序列化,XMLHttpRequest 2 级为此定义了FormData 类型。FormData 为序列化表单以及创建与表单格式相同的数据(用于通过 XHR 传输)提供了便利。下面的代码创建了一个 FormData 对象,并向其中添加了一些数据。

var data = new FormData(); 
data.append("name", "Nicholas");

//也可以参照下面方式传参:
var data2 = new FormData(document.forms[0]); //这样就更方便传递给xhr.send了.

超时设定

虽然我们可以用xhr.status来判断是否提示请求错误, 但请求时间一旦过程, 显然有一个超时设定更为合理:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
try {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){ alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
} catch (ex){
//假设由 ontimeout 事件处理程序处理
}
}
};

xhr.open("get", "timeout.php", true);
xhr.timeout = 1000; //将超时设置为 1 秒钟(仅适用于 IE8+)
xhr.ontimeout = function(){
alert("Request did not return in a second.");
};
xhr.send(null);

overrideMimeType方法

Firefox 最早引入了 overrideMimeType()方法,用于重写 XHR 响应的 MIME 类型。这个方法后来也被纳入了 XMLHttpRequest 2 级规范。因为返回响应的 MIME 类型决定了 XHR 对象如何处理它,所以提供一种方法能够重写服务器返回的 MIME 类型是很有用的。

比如,服务器返回的 MIME 类型是 text/plain,但数据中实际包含的是 XML。根据 MIME 类型, 即使数据是 XML,responseXML 属性中仍然是 null。通过调用 overrideMimeType()方法,可以保证把响应当作 XML 而非纯文本来处理。

var xhr = createXHR(); 
xhr.open("get", "text.php", true);
xhr.overrideMimeType("text/xml"); //必须在send()方法之前
xhr.send(null);

这个例子强迫 XHR 对象将响应当作 XML 而非纯文本来处理。

进度事件

Progress Events 规范是 W3C 的一个工作草案,定义了与客户端服务器通信有关的事件。这些事件最早其实只针对 XHR 操作,但目前也被其他 API 借鉴。有以下 6 个进度事件。

  • loadstart:在接收到响应数据的第一个字节时触发。
  • progress:在接收响应期间持续不断地触发。
  • error:在请求发生错误时触发。
  • abort:在因为调用 abort()方法而终止连接时触发。
  • load:在接收到完整的响应数据时触发。
  • loadend:在通信完成或者触发 error、abort 或 load 事件后触发。

事件触发顺序正如上面排序.

load事件

load事件显然是用来代替readystatechange事件的, 这就意味着不用检查xhr.readyState, 也会有性能上提升.

var xhr = new XMLHttpRequest();
xhr.onload = function(){ //只要浏览器接收到服务器的响应,不管其状态如何,都会触发 load 事件。
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
};

xhr.open("get", "altevents.php", true);
xhr.send(null);

progress事件

Mozilla 对 XHR 的另一个革新是添加了 progress 事件,这个事件会在浏览器接收新数据期间周期性地触发。而 onprogress 事件处理程序会接收到一个 event 对象,其 target 属性是 XHR 对象,但包含若三个额外的属性:lengthComputable、position 和 totalSize。其中,lengthComputable 是一个表示进度信息是否可用的布尔值,position 表示已经接收的字节数,totalSize 表示根据Content-Length 响应头部确定的预期字节数。有了这些信息,我们就可以为用户创建一个进度指示器:

var xhr = new XMLHttpRequest();
xhr.onload = function(event){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
};
// 注意这个事件也要和onload事件一样写在xhr.open前面保证兼容
xhr.onprogress = function(event){
var divStatus = document.getElementById("status");
if (event.lengthComputable){
divStatus.innerHTML = "Received " + event.position + " of " + event.totalSize +" bytes";
}
};

xhr.open("get", "altevents.php", true);
xhr.send(null);

跨域资源共享CORS

通过 XHR 实现 Ajax 通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR 对象只能访问与包含它的页面位于同一个域中的资源。

CORS(Cross-Origin Resource Sharing,跨源资源共享)是 W3C 的一个工作草案,定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想: 就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败

现代浏览器下的CORS看起来和Ajax代码没什么区别(实际也没啥区别), 它的关键点在于后端的支持.

比如一个简单的使用 GET 或 POST 发送的请求,它没有自定义的头部,而主体内容是 text/plain。在发送该请求时,需要给它附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。下面是 Origin 头部的一个示例:

Origin: http://www.nczonline.net

如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin 头部中回发相同的源信息(如果是公共资源,可以回发”*”)。例如:

Access-Control-Allow-Origin: http://www.nczonline.net
Access-Control-Allow-Credentials: true; // 是否接受用户凭据

如果没有这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。注意,请求和响应都不包含 cookie 信息。

IE对CORS的实现

以下是 XDR 与 XHR 的一些不同之处。

  • cookie 不会随请求发送,也不会随响应返回。
  • 只能设置请求头部信息中的 Content-Type 字段。
  • 不能访问响应头部信息。
  • 只支持 GET 和 POST 请求。
    var xdr = new XDomainRequest(); 
    xdr.onload = function(){
    alert(xdr.responseText);
    };
    xdr.onerror = function(){
    alert("An error occurred.");
    };
    xdr.timeout = 1000;
    xdr.ontimeout = function(){
    alert("Request took too long.");
    };
    xdr.open("get", "http://www.somewhere-else.com/page/");
    xdr.contentType = "application/x-www-form-urlencoded"; //只能在这里设置
    xdr.send("name1=value1&name2=value2");

其他浏览器对CORS的实现

现代浏览器都通过 XMLHttpRequest 对象实现了对 CORS 的原生支持。在尝试打开不同来源的资源时,无需额外编写代码就可以触发这个行为。要请求位于另一个域中的资源,使用标准的 XHR 对象并在 open()方法中传入绝对 URL 即可,例如:

var xhr = new XMLHttpRequest(); 
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
alert(xhr.responseText);
} else {
alert("Request was unsuccessful: " + xhr.status);
}
}
};
xhr.withCredentials = true; // 如果要带用户凭据则设置这个属性
xhr.open("get", "http://www.somewhere-else.com/page/", true);
xhr.send(null);

与 IE 中的 XDR 对象不同,通过跨域 XHR 对象可以访问 status 和 statusText 属性,而且还支持同步请求。跨域 XHR 对象也有一些限制,但为了安全这些限制是必需的。以下就是这些限制。

  • 不能使用 setRequestHeader()设置自定义头部。
  • 默认情况不能发送和接收 cookie。
  • 调用 getAllResponseHeaders()方法总会返回空字符串。
  • 由于无论同源请求还是跨源请求都使用相同的接口,因此对于本地资源,最好使用相对 URL,在访问远程资源时再使用绝对 URL。这样做能消除歧义,避免出现限制访问头部或本地 cookie 信息等问题。

Preflighted Request

字面理解可以翻译成: 预检请求.

书中讲得不是很好, 我按照自己的意思解释下:
浏览器的同源策略浏览器会限制从脚本发起的跨域HTTP请求,像XMLHttpRequest和Fetch都遵循同源策略。

浏览器限制跨域请求一般有两种方式:

  1. 浏览器限制发起跨域请求
  2. 跨域请求可以正常发起,但是返回的结果被浏览器拦截了

一般浏览器都是第二种方式限制跨域请求,那就是说请求已到达服务器,并有可能对数据库里的数据进行了操作,但是返回的结果被浏览器拦截了,那么我们就获取不到返回结果,这是一次失败的请求,但是可能对数据库里的数据产生了影响。

为了防止这种情况的发生,规范要求,对这种可能对服务器数据产生副作用的HTTP请求方法,浏览器必须先使用OPTIONS方法发起一个预检请求,从而获知服务器是否允许该跨域请求:如果允许,就发送带数据的真实请求;如果不允许,则阻止发送带数据的真实请求。

点击此处可以查阅预检请求的触发条件: https://my.oschina.net/ososchina/blog/672556

带凭据的请求

参考上面的代码, 分别前后端加代码

  • 前端: xhr.withCredentials = true
  • 后端: Access-Control-Allow-Credentials: true

跨浏览器的CORS

做兼容处理, 代码略

其他跨域技术

在 CORS 出现以前,要实现跨域 Ajax 通信颇费一些周折。开发人员想出了一些办法,利用 DOM 中能够执行跨域请求的功能,在不依赖 XHR 对象的情况下也能发送某种请求。虽然 CORS 技术已经无处不在,但开发人员自已发明的这些技术仍然被广泛使用,毕竟这样不需要修改服务器端代码

图像ping

上述第一种跨域请求技术是使用标签。我们知道,一个网页可以从任何网页中加载图像,不用担心跨域不跨域。这也是在线广告跟踪浏览量的主要方式。正如第 13 章讨论过的,也可以动态地创建图像,使用它们的 onload 和 onerror 事件处理程序来确定是否接收到了响应。

动态创建图像经常用于图像 Ping。图像 Ping 是与服务器进行简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或 204 响应。通过图像 Ping,浏览器得不到任何具体的数据,但通过侦听 load 和 error 事件,它能知道响应是什么时候接收到的。来看下面的例子。

var img = new Image();
img.onload = img.onerror = function(){ alert("Done!");
};
img.src = "http://www.example.com/test?name=Nicholas";

这里创建了一个 Image 的实例,然后将 onload 和 onerror 事件处理程序指定为同一个函数。这样无论是什么响应,只要请求完成,就能得到通知。请求从设置 src 属性那一刻开始,而这个例子在请求中发送了一个 name 参数。

图像 Ping 最常用于跟踪用户点击页面或动态广告曝光次数。图像 Ping 有两个主要的缺点,一是只能发送 GET 请求,二是无法访问服务器的响应文本。因此,图像 Ping 只能用于浏览器与服务器间的单向通信。

JSONP

JSONP全称JSON with padding, 是应用 JSON 的一种新方法, 在后来的 Web 服务中非常流行。JSONP 看起来与 JSON 差不多.

callback({"name":"testdog"})

原理实际上就是先在网站页面上定义好某个函数, 然后用动态创建script标签的方式把函数句给添加进来, 达到立即执行的效果.

值得注意的是你用的JSONP是从其他域请求回来的JS代码, 所以要注意安全问题.

Comet

Comet是Dojo创始人Alex Russell提出的一种叫法, 指的是一种跟高级的Ajax技术(有人称之为服务器推送).

Ajax 是一种从页面向服务器请求数据的技术,而 Comet 则是一种服务器向页面推送数据的技术。Comet 能够让信息近乎实时地被推送到页面上,非常适合处理体育比赛的分数和股票报价。

有两种实现 Comet 的方式:

  • 长轮询: 长轮询和传统轮询(也称为短轮询: 用定时器和xhr就能实现)相反.
  • Http流: 就是浏览器向服务器发送一个请求,而服务器保持连接打开,然后周期性地向浏览器发送数据。

我们依次给到长短轮询的代码


短轮询示例:

shortpolling.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>短轮询(传统轮询)</title>
</head>
<body>
<script>
function ajax(configObj){
var c = configObj;
var xhr = new XMLHttpRequest();
xhr.open(c.method,c.url,true);
xhr.send(null)
xhr.onload = function() {
if ((xhr.status >=200 && xhr.status <300)||xhr.status ==304) {
var res = JSON.parse(xhr.responseText)
c.sucess && c.sucess(res);
}
}
}

setInterval(function(){
ajax({
method:'get',
url:'shortpolling.php',
sucess:function(data){
console.log('服务器响应时间:',data.res)
}
})
},1000)
</script>
</body>
</html>

shortpolling.php

<?php
header('Content-type: text/json');
$arr = [
'res' => time()
];
echo json_encode($arr);
?>


长轮询示例:
longpolling.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>长轮询</title>
</head>
<body>
<script type="text/javascript">
// 实现一个长轮询
function ajax(configObj){
var c = configObj;
var xhr = new XMLHttpRequest();
xhr.open(c.method,c.url,true);
xhr.send(null)
xhr.onload = function() {
if ((xhr.status >=200 && xhr.status <300)||xhr.status ==304) {
var res = (xhr.responseText)
c.sucess && c.sucess(res);
xhr.onload = null; // 性能优化
}
}
xhr.onerror = function(e){
console.log(e)
c.fail && c.fail(xhr,xhr.responseText,e);
}
}


function longPolling(){
var _fn = arguments.callee;
ajax({
method:'get',
url:'longpolling.php?time='+Date.parse(new Date())/1000,
sucess:function(data){
console.log(data)
_fn();
},
fail:function(xhr,responseText,err){

}
})
}

longPolling();
</script>
</body>
</html>

longpolling.php

<?php
header('Content-type: application/json');
set_time_limit(0);//无限请求超时时间
$time = $_GET['time'];
while (true) {
sleep(3);
$i = rand(0,100);
if ($i > 0 && $i < 50) {
$responseTime = time();

echo json_encode(array("resultNum"=>$i,"responseTime"=>$responseTime-$time));
exit();
} else { // 模拟没有数据变化,将休眠 hold住连接
sleep(5);
echo json_encode(array("noData"=>true));
exit();
}
}

长短轮询的差别可以通过浏览器控制点的请求面板看他们之间的差异.


HTTP流 :
http流是Comet的另一种实现, 它的思路就是若不断从服务器接收数据,readyState 的值会周期性地变为 3。当 readyState 值变为 3 时,responseText 属性中就会保存接收到的所有数据。

<!DOCTYPE html>
<html>
<head>
<title>HTTP Streaming Example</title>
</head>
<body>
<p>This example must be run on a server to work properly and will not work in IE.</p>
<script>
function createStreamingClient(url, progress, finished){
var xhr = new XMLHttpRequest(),
received = 0;
xhr.open("get", url, true);
xhr.onreadystatechange = function(){
var result;
if (xhr.readyState == 3){
//get only the new data and adjust counter
result = xhr.responseText.substring(received);
received += result.length;
//call the progress callback
progress(result);
} else if (xhr.readyState == 4){
finished(xhr.responseText);
}
};
xhr.send(null);
return xhr;
}
var client = createStreamingClient("streaming.php", function(data){
alert("Received: " + data);
}, function(data){
alert("Done!");
});
</script>
</body>
</html>

服务器发送事件

SSE(Server-Sent Events,服务器发送事件)是围绕只读 Comet 交互推出的 API 或者模式。SSE API 用于创建到服务器的单向连接,服务器通过这个连接可以发送任意数量的数据。服务器响应的 MIME 类型必须是 text/event-stream,而且是浏览器中的 JavaScript API 能解析格式输出。SSE 支持短轮询、长轮询和 HTTP 流,而且能在断开连接时自动确定何时重新连接。

比起之前comet的实现, 操作自然更加简便.

var src = new EventSource("api.php")
src.onmessage=function(evt){
var data = evt.data;
console.log(data)
};

注意,传入的 URL 必须与创建对象的页面同源(相同的 URL 模式、域及端口)。EventSource 的实例有一个 readyState 属性,值为 0 表示正连接到服务器,值为 1 表示打开了连接,值为 2 表示关闭了连接。

相关API:

  • open:在建立连接时触发。
  • message:在从服务器接收到新事件时触发。
  • error:在无法建立连接时触发。

默认情况下,EventSource 对象会保持与服务器的活动连接。如果连接断开,还会重新连接。这就意味若 SSE 适合长轮询和 HTTP 流。如果想强制立即断开连接并且不再重新连接,可以调用 close() 方法。

src.close();

WebSockets

Web Sockets 的目标是在一个单独的持久连接上提供全双工、双向通信。在 JavaScript 中创建了 Web Socket 之后,会有一个 HTTP 请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会从 HTTP 协议交换为 Web Socket 协议。(ws:// or wss://)

也就是说,使用标准的 HTTP 服务器无法实现 Web Sockets,只有支持这种协议的专门服务器才能正常工作。

使用上述协议而非HTTP协议的好处是,能够在客户端和服务器之间发送非常少量的数据,而不必担心 HTTP 那样字节级的开销。由于传递的数据包很小,因此 Web Sockets 非常适合移动应用。毕竟对移动应用而言,带宽和网络延迟都是关键问题。

  1. Web Sockets API
    要创建 Web Socket,先实例一个 WebSocket 对象并传入要连接的 URL:
    var socket = new WebSocket("ws://www.example.com/server.php");//必须是绝对地址

注意,必须给 WebSocket 构造函数传入绝对 URL。同源策略对 Web Sockets 不适用,因此可以通过它打开到任何站点的连接。

实例化了 WebSocket 对象后,浏览器就会马上尝试创建连接。与 XHR 类似,WebSocket 也有一个表示当前状态的 readyState 属性。不过,这个属性的值与 XHR 并不相同,而是如下所示:

  • WebSocket.OPENING (0):正在建立连接。
  • WebSocket.OPEN (1):已经建立连接。
  • WebSocket.CLOSING (2):正在关闭连接。
  • WebSocket.CLOSE (3):已经关闭连接。

WebSocket 没有 readystatechange 事件;不过,它有其他事件,对应若不同的状态。readyState 的值永远从 0 开始。
要关闭 Web Socket 连接,可以在任何时候调用 close()方法。

socket.close(); //readyState 的值立即变为 2(正在关闭),而在关闭连接后就会变成 3。
  1. 发送和接收数据
var data = "Hello world!";
var message = {
time: new Date(),
text: "Hello world!",
clientId: "asdfp8734rew"
};

var socket = new WebSocket("ws://www.example.com/server.php");

// 发送数据
socket.send(data); //只接受纯文本数据
// socket.send(JSON.stringify(message)); // 复杂数据必须经过序列化

//接受数据
socket.onmessage = function(event){
var data = event.data; //返回的也是字符串
};

// 其他API
socket.onopen = function(){
alert("Connection established.");
};

socket.onerror = function(){
alert("Connection error.");
};

socket.onclose = function(){
alert("Connection closed.");
};

SSE 与WebSockets

面对某个具体的用例,在考虑是使用 SSE 还是使用 Web Sockets 时,可以考虑如下几个因素。首先, 你是否有自由度建立和维护 Web Sockets 服务器?因为 Web Socket 协议不同于 HTTP,所以现有服务器不能用于 Web Socket 通信。SSE 倒是通过常规 HTTP 通信,因此现有服务器就可以满足需求。

第二个要考虑的问题是到底需不需要双向通信。如果用例只需读取服务器数据(如比赛成绩),那么 SSE 比较容易实现。如果用例必须双向通信(如聊天室),那么 Web Sockets 显然更好。别忘了,在不能选择 Web Sockets 的情况下,组合 XHR 和 SSE 也是能实现双向通信的

安全

为确保通过 XHR 访问的 URL 安全,通行的做法就是验证发送请求者是否有权限访问相应的资源。有下列几种方式可供选择。

  • 要求以 SSL 连接来访问可以通过 XHR 请求的资源。
  • 要求每一次请求都要附带经过相应算法计算得到的验证码。请注意,下列措施对防范 CSRF 攻击不起作用。
  • 要求发送 POST 而不是 GET 请求——很容易改变。
  • 检查来源 URL 以确定是否可信——来源记录很容易伪造。
  • 基于 cookie 信息进行验证——同样很容易伪造。
    XHR 对象也提供了一些安全机制,虽然表面上看可以保证安全,但实际上却相当不可靠。实际上, 前面介绍的 open()方法还能再接收两个参数:要随请求一起发送的用户名和密码。带有这两个参数的请求可以通过 SSL 发送给服务器上的页面, 但是要禁止明文get请求发送账号密码.

本文完, 以下有两篇文章可以查阅

https://segmentfault.com/a/1190000011549088
https://segmentfault.com/a/1190000000423616#articleHeader8

访客评论