不再学AJAX了!(三)跨域获取资源 ③ - WebSocket & postMessage

让咱们先简单回顾一下以前谈到的内容,AJAX是一种无页面刷新的获取服务器资源的混合技术。而基于浏览器的“同源策略”,不一样“域”之间不能够发送AJAX请求。可是在某些情境下,咱们须要“跨域获取资源”,为了知足这一需求,咱们可使用“JSONP”与“CORS”两种技术。web

如今,咱们将要简要了解“跨域共享资源”的另外两种方式:WebSocket 和 postMessage。让咱们先大概看看他们是什么,以及到底是基于怎样的原理,知足了咱们的需求 - “跨域获取资源”。segmentfault

1、WebSocket

基于维基百科的定义,WebSocket是一种在单个TCP链接上进行全双工通信的协议。在这里我并不打算解释“TCP链接”和“全双工通信”这两个专业术语(这样作会让这篇文章变得很长,并且也偏离了咱们的主题),让咱们聚焦这段定义的最后两个字协议跨域

说到协议,你是否联想到“HTTP协议”?没错,HTML5标准之因此提出了一种新的互联网通讯协议 - WebSocket,就是为了弥补在某些情景下使用HTTP协议通讯的一些不足。可是注意,这并不意味WebSocket协议就能够彻底取代HTTP协议了,其实二者的关系更像是两兄弟,各自有着各自擅长的领域,并且时不时还一同协做解决难题。浏览器

那么上面提到的某些情景具体是指什么呢?答案是“服务端与客户端的双向通讯”。咱们知道,当咱们使用HTTP协议时,客户端与服务端的通讯模式始终是由客户端向服务端发送请求,服务端只负责验证请求并返回响应。安全

咱们能够这样想象,在HTTP协议下,服务端扮演着“守门人”的角色,而客户端则是一个邮局,它每发送一个请求就像是委托一个信使携带一封信(信里注明本身的身份和须要获取资源的名称)到服务端,当信使到达时,“守门人”会拆开信封,检查里面的身份信息,若是身份合法则打开资源宝库的大门,将相应的资源交给信使,令其返回给客户端。服务器

在这个故事里,服务端的角色有些枯燥呆板对吧?不只如此,故事中服务端扮演的“守门人”角色还患有严重的脸盲症,在工做中他只“认信不认人”,也就是说客户端发送的每个请求,对于服务而言都是全新的,守门人不会由于信使上次来过,或是收到两次相同的信而以为眼熟,对信使有额外的寒暄。这也就是为何咱们说HTTP协议是“无状态的”。乍看起来,这彷佛有些不合理,可是这种设计却使服务器的工做变得简单可控,提高了服务器的工做效率。websocket

可是这样的设计仍然存在两个问题:网络

  1. 每个请求都须要身份验证,这对于用户而言意味着须要在每一次发送请求时输入身份信息;
  2. 当客户端所请求的资源是动态生成的时,客户端没法在资源生成时获得通知(还记得吧,服务器只是一个原地不动的“守门人”);

如何解决这两个问题呢?对于前者,答案是使用“Cookie”,而对于后者,则轮到咱们今天的主角“WebSocket”大显身手。并发

在讨论WebSocket以前,让咱们先稍微绕点路,谈谈“Cookie”是如何解决“每个请求都须要身份验证”的问题的。socket

(一)为HTTP协议添加状态 - Cookie

咱们以前提到,HTTP协议下,客户端与服务端的通讯是“无状态”的,也就是说,若是服务器中的某部分资源是由某个客户专属的,那么每当这个客户想要获取资源时,都须要首先在浏览器中输入帐号密码,而后再发送请求,并在被服务器识别身份信息成功后获取请求的资源。咱们固然不想每次发送一个请求都要输入一遍帐号密码,所以咱们须要Cookie,这个既能够存储在浏览器,又会被浏览器发送HTTP请求时默认发送至服务端,而且还受浏览器“同源策略”保护的东西帮助咱们提升发起一次请求的效率。

在有了Cookie以后,咱们能够在一次会话中(从用户登陆到浏览器关闭)只输入一次帐号密码,而后将其保存在Cookie中,在整个会话期间,Cookie都会伴随着HTTP请求的发送被服务器识别,从而避免了咱们重复的输入身份信息。

