这个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方法来接收处理中心返回给你的数据。
前端逻辑及布局
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秒钟发送一次心跳包
如此而已,非常简便。
运行截图
打开界面,首先提示输入用户昵称:
输入完毕之后,用户上线:
后续两个用户加入进来:
用户聊天内容记录:
用户“浅浅的”正常退出:
用户“书韵妍香”非正常退出: