結合實際需求,在webapi內利用WebSocket建立單向的消息推送平台,讓A頁面和服務端建立WebSocket連接,讓其他頁面可以及時給A頁面推送消息
- 2020 年 7 月 7 日
- 筆記
- .Net基礎學習, Asp.Net, c#, WebApi, Websocket, websocket A頁面連接 B頁面發送資訊給A, websocket單向
1.需求示意圖
2.需求描述
原本是為了給做unity3d客戶端開發的同事提供不定時的消息推送,比如商城購買道具後服務端將道具資訊推送給客戶端。
本篇文章簡化理解,用「相關部門開展活動,向全市人民徵集社會服務改善意見」為例子。但核心想法一致:單向推送。所以這個功能並不是聊天室,不需要客戶端和客戶端之間互相通訊。核心介面只和服務端建立WebSocket連接,推送消息全部來自其他地方。
只有核心頁面和服務端建立WebSocket連接,其他市民們都是通過web開發者耳熟能詳的http協議在發送消息,不要以為是市民們和部門公告欄玩WebSocket互動。
3.程式碼如下,複製即可使用
①WebSocket幫助類,負責建立連接和推送消息


using System; using System.Collections.Generic; using System.Linq; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.WebSockets; namespace WSTest { public class WSHelper { /// <summary> /// 保存客戶端的WebSocket對象 /// </summary> private static readonly Dictionary<string, WebSocket> dicSockets = new Dictionary<string, WebSocket>(); #region 構建執行緒安全的單例模式 private static WSHelper _instance; private WSHelper() { } public static WSHelper GetInstance() { if (_instance == null) { lock (dicSockets) { if (_instance == null) { _instance = new WSHelper(); } } } return _instance; } #endregion /// <summary> /// 和客戶端建立WebSocket連接 /// </summary> /// <param name="arg">客戶端發送的WebSocket相關資訊</param> /// <returns></returns> public async Task ProcessWSChat(AspNetWebSocketContext arg) { // 1.獲取請求的客戶端WebSocket對象 WebSocket socket = arg.WebSocket; // 2.獲取自定義的參數 string adminUserKey = arg.QueryString["adminUserKey"]; if (string.IsNullOrEmpty(adminUserKey)) return; // 3.將用戶編號作為標識客戶端唯一性的Key,保存客戶端的WebSocket對象 dicSockets[adminUserKey] = socket; while (true) { ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[1024 * 10]); WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None); try { if (socket.State != WebSocketState.Open) { dicSockets.Remove(adminUserKey); break; } } catch { break; } } } /// <summary> /// 服務端向客戶端推送消息 /// </summary> public bool SendMsg(string message, string adminUserKey) { WebSocket socket = null; if (dicSockets.ContainsKey(adminUserKey)) { socket = dicSockets[adminUserKey]; } else { return false; } //【重要】執行下面socket.State程式碼可能會拋異常"無法訪問已經釋放的對象", // 因為客戶端已經處於斷電、斷網、強制關閉、刷新等狀態,當前的WebSocket對象已經失去價值,直接刪除即可 try { if (socket.State == WebSocketState.Open) { ArraySegment<byte> buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(message)); socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None); return true; } } catch { dicSockets.Remove(adminUserKey); return false; } return false; } } }
WSHelper
②webapi的控制器,負責建立WebSocket連接


using System.Net; using System.Net.Http; using System.Web; using System.Web.Http; namespace WSTest.Controllers { [RoutePrefix("WebSocketConn")] public class WebSocketConnController : ApiController { /// <summary> /// 創建websocket連接 /// </summary> [HttpGet] [Route("GetConnect")] public HttpResponseMessage GetConnect() { if (HttpContext.Current.IsWebSocketRequest) { HttpContext.Current.AcceptWebSocketRequest(WSHelper.GetInstance().ProcessWSChat); } return new HttpResponseMessage(HttpStatusCode.SwitchingProtocols); } } }
WebSocketConnController
③webapi的業務控制器,徵集意見


using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; namespace WSTest.Controllers { /// <summary> /// 市民服務 /// </summary> [RoutePrefix("CitizenService")] public class CitizenServiceController : ApiController { /// <summary> /// 市民意見徵集 /// </summary> [HttpGet] [Route("GiveOpinion")] public string GiveOpinion(string userName, string msg, string sendTo) { //1.發送消息給客戶端 string sendMsg = string.Format("熱心市民{0}有話要說:{1}", userName, msg); bool result = WSHelper.GetInstance().SendMsg(sendMsg, sendTo); //2.接收結果,若發送失敗,可能客戶端還未成功連接WebSocket return result ? "已提交,您可以去相關部門的官網查看剛發送的資訊了。" : "相關部門的平台還沒開放,請耐心等待"; } } }
CitizenServiceController
④測試用部門公告欄頁面【核心頁面】


<!DOCTYPE html> <html> <head> <title>教育局的市民意見徵集布告欄</title> </head> <script src="//code.jquery.com/jquery-3.1.1.min.js"></script> <body> <div id="titleMsg"></div> <div id="msgMenu"> 來自市民的話:<br> </div> <script type="text/javascript"> var webSocket; var msgCount = 1; //HTTP處理程式的地址 var handlerUrl = "ws://localhost:2465/WebSocketConn/GetConnect?adminUserKey=adminA"; $(function(){ InitWebSocket(); }); function CloseWebSocket() { webSocket.close(); webSocket = undefined; } function InitWebSocket() { //如果WebSocket對象未初始化,則初始化 if (webSocket == undefined) { webSocket = new WebSocket(handlerUrl); //打開連接處理程式 webSocket.onopen = function () { //WebSocket連接成功 $("#titleMsg").text("平台已開放,歡迎大家留言"); }; //消息數據處理程式 webSocket.onmessage = function (e) { updMsgMenu(e.data); }; //關閉事件處理程式 webSocket.onclose = function () { //WebSocket斷開連接 }; //錯誤事件處理程式 webSocket.onerror = function (e) { updMsgMenu(e.message); }; } else { //webSocket.open();沒有open方法 } } function updMsgMenu(str){ var tempStr = $("#msgMenu").html(); tempStr = tempStr + msgCount + "." + str + "</br>"; msgCount++; $("#msgMenu").html(tempStr); } function Clear(){ msgCount = 1; $("#msgMenu").html("消息列表:<br>"); } </script> </body> </html>
部門公告欄頁面
⑤測試用市民意見徵集頁面


<!DOCTYPE html> <html> <head> <title>市民意見徵集平台</title> </head> <body> 您的姓名:<input type="text" id="userName" /><br> 您的意見:<textarea type="text" id="msg"></textarea><br> 您想給哪個部門留言:<select id="sendTo"> <option value="adminA">教育局</option> <option value="adminB">社保局</option> <option value="adminC">勞動局</option> </select> <input type="button" value="提交" onclick="doSend()" /> <script src="//code.jquery.com/jquery-3.1.1.min.js"></script> <script> var msgCount = 1; function doSend(){ $.ajax({ url: "//localhost:2465/CitizenService/GiveOpinion", type: "GET", data:{ userName: $("#userName").val(), msg: $("#msg").val(), sendTo: $("#sendTo").val() }, cache: false, dataType: "json", success: function (res) { console.log(res); alert("收到消息:"+ res); }, error: function (error) { alert("服務端繁忙"); } }); } </script> </body> </html>
市民意見徵集頁面
4.運行如下
①教育部門開放了自己的平台,準備接收市民意見
②有市民向教育部門回饋問題
③公告欄收到及時推送的消息
5.總結
①本案例中標識建立WebSocket連接的客戶端唯一性的是自定義參數,但WebSocket內部標識唯一性的是SecWebSocketKey,可參考//www.jianshu.com/p/8759dda1dbfc 了解原理。
因此只要核心頁面斷開了WebSocket連接(斷點、斷網、重啟、刷新頁面等),這次的WebSocket對象都不再有效,需要重新建立連接。
②本案例的需求是市民們向部門反應意見,不是聊天室。並且市民們反應意見是傳統的http請求,服務端向核心頁面推送消息才用到WebSocket。
③WSHelper.cs類中建立了執行緒安全的單例模式,目的是讓所有用戶訪問到的保存WebSocket對象的字典集合對象唯一,如果不這樣做,那麼只有在發布公告欄那台電腦上才有用。
④案例缺點:服務端沒有監聽核心頁面的WebSocket狀態,因此只有在try-catch捕獲異常時才可以處理掉客戶端的WebSocket對象,對性能上有些不友好。