基于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方法来接收处理中心返回给你的数据。
前端逻辑及布局
- @{
- 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();
基于SignalR的小型IM系统的更多相关文章
- 基于SignalR的消息推送与二维码描登录实现
1 概要说明 使用微信扫描登录相信大家都不会陌生吧,二维码与手机结合产生了不同应用场景,基于二维码的应用更是比较广泛.为了满足ios.android客户端与web短信平台的结合,特开发了基于Singl ...
- 开源Asp.Net Core小型社区系统
源码地址:Github 前言 盼星星盼月亮,Asp.Net Core终于发布啦!! Asp.Net发布时我还在上初中,没有赶上.但是Asp.Net Core我从beta版本便一直关注.最初项目名叫As ...
- 构建嵌入式小型Linux系统
构建嵌入式小型Linux系统 摘要:用buildroot构建x86的交叉编译工具链:裁减linux内核,尽可能做到最小:手工构建根文件系统:安装qemu虚拟机,仿真新配置的Linux系统:为新配置的L ...
- RDIFramework.NET — 基于.NET的快速信息化系统开发框架 — 系列目录
RDIFramework.NET — 基于.NET的快速信息化系统开发框架 — 系列目录 RDIFramework.NET,基于.NET的快速信息化系统开发.整合框架,给用户和开发者最佳的.Net框架 ...
- 基于SignalR的web端即时通讯 - ChatJS
先看下效果. ChatJS 是基于SignalR实现的Web端IM,界面风格模仿的是“脸书”,可以很方便的集成到已有的产品中. 项目官网:http://chatjs.net/ github地址:htt ...
- Kafka 分布式的,基于发布/订阅的消息系统
Kafka是一种分布式的,基于发布/订阅的消息系统.主要设计目标如下: 通过O(1)的磁盘数据结构提供消息的持久化,这种结构对于即使数以TB的消息存储也能够保持长时间的稳定性能. 高吞吐量:即使是非常 ...
- 项目-基于视频压缩的实时监控系统--tiny6410
项目-基于视频压缩的实时监控系统--tiny6410 @国嵌linux学习笔记. 1. 构造服务端结构体 server struct server { int epfd; //保存epoll指针 st ...
- http与websocket(基于SignalR)两种协议下的跨域基于ASP.NET MVC--竹子整理
这段时间,项目涉及到移动端,这就不可避免的涉及到了跨域的问题.这是本人第一次接触跨域,有些地方的配置是有点麻烦,导致一开始的不顺. 至于websocket具体是什么意义,用途如何:请百度. 简单说就是 ...
- RDIFramework.NET — 基于.NET的快速信息化系统开发框架 - 5.3 数据库连接管理模块
RDIFramework.NET — 基于.NET的快速信息化系统开发框架 5.3 数据库连接管理模块 5.3 数据库连接管理模块 我们经常可以看到很多软件直接把数据库连接字符串放在软件执行目录下的配 ...
随机推荐
- Team Leader炖完石头汤后干嘛
在万众创业的互联网年代,挖人组建全明星团队过于奢侈.面对水平参差不齐的团队咋办? 命运真是捉弄,半年前在大美团打工时准备做个NABC的教学项目 ,结果自己就被挖到"Competitors 竞 ...
- My97DatePicker时间控件使用
刚刚工作中遇到一个修改时间空间的bug,顺带学习了My97DatePicker时间空间 网上查到的资料已经很详细: http://www.360doc.com/content/14/0606/11/1 ...
- 对js中Function的浅见
它到底是什么 String Array 都是系统内置对象(已经定义好,可以直接使用)当然,这货也是一样,我们之前定义的函数,其实就是一个这货的实例. 在JS中,所有的对象都是由函数实现的,函数的数据类 ...
- 在unix系统下的 .o文件 .a文件 .so文件说明和相互关系
.o文件 .o文件就是对象文件,包含编译好的可执行代码,当程序执行时,被链接库链接调用[相当于windows里的obj文件] .a文件unix中的静态链接库,包含多个需要包含的.o文件,主要特点是在 ...
- SQL Server 内存相关博文
Don’t confuse error 823 and error 832 本文大意: 错误832: A page that should have been const ...
- SQL Server:孤立用户详解
SQL Server 的用户安全管理分两层,整个SQL Server 服务器一层,每个数据库一层. 在服务器层的帐号,叫登录账户(SQL Server:服务器角色),可以设置它管理整个SQL Serv ...
- android 滑动滚动条调节音量
利用滚动条滑动控制音量: 定义: private SeekBar mseekBarvolume: 以下实现代码: //调节音量--begin------------------------- //音量 ...
- 树莓派搭建安装mysql
最近刚入手了一枚树莓派,突发奇想打算做一个小型的家用服务器,在家7*24小时一直挂着. 真的是非常小,只有巴掌大,给树莓派买了一些配件,外壳.小风扇.2片散热片.32G SD卡.HDMI线,组装之后的 ...
- vim IDE平台-打造属于自己的配置
vim IDE平台-打造属于自己的配置 一.前言 目前工作环境基本以Linux为主,自然用到VIM也很多,很早就对如何提高VIM的使用效率有所研究,限于时间关系,也没做个系统记录和资料积累,时间久了又 ...
- unp TCP 客户端服务器回射程序中对SIGCHLD信号的处理
第五章中,有一个例子模拟客户端并发的终止TCP连接,服务器捕捉并处理SIGCHLD信号并调用waitpid函数防止僵死进程的出现.信号处理函数中核心的一句是: , &statloc, WNOH ...