這個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方法來接收處理中心返回給你的數據。
前端邏輯及布局
@{
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秒鍾發送一次心跳包
如此而已,非常簡便。
運行截圖
打開界面,首先提示輸入用戶昵稱:
輸入完畢之后,用戶上線:
后續兩個用戶加入進來:
用戶聊天內容記錄:
用戶“淺淺的”正常退出:
用戶“書韻妍香”非正常退出:
參考文章:Tutorial: Getting Started with SignalR 1.x
切不要忘記在Global.asax中添加映射:
RouteTable.Routes.MapHubs();







