轉---.net framework4.0基於Socket封裝成WebSocket
阿新 • • 發佈:2021-11-19
公司的專案是基於.net4.0搭建起來的,有客戶的需求是基於WebSocket通訊請求的,但好像WebSocket支援的.net最低版本為4.5。
為了實現這個需求,結合網上找到的例子,自己封裝了一個基於Socket的WebSocket,可能存在一定的Bug,歡迎大家指正。
using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Text; namespace SocketDemo { public class Session {private Socket _sockeclient; private byte[] _buffer; private string _ip; private bool _isweb = false; public Socket SockeClient { set { _sockeclient = value; } get { return _sockeclient; } } public byte[] buffer {set { _buffer = value; } get { return _buffer; } } public string IP { set { _ip = value; } get { return _ip; } } public bool isWeb { set { _isweb = value; } get { return _isweb; } } } }
封裝Socket的WebSocket類:
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; namespace SocketDemo { public class WebSocket { private Dictionary<string, Session> SessionPool = new Dictionary<string, Session>(); #region 啟動WebSocket服務 /// <summary> /// 啟動WebSocket服務 /// </summary> public void start(int port) { Socket SockeServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); SockeServer.Bind(new IPEndPoint(IPAddress.Any, port)); SockeServer.Listen(20); SockeServer.BeginAccept(new AsyncCallback(Accept), SockeServer); Console.WriteLine("服務已啟動"); Console.WriteLine("按任意鍵關閉服務"); Console.ReadLine(); } #endregion #region 處理客戶端連線請求 /// <summary> /// 處理客戶端連線請求 /// </summary> /// <param name="result"></param> private void Accept(IAsyncResult socket) { // 還原傳入的原始套接字 Socket SockeServer = (Socket)socket.AsyncState; // 在原始套接字上呼叫EndAccept方法,返回新的套接字 Socket SockeClient = SockeServer.EndAccept(socket); byte[] buffer = new byte[4096]; try { //接收客戶端的資料 SockeClient.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(Recieve), SockeClient); //儲存登入的客戶端 Session session = new Session(); session.SockeClient = SockeClient; session.IP = SockeClient.RemoteEndPoint.ToString(); session.buffer = buffer; lock (SessionPool) { if (SessionPool.ContainsKey(session.IP)) { this.SessionPool.Remove(session.IP); } this.SessionPool.Add(session.IP, session); } //準備接受下一個客戶端 SockeServer.BeginAccept(new AsyncCallback(Accept), SockeServer); Console.WriteLine(string.Format("客戶端 {0} 連線成功", SockeClient.RemoteEndPoint)); } catch (Exception ex) { Console.WriteLine("Error : " + ex.ToString()); } } #endregion #region 處理接收的資料 /// <summary> /// 處理接受的資料 /// </summary> /// <param name="socket"></param> private void Recieve(IAsyncResult socket) { Socket SockeClient = (Socket)socket.AsyncState; string IP = SockeClient.RemoteEndPoint.ToString(); if (SockeClient == null || !SessionPool.ContainsKey(IP)) { return; } try { int length = SockeClient.EndReceive(socket); byte[] buffer = SessionPool[IP].buffer; SockeClient.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, new AsyncCallback(Recieve), SockeClient); string msg = Encoding.UTF8.GetString(buffer, 0, length); // websocket建立連線的時候,除了TCP連線的三次握手,websocket協議中客戶端與伺服器想建立連線需要一次額外的握手動作 if (msg.Contains("Sec-WebSocket-Key")) { SockeClient.Send(PackageHandShakeData(buffer, length)); SessionPool[IP].isWeb = true; return; } if (SessionPool[IP].isWeb) { msg = AnalyzeClientData(buffer, length); } byte[] msgBuffer = PackageServerData(msg); foreach (Session se in SessionPool.Values) { se.SockeClient.Send(msgBuffer, msgBuffer.Length, SocketFlags.None); } } catch { try { SockeClient.Disconnect(true); } catch { SockeClient.Dispose(); } Console.WriteLine("客戶端 {0} 斷開連線", IP); SessionPool.Remove(IP); } } #endregion #region 客戶端和服務端的響應 /* * 客戶端向伺服器傳送請求 * GET / HTTP/1.1 * Origin: http://localhost:8064 * Sec-WebSocket-Key: vDyPp55hT1PphRU5OAe2Wg== * Connection: Upgrade * Upgrade: Websocket *Sec-WebSocket-Version: 13 * User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko * Host: localhost:8064 * DNT: 1 * Cache-Control: no-cache * Cookie: DTRememberName=admin * * 伺服器給出響應 * HTTP/1.1 101 Switching Protocols * Upgrade: websocket * Connection: Upgrade * Sec-WebSocket-Accept: xsOSgr30aKL2GNZKNHKmeT1qYjA= * * 在請求中的“Sec-WebSocket-Key”是隨機的,伺服器端會用這些資料來構造出一個SHA-1的資訊摘要。把“Sec-WebSocket-Key”加上一個魔幻字串 * “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用 SHA-1 加密,之後進行 BASE-64編碼,將結果做為 “Sec-WebSocket-Accept” 頭的值,返回給客戶端 */ #endregion #region 打包請求連線資料 /// <summary> /// 打包請求連線資料 /// </summary> /// <param name="handShakeBytes"></param> /// <param name="length"></param> /// <returns></returns> private byte[] PackageHandShakeData(byte[] handShakeBytes, int length) { string handShakeText = Encoding.UTF8.GetString(handShakeBytes, 0, length); string key = string.Empty; Regex reg = new Regex(@"Sec\-WebSocket\-Key:(.*?)\r\n"); Match m = reg.Match(handShakeText); if (m.Value != "") { key = Regex.Replace(m.Value, @"Sec\-WebSocket\-Key:(.*?)\r\n", "$1").Trim(); } byte[] secKeyBytes = SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")); string secKey = Convert.ToBase64String(secKeyBytes); var responseBuilder = new StringBuilder(); responseBuilder.Append("HTTP/1.1 101 Switching Protocols" + "\r\n"); responseBuilder.Append("Upgrade: websocket" + "\r\n"); responseBuilder.Append("Connection: Upgrade" + "\r\n"); responseBuilder.Append("Sec-WebSocket-Accept: " + secKey + "\r\n\r\n"); return Encoding.UTF8.GetBytes(responseBuilder.ToString()); } #endregion #region 處理接收的資料 /// <summary> /// 處理接收的資料 /// </summary> /// <param name="recBytes"></param> /// <param name="length"></param> /// <returns></returns> private string AnalyzeClientData(byte[] recBytes, int length) { int start = 0; // 如果有資料則至少包括3位 if (length < 2) return ""; // 判斷是否為結束針 bool IsEof = (recBytes[start] >> 7) > 0; // 暫不處理超過一幀的資料 if (!IsEof) return ""; start++; // 是否包含掩碼 bool hasMask = (recBytes[start] >> 7) > 0; // 不包含掩碼的暫不處理 if (!hasMask) return ""; // 獲取資料長度 UInt64 mPackageLength = (UInt64)recBytes[start] & 0x7F; start++; // 儲存4位掩碼值 byte[] Masking_key = new byte[4]; // 儲存資料 byte[] mDataPackage; if (mPackageLength == 126) { // 等於126 隨後的兩個位元組16位表示資料長度 mPackageLength = (UInt64)(recBytes[start] << 8 | recBytes[start + 1]); start += 2; } if (mPackageLength == 127) { // 等於127 隨後的八個位元組64位表示資料長度 mPackageLength = (UInt64)(recBytes[start] << (8 * 7) | recBytes[start] << (8 * 6) | recBytes[start] << (8 * 5) | recBytes[start] << (8 * 4) | recBytes[start] << (8 * 3) | recBytes[start] << (8 * 2) | recBytes[start] << 8 | recBytes[start + 1]); start += 8; } mDataPackage = new byte[mPackageLength]; for (UInt64 i = 0; i < mPackageLength; i++) { mDataPackage[i] = recBytes[i + (UInt64)start + 4]; } Buffer.BlockCopy(recBytes, start, Masking_key, 0, 4); for (UInt64 i = 0; i < mPackageLength; i++) { mDataPackage[i] = (byte)(mDataPackage[i] ^ Masking_key[i % 4]); } return Encoding.UTF8.GetString(mDataPackage); } #endregion #region 傳送資料 /// <summary> /// 把傳送給客戶端訊息打包處理 /// </summary> /// <returns>The data.</returns> /// <param name="message">Message.</param> private byte[] PackageServerData(string msg) { byte[] content = null; byte[] temp = Encoding.UTF8.GetBytes(msg); if (temp.Length < 126) { content = new byte[temp.Length + 2]; content[0] = 0x81; content[1] = (byte)temp.Length; Buffer.BlockCopy(temp, 0, content, 2, temp.Length); } else if (temp.Length < 0xFFFF) { content = new byte[temp.Length + 4]; content[0] = 0x81; content[1] = 126; content[2] = (byte)(temp.Length & 0xFF); content[3] = (byte)(temp.Length >> 8 & 0xFF); Buffer.BlockCopy(temp, 0, content, 4, temp.Length); } return content; } #endregion } }
啟動WebSocket服務端:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace SocketDemo { class Program { static void Main(string[] args) { WebSocket socket = new WebSocket(); socket.start(9191); } } }
客戶端通過js連線,這也是為什麼要封裝成WebSocket的原因,因為客戶也是使用js發起請求的:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>WebSocket客戶端</title> </head> <script> var webSocket; function connect() { try { var readyState = new Array("正在連線","已建立連線","已關閉連線"); var host = "ws://localhost:9191"; webSocket = new WebSocket(host); var message = document.getElementById("message"); message.innerHTML +="<p>Socket狀態:" + readyState[webSocket.readyState] + "</p>"; webSocket.onopen = function() { message.innerHTML += "<p>Socket狀態:" + readyState[webSocket.readyState] + "</p>"; } webSocket.onmessage = function(msg) { message.innerHTML +="<p>接收資訊:" + msg.data + "</p>"; } webSocket.onclose=function() { message.innerHTML +="<p>Socket狀態:" + readyState[webSocket.readyState] + "</p>"; } } catch(exception) { message.innerHTML += "<p>有錯誤發生</p>"; } } function send() { var text = document.getElementById("text").value; var message = document.getElementById("message"); if(text == "") { message.innerHTML += "<p>請輸入傳送內容</p>"; return ; } try { webSocket.send(text); message.innerHTML += "<p>傳送資料:" +text + "</p>"; } catch(exception) { message.innerHTML += "<p>傳送資料出錯</p>"; } document.getElementById("text").value=""; } function disconnect() { webSocket.close(); window.opener=null; window.close(); } //重新整理頁面關閉連線 window.onbeforeunload = function(event) { webSocket.close(); } </script> <body> <h1>WebSocket客戶端示例</h1> <div id="message"></div> <p>請輸入一些文字</p> <input id="text" type="text"> <button id="connect" onClick="connect();">建立連線</button> <button id="send" onClick="send();">傳送資料</button> <button id="disconnect" onClick="disconnect();">斷開連線</button> </body> </html>
這個功能的實現是參考一位大神的,但後來想找回那篇文章時找不到了。後續看到那篇文章再補回連結。
轉載自地址https://www.yiduk.com/%E6%BA%90%E7%A0%81%E5%88%86%E4%BA%AB/20.html