不再學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. 若是你以爲閱讀個人文章所花費的時間很值得,對你有很大幫助而且也承認個人勞動成果,你大能夠點擊下方紅色的「讚揚支持」按鈕爲這篇文章付費,同時表達你對我創做的承認與支持。寫做可以對人有益又能得到報酬,這着實使人倍感欣慰。

個人創做和成長鬚要大家的幫助和支持,做爲報答,我會持續發佈優質的文章,陪同大家一塊兒成長。關注我,一塊兒加油吧! 🙌

相關文章
相關標籤/搜索