若問到Web應用程式如何執行Server Push,依時間點的不同,會得到許多不同的解釋。從輪詢(Polling)到長輪詢(Long Polling),從Comet、Server-Sent Event到WebSocket,從HTTP Streaming到HTTP/2 Server push。

面對一堆技術名詞,著實令人霧剎剎,那就……忘了這些,先問問自己要的Server Push是什麼吧!

很久前的Server Push

差不多也只是十幾年前的事,若想在Web應用程式伺服端狀態有變時,瀏覽器能夠即時地顯示對應的畫面變更,就已經是個難題了。

因為HTTP是個基於請求/回應的協定,在一來一往之後連線就中斷了,伺服端不可能主動向客戶端建立連線,更別說發送資料。在JavaScript這鹹魚還沒翻身前,變通方式就是重清整個網頁或是iframe,定時查詢伺服端狀態;後來,JavaScript漸漸變得流行,很多網頁會將iframe設為隱藏,使用JavaScript於iframe重新載入資料後,進行相關畫面的更新。

從不瞭解背後原理的使用者觀點來看,這就像是伺服器主動更新了瀏覽器上的畫面。由於隱藏式iframe會有資訊安全上的疑慮,許多防毒軟體或瀏覽器漸漸不允許使用,而是使用彈跳式視窗,讓使用者確認連線的方式。對此,有些人應該有印象。

而隨著JavaScript越來越受到重視,開發者也開始運用XMLHttpRequest,來取代隱藏式iframe或彈跳視窗,但是,基本上,還是定時發出請求,然後伺服端發出回應,無論伺服端狀態是否更新。

這沒什麼不好,只不過伺服端狀態若非頻繁變化,多數的請求與回應都是浪費,在連線客戶端眾多時,這樣的請求與回應,會耗費許多伺服端與網路資源。

既然在HTTP模型上,請求是絕對必要的,那麼就來延後回應吧!當伺服端收到請求時,如果沒有要傳送的資料,就暫停回應,而瀏覽器收到伺服端的回應前,不會進一步發出請求。簡單地說,拉長請求與回應之間的時間,減輕伺服端頻繁處理請求與回應的負擔,同時,也減少網路的流量浪費。

既然可拉長請求與回應之間的時間,這段期間,伺服端若有狀態變更,就能持續送出資料呢?是可以這麼做,由於連線不會關閉,回應也不算完成,在XMLHttpRequest是readyState等於3(而非4)的情況下,處理收到的資料(由於回應沒有完成,不會有HTTP狀態碼),HTML5新增的Server-sent Event,本質上也是這種長時間連線模式,然而回應的格式有規範,像Content-Type須是event-steam,送回資料要有固定資料結構(如data:、id:等)。

非同步伺服端的支援

無論是單純拉長請求與回應之間的時間,或者是伺服端不關閉連線、持續送出資料,都表示伺服端必須耗用資源來保存該次請求。

以Java為例,單純只是暫停回應的話,Servlet容器分配給該次請求的執行緒就無法釋放,而該執行緒也不能去服務其他請求;若連線多到耗盡Servlet容器可分配的執行緒,就無法再接受新的請求了,必須有個方式在保留請求之後,將執行緒還給Servlet容器。

Servlet容器預設是同步版本,執行完service()後,就會進行相關資源的銷毀,因此,並非自行建立執行緒來持有請求與回應物件,就可以解決這件事,而是,容器實作必須支援。

在Servlet 3.0之前,Tomcat、Jetty等容器,採用NIO而實現了非阻塞IO來支援。以Tomcat為例,必須設定使用Http11NioProtocol,而Jetty的話,則透過ContinuationSupport來支援。

在Servlet 3.0之後,開始支援非同步的Servlet。透過將Servlet的asyncSupported標示為true,可以從HttpServletRequest的startAsync(),取得AsyncContext,而這樣的請求、回應物件,就與當次容器分配的執行緒脫勾——在執行完service()之後,執行緒就回到容器之中,然後,等待服務下一次的請求。

若想了解使用Servlet實現非同步的長時間連線的方法,可以參考〈非同步 Long Polling(https://goo.gl/ReSR4Q)〉與〈非同步 Server-Sent Event(https://goo.gl/wmRy8n)〉。

HTTP/2 Server Push

在籠統的定義上,只要從使用者的觀點看來,像是即時地取得伺服端的變化,就可以說是做到了Server Push。

然而,有人更嚴格地看待,認為定時查詢,或者伺服端延遲回應的查詢方式,都不算是Server Push。至於這些方式,哪一個算是Comet的實現?而哪一個不算?也就有得吵了。Comet?如果沒聽過的話,建議也不用去仔細考究,那並不是有嚴格定義的名詞。

在HTTP的基礎上,相關的規格書中,曾經提到Server Push名詞的部分,是在HTTP/2(出現在HTTP/2規格書8.2節),理由之一在於,HTTP/1.1中,瀏覽器下載HTML之後,就關閉連線,需要CSS時,會開啟新連線、取得檔案之後,關閉連線;而需要JavaScript時,開啟一個新連線、取得檔案後,予以關閉……於是,為了能夠在同一個HTTP連線中,儘量傳送必要的資源,於是,就有了合併CSS、JavaScript、內嵌圖片編碼等怪招。

若是支援HTTP/2 Server Push,當瀏覽器與伺服器建立HTTP/2連線後,在同一個連線中,伺服端可以主動將HTML中關聯的CSS、JavaScript、圖片等資源,先行推送到瀏覽器快取起來(像是Servlet 4.0中PushBuilder(https://goo.gl/FwFMpr)的例子),等到瀏覽器在解析HTML後,若需要相關資源,就可以從快取中直接拿到,而不需要建立連線重新發出請求。

或許有一些開發者會問到:這跟Server-Sent Event有什麼不同?

Server-Sent Event本質上是個長時間連線,如果在連線過程中送出的資料,是要在瀏覽器中建立一個<img>,在建立元素之後,瀏覽器會另外開一條連線去取得圖片,而不是在同一連線中取得。

實際上,我們可以在運用Server-Sent Event的同時,併用HTTP/2 Server Push先送出圖片,之後若送出的資料要求建立<img>,瀏覽器會直接在快取中取用已Push的資源。

HTTP/1.1使用純文字來進行傳輸,而HTTP/2以其為基礎,做了協議升級——啟用二進位格式的Frames來傳輸,在Server Push時,PUSH_PROMISE告知瀏覽器,哪個串流編號會是哪個資源;到了後續,HEADERS與DATA的Frame,會攜帶標頭與實際資料(包含串流編號),所以,支援HTTP/2的瀏覽器,會由此知道如何區別與儲存各個資源。然而,如同長時間連線模式,在請求之後,基本上,就是單向地對瀏覽器Push資料。

那麼WebSocket呢?

而同樣是基於HTTP1.1,來進行協議升級的WebSocket,只在一開始利用HTTP進行協議升級,協議完成之後,就是與HTTP無關的連線了,瀏覽器與伺服端可以進行雙向溝通,當然,也就可以做到Server Push的功能。

如果只是伺服端要更新瀏覽器,長時間連線模式下的Server Push就足夠了;若需要在同一個連線中,將瀏覽器會用到的CSS、JavaScript等資源,先型推送過去,那就會是HTTP/2 Server Push的範疇。而這兩種方式可以考慮併用,如果需要的是頻繁地雙向溝通,而不只是單向的Server Push,WebSocket會是可考量的方案之一。

作者簡介


Advertisement

更多 iThome相關內容