C# winform websocket學習筆記(二)winform服務端


1 功能設計

與連接的客戶端同步a,b的值,具體來說:

1.服務端開啟后,可以被客戶端進行連接;

2.客戶端向服務端發送數據,服務端接收數據后將其轉發推送給所有連接的客戶端。應用場景是,客戶端想要修改遠程服務器數據庫,向服務端發送信息,服務端自身或者通過其他程序修改之后,將變更后的數據推送給所有連接的客戶端;

3.當有新的客戶端連接進來,不必等到數據變化,服務端主動將當前最新數據推送給這個客戶端;

4.發送的數據要約定好規則進行解析,否則參數一多就會亂了套。不過這里為了省事我暫且只將其包裝成json形式。

文末提供源碼下載。

2 界面設計

 界面將就着看吧

2.1 界面說明

大致可分為:

監聽地址設置;

監聽開啟/關閉,及服務端狀態標簽;

a,b值的顯示與廣播;

信息框,用於將一些信息顯示出來。

3 主要代碼實現

3.1 命名空間及引用

using Newtonsoft.Json;
using System.Threading;
using System.Web;
using System.Net.WebSockets;
using System.Net;
using System.Diagnostics;//AddAddress方法使用

在默認生成的using后面添加這些。

注意Newtonsoft.Json適用於處理json,需要將這個dll文件添加進引用,可以右鍵引用-管理nuget程序包搜索,也可以 工具-nuget包管理器-程序包管理控制台 輸入

Install-Package Newtonsoft.Json

3.1 開啟監聽

實例化httplistener對象監聽端口,如果是websocket類型請求,則進行處理

//開啟監聽
        private void btnStart_Click(object sender, EventArgs e)
        {
            if (txtIPAddress.Enabled == true)
            {
                MessageBox.Show("請先確認地址");
                return;
            }
            string IpAdress = txtIPAddress.Text;
            txtInfo.AppendText("打開監聽" + DateTime.Now.ToString() + "\n");
            Start(IpAdress);
        }
View Code
//存儲當前所有連接的靜態列表
        private static List<WebSocket> _sockets = new List<WebSocket>();

        /// <summary>
        /// 對參數地址進行監聽,如果是websocket請求,轉入相應方法
        /// </summary>
        /// <param name="httpListenerPrefix">http監聽地址,記得以 / 結尾</param>
        public async void Start(string httpListenerPrefix)
        {
            try
            {
                //HttpListener httpListener = new HttpListener();//改為靜態對象,方便關閉
                httpListener.Prefixes.Add(httpListenerPrefix);
                //通過連接名稱可以區分多個websocket服務。如可以通過 http://localhost:8080/learn http://localhost:8080/work 使用兩個服務,不過需要多線程和兩個http監聽對象等
                httpListener.Start(); 
                lblListen.Text = "listening...";
                while (true)
                {
                    //http端口監聽獲取內容
                    HttpListenerContext httpListenerContext = await httpListener.GetContextAsync();
                    if (httpListenerContext.Request.IsWebSocketRequest)//如果是websocket請求
                    {
                        //進入此方法
                        ProcessRequest(httpListenerContext);
                    }
                    else
                    {
                        httpListenerContext.Response.StatusCode = 400;
                        httpListenerContext.Response.Close();
                    }
                }
            }
            catch (Exception ex)
            {
                txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "\n");

            }                     
        }
View Code

3.2 數據處理

進行請求處理之前,先約定好數據格式

/// <summary>
        /// //以dictionary將數據的鍵值對匹配,然后進行json序列化,避免定義類的麻煩。
        /// </summary>
        /// <param name="valueA">a的值</param>
        /// <param name="valueB">b的值</param>
        /// <returns></returns>
        public static string SerializeJson(string valueA, string valueB)
        {
            if (valueA.Length == 0) { valueA = "-"; }
            if (valueB.Length == 0) { valueB = "-"; }
            //以dictionary將數據的鍵值對匹配,然后進行json序列化,避免定義類的麻煩。參考:https://www.cnblogs.com/kevinWu7/p/10163455.html
            Dictionary<string, string> dic = new Dictionary<string, string>(){
                { "a",valueA },
                { "b",valueB }
            };
            string Jsondata = JsonConvert.SerializeObject(dic);
            return Jsondata;
        }
View Code
    //用於json反序列化獲取實體
    public class TestValue
    {
        public string a { get; set; }
        public string b { get; set; }
    }

3.3 處理請求

 先通過webSocketContext從傳入參數中讀取內容,然后通過WebSocket webSocket = webSocketContext.WebSocket;獲取websocket連接。

將連接加入到一個靜態list列表中,用於向所有客戶端廣播。(其實這里可以用dictionary等來存儲,並且可以將連接的一些其他信息比如IP等一起放進去)

