在上一篇(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