第194期 / December 5, 2013

研發新視界

分享到臉書!分享到維特!分享到噗浪!分享到Google+!分享到微博!轉寄友人友善列印

WebSocket – 新一代網路傳輸技術

作者/林憲良

[發表日期:2013/11/19]

前言

在Client-Server的傳輸架構中,雙向溝通是必要的,過去網頁透過許多種方式與Server溝通來達到雙向溝通的目的,最常見的方式是使用Comet機制,讓瀏覽器每隔一段時間對Server發出HTTP Request來確認Server是否有資料回傳,此種方式會造成許多問題:
  • 伺服器需要處理許多不同Client的TCP連線,這些連線包含了接收Client端資料與傳送資料至Client端的2條連線,因此對Server的負擔極大。

  • 由於Client不斷發出HTTP(Hyper Text Transfer Protocol) Request給Server確認是否有資料回傳,而HTTP Request Header占了大量的Bytes,傳送時不僅占了大量網路頻寬,對Server,資源的消耗量也會變大。

  • 對Client端而言,也需要維持與Server的連線資訊來判斷Server回傳的資料並進行動作。

為了解決上述的問題,IETF定義了WebSocket協定,透過單一條雙向溝通的TCP連線克服了長久以來的困境。

首先,我們先來了解在WebSocket出現之前,普遍使用的傳輸技術Comet的概念,再與WebSocket比較看出彼此的差異。

Comet簡介

在WebSocket尚未問世前,Comet是最常用來解決網頁Client-Server雙向溝通的方法,Comet主要有兩種實作方式:
  • Long Polling

    Long Polling原理是瀏覽器發出一個Request,而Server讓這個Request持續開啟一段時間,若在這時間間隔內Server有資料就會回傳給Client,如果沒有Server則會關上Request。但是,當傳送的訊息相當龐大時,可能會造成傳送不完全,使得控制失靈。

  • Streaming

    Streaming原理是Server會與Client建立起一條持續的連線,為了使連線不中斷,Server每隔一段時間會發送Response給Client,確保連線不中斷,在Streaming中使用隱藏的iframe tag,Server將資料傳入iframe,交給iframe內的javascript執行頁面的更新。使用Streaming有一些缺點,因為使用Streaming傳送時,訊息會被包裝起來,因此到防火牆前會被放入Buffer中,導致傳送時間延遲。

看完了Comet的原理後,大致了解了Comet雖然解決了雙向溝通的問題,但瓶頸在於它在控制連線生命週期上花了太多Effort,導致效能會較差,而WebSocket解決了這個問題,讓效能不會卡在連線生命週期,增強了資料傳輸的效率,接著我們來看WebSocket如何解決了Comet的問題。

WebSocket簡介

WebSocket是於2011年12月,由IETF(Internet Engineering Task Force)制定的協定(RFC-6455),W3C在HTML5規範內制定了WebSocket API的標準。由於WebSocket是建立於HTTP原有的架構之上,基本上還是以HTTP作為傳輸層,因此和一般HTTP一樣使用80與443port(https),Proxy與Firewall沿用HTTP。WebSocket大幅改善了Comet的缺點,不僅將連線數量減少為一條,當Server端有資料更新時,會自動傳送給Client端,進行即時更新的動作,傳送的HTTP Request Header也僅僅只有2bytes長度大小(如圖一),所以WebSocket非常適合應用於一些即時系統上,例如:遊戲、證券交易系統、多人共同編輯等。


《圖一》WebSocket訊息格式


建立WebSocket連線時,Client會發送HTTP Request給Server(如圖二),並要求Server將HTTP Protocol更新為WebSocket Protocol(如圖三),待Server更新完後即完成了HandShake,此時Server與Client就建立了一條雙向的WebSocket連線,資料就可以在兩者之間即時的傳輸。


《圖二》Http Request for Upgrade Http Protocol to WebSocket Protocol



《圖三》Protocol Switch HandShake


光談WebSocket的概念無法看出它的魅力與應用,接著我們要時做出一套WebSocket的架構,透過實作可以更加瞭解它的運作原理。

如同一般的Socket一樣,要使用WebSocket需要有一個Server來建立連線與處理資料的傳遞,Client可以有多個來自瀏覽器、應用程式、手機App…等,接下來我們要來分別介紹WebSocket的兩個重要的組件概念,並且解釋我們如何用C#實作出WebSocket Protocol,完成快速的資料傳輸功能。

WebSocket Client API

