结合实际需求,在webapi内利用WebSocket建立单向的消息推送平台,让A页面和服务端建立WebSocket连接,让其他页面可以及时给A页面推送消息

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对象,对性能上有些不友好。