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); }

//存儲當前所有連接的靜態列表 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"); } }
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; }
//用於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"); } } }
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("某些連接出了問題,如果廣播多次出問題,請重啟服務端"); } }
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"); } }
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]); } } } } }
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客戶端在局域網手機瀏覽器上。(注意,服務端以管理員身份打開)