W3C在HTML5標準之下制定了瀏覽器與WebSocket Server溝通的WebSocket API,其API主有有四個,分別為onopen、onmessage、onerror、onclose。onopen為當瀏覽器與WebSocket Server完成HandShake後,會觸發onopen的API;onmessage為當瀏覽器從WebSocket Server端接收到資料後,會觸發onmessage的API;onerror為在整個WebSocket連線的過程中,無論是傳送資料錯誤或連線中斷…等狀況發生時,onerror會被觸發;onclose為與當WebSocket Server中斷連線後,onclose會被觸發,另外,當onerror發生後,瀏覽器接著會觸發onclose,將連線中斷。綜合上述API說明來看,整個WebSocket API觸發的流程如下圖:


《圖四》WebSocket API Life Cycle


WebSocket Server

使用WebSocket的先決條件不僅需要支援WebSocket的瀏覽器,還需要一個實作WebSocket協定的WebSocket Server。WebSocket Server詳細的實作方式必須根據RFC-6455規範來實作,WebSocket Server主要實作重點在於三個功能:(1)建立HandShake;(2)接收資料;(3)傳送資料。接下來我們將以一個由C#撰寫的WebSocket範例來說明整個WebSocket協定的基本收送功能的實作過程:

在範例中,實作了兩個類別,WebSocketServer和WebSocketConnection,WebSocketServer為主要Server核心,負責管理整個Server運作(包含建立HandShake、管理每個Client連線等)。WebSocketConnection為一個Client連線,負責處理該Client連線的訊息收發與生命週期,接著我們將在以下講解WebSocket Server的實作方式:

一、WebSocketServer
  • 欄位與屬性:

    _serverSocket:Server必須有一個持續監聽的Socket等待Client端進行連線,和一般Socket不同的是這裡的ProtocolType需要設定為IP。

    _sha1:WebSocket協定中使用SHA1(Security Hash Algorithm)將Socket-Accept-Key進行加密。
    GUID:在WebSocket協定中,規定進行HandShake時,在Client Request的Sec-WebSocket-Key後方加上固定的GUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11),經過SHA1加密後產生Sec-WebSocket-Accept字串。

    _connections:儲存與Server連線的所有Client,以便控制Client之間的資料傳輸與生命週期管理。
    OnConnected:當Client與Server建立連線後,會觸發Connnected事件。

    Port:建立Server連線所使用的通訊埠。



  • 《圖五》


  • Start(), onConncect():

    Start()為Server啟動的方法,當Server啟動後,_serverSocket會開始監聽連線該通訊埠的Client,並開始接收Client的連線資訊。Client連線上後,進入onConnect()內產生一個Client Socket連線,接著Server與Client開始進行HandShake動作。



  • 《圖六》


  • ShakeHand():

    ShakeHand()中進行Client-Server的HandShake動作,首先Server會從Client接收Request訊息,取出Sec-WebSocket-Key的值後方加上GUID加密後,再組成HandShake訊息字串後以Byte陣列型式回傳給Client完成HandShake。我們在HandShake後產生一個WebSocketConnection實體並加入_connection集合中,並註冊WebSocketConnection的Disconnected事件,最後_serverSocket再持續監聽是否有其他Client連上。



  • 《圖七》


  • DisconnectedWork, CreateAcceptKey, ComputeHash:

    當Disconnected事件被觸發後,DisconnectedWork會在_connection中移除中斷連線的Client。CreateAcceptKey為進行Sec-WebSocket-Key轉換成Base64字串格式的方法。ComputeHash則為使用SHA1加密成ASCII Byte陣列。



《圖八》