對這一個新接入連接,向其同步最新數據。

當此連接狀態是打開情況,准備一些數組之類,用於接收讀取數據。

通過webSocket.ReceiveAsync()進行異步接收。

將讀取到的字節數組轉成字符串,由於客戶端發送的是序列化后的json字符串,因此需要反序列化轉成對象實例,獲取a,b的值,將其顯示到文本框。

刷新判斷一下靜態列表中連接的狀態,清理無效連接,將之前的字節數組發送給剩余websocket連接。

/// <summary>
        /// 處理端口監聽到的請求
        /// </summary>
        /// <param name="httpListenerContext"></param>
        private async void ProcessRequest(HttpListenerContext httpListenerContext)
        {
            //WebSocketContext 類用於訪問websocket握手中的信息
            WebSocketContext webSocketContext = null;
            try
            {
                webSocketContext = await httpListenerContext.AcceptWebSocketAsync(subProtocol: null);
                //獲取客戶端IP
                string ipAddress = httpListenerContext.Request.RemoteEndPoint.Address.ToString();
                txtInfo.AppendText("connected:IPAddress" + ipAddress + "\n");
            }
            catch (Exception e)//如果出錯
            {
                httpListenerContext.Response.StatusCode = 500;
                httpListenerContext.Response.Close();
                txtInfo.AppendText("Exception:" + e.ToString() + DateTime.Now.ToString() + "\n");
                return;
            }
            //獲取websocket連接
            WebSocket webSocket = webSocketContext.WebSocket;
            _sockets.Add(webSocket);//此處將web socket對象加入一個靜態列表中
            SendToNewConnection(webSocket);//將當前服務器上最新的數據(a,b的值)發送過去

            try
            {
                //我們定義一個常數,它將表示接收到的數據的大小。 它是由我們建立的,我們可以設定任何值。 我們知道在這種情況下,發送的數據的大小非常小。
                const int maxMessageSize = 2048;
                //received bits的緩沖區

                while (webSocket != null && webSocket.State == WebSocketState.Open)//如果連接是打開的
                {   
                    //此句放在while里面,每次使用都重新初始化。如果放在外面,由於沒有進行清空操作,下一次接收的數據若比上一次短,則會多出一部分內容。
                    var receiveBuffer = new ArraySegment<Byte>(new Byte[maxMessageSize]);

                    WebSocketReceiveResult receiveResult=null;
                    byte[] payloadData = null;
                    do
                    {
                        //讀取數據。此類的實例表示在 WebSocket 上執行單個 ReceiveAsync 操作所得到的結果
                        receiveResult = await webSocket.ReceiveAsync(receiveBuffer, CancellationToken.None);
                        //字節數組
                        payloadData = receiveBuffer.Array.Where(b => b != 0).ToArray();
                    }
                    while (!receiveResult.EndOfMessage);//如果指示已完整接收消息則停止
                    
                    //如果輸入幀為取消幀,發送close命令。
                    //MessageType指示當前消息是utf-8消息還是二進制信息。Text(0,明文形式),Close(2,收到關閉消息,接受已完成),Binary(1,消息采用二進制格式)
                    if (receiveResult.MessageType == WebSocketMessageType.Close)
                    {
                        await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, CancellationToken.None);
                        _sockets.Remove(webSocket);//從列表移除當前連接

                    }
                    else
                    {                        
                        //因為我們知道這是一個字符串,我們轉換它                        
                        string receiveString = System.Text.Encoding.UTF8.GetString(payloadData, 0, payloadData.Length);

                        try//將反序列化內容放入try中,避免無法匹配、內容為空等可能報錯的地方
                        {
                            //將轉換后的字符串內容進行json反序列化。參考:https://www.cnblogs.com/yinmu/p/12160343.html
                            TestValue tv = JsonConvert.DeserializeObject<TestValue>(receiveString);
                            //將收到的a,b的值顯示到文本框
                            if (tv != null)
                            {
                                string valueA = string.Empty, valueB = string.Empty;
                                if (tv.a != null && tv.a.Length > 0) { valueA = tv.a; }
                                if (tv.a != null && tv.b.Length > 0) { valueB = tv.b; }
                                txtAvalue.Text = valueA;
                                txtBvalue.Text = valueB;
                            }
                            
                            RefreshConnectionList();//先清理無效的連接,否則會導致服務端websocket被dispose
                            //當接收到文本消息時,對當前服務器上所有web socket連接進行廣播
                            foreach (var innerSocket in _sockets)
                            {
                                await innerSocket.SendAsync(new ArraySegment<byte>(payloadData), WebSocketMessageType.Text, true, CancellationToken.None);
                            }
                        }
                        catch (Exception ex)
                        {
                            //如果json反序列化出了問題
                            txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "\n");//將錯誤類型顯示出來
                            txtInfo.AppendText(receiveString + DateTime.Now.ToString() + "\n");//將收到的原始字符串顯示出來
                        }
                    }
                }
            }
            catch (Exception e)
            {
                if (e.GetType().ToString() == "System.Net.WebSockets.WebSocketException")
                {
                    //客戶端關閉時會拋出此錯誤
                    txtInfo.AppendText("a connection closed" + DateTime.Now.ToString() + "\n");
                }
                else
                {
                    txtInfo.AppendText(e.ToString() + DateTime.Now.ToString() + "\n");
                }
            }
        }
