WebSocket服務器

握手協議的客戶端數據已經由瀏覽器代勞了,服務器端需要我們自己來實現,目前市場上開源的實現也比較多如:
  • Kaazing WebSocket Gateway(一個 Java 實現的 WebSocket Server);
  • mod_pywebsocket(一個 Python 實現的 WebSocket Server);
  • Netty(一個 Java 實現的網絡框架其中包括了對 WebSocket 的支持);
  • node.js(一個 Server 端的 JavaScript 框架提供了對 WebSocket 的支持);
  • WebSocket4Net(一個.net的服務器端實現);

其實在目前的.net4.5框架中已經實現了WebSocket,不用官方實現,我們自己來寫個簡單的。服務器端需要根據協議來握手、接收和發送。


握手

首先我們再來回顧下握手協議:

  1. 客戶端發到服務器的內容:
  2. GET /chat HTTP/1.1
  3. Host: server.example.com
  4. Upgrade: websocket
  5. Connection: Upgrade
  6. Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
  7. Origin: http://example.com
  8. Sec-WebSocket-Protocol: chat, superchat
  9. Sec-WebSocket-Version: 13
  10.  
  11. 從服務器到客戶端的內容:
  12. HTTP/1.1 101 Switching Protocols
  13. Upgrade: websocket
  14. Connection: Upgrade
  15. Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  16. Sec-WebSocket-Protocol: chat
關鍵是服務器端Sec-WebSocket-Accept,它是根據Sec-WebSocket-Key計算出來的:
  1. 取出Sec-WebSocket-Key,與一個magic string “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” 連接成一個新的key串;
  2. 將新的key串SHA1編碼,生成一個由多組兩位16進制數構成的加密串;
  3. 把加密串進行base64編碼生成最終的key,這個key就是Sec-WebSocket-Key;
實例代碼如下:
  1. /// <summary>
  2. /// 生成Sec-WebSocket-Accept
  3. /// </summary>
  4. /// <param name="handShakeText">客戶端握手信息</param>
  5. /// <returns>Sec-WebSocket-Accept</returns>
  6. private static string GetSecKeyAccetp(byte[] handShakeBytes,int bytesLength)
  7. {
  8. string handShakeText = Encoding.UTF8.GetString(handShakeBytes, 0, bytesLength);
  9. string key = string.Empty;
  10. Regex r = new Regex(@"Sec\-WebSocket\-Key:(.*?)\r\n");
  11. Match m = r.Match(handShakeText);
  12. if (m.Groups.Count != 0)
  13. {
  14. key = Regex.Replace(m.Value, @"Sec\-WebSocket\-Key:(.*?)\r\n", "$1").Trim();
  15. }
  16. byte[] encryptionString = SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));
  17. return Convert.ToBase64String(encryptionString);
  18. }
如果握手成功,將會觸發客戶端的onopen事件。


解析接收的客戶端信息

接收到客戶端數據解析規則如下:

  • 1byte
    1bit: frame-fin,x0表示該message後續還有frame;x1表示是message的最後一個frame
    3bit: 分別是frame-rsv1、frame-rsv2和frame-rsv3,通常都是x0
    4bit: frame-opcode,x0表示是延續frame;x1表示文本frame;x2表示二進制frame;x3-7保留給非控制frame;x8表示關 閉連接;x9表示ping;xA表示pong;xB-F保留給控制frame
  • 2byte
    1bit: Mask,1表示該frame包含掩碼;0,表示無掩碼
    7bit、7bit+2byte、7bit+8byte: 7bit取整數值,若在0-125之間,則是負載數據長度;若是126表示,後兩個byte取無符號16位整數值,是負載長度;127表示後8個 byte,取64位無符號整數值,是負載長度
  • 3-6byte: 這裏假定負載長度在0-125之間,並且Mask爲1,則這4個byte是掩碼
  • 7-end byte: 長度是上面取出的負載長度,包括擴展數據和應用數據兩部分,通常沒有擴展數據;若Mask爲1,則此數據需要解碼,解碼規則爲1-4byte掩碼循環和數據byte做異或操作。