二、WebSocketConnection
  • 欄位與屬性:

    _connection:_connection為_serverSocket所接收到的Client端Socket。

    _dataBuffer:主要作為Client端接收過來的資料緩衝區。

    OnDataReceived:當此連線收到Client端所傳送的資料時,會觸發DataReceived事件。

    OnDisconnected:當該Client連線中斷時,會觸發Disconnected事件,將連線中斷。

    WebSocketConnection在建立實體後會直接進行監聽的動作,接收Client端傳送的資料。



  • 《圖九》

  • listen():

    listen()方法會監聽Client是否有傳送資料,如果有收到資料,就將資料暫存到_dataBuffer裡,然後呼叫Read()方法進行資料的讀取。


  • 《圖十》

  • Read():

    Read()將_dataBuffer中的資料根據RFC-6455所規定的訊息格式進行解析,首先判斷傳入的訊息是否為正常長度(程式碼48行),正常的訊息長度最少包含了訊息格式的前2個byte(詳見圖一)。接著要判斷第一個byte內的FIN bit是否為1(程式碼51行),FIN為1代表此Data Frame為該訊息串的最後一個Frame,在這裡我們使用&運算子與0x80(10000000),因為本範例為簡易的Server範例,僅簡單處理Single Data Frame的狀況,故暫不處理超過一個Frame的訊息串。下一步判斷Client傳送過來的訊息串是否有Mask,在WebSocket協定中規定Client端傳給Server端的資料需要進行Mask,而Server傳給Client的資料不須進行Mask,這裡我們將第二個Byte與0x80進行&運算(程式碼第57行),如果Mask bit不為1代表Client端傳送的資料沒有Mask,是個錯誤的訊息格式,依照標準需要中斷連線。再來要由第二個Byte中取出資料長度與Mask Key,由計算出的長度從訊息串中取出等長的byte為Client Mask後的實際訊息,然後在透過Mask Key與協定中所提供的解碼演算法(程式碼68, 69行)將訊息進行解碼,最後再將解碼後的Byte陣列轉換為UTF8字串,即為實際訊息。



  • 《圖十一》


  • filterPayloadData():

    在WebSocket協定中,規定如果Payload Len為126或127時,代表Payload Data裡含有Extend Data(一般Single Data Frame中只含有Application Data),若為126,則Payload Len為_dataBuffer第二個Byte的後7個bit加上_dataBuffer第二個Byte後一個Byte,共7+16 bit;若為127,則加上後8個Byte,共7+64 bit。



  • 《圖十二》


  • Send():

    當Server要傳送資料至Client端時,我們需要將傳送的訊息編組成標準的訊息字串Byte陣列送出,Client端的瀏覽器收到資料後才能解析出訊息。首先我們先分析訊息長度,如果訊息長度大於等於126且小於等於65535(0xFFFF)則需要將長度加長到2 Bytes且長度前須加上一個值為126的Byte;若訊息長度大於65535時,需要將長度加長到8 Bytes且長度前須加上一個值為127的Byte。


  • 《圖十三》


三、DataReceivedEventArgs

為了在觸發事件中取得Server接收到Client傳送的資料進行處理,我們實作了DataReceivedEventArgs取得接收資料,並根據需求可傳送到其他Client端。


《圖十四》


WebSocket Client

WebSocket Client的實作方式非常簡單,只要在HTML5網頁Java Script區段中使用WebSocket API,並指定WebSocket Server的位址就可以連線。在範例程式碼中,要先在 tag上加上表示該網頁為HTML5,接著再Java Script區段中使用if ("WebSocket" in window)可以判斷該瀏覽器是否有支援WebSocket,在建立WebSocket時要先宣告WebSockt物件並指定位址(程式碼第8行),圖十五為JavaScript區段的範例程式碼,表一為各瀏覽器版本支援WebSocket的情形。


《圖十五》WebSocket Client 範例程式碼(JavaScrip區段)



《表一》各瀏覽器版本支援WebSocket的情形


以上範例解說說明了WebSocket協定中所制定的基本收送Data Frame格式,在協定中還有許多進階的Data Frame參數功能說明,在此便不多作說明。另外,在.Net Framework中已有許多開發團隊已開發了許多WebSocket套件,如Alchemy WebSocket、SuperWebSocket、WebSocket4Net…等,Java則有JWebSocket、WebSocket4J等函式庫,這些套件都是為了讓開發者能夠更簡單快速的使用WebSocket進行開發,在圖十六範例執行結果中,我可以看到範例執行的結果,透過WebSocket,來自不同Client端的訊息能夠快速的Broadcast到其他Client的視窗上,連線的生命週期也能有效的控管。


《圖十六》範例執行結果


總結

WebSocket大幅的改善了瀏覽器與Server溝通的效能,打破了互動式網頁的瓶頸,Google、Facebook相繼使用WebSocket來使得他們的社群網頁反應更加即時。雖然WebSocket掀起了網頁技術新革命,但WebSocket屬於HTML5其中之一的標準,因此相容於WebSocket的瀏覽器數量不多,使得WebSocket在目前市場上不太普遍。世界知名網頁服務公司Google尤其推崇WebSocket,在WebSocket還沒正式制定完整的標準時,他們在Chrome 4.0就開始就部分支援WebSocket,且為了推廣自家瀏覽器Chrome,Google製作了使用WebSocket的遊戲(Super Sync Sports,如圖十七)來吸引使用者,這個遊戲有趣的地方在於他利用WebSocket讓每個玩家透過手機連接上Server來同時競賽,相信玩過這款遊戲的使用者能夠體會到WebSocket的強大。


《圖十七》Google Chrome WebSocket Game (Super Sync Sports)


參考來源

Internet Engineering Task Force (IETF) Request for Comments: 6455 (RFC-6455)
WebSocket.org
HTML5-WebSockets Tutorial