• 第二个 SignalR,可以私聊的聊天室


    一、简介

    上一次,我们写了个简单的聊天室,接下来,我们来整一个可以私聊的聊天室。

    SignalR 官方 API 文档

    需求简单分析:

    1.私聊功能,那么要记录用户名或用户ID,用于发送消息。

    2.怎么向单人发消息,查看 文档,得知 SignalR 的推送方式 有组推、ID 推等等(参考 Calling client methods 这一节 ).

    3.怎么在推送消息的方法里面取得 cookie 、querystirng ?查看文档,得知在其推送方法中应该怎么取参数。

    4.用户连接上聊天室,应该有广播:xx用户上线了,用户列表更新成最新的,用户关闭浏览器后,也应该有广播:XX用户下线了。查看文档,可知 在哪里处理用户重连接、连接、断开连接的事件。

     二、Demo

    接下来,我们来一步步实现这个简单的 Demo .

    1.0 创建一个新的聊天集线器.名字叫:GroupChatHub ,并给它起个别名,叫 groupChatHub

    1.1考虑到用户会有用户名,这儿以简单的方式处理下,让用户一进入系统,先输入名称,之后再跳转到聊天室,以 Get 方式传递用户名.并把用户名保存进 cookie 中。代码如下

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    using System.Threading.Tasks;
    
    using System.Collections.Concurrent;
    
    namespace TestSignalR.Models
    {
        [HubName("groupChatHub")]
        public class GroupChatHub : Hub
        {
    
            private string userName
            {
                get 
                {
                    var ck_userName = Context.RequestCookies["userName"];
                    return ck_userName == null ? "" : ck_userName.Value;
                }
            }
        }
    }

    1.2 既然我们要有用户列表,我们应该保存用户名 和 用户的连接ID,添加一个静态的线程安全字典 _onlineDic .

            private static ConcurrentDictionary<string, string> _onlineDic = new ConcurrentDictionary<string, string>();

    1.3 接下来,我们处理一下用户连接上的 OnConnected 事件,把用户ID、用户名保存起来。方便等会推送用。

            public override Task OnConnected()
            {
                
                if (!_onlineDic.ContainsKey(userName))
                {
                    _onlineDic.AddOrUpdate(userName, Context.ConnectionId, (k, ov) => ov = Context.ConnectionId);
                }
                else
                {
                    _onlineDic.AddOrUpdate(userName, Context.ConnectionId, (k, ov) => ov = Context.ConnectionId);
    
                }
    return base.OnConnected();
            }

    1.4 忘记处理广播 XX用户上线通知了,还有用户列表推送信息。那么在 OnConnected 事件中修改下 :

            public override Task OnConnected()
            {
                
                if (!_onlineDic.ContainsKey(userName))
                {
                    _onlineDic.AddOrUpdate(userName, Context.ConnectionId, (k, ov) => ov = Context.ConnectionId);
              Clients.All.publishMsg(new {sender="系统通知",receiver= "所有人", msg="系统消息" + userName + "加入聊天",msgTime=DateTime.Now});
                }
                else
                {
                    _onlineDic.AddOrUpdate(userName, Context.ConnectionId, (k, ov) => ov = Context.ConnectionId);
                }
                Clients.All.publishUser(_onlineDic.Select(c => new { key = c.Key, value = c.Value }));
                return base.OnConnected();
            }

    1.5 处理一下用户重连接,当用户断线重连时,要广播用户上线,加入静态缓存中。其实重连的逻辑处理应该和连接时的逻辑是一样的,所以直接提成一个方法去调用。

        public override Task OnConnected()
            {
                Connect();
                return base.OnConnected();
            }
    
    
            public override Task OnReconnected()
            {
                Connect();
                return base.OnReconnected();
            }
    
    
            public void Connect()
            {
                if (!_onlineDic.ContainsKey(userName))
                {
                    _onlineDic.AddOrUpdate(userName, Context.ConnectionId, (k, ov) => ov = Context.ConnectionId);
    
                    Clients.All.publishMsg(FormateMsg("系统通知", "所有人", "系统消息" + userName + "加入聊天"));
                }
                else
                {
                    _onlineDic.AddOrUpdate(userName, Context.ConnectionId, (k, ov) => ov = Context.ConnectionId);
                }
                Clients.All.publishUser(_onlineDic.Select(c => new { key = c.Key, value = c.Value }));
            }
    
    
            public dynamic FormateMsg(string sender, string receiver, string msg)
            {
                return new
                {
                    sender = sender,
                    receiver = receiver,
                    msgTime = DateTime.Now,
                    msg = msg
                };
            }

    1.6 接下来,处理一下断开连接的时候的事件:

            public override Task OnDisconnected(bool stopCalled)
            {
                string Temp = "";
                _onlineDic.TryRemove(userName, out Temp);
                Clients.All.publishUser(_onlineDic.Select(c => new { key = c.Key, value = c.Value }));
    
                Clients.All.publishMsg(FormateMsg("系统通知", "所有人", "系统消息" + userName + "离开聊天"));
    
                return base.OnDisconnected(stopCalled);
            }

    1.7 用户推送消息的事件,我们定义为 Send 方法:

            /// <summary>
            /// 发送消息
            /// </summary>
            /// <param name="sendToClientID">接收者ID</param>
            /// <param name="msg">消息</param>
            public void Send(string sendToClientID,string msg)
            {
                if (sendToClientID == "0")
                {
                    Clients.All.publishMsg(FormateMsg(userName,"所有人",msg));
                }
                else
                {
                    string sendToUsername="";
    
                    var sendToUserItem= _onlineDic.FirstOrDefault(c => c.Value == sendToClientID);
                    if (sendToUserItem.Key != null)
                    {
                        sendToUsername = sendToUserItem.Key;
                    }
    
                    Clients.Clients(new List<string> { sendToClientID, Context.ConnectionId }).publishMsg(FormateMsg(userName, sendToUsername, msg));
                }
            }

    1.8 OK,我们集线器的事件就这样写完了,准备写前端的界面和事件了。GroupChatHub 的代码如下:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using Microsoft.AspNet.SignalR;
    using Microsoft.AspNet.SignalR.Hubs;
    using System.Threading.Tasks;
    
    using System.Collections.Concurrent;
    
    namespace TestSignalR.Models
    {
        [HubName("groupChatHub")]
        public class GroupChatHub : Hub
        {
    
            private string userName
            {
                get 
                {
                    var ck_userName = Context.RequestCookies["userName"];
                    return ck_userName == null ? "" : ck_userName.Value;
                }
            }
    
    
    
    
            private static ConcurrentDictionary<string, string> _onlineDic = new ConcurrentDictionary<string, string>();
    
    
            public override Task OnConnected()
            {
                Connect();
                return base.OnConnected();
            }
    
    
    
    
            public override Task OnReconnected()
            {
                Connect();
                return base.OnReconnected();
            }
    
    
            public void Connect()
            {
                if (!_onlineDic.ContainsKey(userName))
                {
                    _onlineDic.AddOrUpdate(userName, Context.ConnectionId, (k, ov) => ov = Context.ConnectionId);
    
                    Clients.All.publishMsg(FormateMsg("系统通知", "所有人", "系统消息" + userName + "加入聊天"));
                }
                else
                {
                    _onlineDic.AddOrUpdate(userName, Context.ConnectionId, (k, ov) => ov = Context.ConnectionId);
                }
                Clients.All.publishUser(_onlineDic.Select(c => new { key = c.Key, value = c.Value }));
            }
    
    
            public dynamic FormateMsg(string sender, string receiver, string msg)
            {
                return new
                {
                    sender = sender,
                    receiver = receiver,
                    msgTime = DateTime.Now,
                    msg = msg
                };
            }
    
    
            public override Task OnDisconnected(bool stopCalled)
            {
                string Temp = "";
                _onlineDic.TryRemove(userName, out Temp);
                Clients.All.publishUser(_onlineDic.Select(c => new { key = c.Key, value = c.Value }));
    
                Clients.All.publishMsg(FormateMsg("系统通知", "所有人", "系统消息" + userName + "离开聊天"));
    
                return base.OnDisconnected(stopCalled);
            }
    
    
    
            /// <summary>
            /// 发送消息
            /// </summary>
            /// <param name="sendToClientID">接收者ID</param>
            /// <param name="msg">消息</param>
            public void Send(string sendToClientID,string msg)
            {
                if (sendToClientID == "0")
                {
                    Clients.All.publishMsg(FormateMsg(userName,"所有人",msg));
                }
                else
                {
                    string sendToUsername="";
    
                    var sendToUserItem= _onlineDic.FirstOrDefault(c => c.Value == sendToClientID);
                    if (sendToUserItem.Key != null)
                    {
                        sendToUsername = sendToUserItem.Key;
                    }
    
                    Clients.Clients(new List<string> { sendToClientID, Context.ConnectionId }).publishMsg(FormateMsg(userName, sendToUsername, msg));
                }
            }
    
    
    
    
        }
    }
    View Code

    2.0 创建控制器 HomeController ,增加 Index ,  ChatRoom 方法:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    
    namespace TestSignalR.Controllers
    {
        public class HomeController : Controller
        {
    
            public ActionResult Index()
            {
                //ViewBag.ClientName = "用户-" + new Random().Next(10000, 99999);
                return View();
            }
    
    
            public ActionResult ChatRoom()
            {
                string userName= Request.QueryString["txt_name"];
                Response.Cookies.Add(new HttpCookie("userName",userName));
                ViewBag.userName = userName;
                return View();
            }
    
    
        }
    }
    View Code

    2.1 首先创建一个输入用户名的视图,Index 文件,先不做用任何户名合法判断处理。

    <form method="get" action="/Home/ChatRoom">
        用户名:
        <input type="text" id="txt_name" name="txt_name" value="Frank" />
    
        <button type="submit">进入</button>
    </form>

    2.2 创建聊天室视图 ChatRoom ,引入 JQ包, SignalR 的文件,虚拟的 集线器文件。

    @{
        ViewBag.Title = "ChatRoom";
        Layout = "~/Views/Shared/_Layout.cshtml";
    }
    
    <script src="~/Scripts/jquery-1.8.2.min.js"></script>
    
    <script src="~/Scripts/jquery.signalR-2.2.3.min.js"></script>
    
    <script src="@Url.Content("~/signalr/hubs")"></script>
    <h2>聊天室(当前<div id="userCount">1</div>人)</h2>
    
    
    <div>
        你是:@ViewBag.userName
    </div>
    <div>
        <select id="ddl_userList">
            <option value="0">所有人</option>
        </select>
        <input type="text" id="txt_msg" />
        <input type="button" onclick="SendMsg()" value="发送" />
    </div>
    
    
    <div>
        聊天记录:
        <div id="div_msg">
    
        </div>
    </div>

    2.3 开始写对应的JS事件:

    <script type="text/javascript">
    
        var chat = $.connection.groupChatHub;//创建聊天的集线器,告诉客户端,你用的就是我们刚才创建的 GroupChatHub 
        $(function () {
            chat.client.broadcastMessage = function (name, message) {//当客户端收到广播所要操作的事件
                var encodedName = $('<div />').text(name).html();
                var encodedMsg = $('<div />').text(message).html();
                $('#div_msg').append('<li><strong>' + encodedName
                    + '</strong>:&nbsp;&nbsp;' + encodedMsg + '</li>');
            };
    
            chat.client.publishMsg = function (data) {//收到新信息的处理事件
                writeMsg(data.sender,data.receiver, data.msg);
            };
    
            chat.client.publishUser = function (data) {//当客户端收到用户列表(就是我们自定义的 publishUser 的消息),处理的事件
                var _html = "<option value='0'>所有人</option>";
                for (var item in data) {
                    _html+= "<option value='"+data[item].value+"'>" +data[item].key  + "</option>";
                }
                $("#userCount").text(data.length);
                $("#ddl_userList").html(_html);
            }
    
    
            $.connection.hub.start().done(function () {//初始化集线器
                console.log("connect ok.");
            });
    
    
            function writeMsg(sender,receiver,eventLog) {//输出记录
                var now = new Date();
                var nowStr = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds();
                $('#div_msg').prepend('<div><b>' + nowStr + '</b>&nbsp;&nbsp;<b>' + sender + '</b>对 <b>' + receiver + '</b> ' + eventLog + '.</div>');
            }
    
        });
    
    
        function SendMsg() {
            chat.server.send($("#ddl_userList").val(), $("#txt_msg").val()).done(function () {//调用服务端的 Send 方法
                console.log("send OK");
            });
        }
    
    
    
    </script>

    运行起来,看下效果:

    消息广播结果:

     消息单独发送结果:

     

    简单的可广播、可私聊的聊天室就这样完成了。

    三、结束语

    总结一下:

    1.多看官方文档,英文不好可以看下机翻。

    2.写代码之前先理清你要做什么,想做什么,打算怎么做。

    3.从最简单的开始写起,如上个 DEMO 的,只能全体聊天的那种。一步步做下去。

    4.注意 JQ 包版本,好像低版本的 JQ 包,前端 SignalR 会报错。

     感谢大家的阅读。

  • 相关阅读:
    1.7 Matrix Zero
    1.6 Image Rotation
    Snake Sequence
    安装 Docker
    开源蓝牙协议栈 BTstack学习笔记
    无法从 repo.msys2.org : Operation too slow. Less than 1 bytes/sec transferred the last 10 seconds 获取文件
    KEIL生成预编译文件
    Duff's device
    Pyinstaller : unable to find Qt5Core.dll on PATH
    HCI 获取蓝牙厂商信息
  • 原文地址:https://www.cnblogs.com/Frank-Jan/p/8929835.html
Copyright © 2020-2023  润新知