解析代碼如下,但沒有處理多幀和不包含掩碼的包:
  1. /// <summary>
  2. /// 解析客戶端數據包
  3. /// </summary>
  4. /// <param name="recBytes">服務器接收的數據包</param>
  5. /// <param name="recByteLength">有效數據長度</param>
  6. /// <returns></returns>
  7. private static string AnalyticData(byte[] recBytes, int recByteLength)
  8. {
  9. if (recByteLength < 2) { return string.Empty; }
  10.  
  11. bool fin = (recBytes[0] & 0x80) == 0x80; // 1bit,1表示最後一幀
  12. if (!fin){
  13. return string.Empty;// 超過一幀暫不處理
  14. }
  15.  
  16. bool mask_flag = (recBytes[1] & 0x80) == 0x80; // 是否包含掩碼
  17. if (!mask_flag){
  18. return string.Empty;// 不包含掩碼的暫不處理
  19. }
  20.  
  21. int payload_len = recBytes[1] & 0x7F; // 數據長度
  22.  
  23. byte[] masks = new byte[4];
  24. byte[] payload_data;
  25.  
  26. if (payload_len == 126){
  27. Array.Copy(recBytes, 4, masks, 0, 4);
  28. payload_len = (UInt16)(recBytes[2] << 8 | recBytes[3]);
  29. payload_data = new byte[payload_len];
  30. Array.Copy(recBytes, 8, payload_data, 0, payload_len);
  31.  
  32. }else if (payload_len == 127){
  33. Array.Copy(recBytes, 10, masks, 0, 4);
  34. byte[] uInt64Bytes = new byte[8];
  35. for (int i = 0; i < 8; i++){
  36. uInt64Bytes[i] = recBytes[9 - i];
  37. }
  38. UInt64 len = BitConverter.ToUInt64(uInt64Bytes, 0);
  39.  
  40. payload_data = new byte[len];
  41. for (UInt64 i = 0; i < len; i++){
  42. payload_data[i] = recBytes[i + 14];
  43. }
  44. }else{
  45. Array.Copy(recBytes, 2, masks, 0, 4);
  46. payload_data = new byte[payload_len];
  47. Array.Copy(recBytes, 6, payload_data, 0, payload_len);
  48.  
  49. }
  50.  
  51. for (var i = 0; i < payload_len; i++){
  52. payload_data[i] = (byte)(payload_data[i] ^ masks[i % 4]);
  53. }
  54. return Encoding.UTF8.GetString(payload_data);
  55. }

發送數據至客戶端


服務器發送的數據以0x81開頭,緊接發送內容的長度(若長度在0-125,則1個byte表示長度;若長度不超過0xFFFF,則後2個byte 作爲無符號16位整數表示長度;若超過0xFFFF,則後8個byte作爲無符號64位整數表示長度),最後是內容的byte數組。

代碼如下:

  1. /// <summary>
  2. /// 打包服務器數據
  3. /// </summary>
  4. /// <param name="message">數據</param>
  5. /// <returns>數據包</returns>
  6. private static byte[] PackData(string message)
  7. {
  8. byte[] contentBytes = null;
  9. byte[] temp = Encoding.UTF8.GetBytes(message);
  10.  
  11. if (temp.Length < 126){
  12. contentBytes = new byte[temp.Length + 2];
  13. contentBytes[0] = 0x81;
  14. contentBytes[1] = (byte)temp.Length;
  15. Array.Copy(temp, 0, contentBytes, 2, temp.Length);
  16. }else if (temp.Length < 0xFFFF){
  17. contentBytes = new byte[temp.Length + 4];
  18. contentBytes[0] = 0x81;
  19. contentBytes[1] = 126;
  20. contentBytes[2] = (byte)(temp.Length & 0xFF);
  21. contentBytes[3] = (byte)(temp.Length >> 8 & 0xFF);
  22. Array.Copy(temp, 0, contentBytes, 4, temp.Length);
  23. }else{
  24. // 暫不處理超長內容
  25. }
  26.  
  27. return contentBytes;
  28. }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章