1. 程式人生 > >使用SuperWebSocket 構建實時 Web 應用

使用SuperWebSocket 構建實時 Web 應用

Web 應用的資訊互動過程通常是客戶端通過瀏覽器發出一個請求,伺服器端接收和稽核完請求後進行處理並返回結果給客戶端,然後客戶端瀏覽器將資訊呈現出來,這種機制對於資訊變化不是特別頻繁的應用尚能相安無事,但是對於那些實時要求比較高的應用來說,比如說線上遊戲、線上證券、裝置監控、新聞線上播報、RSS 訂閱推送等等,當客戶端瀏覽器準備呈現這些資訊的時候,這些資訊在伺服器端可能已經過時了。所以保持客戶端和伺服器端的資訊同步是實時 Web 應用的關鍵要素,對 Web 開發人員來說也是一個難題。在 WebSocket 規範出來之前,開發人員想實現這些實時的 Web 應用,不得不採用一些折衷的方案,其中最常用的就是輪詢 (Polling) 和 Comet 技術,而 Comet 技術實際上是輪詢技術的改進,又可細分為兩種實現方式,一種是長輪詢機制,一種稱為流技術。下面我們簡單介紹一下這幾種技術:

輪詢

這是最早的一種實現實時 Web 應用的方案。客戶端以一定的時間間隔向服務端發出請求,以頻繁請求的方式來保持客戶端和伺服器端的同步。這種同步方案的最大問題是,在一些資料更新比較頻繁的應用裡,頁面的資料要想得到最新的結果需要重新重新整理頁面,但這樣會產生大量的冗餘資料在伺服器和客戶端傳輸,另外由於頁面是同步處理的,所以在頁面載入完畢之前是不能繼續操作的。當客戶端以固定頻率向伺服器發起請求的時候,伺服器端的資料可能並沒有更新,這樣會帶來很多無謂的網路傳輸,所以這是一種非常低效的實時方案。

長輪詢:

長輪詢是對定時輪詢的改進和提高,目地是為了降低無效的網路傳輸。當伺服器端沒有資料更新的時候,連線會保持一段時間週期直到資料或狀態改變或者時間過期,通過這種機制來減少無效的客戶端和伺服器間的互動。當然,如果服務端的資料變更非常頻繁的話,這種機制和定時輪詢比較起來沒有本質上的效能的提高。

Comet

流:

流技術方案通常就是在客戶端的頁面使用一個隱藏的視窗向服務端發出一個長連線的請求。伺服器端接到這個請求後作出迴應並不斷更新連線狀態以保證客戶端和伺服器端的連線不過期。通過這種機制可以將伺服器端的資訊源源不斷地推向客戶端。這種機制在使用者體驗上有一點問題,需要針對不同的瀏覽器設計不同的方案來改進使用者體驗,同時這種機制在併發比較大的情況下,對伺服器端的資源是一個極大的考驗。

綜合這幾種方案,您會發現這些目前我們所使用的所謂的實時技術並不是真正的實時技術,它們只是在用 Ajax 方式來模擬實時的效果,在每次客戶端和伺服器端互動的時候都是一次 HTTP 的請求和應答的過程,而每一次的 HTTP 請求和應答都帶有完整的 HTTP 頭資訊,這就增加了每次傳輸的資料量,而且這些方案中客戶端和伺服器端的程式設計實現都比較複雜,在實際的應用中,為了模擬比較真實的實時效果,開發人員往往需要構造兩個 HTTP 連線來模擬客戶端和伺服器之間的雙向通訊,一個連線用來處理客戶端到伺服器端的資料傳輸,一個連線用來處理伺服器端到客戶端的資料傳輸,這不可避免地增加了程式設計實現的複雜度,也增加了伺服器端的負載,制約了應用系統的擴充套件性。

