前言在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 APIW3C在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 ClientWebSocket 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