1.需求示意图
2.需求描述
原本是为了给做unity3d客户端开发的同事提供不定时的消息推送,比如商城购买道具后服务端将道具信息推送给客户端。
本篇文章简化理解,用“相关部门开展活动,向全市人民征集社会服务改善意见”为例子。但核心想法一致:单向推送(指这个需求上只需要单向)。所以这个功能并不是聊天室,即便websocket技术是做双向通信的,但在本需求中不需要核心页面和客户端之间互相通信。核心界面只和服务端建立WebSocket连接,推送消息全部来自其他地方。
只有核心页面和服务端建立WebSocket连接,其他市民们都是通过web开发者耳熟能详的http协议在发送消息,不要以为是市民们和部门公告栏玩WebSocket互动。
3.代码如下,复制即可使用(webapi跨域的代码不演示)
①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; } } }
②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); } } }
③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 ? "已提交,您可以去相关部门的官网查看刚发送的信息了。" : "相关部门的平台还没开放,请耐心等待"; } } }
④测试用部门公告栏页面【核心页面】
<!DOCTYPE html> <html> <head> <title>教育局的市民意见征集布告栏</title> </head> <script src="https://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="https://code.jquery.com/jquery-3.1.1.min.js"></script> <script> var msgCount = 1; function doSend(){ $.ajax({ url: "http://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,可参考https://www.jianshu.com/p/8759dda1dbfc 了解原理。
因此只要核心页面断开了WebSocket连接(断点、断网、重启、刷新页面等),这次的WebSocket对象都不再有效,需要重新建立连接。
②本案例的需求是市民们向部门反应意见,不是聊天室。并且市民们反应意见是传统的http请求,服务端向核心页面推送消息才用到WebSocket。
③WSHelper.cs类中建立了线程安全的单例模式,目的是让所有用户访问到的保存WebSocket对象的字典集合对象唯一,如果不这样做,那么只有在发布公告栏那台电脑上才有用。
④案例缺点:服务端没有监听核心页面的WebSocket状态,因此只有在try-catch捕获异常时才可以处理掉客户端的WebSocket对象,对性能上有些不友好。
结合实际需求,在webapi内利用WebSocket建立单向的消息推送平台,让A页面和服务端建立WebSocket连接,让其他页面可以及时给A页面推送消息