HTML5 WebSocket 設計出來的目的就是要取代輪詢和 Comet 技術,使客戶端瀏覽器具備像 C/S 架構下桌面系統的實時通訊能力。 瀏覽器通過 JavaScript 向伺服器發出建立 WebSocket 連線的請求,連線建立以後,客戶端和伺服器端就可以通過 TCP 連線直接交換資料。因為 WebSocket 連線本質上就是一個 TCP 連線,所以在資料傳輸的穩定性和資料傳輸量的大小方面,和輪詢以及 Comet 技術比較,具有很大的效能優勢。Websocket.org 網站對傳統的輪詢方式和 WebSocket 呼叫方式作了一個詳細的測試和比較,將一個簡單的 Web 應用分別用輪詢方式和 WebSocket 方式來實現,在這裡引用一下他們的測試結果圖(http://www.websocket.org/quantum.html ):

websocket

輪詢和 WebSocket 實現方式的網路負載對比圖

通過這張圖可以清楚的看出,在流量和負載增大的情況下,WebSocket 方案相比傳統的 Ajax 輪詢方案有極大的效能優勢。這也是為什麼我們認為 WebSocket 是未來實時 Web 應用的首選方案的原因。

WebSocket協議設計用來取代使用HTTP作為傳輸層的雙向通訊技術,並從現有的基礎設施(代理、過濾器、認證)受益。這些技術作為效率與可靠性的平衡而實現,因為HTTP最初並不是用於雙向通訊的。WebSocket嘗試解決在現有HTTP基礎設施的環境下現有HTTP雙向通訊技術的目標;像這樣,它設計來工作於HTTP 80、443埠上,並支援HTTP代理和中間設施,即使這意味著增加現有環境的一些複雜性。

然後,設計並沒有將WebSocket侷限於HTTP,未來的實現可以在特定的埠上使用更簡單的握手,而不需要重新發明整個協議。最後點是重要的,因為互動式訊息的傳輸模式並不緊密符合標準的HTTP傳輸,會在一些部件上引起異常的負載。

SuperWebSocket是基於.NET開源Socket框架開發的, SuperSocket所支援的大部分功能在SuperWebSocket中得到了繼承。使用者可通過SuperWebSocket來快速的構建可靠的,高效能的websocket伺服器端應用程式。

和SuperSocket一樣,SuperWebSocket可以控制檯和windows服務的形式執行,同時它還支援直接執行在Website之內,這樣更簡化了使用者的部署。

WebSocket 協議本質上是一個基於 TCP 的協議。為了建立一個 WebSocket 連線,客戶端瀏覽器首先要向伺服器發起一個 HTTP 請求,這個請求和通常的 HTTP 請求不同,包含了一些附加頭資訊,其中附加頭資訊”Upgrade: WebSocket”表明這是一個申請協議升級的 HTTP 請求,伺服器端解析這些附加的頭資訊然後產生應答資訊返回給客戶端,客戶端和伺服器端的 WebSocket 連線就建立起來了,雙方就可以通過這個連線通道自由的傳遞資訊,並且這個連線會持續存在直到客戶端或者伺服器端的某一方主動的關閉連線。

下面我們來詳細介紹一下 WebSocket 規範,WebSocket 協議有兩部分:握手和資料傳輸。

客戶端發出的握手資訊:

GET /chat HTTP/1.1

Host: server.example.com

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Origin: http://example.com

Sec-WebSocket-Protocol: chat, superchat

Sec-WebSocket-Version: 13

伺服器端返回的握手資訊:

HTTP/1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Protocol: chat

客戶端握手的引導行遵從(HTTP)Request-Line格式,伺服器發出的引導行遵從(HTTP)Status-Line格式。在兩種情況下,引導行後面跟著一組未排序的頭域。額外的頭域也可能出現,如cookie。頭的格式和解析在RFC2616定義。

一旦客戶端和伺服器都發送了他們的握手,如果握手成功,傳輸資料部分開始。

在實際的開發過程中,為了使用 WebSocket 介面構建 Web 應用,我們首先需要構建一個實現了 WebSocket 規範的伺服器,伺服器端的實現不受平臺和開發語言的限制,只需要遵從 WebSocket 規範即可,目前已經出現了一些比較成熟的 WebSocket 伺服器端實現,比如:

  • Kaazing WebSocket Gateway — 一個 Java 實現的 WebSocket Server
  • mod_pywebsocket — 一個 Python 實現的 WebSocket Server
  • Netty —一個 Java 實現的網路框架其中包括了對 WebSocket 的支援
  • node.js —一個 Server 端的 JavaScript 框架提供了對 WebSocket 的支援
  • SuperWebSocket --一個.NET/Mono 實現的WebSocket Server(本文的主角)

瀏覽器支援

下面是主流瀏覽器對 HTML5 WebSocket 的支援情況:

瀏覽器

支援情況

Chrome

Supported in version 4+

Firefox

Supported in version 4+

Internet Explorer

Supported in version 10+

Opera

Supported in version 10+

Safari

Supported in version 5+

SuperWebSocket是基於.NET開源Socket框架開發的, SuperSocket所支援的大部分功能在中得到了繼承。使用者可通過SuperWebSocket來快速的構建可靠的,高效能的websocket伺服器端應用程式。和SuperSocket一樣,SuperWebSocket可以控制檯和windows服務的形式執行,同時它還支援直接執行在Website之內,這樣更簡化了使用者的部署。

在Global.asax檔案裡看StartSuperWebSocketByConfig:

void StartSuperWebSocketByConfig()
      {
          var serverConfig = ConfigurationManager.GetSection("socketServer") as SocketServiceConfig;
          if (!SocketServerManager.Initialize(serverConfig))
              return;

          var socketServer = SocketServerManager.GetServerByName("SuperWebSocket") as WebSocketServer;
          var secureSocketServer = SocketServerManager.GetServerByName("SecureSuperWebSocket") as WebSocketServer;

          Application["WebSocketPort"] = socketServer.Config.Port;
          Application["SecureWebSocketPort"] = secureSocketServer.Config.Port;

          socketServer.NewMessageReceived += new SessionEventHandler<WebSocketSession, string>(socketServer_NewMessageReceived);
          socketServer.NewSessionConnected += new SessionEventHandler<WebSocketSession>(socketServer_NewSessionConnected);
          socketServer.SessionClosed += new SessionEventHandler<WebSocketSession, CloseReason>(socketServer_SessionClosed);

          secureSocketServer.NewSessionConnected += new SessionEventHandler<WebSocketSession>(secureSocketServer_NewSessionConnected);
          secureSocketServer.SessionClosed += new SessionEventHandler<WebSocketSession, CloseReason>(secureSocketServer_SessionClosed);

          if (!SocketServerManager.Start())
              SocketServerManager.Stop();
      }

有三個事件(CommandHandler, NewSessionConnected, SessionClosed),在每個會話到達的時候,將建立新的處理程式來處理。

客戶端的實現相對於伺服器端的實現來說要簡單得多了,我們只需要發揮想象去設計 HTML 使用者介面,然後呼叫 WebSocket JavaScript 介面來和 WebSocket 伺服器端來互動就可以了。當然別忘了使用一個支援 HTML5 和 WebSocket 的瀏覽器。

當頁面初次載入的時候,首先會檢測當前的瀏覽器是否支援 WebSocket 並給出相應的提示資訊。頁面會初始化一個到聊天伺服器的 WebSocekt 連線,初始化成功以後,頁面會載入對應的 WebSocket 事件處理函式,客戶端 JavaScript 程式碼如下所示:

<script type="text/javascript">
     var noSupportMessage = "Your browser cannot support WebSocket!";
     var ws;

     function resizeFrame() {
         var h = $(window).height();
         var w = $(window).width();
         //Adapt screen height
         $('#messageBoard').css("height", (h - 80 - 50 - 100) + "px");
         $('#messageBoxCell').css("width", (w - 100) + "px");
         $('#messageBox').css("width", (w - 110) + "px");
     }

     $(document).keypress(function (e) {
         if (e.ctrlKey && e.which == 13 || e.which == 10) {
             $("#btnSend").click();
             document.body.focus();
         } else if (e.shiftKey && e.which == 13 || e.which == 10) {
             $("#btnSend").click();
             document.body.focus();
         }
     })

     function scrollToBottom(target) {
         target.animate({ scrollTop: target[0].scrollHeight });
     }

     function connectSocketServer() {
         var messageBoard = $('#messageBoard');

         var support = "MozWebSocket" in window ? 'MozWebSocket' : ("WebSocket" in window ? 'WebSocket' : null);

         if (support == null) {
             alert(noSupportMessage);
             messageBoard.append("* " + noSupportMessage + "<br/>");
             return;
         }

         messageBoard.append("* Connecting to server ..<br/>");
         // create a new websocket and connect
         ws = new window[support]('ws://<%= Request.Url.Host %>:<%= WebSocketPort %>/sample');

         // when data is comming from the server, this metod is called
         ws.onmessage = function (evt) {
             messageBoard.append("# " + evt.data + "<br />");
             scrollToBottom(messageBoard);
         };

         // when the connection is established, this method is called
         ws.onopen = function () {
             messageBoard.append('* Connection open<br/>');
         };

         // when the connection is closed, this method is called
         ws.onclose = function () {
             messageBoard.append('* Connection closed<br/>');
         }

         //setup secure websocket
         var wss = new window[support]('wss://<%= Request.Url.Host %>:<%= SecureWebSocketPort %>/sample');

         // when data is comming from the server, this metod is called
         wss.onmessage = function (evt) {
             messageBoard.append("# " + evt.data + "<br />");
             scrollToBottom(messageBoard);
         };

         // when the connection is established, this method is called
         wss.onopen = function () {
             messageBoard.append('* Secure Connection open<br/>');
         };

         // when the connection is closed, this method is called
         wss.onclose = function () {
             messageBoard.append('* Secure Connection closed<br/>');
         }
     }

     function sendMessage() {
         if (ws) {
             var messageBox = document.getElementById('messageBox');
             ws.send(messageBox.value);
             messageBox.value = "";
         } else {
             alert(noSupportMessage);
         }
     }

     jQuery.event.add(window, "resize", resizeFrame);

     window.onload = function () {
         resizeFrame();
         connectSocketServer();
     }
</script>

本文介紹了 WebSocket 規範和 WebSocket 介面,以及和傳統的實時技術相比在效能上的優勢,並且演示了怎樣使用 WebSocket 構建一個實時的 Web 應用,最後我們介紹了當前的主流瀏覽器對 HTML5 的支援情況。微軟也明確表達了未來對 HTML5 的支援,而且這些支援我們可以在 Windows 8 和 IE10 裡看到,我們也在各種移動裝置,平板電腦上看到了 HTML5 和 WebSocket 的身影。WebSocket 將會成為未來開發實時 Web 應用的生力軍應該是毫無懸念的了,作為 Web 開發人員,關注 HTML5,關注 WebSocket。

參考文章: