1. 程式人生 > 其它 >轉---.net framework4.0基於Socket封裝成WebSocket

轉---.net framework4.0基於Socket封裝成WebSocket

公司的專案是基於.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