View Code

4 對websocket連接的處理

4.1 廣播

/// <summary>
        /// 服務端主動向所有客戶端廣播
        /// </summary>
        /// <param name="jsonmessage">傳過來的應該是序列化后的json字符串,接收方會通過TestValue類進行反序列化獲取a,b的內容</param>
        public  async void Broadcast(string jsonmessage)
        {
            try
            {
                Byte[] bytes = System.Text.Encoding.UTF8.GetBytes(jsonmessage);
                RefreshConnectionList();//先清理無效的連接,否則會導致服務端websocket被dispose
                //當接收到文本消息時,對當前服務器上所有web socket連接進行廣播
                foreach (var innerSocket in _sockets)
                {                
                    await innerSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
                }
            }
            catch (Exception ex)
            {
                txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "\n");
                MessageBox.Show("某些連接出了問題,如果廣播多次出問題,請重啟服務端");
            }
            
        }
View Code

4.2 向新連接客戶端同步數據

/// <summary>
        /// 監聽到一個新的websocket連接后,將服務器當前最新數據同步過去
        /// </summary>
        /// <param name="currentWebsocket">當前新接入的websocket連接</param>
        public async void SendToNewConnection(WebSocket currentWebsocket)
        {
            try
            {
                string jsonmessage = SerializeJson(txtAvalue.Text, txtBvalue.Text);
                Byte[] bytes = System.Text.Encoding.UTF8.GetBytes(jsonmessage);

                if (currentWebsocket.State == WebSocketState.Open)
                {
                    await currentWebsocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);//try訪問已釋放對象問題
                }
                else
                {
                    //此處並不對該鏈接進行移除,會導致調用本方法后的代碼出問題,只需在進行發送時確認狀態即可
                    txtInfo.AppendText("新接入連接:" + currentWebsocket.GetHashCode().ToString() + "的連接狀態不為open,因此停止向其同步數據" + DateTime.Now.ToString() + "\n");
                }
            }
            catch (Exception ex)
            {
                txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "\n");
            }
        }
View Code

4.3 清理連接

/// <summary>
        /// 刷新當前websocket連接列表,如果狀態為Closed,則移除。連接異常斷開后會被dispose,如果訪問會報錯,但可以獲取狀態為closed
        /// </summary>
        public static void RefreshConnectionList()
        {
            if (_sockets != null)//lock不能鎖定空值
            {
                lock (_sockets)//鎖定數據源
                {
                    //System.InvalidOperationException: 集合已修改;可能無法執行枚舉操作。
                    //使用foreach不能執行刪除、修改,這是規定。你可以使用for循環遍歷修改。刪除數據正確做法,for循環 i 要從大到小
                    for (int i = _sockets.Count-1; i >=0; i--)
                    {
                        if (_sockets[i].State != WebSocketState.Open)
                        {
                            _sockets.Remove(_sockets[i]);
                        }
                    }
                }
            }
            
        }
View Code

 

5 權限問題

5.1 問題介紹

服務端客戶端完成后,VS調試正常,如果想在局域網下測試,結果發現服務端報httplistener拒絕訪問,客戶端報無法連接遠程主機,即使關閉防火牆也沒用。

這是因為這個執行程序沒有獲得權限來監聽。有以下幾種方法。

不過不管哪種方法都需要添加服務端電腦的監聽端口的入站規則,或者關閉防火牆。添加入站規則可見2.

5.2 管理員權限運行

這是最簡單粗暴的,直接右鍵以管理員運行即可

5.3 cmd添加

參考:https://blog.csdn.net/chenludaniel/article/details/79720024

5.4 提升權限

參考:https://www.cnblogs.com/cmdszh/archive/2012/08/16/httplistener.html

不過第四種方法經過實測,由於通過clickonce添加的manifest配置文件無法獲取管理員權限,導致此種方法無效。不過原理上和3是一致的。

6 功能測試 

測試一個服務端與三個客戶端,winform服務端與兩個winform客戶端在一台電腦上,一個HTML頁web客戶端在局域網手機瀏覽器上。(注意,服務端以管理員身份打開)

 

 

 

 

 點我下載源碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM