基于SignalR的小型IM系统

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

1.聊天内容美化

2.用户上下线提示

3.心跳包检测机制

4.加入用户可群聊

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

开篇准备工作

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

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

1
Install-Package Microsoft.AspNet.SignalR -Version 1.1.3

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

后台交互部分

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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的类,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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>,这个缓存集合的定义如下:

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

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

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

用户实体设计如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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事件,通知掉线:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

前端逻辑及布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
@{
     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{width:815px;margin:0;padding:0;height:160px;border:1px #999 solid;clear:both}
     .rte-toolbar{width:800px;margin-top:10px;}
     .rte-toolbar div{ float :left;width: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;width: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;width: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;width:800px;}
     #messagecontainer div{float:right;}
     #message{border:1px solid #B6DF7D;width: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{width: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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值