不只如此,基于Cookie的特性:能够保存在浏览器内,还会在浏览器发送HTTP请求时默认携带,服务端也能够操做Cookie。Cookie还能够帮助咱们节省网络请求的发起数量。例如,当咱们在制做一个购物网站时,咱们固然不但愿用户在每添加一个商品到购物车就向服务器发送一个请求(请求数量越少,服务器压力就越小),此时,咱们就能够将添加商品所致使的数据变更存储在Cookie内,而后等待下次发送请求时,一并发送给服务器处理。

如今咱们能够说,Cookie的出现,为无状态的HTTP协议通讯添加了状态。

最后须要注意,Cookie大多数状况下,都保存着用户的身份信息,所以各类恶意攻击者对于Cookie的攻击便花样百出,层出不穷。其本质上就是想要得到用户的Cookie,再利用其中的身份信息假装成用户获取相应资源,而浏览器的“同源策略”本质上就是保护用户的Cookie信息不会泄露。

(二)让服务器也动起来 - WebSocket

绕了一个小弯,如今能够回过头来继续谈谈咱们的主角WebSocket了。再让咱们回忆一下WebSocket要解决的问题:

客户端没法获知请求的动态资源什么时候到位“,让咱们描述的更详细一点,有时候客户端想要请求的资源,服务器须要必定时间后才能返回(好比该资源依赖于其余服务器的计算返回结果),因为在HTTP协议下,网络通讯是单向的,所以服务器并不具有当资源准备就绪时,通知浏览器的功能(由于咱们要保障服务器的工做效率)。所以,基于HTTP协议一般的作法是,设置一个定时器,每隔必定时间由浏览器向服务器发送一次请求以探测资源是否到位。

这种作法显然浪费了不少请求,换句话说,浪费了不少带宽(咱们每一个请求都要携带Cookie和报头,这些都会占用带宽传输),不只低效率,并且也不够优雅。

理所固然的,在这种状况下,咱们但愿当服务器资源到位时,可以主动通知浏览器并返回相应资源。而为了实现这一点,HTML5标准推出了WebSocket协议,使浏览器和服务器实现了双向通讯,更妙的是,除了IE9及如下的IE浏览器,全部的浏览器都支持WebSocket协议。

让咱们也一样构建一个基于WebSocket协议的心智模型,在这个心智模型中,服务端扮演的角色发生了一些改变,服务端再也不只是一个“守门人”,同时它也运营着一个和客户端同样的“邮局”,也就是说,他也拥有了能够向客户端发送数据的能力。至此一个完整的基于WebSocket协议的通讯流程为:

客户端派发一个信使向服务器送信,服务器扮演的“守门人”检查信件,发现信件中写到“让咱们用更加潮流的WebSocket方式交流吧”,服务器在在信件末尾添加上一句“没问题,浏览器伙计”,让信使原路返回告知浏览器。当浏览器再次向服务器告知收到消息时(第三次握手),服务器就开始运转“邮局”,向客户端派发信使与浏览器互发信息,转发资源。

让咱们看看这个模型的具体实现:

下面是客户端告知服务端要升级为WebSocket协议的报头:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

下面是服务端向客户端返回的响应报头:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

想知道这些报头中的字段中表明什么?能够参考维基百科下的说明。

(三)客户端发起WebSocket请求

既然咱们已经为了解释“什么是WebSocket”,“WebSocket的意义”花了那么多篇幅,那么不妨添加上最后一个环节,让这个主题变得更加完整,接下来咱们将要简单讲解一下客户端如何发起一个WebSocket请求。

像发起AJAX请求同样,发起WebSocket请求须要借助浏览器提供的WebSocket对象,该对象提供了用于建立和管理WebSocket链接,以及经过该链接收发数据的API。全部的浏览器都默认提供了WebSocket对象。让咱们看看该对象的用法:

和使用XHRHttpRequest对象同样,咱们首先要实例化一个WebSocket对象:

var ws = new WebSocket("wss://echo.websocket.org")

传入的参数为响应WebSocket请求的地址。

一样相似AJAX的是,WebSocket对象也有一个readyState属性,用来表示对象实例当前所处的连接状态,有四个值:

  • 0:表示正在链接中(CONNECTING);
  • 1:表示链接成功,能够通讯(OPEN);
  • 2:表示链接正在关闭(CLOSING);
  • 3:表示链接已经关闭或打开链接失败(CLOSED);

咱们能够经过判断这个值来执行咱们相应的代码。

除此以外,WebSocket对象还提供给咱们一系列事件属性,使咱们控制链接过程当中的通讯行为:

  • onopen:用于指定链接成功后的回调函数;
  • onclose:用于指定链接关闭后的回调函数;
  • onmessage:用于指定收到服务器数据后的回调函数;
  • onerror:用于指定报错时的回调函数;

经过.send()方法,咱们拥有了向服务器发送数据的能力(WebSocket还容许咱们发送二进制数据):

ws.send('Hi, server!')

如何知道什么时候咱们的数据发送完毕呢?咱们须要使用WebSocket对象的bufferedAmount属性,该属性的返回值表示了还有多少字节的二进制数据没有发送出去,因此咱们能够经过判断该值是否为0而肯定数据是否发送结束。

var data = new ArrayBuffer(1000000)
ws.send(data)

if (socket.bufferedAmount === 0) {
    // 发送完毕
} else {
    // 还在发送
}

OK,目前为止咱们花了大量篇幅解释了WebSocket协议是什么,它可以帮助咱们作什么,以及客户端发送WebSocket请求的方式。可是目前为止,咱们仍是没有谈论一丁点关于WebSocket是如何帮助咱们绕过浏览器的“同源策略”让咱们实现“跨域资源共享”,你是否已经有点等的不耐烦了?

可是别急,当你清楚的了解到WebSocket是什么以后,答案就呼之欲出了,那就是当客户端与服务端建立WebSocket链接后,自己就能够自然的实现跨域资源共享,WebSocket协议自己就不受浏览器“同源策略”的限制(还记得吧,同源策略只是限制了跨域的AJAX请求?),因此问题自己就不成立(有点赖皮是吧?)。

可是你可能又会问,若是没有浏览器“同源策略”的限制,那么用户的Cookie安全又由谁来保护呢?问得好,看来你有认真阅读上面的文字,为了解答这个问题,让咱们换一种角度思考,咱们说过Cookie的存在就是为了给无状态的HTTP协议通信添加状态,由于Cookie是明文传输的,且一般包含用户的身份信息,因此很是受到网络攻击者的“关注”。可是想一想WebSocket协议下的通信机制,客户端和服务端一旦创建链接,就能够顺畅的互发数据,所以WebSocket协议自己就是“有状态的”,不须要Cookie的帮忙,既然没有Cookie,天然也不须要“同源策略”去保护,所以其实这个问题也不成立。

至此,已经将关于WebSocket的全部内容都大体讲述了一遍,真没想到是如此巨大的工做量。看来本篇文章不该该叫作“不再学AJAX了”,而是“不再学AJAX,JSONP,CORS,WebSocket..”。

真是了不得。


2、postMessage

回头一看,咱们已经在“跨域”这个主题上整整停留了三篇文章,涉及的技术包括JSONP,CORS与WebSocket。须要注意的是,以上这些跨域技术都只适用于客户端请求异域服务端资源的情景。而除此以外,有时候咱们还须要在异域的两个客户端之间共享数据,例如页面与内嵌iframe窗口通信,页面与新打开异域页面通信。

这就是使用HTML5提供的新API -- postMessage的时候了。

使用postMessage技术实现跨域的原理很是简单,一方面,主窗口经过postMessageAPI向异域的窗口发送数据,另外一方面咱们在异域的页面脚本中始终监听message事件,当获取主窗口数据时处理数据或者以一样的方式返回数据从而实现跨窗口的异域通信。

让咱们用具体的业务场景与代码进一步说明,假如咱们的页面如今有两个窗口,窗口1命名为“window_1”, 窗口2命名为“window_2”,固然,窗口1与窗口2的“域”是不一样的,咱们的需求是由窗口1向窗口2发送数据,而当窗口2接收到数据时,将数据再返回给窗口1。先让咱们看看窗口1script标签内的代码:

// window_1 域名为 http://winodow1.com:8080
window.postMessage("Hi, How are you!", "http://window2.com:8080")

能够看到,postMessage函数接收两个参数,第一个为要发送的信息(能够是任何JavaScript类型数据,但部分浏览器只支持字符串格式),第二个为信息发送的目标地址。让咱们再看看窗口2script标签内的代码:

// window_2 域名为 http://window2.com:8080
window.addEventListener("message", receiveMessage, false)

function receiveMessage(event) {
    // 对于Chorme,origin属性为originalEvent.origin属性
    var origin = event.origin || event.originalEvent.origin
    if (origin !== "http://window1.com:8080") {
        return 
    }
    window.postMessage("I\'m ok", "http://window1.com:8080")
}

