在上一篇(WebSocket - 新一代網路傳輸技術WebSocket簡介)中,限量大略簡單介紹了WebSocket的原理與背景,接下來在篇文章中,限量要來根據WebSocket標準(RFC-6455)來時做一套簡單的WebSocket Server Console,來接收Client端的訊息並進行處理。
使用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的實作方式:
_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連線所使用的通訊埠。
(2) 相關方法
Start:Start()為Server啟動的方法,當Server啟動後,_serverSocket會開始監聽連線該通訊埠的Client,並開始接收Client的連線資訊。
onConnect: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:當Disconnected事件被觸發後,DisconnectedWork會在_connection中移除中斷連線的Client。
CreateAcceptKey:CreateAcceptKey為進行Sec-WebSocket-Key轉換成Base64字串格式的方法。
ComputeHash:ComputeHash為使用SHA1加密成ASCII Byte陣列。
_connection:_connection為_serverSocket所接收到的Client端Socket。
_dataBuffer:主要作為Client端接收過來的資料緩衝區。
OnDataReceived:當此連線收到Client端所傳送的資料時,會觸發DataReceived事件。
OnDisconnected:當該Client連線中斷時,會觸發Disconnected事件,將連線中斷。
(2) 相關方法
listen:listen()方法會監聽Client是否有傳送資料,如果有收到資料,就將資料暫存到_dataBuffer裡,然後呼叫Read()方法進行資料的讀取。
Read:Read()將_dataBuffer中的資料根據RFC-6455所規定的訊息格式進行解析,首先判斷傳入的訊息是否為正常長度(程式碼48行),正常的訊息長度最少包含了訊息格式的前2個byte。接著要判斷第一個byte內的FIN bit是否為1,FIN為1代表此Data Frame為該訊息串的最後一個Frame,在這裡我們使用&運算子與0x80(10000000),因為本範例為簡易的Server範例,僅簡單處理Single Data Frame的狀況,故暫不處理超過一個Frame的訊息串。下一步判斷Client傳送過來的訊息串是否有Mask,在WebSocket協定中規定Client端傳給Server端的資料需要進行Mask,而Server傳給Client的資料不須進行Mask,這裡我們將第二個Byte與0x80進行&運算,如果Mask bit不為1代表Client端傳送的資料沒有Mask,是個錯誤的訊息格式,依照標準需要中斷連線。再來要由第二個Byte中取出資料長度與Mask Key,由計算出的長度從訊息串中取出等長的byte為Client Mask後的實際訊息,然後在透過Mask Key與協定中所提供的解碼演算法將訊息進行解碼,最後再將解碼後的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。
以上範例解說說明了WebSocket協定中所制定的基本收送Data Frame格式,在協定中還有許多進階的Data Frame參數功能說明,在此便不多作說明。另外,在.Net Framework中已有許多開發團隊已開發了許多WebSocket套件,如Alchemy WebSocket、SuperWebSocket、WebSocket4Net…等,Java則有JWebSocket、WebSocket4J等函式庫,這些套件都是為了讓開發者能夠更簡單快速的使用WebSocket進行開發,在下圖中,我可以看到範例執行的結果,透過WebSocket,來自不同Client端的訊息能夠快速的Broadcast到其他Client的視窗上,連線的生命週期也能有效的控管。
參考來源:
WebSocket – 新一代網路傳輸技術
站內相關文章:
SignalR - .Net Real Time Web傳輸技術
WebSocket - 新一代網路傳輸技術WebSocket簡介
HTML5 - Server-Send Event (SSE)
使用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
(1) 相關欄位與屬性:_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連線所使用的通訊埠。
// Server端的Socket private Socket _serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP); // SHA1加密 private SHA1 _sha1 = SHA1CryptoServiceProvider.Create(); // WebSocket專用GUID private static readonly String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // 儲存所有Client連線的佇列 private List<WebSocketConnection> _connections = new List<WebSocketConnection>(); // 建立連線後觸發的事件 public event ClientConnectedHandler OnConnected; // 通訊埠 public Int32 Port { get; private set; }
(2) 相關方法
Start:Start()為Server啟動的方法,當Server啟動後,_serverSocket會開始監聽連線該通訊埠的Client,並開始接收Client的連線資訊。
public void Start() { // 啟動Server Socket並監聽 _serverSocket.Bind(new IPEndPoint(IPAddress.Any, Port)); _serverSocket.Listen(128); // Server Socket準備接收Client端連線 _serverSocket.BeginAccept(new AsyncCallback(onConnect), null); }
onConnect:Client連線上後,進入onConnect()內產生一個Client Socket連線,接著Server與Client開始進行HandShake動作。
private void onConnect(IAsyncResult result) { var clientSocket = _serverSocket.EndAccept(result); // 進行ShakeHand動作 ShakeHands(clientSocket); }
ShakeHand:ShakeHand()中進行Client-Server的HandShake動作,首先Server會從Client接收Request訊息,取出Sec-WebSocket-Key的值後方加上GUID加密後,再組成HandShake訊息字串後以Byte陣列型式回傳給Client完成HandShake。我們在HandShake後產生一個WebSocketConnection實體並加入_connection集合中,並註冊WebSocketConnection的Disconnected事件,最後_serverSocket再持續監聽是否有其他Client連上。
private void ShakeHands(Socket socket) { // 存放Request資料的Buffer byte[] buffer = new byte[1024]; // 接收的Request長度 var length = socket.Receive(buffer); // 將buffer中的資料解碼成字串 var data = Encoding.UTF8.GetString(buffer, 0, length); Console.WriteLine(data); // 將資料字串中的空白位元移除 var dataArray = data.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); // 從Client傳來的Request Header訊息中取 var key = dataArray.Where(s => s.Contains("Sec-WebSocket-Key: ")).Single().Replace("Sec-WebSocket-Key: ", String.Empty).Trim(); var acceptKey = CreateAcceptKey(key); // WebSocket Protocol定義的ShakeHand訊息 var handShakeMsg = "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: " + acceptKey + "\r\n\r\n"; socket.Send(Encoding.UTF8.GetBytes(handShakeMsg)); Console.WriteLine(handShakeMsg); // 產生WebSocketConnection實體並加入佇列中管理 var clientConn = new WebSocketConnection(socket); _connections.Add(clientConn); // 註冊Disconnected事件 clientConn.OnDisconnected += new ClientDisconnectedEventHandler(DisconnectedWork); // 確認Connection是否繼續存在,並持續監聽 if (OnConnected != null) OnConnected(clientConn, EventArgs.Empty); _serverSocket.BeginAccept(new AsyncCallback(onConnect), null); }
DisconnectedWork:當Disconnected事件被觸發後,DisconnectedWork會在_connection中移除中斷連線的Client。
private void DisconnectedWork(WebSocketConnection sender, EventArgs ev) { _connections.Remove(sender); sender.Close(); }
CreateAcceptKey:CreateAcceptKey為進行Sec-WebSocket-Key轉換成Base64字串格式的方法。
private String CreateAcceptKey(String key) { String keyStr = key + GUID; byte[] hashBytes = ComputeHash(keyStr); return Convert.ToBase64String(hashBytes); }
ComputeHash:ComputeHash為使用SHA1加密成ASCII Byte陣列。
private byte[] ComputeHash(String str) { return _sha1.ComputeHash(System.Text.Encoding.ASCII.GetBytes(str)); }
WebSocketConnection
(1) 相關欄位與屬性:_connection:_connection為_serverSocket所接收到的Client端Socket。
_dataBuffer:主要作為Client端接收過來的資料緩衝區。
OnDataReceived:當此連線收到Client端所傳送的資料時,會觸發DataReceived事件。
OnDisconnected:當該Client連線中斷時,會觸發Disconnected事件,將連線中斷。
private Socket _connection = null; // 存放資料的buffter private Byte[] _dataBuffer = new Byte[256]; public event DataReceivedEventHandler OnDataReceived; public event ClientDisconnectedEventHandler OnDisconnected;
(2) 相關方法
listen:listen()方法會監聽Client是否有傳送資料,如果有收到資料,就將資料暫存到_dataBuffer裡,然後呼叫Read()方法進行資料的讀取。
private void listen() { _connection.BeginReceive(_dataBuffer, 0, _dataBuffer.Length, SocketFlags.None, Read, null); }
Read:Read()將_dataBuffer中的資料根據RFC-6455所規定的訊息格式進行解析,首先判斷傳入的訊息是否為正常長度(程式碼48行),正常的訊息長度最少包含了訊息格式的前2個byte。接著要判斷第一個byte內的FIN bit是否為1,FIN為1代表此Data Frame為該訊息串的最後一個Frame,在這裡我們使用&運算子與0x80(10000000),因為本範例為簡易的Server範例,僅簡單處理Single Data Frame的狀況,故暫不處理超過一個Frame的訊息串。下一步判斷Client傳送過來的訊息串是否有Mask,在WebSocket協定中規定Client端傳給Server端的資料需要進行Mask,而Server傳給Client的資料不須進行Mask,這裡我們將第二個Byte與0x80進行&運算,如果Mask bit不為1代表Client端傳送的資料沒有Mask,是個錯誤的訊息格式,依照標準需要中斷連線。再來要由第二個Byte中取出資料長度與Mask Key,由計算出的長度從訊息串中取出等長的byte為Client Mask後的實際訊息,然後在透過Mask Key與協定中所提供的解碼演算法將訊息進行解碼,最後再將解碼後的Byte陣列轉換為UTF8字串,即為實際訊息。
private void Read(IAsyncResult result) { var receivedSize = _connection.EndReceive(result); if (receivedSize > 2) { // 判斷是否為最後一個Frame(第一個bit為FIN若為1代表此Frame為最後一個Frame),超過一個Frame暫不處理 if (!((_dataBuffer[0] & 0x80) == 0x80)) { Console.WriteLine("Exceed 1 Frame. Not Handle"); return; } // 是否包含Mask(第一個bit為1代表有Mask),沒有Mask則不處理 if (!((_dataBuffer[1] & 0x80) == 0x80)) { Console.WriteLine("Exception: No Mask"); OnDisconnected(this, EventArgs.Empty); return; } // 資料長度 = dataBuffer[1] - 127 var payloadLen = _dataBuffer[1] & 0x7F; var masks = new Byte[4]; var payloadData = filterPayloadData(ref payloadLen, ref masks); // 使用WebSocket Protocol中的公式解析資料 for (var i = 0; i < payloadLen; i++) payloadData[i] = (Byte)(payloadData[i] ^ masks[i % 4]); // 解析出的資料 var content = Encoding.UTF8.GetString(payloadData); Console.WriteLine("Received Data: {0}", content); // 確認是否繼續接收資料,並持續監聽 if (OnDataReceived != null) OnDataReceived(this, new DataReceivedEventArgs(content)); listen(); } else { Console.WriteLine("Receive Error Data Frame"); if (OnDisconnected != null) OnDisconnected(this, EventArgs.Empty); } }
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。
private Byte[] filterPayloadData(ref int length, ref Byte[] masks) { Byte[] payloadData; switch (length) { // 包含16 bit Extend Payload Length case 126: Array.Copy(_dataBuffer, 4, masks, 0, 4); length = (UInt16)(_dataBuffer[2] << 8 | _dataBuffer[3]); payloadData = new Byte[length]; Array.Copy(_dataBuffer, 8, payloadData, 0, length); break; // 包含 64 bit Extend Payload Length case 127: Array.Copy(_dataBuffer, 10, masks, 0, 4); var uInt64Bytes = new Byte[8]; for (int i = 0; i < 8; i++) { uInt64Bytes[i] = _dataBuffer[9 - i]; } UInt64 len = BitConverter.ToUInt64(uInt64Bytes, 0); payloadData = new Byte[len]; for (UInt64 i = 0; i < len; i++) payloadData[i] = _dataBuffer[i + 14]; break; // 沒有 Extend Payload Length default: Array.Copy(_dataBuffer, 2, masks, 0, 4); payloadData = new Byte[length]; Array.Copy(_dataBuffer, 6, payloadData, 0, length); break; } return payloadData; }
Send:當Server要傳送資料至Client端時,我們需要將傳送的訊息編組成標準的訊息字串Byte陣列送出,Client端的瀏覽器收到資料後才能解析出訊息。首先我們先分析訊息長度,如果訊息長度大於等於126且小於等於65535(0xFFFF)則需要將長度加長到2 Bytes且長度前須加上一個值為126的Byte;若訊息長度大於65535時,需要將長度加長到8 Bytes且長度前須加上一個值為127的Byte。
public void Send(Object data) { if (_connection.Connected) { try { // 將資料字串轉成Byte var contentByte = Encoding.UTF8.GetBytes(data.ToString()); var dataBytes = new List(); if (contentByte.Length < 126) // 資料長度小於126,Type1格式 { // 未切割的Data Frame開頭 dataBytes.Add((Byte)0x81); dataBytes.Add((Byte)contentByte.Length); dataBytes.AddRange(contentByte); } else if (contentByte.Length <= 65535) // 長度介於126與65535(0xFFFF)之間,Type2格式 { dataBytes.Add((Byte)0x81); dataBytes.Add((Byte)0x7E); // 126 // Extend Data 加長至2Byte dataBytes.Add((Byte)((contentByte.Length >> 8) & 0xFF)); dataBytes.Add((Byte)((contentByte.Length) & 0xFF)); dataBytes.AddRange(contentByte); } else // 長度大於65535,Type3格式 { dataBytes.Add((Byte)0x81); dataBytes.Add((Byte)0x7F); // 127 // Extned Data 加長至8Byte dataBytes.Add((Byte)((contentByte.Length >> 56) & 0xFF)); dataBytes.Add((Byte)((contentByte.Length >> 48) & 0xFF)); dataBytes.Add((Byte)((contentByte.Length >> 40) & 0xFF)); dataBytes.Add((Byte)((contentByte.Length >> 32) & 0xFF)); dataBytes.Add((Byte)((contentByte.Length >> 24) & 0xFF)); dataBytes.Add((Byte)((contentByte.Length >> 16) & 0xFF)); dataBytes.Add((Byte)((contentByte.Length >> 8) & 0xFF)); dataBytes.Add((Byte)((contentByte.Length) & 0xFF)); dataBytes.AddRange(contentByte); } _connection.Send(dataBytes.ToArray()); } catch(Exception ex) { Console.WriteLine(ex.Message); if (OnDisconnected != null) OnDisconnected(this, EventArgs.Empty); } } }
DataReceivedEventArgs
為了在觸發事件中取得Server接收到Client傳送的資料進行處理,在這裡實作了DataReceivedEventArgs取得接收資料,並根據需求可傳送到其他Client端。public class DataReceivedEventArgs : EventArgs { // OnReceive事件發生時傳入的資料字串 public String Data { get; private set; } public DataReceivedEventArgs(String data) { Data = data; } }
WebSocket Client
WebSocket Client的實作方式非常簡單,只要在HTML5網頁Java Script區段中使用WebSocket API,並指定WebSocket Server的位址就可以連線。在範例程式碼中,要先在<html> tag上加上<!DOCTYPE html>表示該網頁為HTML5,接著再Java Script區段中使用if ("WebSocket" in window)可以判斷該瀏覽器是否有支援WebSocket,在建立WebSocket時要先宣告WebSockt物件並指定位址,以下為JavaScript區段的範例程式碼。$(function() { if ("WebSocket" in window) { var url = "ws://localhost:1104"; var ws = new WebSocket(url); ws.onopen = function() { alert("Connected"); }; ws.onmessage = function (evt) { var received_msg = evt.data; alert("Received " + received_msg) }; ws.onclose = function() { alert("Closed"); } ws.onerror = function() { alert("Error"); } $('#SendMsgBtn').click(function(event){ ws.send($('#MyInput').val()); }); $('#CloseBtn').click(function(event){ ws.close(); }); } else { alert("WebSocket NOT supported by your Browser!"); } });
以上範例解說說明了WebSocket協定中所制定的基本收送Data Frame格式,在協定中還有許多進階的Data Frame參數功能說明,在此便不多作說明。另外,在.Net Framework中已有許多開發團隊已開發了許多WebSocket套件,如Alchemy WebSocket、SuperWebSocket、WebSocket4Net…等,Java則有JWebSocket、WebSocket4J等函式庫,這些套件都是為了讓開發者能夠更簡單快速的使用WebSocket進行開發,在下圖中,我可以看到範例執行的結果,透過WebSocket,來自不同Client端的訊息能夠快速的Broadcast到其他Client的視窗上,連線的生命週期也能有效的控管。
參考來源:
WebSocket – 新一代網路傳輸技術
站內相關文章:
SignalR - .Net Real Time Web傳輸技術
WebSocket - 新一代網路傳輸技術WebSocket簡介
HTML5 - Server-Send Event (SSE)
您好,我利用範例code試寫OK了,
回覆刪除但當Send資料長度大於65535,瀏覽器會收不到資料,約幾秒後C#程式就會斷訊.
不知該如何解?
大大比較厲害..小弟是連編譯都有問題
刪除比方說 public event ClientConnectedHandler OnConnected;
直接紅色一條線 = =
https://stackoverflow.com/questions/17693956/websocket-sending-messages-over-65535-bytes-fails
刪除contentByte.Length 轉型為long