為什麼即時Web這麼重要?我們生活在一個即時(real-time)的世界中,因此Web的最終最自然的狀態也應該是即時的。用戶需要即時的溝通、數據和搜尋。我們對網路資訊即時性的要求也越來越高,如果資訊或訊息延遲幾分鐘後才更新,簡直讓人無法忍受。現在許多大公司(如Google、Facebook和Twitter)已經開始關注即時Web,並提供了即時性服務。即時Web將是未來最熱門的話題之一。
實時Web的發展歷史
傳統的Web是基於HTTP的請求/響應模型的:客戶端請求一個新頁面,伺服器將內容發送到客戶端,客戶端再請求另外一個頁面時又要重新發送請求。後來有人提出了AJAX,AJAX使得頁面的體驗更加“動態”,可以在後台發起到伺服器的請求。但是,如果伺服器有更多資料需要推送到客戶端,在頁面載入完成後是無法實現直接將資料從伺服器傳送給客戶端的。即時數據無法被「推送」給客戶端。
為了解決這個問題,有人提出了許多解決方案。最簡單(暴力)的方案是用輪詢:每隔一段時間都會向伺服器請求新資料。這讓用戶感覺應用程式是即時的。實際上這會造成延遲和效能問題,因為伺服器每秒都要處理大量的連接請求,每次請求都會有TCP三次握手並附帶HTTP的頭資訊。儘管現在許多應用程式仍在使用輪詢,但這並不是最理想的解決方案。
後來隨著Comet技術的提出,又出現了許多更進階的解決方案。這些技術方案包括永久幀(forever frame)、XHR串流(xhr-multipart)、htmlfile,以及長輪詢。長輪詢是指,客戶端發起一個到伺服器的XHR連接,這個連線永遠不會關閉,對客戶端來說連線總是掛起狀態。當伺服器有新資料時,就會及時地將回應傳送給客戶端,接著再將連線關閉。然後重複整個過程,透過這種方式就實作了「伺服器推」(server push)。
Comet技術是非標準的hack技術,正因為此,瀏覽器端的相容性就成了問題。首先,效能問題無法解決,向伺服器發起的每個連線都帶有完整的HTTP頭訊息,如果你的應用程式需要很低的延時,這將是一個棘手的問題。當然不是說Comet本身有問題,因為還沒有其他替代方案前Comet是我們的唯一選擇。
瀏覽器插件(如Flash)和Java同樣被用來實作伺服器推。它們可以基於TCP直接和伺服器建立socket連接,這種連接非常適合將即時數據推給客戶端。問題是並不是所有的瀏覽器都安裝了這些插件,而且它們常常被防火牆攔截,特別是在公司網路中。
現在HTML5規範為我們準備了一個替代方案。但這個規範稍微有些超前,很多瀏覽器都還不支持,特別是IE,對於現在很多開發者來說幫助不大,鑑於大部分瀏覽器還未實現HTML5的WebSocket,現行最好的辦法仍然是使用Comet。
WebSocket 前世今生
眾所周知,Web 應用的交互過程通常是客戶端透過瀏覽器發出一個請求,伺服器端接收請求後進行處理並返回結果給客戶端,客戶端瀏覽器將資訊呈現,這種機制對於資訊變化不是特別頻繁的應用尚可,但對於即時要求高、海量並發的應用來說顯得捉襟見肘,尤其在當前業界移動互聯網蓬勃發展的趨勢下,高並發與用戶實時響應是Web 應用經常面臨的問題,例如金融證券的即時訊息,Web 導航應用中的地理位置獲取,社交網路的即時訊息推送等。
傳統的請求-回應模式的Web 開發在處理此類業務場景時,通常採用即時通訊方案,常見的是:
輪詢,原理簡單易懂,就是客戶端透過一定的時間間隔以頻繁請求的方式向伺服器發送請求,來保持客戶端和伺服器端的資料同步。問題很明顯,當客戶端以固定頻率向伺服器端發送請求時,伺服器端的資料可能並沒有更新,帶來許多無謂請求,浪費頻寬,效率低。
基於 Flash,AdobeFlash 透過自己的 Socket 實現完成資料交換,再利用 Flash 暴露出相應的介面為 JavaScript 調用,從而達到即時傳輸目的。此方式比輪詢更有效率,而且因為 Flash 安裝率高,應用場景比較廣泛,但在行動互聯網終端上 Flash 的支援並不好。 IOS 系統中沒有 Flash 的存在,在 Android 中雖然有 Flash 的支持,但實際的使用效果差強人意,且對行動裝置的硬體配置要求較高。 2012 年 Adobe 官方宣布不再支援 Android4.1+系統,宣告了 Flash 在行動終端上的死亡。
從上文可以看出,傳統 Web 模式在處理高並發及即時性需求的時候,會遇到難以逾越的瓶頸,我們需要一種高效節能的雙向通訊機制來保證資料的即時傳輸。在此背景下,基於 HTML5 規範的、有 Web TCP 之稱的 WebSocket 應運而生。
早期HTML5 並沒有形成業界統一的規範,各個瀏覽器和應用程式伺服器廠商有著不同的類似實現,如IBM 的MQTT,Comet 開源框架等,直到2014 年,HTML5 在IBM、微軟、Google 等巨頭的推動與協作下終於塵埃落地,正式從草案落實為實際標準規範,各個應用伺服器及瀏覽器廠商逐步開始統一,在JavaEE7 中也實現了WebSocket 協議,從而無論是客戶端還是服務端的WebSocket 都已完備,讀者可查閱HTML5 規範,熟悉新的HTML 協定規範及WebSocket 支援。
WebSocket 機制
以下簡單介紹 WebSocket 的原理及運作機制。
WebSocket 是 HTML5 一種新的協定。它實現了瀏覽器與伺服器全雙工通信,能更好的節省伺服器資源和頻寬並達到即時通訊,它建立在TCP 之上,同HTTP 一樣透過TCP 來傳輸數據,但是它和HTTP 最大不同是:
WebSocket 是一種雙向通訊協議,在建立連接後,WebSocket 伺服器和Browser/Client Agent 都能主動的向對方發送或接收數據,就像Socket 一樣;
WebSocket 需要類似TCP 的客戶端和伺服器端透過握手連接,連接成功後才能相互通訊。
非WebSocket 模式傳統HTTP 用戶端與伺服器的交互如下圖所示:
非WebSocket 模式傳統HTTP 用戶端與伺服器的交互如下圖所示:圖1.傳統HTTP 請求響應客戶端伺服器交互圖 使用WebSocket 模式端與伺服器端的互動圖2.WebSocket 請求回應客戶端伺服器互動圖
圖比較可以看出,相對於傳統HTTP 每次請求-應答都需要客戶端與服務端建立連線的模式,WebSocket 是類似Socket 的TCP長連線的通訊模式,一旦WebSocket 連線建立後,後續資料都以訊框序列的形式傳輸。在用戶端中斷 WebSocket 連線或 Server 端中斷連線前,不需要用戶端和服務端重新發起連線請求。在大量並發及客戶端與伺服器交互負載流量大的情況下,極大的節省了網路頻寬資源的消耗,有明顯的效能優勢,且客戶端發送和接受訊息是在同一個持久連線上發起,即時性優勢明顯。
我們再透過客戶端和服務端互動的封包看一下WebSocket 通訊與傳統HTTP 的不同:
在客戶端,new WebSocket 實例化一個新的WebSocket 客戶端對象,連接類似ws://yourdomain: port/path 的服務端WebSocket URL,WebSocket 用戶端物件會自動解析並識別為WebSocket 請求,從而連接服務端端口,執行雙方握手過程,客戶端發送資料格式類似:
清單1.WebSocket 用戶端連接報文
GET /webfin/websocket/ HTTP/1.1 Host: localhost Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg== Origin: http://localhost:8080 Sec-WebSocket-Version: 13
可以看到,客戶端發起的WebSocket 連線封包類似傳統HTTP 封包,」Upgrade:websocket」參數值顯示這是WebSocket 類型請求,「Sec-WebSocket-Key」是WebSocket 用戶端發送的一個base64 編碼的密文,要求服務端必須傳回一個對應加密的「Sec-WebSocket-Accept」應答,否則客戶端會拋出「Error during WebSocket handshake」錯誤,並關閉連線。
服務端收到封包後回傳的資料格式類似:
清單2.WebSocket 服務端回應封包
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=
「Sec-WebSocket-Accept」的值是服務端採用與客戶端一致的金鑰計算出來後返回客戶端的,“HTTP/1.1 101 Switching Protocols”表示服務端接受WebSocket 協議的客戶端連接,經過這樣的請求-響應處理後,客戶端服務端的WebSocket 連接握手成功, 後續就可以進行TCP 通訊了。讀者可以查閱WebSocket 協定堆疊以了解 WebSocket 用戶端和服務端更詳細的互動資料格式。
在開發方面,WebSocket API 也十分簡單,我們只需要實例化WebSocket,創建連接,然後服務端和客戶端就可以相互發送和響應消息,在下文WebSocket 實現及案例分析部分,可以看到詳細的WebSocket API 及程式碼實作。
WebSocket 實作
如上文所述,WebSocket 的實現分為客戶端和服務端兩部分,客戶端(通常為瀏覽器)發出WebSocket 連接請求,服務端響應,實現類似TCP 握手的動作,從而在瀏覽器用戶端和WebSocket 服務端之間形成一條HTTP 長連線快速通道。兩者之間後續進行直接的資料互相傳送,不再需要發起連線和對應。
以下簡要描述 WebSocket 服務端 API 及客戶端 API。
WebSocket 服務端API
WebSocket 服務端在各個主流應用伺服器廠商中已基本獲得符合JEE JSR356 標準規範API 的支援(詳見JSR356 WebSocket API 規範),以下列舉了部分常見的商用及開源伺服器WebSocket Server 端的支援:
表1.WebSocket 服務端支援
廠商 應用伺服器 備註
IBM WebSphere WebSphere 8.0 以上版本支持,7.X 之前版本結合MQTT 支援類似的HTTP長連結 甲骨文 WebLogic WebLogic 12c 支持,11g 與10g 版本透過HTTP Publish 支援類似的HTTP 長連結 Tomcat Tomcat 7.0.5+支持,7.0.2X 及7.0.3X 透過自定義API 支援 Jetty Jetty 7.0+支援 ,WebT7.0.5757.0.573757.3677.367.37.36 程式碼的服務。 et 規格使用javax.websocket.*的API,可以將一個普通Java物件(POJO)使用@ServerEndpoint 註解作為WebSocket 伺服器的端點,程式碼範例如下:清單3.WebSocket 服務端API 範例@ServerEndpoint("/echo") public class EchoEndpoint { @OnOpen public void onOpen(Session session) throws IOException { //以下代码省略... } @OnMessage public String onMessage(String message) { //以下代码省略... } @Message(maxMessageSize=6) public void receiveMessage(String s) { //以下代码省略... } @OnError public void onError(Throwable t) { //以下代码省略... } @OnClose public void onClose(Session session, CloseReason reason) { //以下代码省略... } }程式碼解釋:上文的簡潔程式碼即建立了一個WebSocket 的服務端,上文的簡潔程式碼即建立了一個WebSocket @ServerEndpoint("/echo") 的annotation 註解端點表示將WebSocket 服務端運行在ws://[Server 端IP 或網域名稱]:[Server 連接埠]/websockets/echo 的存取端點,用戶端瀏覽器已經可以對WebSocket 用戶端API 發起HTTP 長連線了。 使用 ServerEndpoint 註解的類別必須有一個公共的無參數建構函數,@onMessage 註解的 Java 方法用於接收傳入的 WebSocket 訊息,這個訊息可以是文字格式,也可以是二進位格式。
OnOpen 在这个端点一个新的连接建立时被调用。参数提供了连接的另一端的更多细节。Session 表明两个 WebSocket 端点对话连接的另一端,可以理解为类似 HTTPSession 的概念。
OnClose 在连接被终止时调用。参数 closeReason 可封装更多细节,如为什么一个 WebSocket 连接关闭。
更高级的定制如 @Message 注释,MaxMessageSize 属性可以被用来定义消息字节最大限制,在示例程序中,如果超过 6 个字节的信息被接收,就报告错误和连接关闭。
注意:早期不同应用服务器支持的 WebSocket 方式不尽相同,即使同一厂商,不同版本也有细微差别,如 Tomcat 服务器 7.0.5 以上的版本都是标准 JSR356 规范实现,而 7.0.2x/7.0.3X 的版本使用自定义 API (WebSocketServlet 和 StreamInbound, 前者是一个容器,用来初始化 WebSocket 环境;后者是用来具体处理 WebSocket 请求和响应,详见案例分析部分),且 Tomcat7.0.3x 与 7.0.2x 的 createWebSocketInbound 方法的定义不同,增加了一个 HttpServletRequest 参数,使得可以从 request 参数中获取更多 WebSocket 客户端的信息,如下代码所示:
清单 4.Tomcat7.0.3X 版本 WebSocket API
public class EchoServlet extends WebSocketServlet { @Override protected StreamInbound createWebSocketInbound(String subProtocol, HttpServletRequest request) { //以下代码省略.... return new MessageInbound() { //以下代码省略.... } protected void onBinaryMessage(ByteBuffer buffer) throws IOException { //以下代码省略... } protected void onTextMessage(CharBuffer buffer) throws IOException { getWsOutbound().writeTextMessage(buffer); //以下代码省略... } }; } }
因此选择 WebSocket 的 Server 端重点需要选择其版本,通常情况下,更新的版本对 WebSocket 的支持是标准 JSR 规范 API,但也要考虑开发易用性及老版本程序移植性等方面的问题,如下文所述的客户案例,就是因为客户要求统一应用服务器版本所以使用的 Tomcat 7.0.3X 版本的 WebSocketServlet 实现,而不是 JSR356 的 @ServerEndpoint 注释端点。
WebSocket 客户端 API
对于 WebSocket 客户端,主流的浏览器(包括 PC 和移动终端)现已都支持标准的 HTML5 的 WebSocket API,这意味着客户端的 WebSocket JavaScirpt 脚本具备良好的一致性和跨平台特性,以下列举了常见的浏览器厂商对 WebSocket 的支持情况:
表 2.WebSocket 客户端支持
浏览器 支持情况
Chrome Chrome version 4+支持
Firefox Firefox version 5+支持
IE IE version 10+支持
Safari IOS 5+支持
Android Brower Android 4.5+支持
客户端 WebSocket API 基本上已经在各个主流浏览器厂商中实现了统一,因此使用标准 HTML5 定义的 WebSocket 客户端的 JavaScript API 即可,当然也可以使用业界满足 WebSocket 标准规范的开源框架,如 Socket.io。
以下以一段代码示例说明 WebSocket 的客户端实现:
清单 5.WebSocket 客户端 API 示例
var ws = new WebSocket(“ws://echo.websocket.org”); ws.onopen = function(){ws.send(“Test!”); }; ws.onmessage = function(evt){console.log(evt.data);ws.close();}; ws.onclose = function(evt){console.log(“WebSocketClosed!”);}; ws.onerror = function(evt){console.log(“WebSocketError!”);};
第一行代码是在申请一个 WebSocket 对象,参数是需要连接的服务器端的地址,同 HTTP 协议开头一样,WebSocket 协议的 URL 使用 ws://开头,另外安全的 WebSocket 协议使用 wss://开头。
第二行到第五行为 WebSocket 对象注册消息的处理函数,WebSocket 对象一共支持四个消息 onopen, onmessage, onclose 和 onerror,有了这 4 个事件,我们就可以很容易很轻松的驾驭 WebSocket。
當Browser 和WebSocketServer 連線成功後,會觸發onopen 訊息;如果連線失敗,發送、接收資料失敗或處理資料出現錯誤,browser 會觸發onerror 訊息;當Browser 接收到WebSocketServer 傳送過來的資料時,就會觸發onmessage訊息,參數evt 中包含Server 傳送過來的資料;當Browser 接收到WebSocketServer 端發送的關閉連線要求時,就會觸發onclose 訊息。我們可以看出所有的操作都是採用非同步回呼的方式觸發,這樣不會阻塞 UI,可以獲得更快的回應時間,更好的使用者體驗。