看到了吗,咱们在window上绑定了一个事件监听函数,监听message事件。一旦咱们接收到其余域经过postMessage发送的信息,就会触发咱们的receiveMessage回调函数。该函数会首先检查发送信息的域是不是咱们想要的(以后咱们会对此详细说明),若是验证成功则会像窗口1发送一条消息。

看起来很好懂不是吗,一方发送信息,一方捕捉信息。可是,我须要格外提醒你的是全部“跨域”技术都须要关注的“安全问题”。让咱们想一想postMessage技术之因此能实现跨域资源共享,本质上是要依赖于客户端脚本设置了相应的message监听事件。所以只要有消息经过postMessage发送过来,咱们的脚本都会接收并进行处理。因为任何域均可以经过postMessage发送跨域信息,所以对于设置了事件监听器的页面来讲,判断到达页面的信息是不是安全的是很是重要的事,由于咱们并不想要执行有危险的数据。

那么接下来的问题即是,如何鉴别发送至页面的信息呢?答案是经过 message事件监听函数的事件对象,咱们称它为event,该对象有三个属性:

  • data:值为其余window传递过来的对象;
  • origin:值为消息发送方窗口的域名;
  • source:值为对发送消息的窗口对象的引用;

很显然的,咱们应该着重检测event对象的origin属性,创建一个白名单对origin属性进行检测一般是一个明智的作法。

最后,再让咱们谈谈postMessage对象的浏览器兼容性,这方面到是很幸运,除了IE8如下的IE浏览器,全部的浏览器都支持postMessage方法!


至此,咱们终于彻底讲完了“跨域共享资源”这一主题。花了很多力气是吧?但愿这是值得的。








? Hey!到这里《不再学AJAX了!》这个专题系列就彻底结束了,还记得咱们的初心吗?我但愿你能经过阅读这个系列的文章,以较为轻松的方式,系统完整地掌握AJAX技术,今后不再用刻意学习零散的AJAX知识。但愿我达成了个人目标,也但愿你在阅读学习的过程当中感到愉快。

关于AJAX技术这个专题,其实我还想讲述的两个话题是:更优雅的资源获取方式:fetch API 以及 深刻jQuery:AJAX的实现,可是鉴于我我的时间精力有限(完成一个系列文章真的比我想的要付出更多时间!),就决定暂时先放下,等未来有机会再以这个系列的番外篇的形式补充上去,但愿大家能够理解和接受:)。

这是我第一次在技术平台中以“系列”的方式发表技术文章,我我的以为这样的方式更容易使人在总体上把握和理解一个技术,从而作到更灵活熟练的使用。但愿大家也认同这一点并在阅读过程当中感到愉快。以后,我也会继续在专栏中发表关于Web开发技术的系列文章,但愿获得大家的承认和支持。

最后,再谈谈我在技术平台发表文章的初心:之因此开始在各平台(目前为稀土掘金和segmentfault)发表技术文章,主要是为了帮助我消化知识,锻炼写做的文笔,验证我对某个技术的理解是否正确,以及积攒人气知足虚荣心。在这个过程当中,也但愿读者可以经过阅读个人文章,加深对某一技术的理解。我认为这是一件共赢的事情,所以我十分欢迎,甚至是期待你在阅读我任何文章的过程当中都可以:

  1. 若是以为有所收获,绝不犹豫的点击赞扬按钮(我真的真的会很开心?);
  2. 若是想到了其余相关知识,或发现我对某个技术的理解不正确,绝不犹豫的在评论区留言与我交流
  3. 若是对于我讲述中的某个概念仍是不懂,绝不犹豫的在留言区告知我你的困惑,我会思考怎么样把这个概念讲述的更加清楚明白;
  4. 若是以为个人文章不错,绝不犹豫的将个人文章推荐给他人,邀请他们成为个人读者;
  5. 若是你以为阅读个人文章所花费的时间很值得,对你有很大帮助而且也承认个人劳动成果,你大能够点击下方红色的“赞扬支持”按钮为这篇文章付费,同时表达你对我创做的承认与支持。写做可以对人有益又能得到报酬,这着实使人倍感欣慰。

个人创做和成长须要大家的帮助和支持,做为报答,我会持续发布优质的文章,陪同大家一块儿成长。关注我,一块儿加油吧! ?

相关文章
相关标签/搜索