zoukankan      html  css  js  c++  java
  • 基于SignalR的小型IM系统

    这个IM系统真是太轻量级了,提供的功能如下:

    1.聊天内容美化

    2.用户上下线提示

    3.心跳包检测机制

    4.加入用户可群聊

    下面来一步一步的讲解具体的制作方法。

    开篇准备工作

    首先,巧妇难为无米之炊,这是总所周知的。这里我们需要两个东西,一个是Asp.net MVC4项目;另一个是Signalr组件。

    新建一个Asp.net MVC4项目,然后通过以下命令安装Signalr组件:

    Install-Package Microsoft.AspNet.SignalR -Version 1.1.3

    这样我们就将组件安装完毕了。

    后台交互部分

    接着在项目中,新建一个文件夹名称为Hubs,在这个文件夹下面新建一个名称为IChatHub的接口,定义如下:

     interface IChatHub
        {
            //服务器下发消息到各个客户端
            void SendChat(string id, string name, string message);
    
            //用户上线通知
            void SendLogin(string id, string name);
    
            //用户下线通知
            void SendLogoff(string id, string name);
    
            //接收客户端发送的心跳包并处理
            void TriggerHeartbeat(string id, string name);
        }

    其中,SendChat方法主要用户Signalr后端向前台发送数据;SendLogin方法主要用于通知用户上线;SendLogoff方法主要用于通知用户下线;而TriggerHeartbeat方法主要用于接收前端发送的心跳包并做处理,以便于判断用户是否断开连接(有时候用户直接关闭浏览器或者在任务管理器中关闭浏览器,是无法检测用户离线与否的,所以这里引入了心跳包机制,一旦用户在20秒之后未发送任何心跳包到后端,则视为掉线)。

    接下来添加一个ChatHub的类,具体实现如下:

    public class ChatHub:Hub, IChatHub
        {
            private IList<UserChat> userList = ChatUserCache.userList;
    
            public void SendChat(string id, string name, string message)
            {
                Clients.All.addNewMessageToPage(id, name + " " + DateTime.Now.ToString("yyyy/MM/dd hh:mm:ss"), message);
            }
    
            public void TriggerHeartbeat(string id, string name)
            {
                var userInfo = userList.Where(x => x.ID.Equals(id) && x.Name.Equals(name)).FirstOrDefault();
                userInfo.count = 0;  //收到心跳,重置计数器
            }
    
            public void SendLogin(string id,string name)
            {
                var userInfo = new UserChat() { ID = id, Name = name };
                userInfo.action += () =>
                {
                    //用户20s无心跳包应答,则视为掉线,会抛出事件,这里会接住,然后处理用户掉线动作。
                    SendLogoff(id, name);
                };
    
                var comparison = new ChatUserCompare();
                if (!userList.Contains<UserChat>(userInfo, comparison))
                    userList.Add(userInfo);
                Clients.All.loginUser(userList);
                SendChat(id, name, "<====用户 " + name + " 加入了讨论组====>");
            }
    
            public void SendLogoff(string id,string name)
            {
                var userInfo = userList.Where(x => x.ID.Equals(id) && x.Name.Equals(name)).FirstOrDefault();
                if (userInfo != null)
                {
                    if (userList.Remove(userInfo))
                    {
                        Clients.All.logoffUser(userList);
                        SendChat(id, name, "<====用户 " + name + " 退出了讨论组====>");
                    }
                }
            }
        }

    这个类的设计思想有如下几个部分:

    首先,所有用户的登陆信息,我持久化到了缓存集合中:IList<UserChat>,这个缓存集合的定义如下:

    public static class ChatUserCache
        {
            public static IList<UserChat> userList = new List<UserChat>();
        }

    这样,用户登陆信息就会保存到内存中,一旦有新用户进来或者是旧用户退出,我就可以通过新增条目或者删除条目来维护这个列表,维护完毕,将这个列表推到前端。这样前台用户就能实时看到,哪些用户上线,哪些用户下线了。

    其次,心跳包检测机制部分,前端用户每隔5秒钟会发送一次心跳包到处理中心,处理中心收到心跳包,会将实体类的计数器置为0;也就是说,如果用户登陆正常,那么用户实体中的计数器每隔5秒钟自动置为0;但是如果用户不按正常渠道退出(直接关闭浏览器或者在任务管理器中关闭浏览器),那么用户实体中的计数器就会一直递增,直到加到第20秒,然后会抛出事件,提示当前用户已经断开连接。

    用户实体设计如下:

    public class UserChat
        {
            public UserChat()
            {
                count = 0;
                if (Timer == null) Timer = new Timer();
                Timer.Interval = 1000;  //1s触发一次
                Timer.Start();
                Timer.Elapsed += (sender, args) =>
                {
                    count++;
                    if (count >= 20)
                        action();  //该用户掉线了,抛出事件通知
                };
            }
    
            private readonly Timer Timer;
            public event Action action;
            
            public string ID { get; set; }
            public string Name { get; set; }
    
            //内部计数器(每次递增1),如果服务端每5s能收到客户端的心跳包,那么count被重置为0;
            //如果服务端20s后仍未收到客户端心跳包,那么视为掉线
            public int count{get;set;}
    
        }

    当用户意外退出,会有一个action事件抛出,我们在SendLogin方法中进行了接收,当这个事件抛出,就会立马触发用户的Logoff事件,通知掉线:

     public void SendLogin(string id,string name)
            {
                var userInfo = new UserChat() { ID = id, Name = name };
                userInfo.action += () =>
                {
                    //用户20s无心跳包应答,则视为掉线,会抛出事件,这里会接住,然后处理用户掉线动作。
                    SendLogoff(id, name);
                };
    
                var comparison = new ChatUserCompare();
                if (!userList.Contains<UserChat>(userInfo, comparison))
                    userList.Add(userInfo);
                Clients.All.loginUser(userList);
                SendChat(id, name, "<====用户 " + name + " 加入了讨论组====>");
            }

    这就是处理中心的所有内容了。

    需要注意的是,在ChatHub类中,SendChat方法,TriggerHeartbeat方法,SendLogin方法,SendLogoff方法都是Singnalr处理对象所拥有的方法,而addNewMessageToPage方法,loginUser方法,logoffUser方法则是其回调方法。也就是说,当你在前台通过SendChat方法向处理中心发送数据的时候,你可以注册addNewMessageToPage方法来接收处理中心返回给你的数据。

    image

    前端逻辑及布局

    @{
        Layout = "~/Views/Shared/_Layout.cshtml"; 
    }
    <div id="tb" class="easyui-panel panel" title="专家在线咨询系统" >
        <div id="messageboard">
            <ul id="discussion"></ul>
        </div>
        <div id="userContainer">
            <ul id="userList"></ul>
        </div>
        <div id="messagecontainer" >
            <textarea id="message" class="rte-zone" rows="3"></textarea>
            <div>
                <input type="button" id="send" class="btn" value="发送" />
                <input type="button" id="close" class="btn" value="关闭" /><input type="hidden" id="displayname" />
            </div>
        </div>
    </div>
    @section scripts{
        <style>
        .panel{padding:5px;height:auto;min-height:650px;}
        .current{color:Green;}
        .rte-zone{815px;margin:0;padding:0;height:160px;border:1px #999 solid;clear:both}
        .rte-toolbar{800px;margin-top:10px;}
        .rte-toolbar div{float:left;100%;}
        .rte-toolbar a,.rte-toolbar a img{border:0}
        .rte-toolbar p{float:left;margin:0;padding-right:5px}
        #messageboard{border:1px solid #B6DF7D;float:left;800px;padding:10px;height:450px;overflow:auto;border-radius:10px; -moz-box-shadow:2px 2px 5px #333333; -webkit-box-shadow:2px 2px 5px #333333; box-shadow:2px 2px 5px #333333;}
        #userContainer{border:1px solid #B6DF7D;float:right;200px;height:565px;padding:5px;border-radius:10px; -moz-box-shadow:2px 2px 5px #333333; -webkit-box-shadow:2px 2px 5px #333333; box-shadow:2px 2px 5px #333333;}
        #messagecontainer{float:left;800px;}
        #messagecontainer div{float:right;}
        #message{border:1px solid #B6DF7D;815px; height:70px;margin-top:5px;border-radius:10px; -moz-box-shadow:2px 2px 5px #333333; -webkit-box-shadow:2px 2px 5px #333333; box-shadow:2px 2px 5px #333333;}
        #userList li{border-bottom:1px solid #B6DF7D;cursor:pointer;}
        #userList li:hover{background-color:#ccc;}
        .btn{75px;height:25px;}
        </style>
        <script src="../../Content/jqueryplugin/jquery.rte.js" type="text/javascript"></script>
        <!--Reference the SignalR library. -->
        <script src="../../Scripts/jquery.signalR-1.1.4.min.js" type="text/javascript"></script>
        <!--Reference the autogenerated SignalR hub script. -->
        <script src="../../signalr/hubs"></script>
    
        <script>
            $(function () {
    
                $('.rte-zone').rte("css url", "http://batiste.dosimple.ch/blog/posts/2007-09-11-1/");
                //添加对自动生成的Hub的引用
                var chat = $.connection.chatHub;
    
                //调用Hub的callback回调方法
    
                //后端SendChat调用后,产生的addNewMessageToPage回调
                chat.client.addNewMessageToPage = function (id, name, message) {
                    $('#discussion').append('<li style="color:blue;">' + htmlEncode(name) + '</li><li> ' + htmlEncode(message) + '</li>')
                };
    
                //后端SendLogin调用后,产生的loginUser回调
                chat.client.loginUser = function (userlist) {
                    reloadUser(userlist);
                };
    
                //后端SendLogoff调用后,产生的logoffUser回调
                chat.client.logoffUser = function (userlist) {
                    reloadUser(userlist);
                };
    
                $('#displayname').val(prompt('请输入昵称:', ''));
    
                //启动链接
                $.connection.hub.start().done(function () {
    
                    var userid = guid();
                    var username = $('#displayname').val();
    
                    //发送上线信息
                    chat.server.sendLogin(userid, username);
    
                    //点击按钮,发送聊天内容
                    $('#send').click(function () {
                        var chatContent = $('#message').contents().find('.frameBody').html();
                        chat.server.sendChat(userid, username, chatContent);
                    });
    
                    //点击按钮,发送用户下线信息
                    $('#close').click(function () {
                        chat.server.sendLogoff(userid, username);
                        $("#send").css("display", "none");
                    });
    
                    //每隔5秒,发送心跳包信息
                    setInterval(function () {
                        chat.server.triggerHeartbeat(userid, username);
                    }, 5000);
                });
    
            });
    
            //重新加载用户列表
            var reloadUser = function (userlist) {
                $("#userList").children("li").remove();
                for (i = 0; i < userlist.length; i++) {
                    $("#userList").append("<li><img src='../../Content/images/charge_100.png' />" + userlist[i].Name + "</li>");
                }
            }
    
            //div内容html化
            var htmlEncode = function (value) {
                var encodedValue = $('<div />').html(value).html();
                return encodedValue;
            }
    
            //guid序号生成
            var guid = (function () {
                function s4() {
                    return Math.floor((1 + Math.random()) * 0x10000)
                               .toString(16)
                               .substring(1);
                }
                return function () {
                    return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
                           s4() + '-' + s4() + s4() + s4();
                };
            })();
        </script>
    }

    在如上代码中:

    第49行,加载一个富文本编辑器

    第51行,添加对自动生成的proxy的引用

    第56行~第68行,注册回调方法,以便于更新前台UI

    第73行,打开处理中心hub

    第79行,发送用户上线信息

    第94行,每隔5秒钟发送一次心跳包

    如此而已,非常简便。

    运行截图

    打开界面,首先提示输入用户昵称:

    image

    输入完毕之后,用户上线:

    image

    后续两个用户加入进来:

    image

    用户聊天内容记录:

    image

    用户“浅浅的”正常退出:

    image

    用户“书韵妍香”非正常退出:

    image

    点击下载

    参考文章:Tutorial: Getting Started with SignalR 1.x

    切不要忘记在Global.asax中添加映射:

     RouteTable.Routes.MapHubs();

  • 相关阅读:
    如何在IIS7/7.5上配置IISADMPWD
    运用DebugDiag诊断ASP.Net异常
    vuecli3修改项目启动端口
    彻底删除vscode及安装的插件和个人配置信息
    angular中的 input select 值绑定无效,以及多出一个空白选项问题
    简述MVC模式
    vuecli3 运行报错
    前端开发规范
    nodejs 下载最新版本
    小程序 自定义弹窗出现后存在滚动穿透问题
  • 原文地址:https://www.cnblogs.com/scy251147/p/3808966.html
Copyright © 2011-2022 走看看