SignalR实现在线聊天室功能
一、在线聊天室
1、新建解决方案 SignalROnlineChatDemo
2、新建MVC项目 SignalROnlineChatDemo.Web
(无身份验证)
3、安装SignalR
PM> install-package Microsoft.AspNet.SignalR
4、 创建一个称为 Startup.cs 的新类
1 public class Startup 2 { 3 public void Configuration(IAppBuilder app) 4 { 5 // 有关如何配置应用程序的详细信息,请访问 http://go.microsoft.com/fwlink/?LinkID=316888 6 app.MapSignalR(); 7 } 8 }
5、添加Hubs
1 public class ChatHub : Hub 2 { 3 public void Hello() 4 { 5 Clients.All.hello(); 6 } 7 }
6、Action/View
1 /// <summary> 2 /// 在线聊天室 3 /// </summary> 4 /// <returns></returns> 5 public ActionResult Chat(string groupName) 6 { 7 if (string.IsNullOrWhiteSpace(groupName)) 8 { 9 return Content("groupName is nllOrWhiteSpace"); 10 } 11 return View((object)groupName); 12 }
1 @model string 2 @{ 3 ViewBag.Title = "Chat"; 4 } 5 6 <style> 7 .chat-container div { 8 margin: 10px 0; 9 } 10 .dN { 11 display: none; 12 } 13 </style> 14 15 <h2>Chat <span id="chatRoomName"></span></h2> 16 17 <div class="chat-container"> 18 <ul id="discussion"></ul> 19 <div class="sendto-wrap dN"> 20 发送给: 21 <span></span> 22 </div> 23 <div class="sendcontent-wrap"> 24 <input type="text" name="message" /> 25 <input type="button" id="btnSendMessage" value="Send" /> 26 </div> 27 </div> 28 29 @section scripts{ 30 <script src="~/Scripts/jquery.signalR-2.2.0.min.js"></script> 31 <!-- Reference the autogenerated SignalR hub script. --> 32 <script src="~/signalr/hubs"></script> 33 <script type="text/javascript"> 34 $(function () { 35 //聊天室编号 36 var groupName = '@Model'; 37 //成员昵称 38 var nickName = ''; 39 //成员聊天Id 40 var connectionId = ''; 41 42 //Reference the auto-generated proxy for the hub. 43 var chat = $.connection.chatHub; 44 45 $("#chatRoomName").html(groupName); 46 47 //Get the user name and store it to prepend to message 48 while (nickName == '' || $.trim(nickName) == '') { 49 nickName = prompt('Enter your name:', '') 50 $('#displayname').val(nickName); 51 } 52 53 $('#message').focus(); 54 55 }); 56 57 </script> 58 }
7、功能1:新成员加入,群发欢迎
1 /// <summary> 2 /// newcomer 进入聊天室 3 /// </summary> 4 /// <param name="groupName"></param> 5 public void JoinGroup(string groupName, string userNickName) 6 { 7 //对聊天室成员群发‘新成员加入’ 8 var conId = Context.ConnectionId; 9 10 Groups.Add(conId, groupName); 11 12 var psn = new ChatPerson() 13 { 14 ConnectionId = conId, 15 NickName = userNickName, 16 GroupName = groupName, 17 }; 18 19 Clients.Caller.setCallerInfo(psn); 20 //Clients.Group(groupName).welcome(psn); //不能广播给自己,所以分成了两句 21 Clients.Caller.welcome(psn); 22 Clients.Group(groupName, conId).welcome(psn); 23 24 }
1 //新成员身份信息(connectionId) 2 chat.client.setCallerInfo = function (psn) { 3 connectionId = psn.ConnectionId; 4 groupName = psn.groupName; 5 }; 6 //welcome newcomer 7 chat.client.welcome = function (psn) { 8 $("#discussion").append('<li><a href="javascript:;" data-conId="' + psn.ConnectionId + '">' + psn.NickName + '</a>加入了聊天室</li>'); 9 };
结果截图:
8、功能2:群发
1 /// <summary> 2 /// newcomer进入聊天室,对聊天室成员群发‘新成员加入’ 3 /// </summary> 4 /// <param name="groupName"></param> 5 public void JoinGroup(string groupName, string userNickName) 6 { 7 var conId = Context.ConnectionId; 8 var psn = new ChatPerson() 9 { 10 ConnectionId = conId, 11 NickName = userNickName, 12 GroupName = groupName, 13 }; 14 15 //成员信息计入Redis中 16 //var redisClient = RedisManager.GetClient(); 17 //if (redisClient.Get<ChatPerson>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_ChatPerson, psn.ConnectionId)) != null) 18 //{ 19 // //connected 20 // return; 21 //} 22 //redisClient.Set<ChatPerson>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_ChatPerson, psn.ConnectionId), psn); 23 //redisClient.SaveAsync(); 24 using (var redisClient = RedisManager.GetClient()) 25 { 26 IRedisTypedClient<ChatPerson> psns = redisClient.As<ChatPerson>(); 27 if (psns.GetValue(string.Format(RedisKey.SignalROnlineChatDemoWebModels_ChatPerson, psn.ConnectionId)) != null) 28 { 29 //connected 30 return; 31 } 32 psns.SetValue(string.Format(RedisKey.SignalROnlineChatDemoWebModels_ChatPerson, psn.ConnectionId), psn); 33 } 34 35 Groups.Add(conId, groupName); 36 Clients.Caller.setCallerInfo(psn); 37 //Clients.Group(groupName).welcome(psn); //不能广播给自己,所以分成了两句 38 Clients.Caller.welcome(psn); 39 Clients.Group(groupName, conId).welcome(psn); 40 } 41 42 /// <summary> 43 /// 群发内容 44 /// </summary> 45 public void SendMessage(string message) 46 { 47 if (string.IsNullOrWhiteSpace(message)) 48 { 49 return; 50 } 51 var conId = Context.ConnectionId; 52 ChatPerson psn = null; 53 var redisClient = RedisManager.GetClient(); 54 if ((psn = redisClient.Get<ChatPerson>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_ChatPerson, conId))) == null 55 || string.IsNullOrWhiteSpace(psn.GroupName) || string.IsNullOrWhiteSpace(psn.NickName) 56 ) 57 { 58 //invalid ConnectionId 59 return; 60 } 61 Clients.Group(psn.GroupName).sendMessage(conId, psn.NickName, message); 62 }
1 //群发内容 2 chat.client.sendMessage = function (sendFromConnectionId, sendFromNickName, message) { 3 $("#discussion").append('<li><a href="javascript:;" data-conId="' + sendFromConnectionId + '">' + sendFromNickName + '</a>:' + message + '</li>'); 4 };
结果截图:
9、功能3:回复
1 /// <summary> 2 /// 回复(@) 3 /// </summary> 4 /// <param name="sendTo"></param> 5 /// <param name="message"></param> 6 public void SendMessageTo(string sendTo, string message) 7 { 8 if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(sendTo)) 9 { 10 return; 11 } 12 var connId = Context.ConnectionId; 13 if (connId == sendTo) 14 { 15 return; 16 } 17 ChatPerson curPerson = null; 18 ChatPerson desPerson = null; 19 var redisClient = RedisManager.GetClient(); 20 if ((curPerson = redisClient.Get<ChatPerson>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_ChatPerson, connId))) == null 21 || string.IsNullOrWhiteSpace(curPerson.GroupName) || string.IsNullOrWhiteSpace(curPerson.NickName) 22 ) 23 { 24 //invalid ConnectionId 25 return; 26 } 27 if ((desPerson = redisClient.Get<ChatPerson>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_ChatPerson, sendTo))) == null 28 || string.IsNullOrWhiteSpace(desPerson.GroupName) || string.IsNullOrWhiteSpace(desPerson.NickName) 29 ) 30 { 31 //invalid ConnectionId 32 return; 33 } 34 if (curPerson.GroupName != desPerson.GroupName) 35 { 36 return; 37 } 38 39 Clients.Group(curPerson.GroupName).sendMessageTo(curPerson.ConnectionId, curPerson.NickName, desPerson.ConnectionId, desPerson.NickName, message); 40 }
1 //at回复 2 chat.client.sendMessageTo = function (fromConnId, fromNickName, toConnId, toNickName, message) { 3 $("#discussion").append('<li data-connId="' + fromConnId + '" data-nickName="' + fromNickName + '"><a href="javascript:;">' + fromNickName + '</a>对<a href="javascript:;">' + toNickName + '</a> 说:' + message + ' ' 4 + (fromConnId == connectionId ? '' : ' <a href="javascript:;" action="at">@他</a>') + '' + (fromConnId == connectionId ? '' : ' <a title="屏蔽其发言" href="javascript:;" action="shielding">屏蔽</a>') + '</li>'); 5 };
1 //发送 2 $("#btnSendMessage").click(function () { 3 var desConnId = $('.sendto-wrap a').attr('data-desConnId'); 4 if (!desConnId || desConnId.length == 0) { 5 //群发 6 chat.server.sendMessage($('[name=message]').val()); 7 } 8 else { 9 //回复 10 chat.server.sendMessageTo(desConnId, $('[name=message]').val()); 11 } 12 $('.sendto-wrap').addClass('dN'); 13 $('.sendto-wrap a').attr('data-desConnId', ''); 14 $('[name=message]').val('').focus(); 15 }); 16 //at 17 $('#discussion').on('click', '[action=at]', function () { 18 var desConnId = $(this).closest('li').attr('data-connId'); 19 var desNickName = $(this).closest('li').attr('data-nickName'); 20 $('.sendto-wrap').removeClass('dN'); 21 $('.sendto-wrap a').attr('data-desConnId', desConnId).html(desNickName); 22 });
结果截图:
10、功能4:私信
1 /// <summary> 2 /// 私信给 3 /// </summary> 4 /// <param name="sendTo"></param> 5 /// <param name="message"></param> 6 public void PrivateMessageTo(string sendTo, string message) 7 { 8 if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(sendTo)) 9 { 10 return; 11 } 12 var connId = Context.ConnectionId; 13 if (connId == sendTo) 14 { 15 return; 16 } 17 ChatPerson curPerson = null; 18 ChatPerson desPerson = null; 19 var redisClient = RedisManager.GetClient(); 20 if ((curPerson = redisClient.Get<ChatPerson>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_ChatPerson, connId))) == null 21 || string.IsNullOrWhiteSpace(curPerson.GroupName) || string.IsNullOrWhiteSpace(curPerson.NickName) 22 ) 23 { 24 //invalid ConnectionId 25 return; 26 } 27 if ((desPerson = redisClient.Get<ChatPerson>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_ChatPerson, sendTo))) == null 28 || string.IsNullOrWhiteSpace(desPerson.GroupName) || string.IsNullOrWhiteSpace(desPerson.NickName) 29 ) 30 { 31 //invalid ConnectionId 32 return; 33 } 34 if (curPerson.GroupName != desPerson.GroupName) 35 { 36 return; 37 } 38 Clients.Caller.myPrivateMessageTo(desPerson.ConnectionId, desPerson.NickName, message); 39 Clients.Client(sendTo).bePrivateMessageTo(curPerson.ConnectionId, curPerson.NickName, message); 40 }
1 //私信 2 $('#discussion').on('click', '[action=privateAt]', function () { 3 var desConnId = $(this).closest('li').attr('data-connId'); 4 var desNickName = $(this).closest('li').attr('data-nickName'); 5 $('.sendto-wrap span.sendto-to').html('私信给'); 6 $('.sendto-wrap').removeClass('dN'); 7 $('.sendto-wrap a').attr('data-desConnId', desConnId).attr('data-desAction', 'privateAt').html(desNickName); 8 });
1 //privateAt私信 2 //我发的 3 chat.client.myPrivateMessageTo = function (toConnId, toNickName, message) { 4 $("#discussion").append('<li data-connId="' + connectionId + '" data-nickName="' + nickName + '">我对<a href="javascript:;">' + toNickName + '</a> 说:' + message + ' ' 5 //+ getActionBlockHtml(connectionId) 6 ); 7 }; 8 //发给我的 9 chat.client.bePrivateMessageTo = function (fromConnId, fromNickName, message) { 10 $("#discussion").append('<li data-connId="' + fromConnId + '" data-nickName="' + fromNickName + '"><a href="javascript:;">' + fromNickName + '</a>对我私信说:' + message + ' ' 11 + getActionBlockHtml(fromConnId) 12 ); 13 };
结果截图:
11、功能5:屏蔽
1 /// <summary> 2 /// 3 /// </summary> 4 public class PersonShielding 5 { 6 /// <summary> 7 /// 成员的ConnectionId 8 /// </summary> 9 public string ConnectionId { get; set; } 10 11 /// <summary> 12 /// 被屏蔽 我被哪些人屏蔽(这样设计似乎不合理,但好用) 13 /// </summary> 14 public string[] BeShieldingByConnIdArr { get; set; } 15 }
1 /// <summary> 2 /// 屏蔽某人的发言 3 /// </summary> 4 /// <param name="desConId"></param> 5 public void Shielding(string desConnId) 6 { 7 if (string.IsNullOrWhiteSpace(desConnId)) 8 { 9 return; 10 } 11 var connId = Context.ConnectionId; 12 if (connId == desConnId) 13 { 14 return; 15 } 16 var redisClient = RedisManager.GetClient(); 17 ChatPerson curPerson = null; 18 ChatPerson desPerson = null; 19 if ((curPerson = redisClient.Get<ChatPerson>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_ChatPerson, connId))) == null 20 || string.IsNullOrWhiteSpace(curPerson.GroupName) || string.IsNullOrWhiteSpace(curPerson.NickName) 21 ) 22 { 23 //invalid ConnectionId 24 return; 25 } 26 if ((desPerson = redisClient.Get<ChatPerson>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_ChatPerson, desConnId))) == null 27 || string.IsNullOrWhiteSpace(desPerson.GroupName) || string.IsNullOrWhiteSpace(desPerson.NickName) 28 ) 29 { 30 //invalid ConnectionId 31 return; 32 } 33 var personShielding = redisClient.Get<PersonShielding>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_PersonShielding, desConnId)); 34 if (personShielding == null) 35 { 36 personShielding = new PersonShielding() 37 { 38 ConnectionId = desConnId, 39 BeShieldingByConnIdArr = new string[] { connId } 40 }; 41 } 42 else 43 { 44 if (personShielding.BeShieldingByConnIdArr == null) 45 { 46 personShielding.BeShieldingByConnIdArr = new string[] { connId }; 47 } 48 else if (!personShielding.BeShieldingByConnIdArr.Contains(connId)) 49 { 50 personShielding.BeShieldingByConnIdArr = personShielding.BeShieldingByConnIdArr.Union(new string[] { connId }).ToArray(); 51 } 52 } 53 redisClient.Set<PersonShielding>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_PersonShielding, desConnId), personShielding); 54 redisClient.SaveAsync(); 55 56 Clients.Caller.shieldingSuccess(desConnId, desPerson.NickName); 57 }
1 var personShielding = redisClient.Get<PersonShielding>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_PersonShielding, connId)); 2 if (personShielding != null && personShielding.BeShieldingByConnIdArr != null && personShielding.BeShieldingByConnIdArr.Length > 0) 3 { 4 //屏蔽我的 不发 5 Clients.Group(curPerson.GroupName, personShielding.BeShieldingByConnIdArr).sendMessage(connId, curPerson.NickName, message); 6 } 7 else 8 { 9 Clients.Group(curPerson.GroupName).sendMessage(connId, curPerson.NickName, message); 10 }
1 var personShielding = redisClient.Get<PersonShielding>(string.Format(RedisKey.SignalROnlineChatDemoWebModels_PersonShielding, connId)); 2 if (personShielding == null || personShielding.BeShieldingByConnIdArr == null || !personShielding.BeShieldingByConnIdArr.Contains(sendTo)) 3 { 4 //sendTo没有屏蔽我,那我就发 5 Clients.Group(curPerson.GroupName).sendMessageTo(curPerson.ConnectionId, curPerson.NickName, desPerson.ConnectionId, desPerson.NickName, message); 6 }
1 //屏蔽 2 $('#discussion').on('click', '[action=shielding]', function () { 3 if (confirm('确定屏蔽其发言么!')) { 4 var desConnId = $(this).closest('li').attr('data-connId'); 5 chat.server.shielding(desConnId); 6 } 7 });
1 //屏蔽成功 callback 2 chat.client.shieldingSuccess = function (desConnId, desNickName) { 3 $("#discussion").append('<li data-connId="' + connectionId + '" data-nickName="' + nickName + '">你成功屏蔽了<a href="javascript:;">' + desNickName + '</a>的发言' 4 + '</li>' 5 ); 6 };
结果截图:
二、其他工作
1、退出后重新接入,能确定唯一身份么?
关于这个问题,通常的解决方案是:使用UserId,以UserId作为主线。当时是想到一点就写一点的!
如果能在页面加载$.connection.hub.start()的时候带入上次使用的connectionId就好了,但暂时我还没发现能这么做
(如果谁知道怎么解决,分享给我下,谢谢!!!)
2、退出聊天室时从Redis清理用户的相关数据
先来看看Hub的这三个方法:Hub.OnConnected 、Hub.OnDisconnected 、 Hub.OnReconnected
1 /// <summary> 2 /// Called when the connection connects to this hub instance. 3 /// </summary> 4 /// <returns></returns> 5 public override Task OnConnected() 6 { 7 DefaultLoggerProvider.Instance.InfoFormat("ChatHub.OnConnected, ConnectionId:{0}", Context.ConnectionId); 8 return base.OnConnected(); 9 } 10 11 /// <summary> 12 /// Called when a connection disconnects from this hub gracefully or due to a timeout. 13 /// </summary> 14 /// <param name="stopCalled"></param> 15 /// <returns></returns> 16 public override Task OnDisconnected(bool stopCalled) 17 { 18 DefaultLoggerProvider.Instance.InfoFormat("ChatHub.OnDisconnected, ConnectionId:{0}", Context.ConnectionId); 19 return base.OnDisconnected(stopCalled); 20 } 21 22 /// <summary> 23 /// Called when the connection reconnects to this hub instance. 24 /// </summary> 25 /// <returns></returns> 26 public override Task OnReconnected() 27 { 28 DefaultLoggerProvider.Instance.InfoFormat("ChatHub.OnReconnected, ConnectionId:{0}", Context.ConnectionId); 29 return base.OnReconnected(); 30 }
从查看日志可以得出结论:
Onconnected 是在加载页面/刷新页面重新加载的时候触发($.connection.hub.start())。
OnDisConnected 在离开(关闭选项卡/刷新页面)的时候触发($.connection.hub.stop())。
OnReconnected 仅会在生成网站的时候触发。重启网站不会触发。(暂不知所以然)
所以可以这样
1 public override Task OnDisconnected(bool stopCalled) 2 { 3 var connId = Context.ConnectionId; 4 5 DefaultLoggerProvider.Instance.InfoFormat("ChatHub.OnDisconnected, ConnectionId:{0}, stopCalled:{1}", connId, stopCalled); 6 7 using (var redisClient = RedisManager.GetClient()) 8 { 9 redisClient.Remove(string.Format(RedisKey.SignalROnlineChatDemoWebModels_ChatPerson, connId)); 10 redisClient.Remove(string.Format(RedisKey.SignalROnlineChatDemoWebModels_PersonShielding, connId)); 11 redisClient.SaveAsync(); 12 } 13 14 return base.OnDisconnected(stopCalled); 15 }
附:源码下载