利用SignalR实现实时聊天
2018/10/10:博主第一次写原创博文而且还是关于C#的(博主是从前端转过来的),菜鸟一枚,如果有什么写的不对,理解错误,还望各位轻喷。,从SignalR开始!
首先先介绍一下关于SignalR的一些基本概念,
ASP.NET SignalR是为简化开发开发人员将实时web内容添加到应用程序过程而提供的类库。实时web功能指的是让服务器代码可以随时主动推送内容给客户端,而不是让服务器等待客户端的请求(才返回内容)。 所有"实时"种类的web功能都可以使用SignalR来添加到你的ASP.NET应用程序中。最常用的例子有聊天室,但我们能做的比这要多得多。考虑以下情况:用户需要不停的刷新网页来看最新的数据;或者在页面上通过实现长轮询来检索新数据(并显示),那你就可以考虑使用SignalR来实现了。比如:仪表板及监视型应用程序;协作型应用程序(如多人同时对文档进行编辑);作业进度更新及实时呈现表单等。 SignalR也适合新型的,需要从服务器上进行高频率更新的web应用程序,例如实时游戏。这里有一个好例子:ShoorR。 SignalR提供了一个简单的API用户创建服务器到客户端的远程过程调用(RPC),可以方便地从服务器端的.Net代码中对客户端浏览器及其他客户端平台中的的JS函数进行调用。SignalR还包括了用于管理连接(例如:连接和断开事件)及连接分组。
SignalR可以自动对连接进行管理。并让你发送广播消息到所有已连接的客户端上,就像一个聊天室一样。当然除了群发外,你也可以发送到消息到特定的客户端。客户端和服务器的连接是持久的,不像传统的每次通信都需要重新建立连接的HTTP协议。 SignalR支持“服务器推送”功能,即服务器代码可以通过使用远程过程调用(RPC)来调用浏览器中的客户端代码,而不是当前在web上常用的请求-相应处理模型。 SignalR的应用可以使用服务总线,SQL SERVER或者Redis来扩展到数以千计的客户端上。 SignalR是开源的,可以通过GitHub访问。
2、有人可能会问SignalR和WebSocket有什么区别
ignalR使用WebSocket传输方式——在可能的情况下。并且会自动切换到旧的传输方式(如HTTP长连接)。你当然可以直接使用WebSocket来编写你的应用程序,但使用SignalR意味着你将有更多的额外功能而无需重新发明轮子。最重要的是,你可以将注意力关注在业务实现上,而无需考虑为旧的客户端单独创建兼容代码。SignalR还能够使你不必担心WebSocket更新,因为SignalR将会持续更新以支持变化的底层传输方式,跨不同版本的WebSocket来为应用程序提供一个一致的访问接口。
当然,你可以创建只使用WebSocket传输的解决方案,SignalR提供了你可能需要自行编写代码的所有功能,比如回退到其他传输方式及针对更新的WebSocket实现来修改你的应用程序。博主的理解中就是SignalR是微软为了简化开发开发人员工作而造出的轮子,他不仅拥有WebSocket的功能,而且还有传统的HTTP长连接的方式。
接下来就是安装SignalR,SignalR在nuget上可以下载安装(SignalR要求.net 4.5的框架),在vs的工具里能找到nuget管理:搜Microsoft.AspNet.SignalR安装就好了,安装后会自动生成一下文件夹。
服务端/接口Api端代码
- 从Nuget上搜索SignalR并引入
- 创建的脚本拷贝到客户端Lib中到时使用requirejs引用,并删除服务端生成的脚本
- 在Startup.cs中注册SignalR中间件
1、注册视频聊天中间件:LiveVideoChat(app); 其中"/LiveVideoChat"是前端脚本要调用的地址
2、中间件需要授权登录的情况在,需要配置【QueryStringOAuthBearerProvider】,以及在集线器(LiveVideoChat)标注【Authorize】
using
System;
using
System.Threading.Tasks;
using
Ilikexx.Framework;
using
Microsoft.AspNet.SignalR;
using
Microsoft.Owin;
using
Microsoft.Owin.Cors;
using
Microsoft.Owin.Security.OAuth;
using
Owin;
[assembly: OwinStartup(
typeof
(Brand.Api.Startup))]
namespace
Brand.Api
{
public
partial
class
Startup
{
private
readonly
ILog logger = LogManager.GetLogger(
typeof
(Startup));
/// <summary>
///
/// </summary>
/// <param name="app"></param>
public
void
Configuration(IAppBuilder app)
{
logger.Info(
"Startup开始运行"
);
ConfigureAuth(app);
LogManager.Flush();
LiveVideoChat(app);
}
/// <summary>
/// 注册视频聊天模块
/// </summary>
/// <param name="app"></param>
private
void
LiveVideoChat(IAppBuilder app)
{
//LiveVideoChat 前端脚本要调用的地址
app.Map(
"/LiveVideoChat"
, map =>
{
map.UseCors(CorsOptions.AllowAll);
map.UseOAuthBearerAuthentication(
new
OAuthBearerAuthenticationOptions()
{
Provider =
new
QueryStringOAuthBearerProvider()
});
var hubConfiguration =
new
HubConfiguration
{
Resolver = GlobalHost.DependencyResolver,
EnableJavaScriptProxies =
true
};
map.RunSignalR(hubConfiguration);
});
}
}
}
/// <summary>
/// 验证token
/// </summary>
public
class
QueryStringOAuthBearerProvider : OAuthBearerAuthenticationProvider
{
/// <summary>
/// 从请求地址中获取token
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public
override
Task RequestToken(OAuthRequestTokenContext context)
{
var value = context.Request.Query.Get(
"access_token"
);
if
(!
string
.IsNullOrEmpty(value))
{
context.Token = value;
}
return
Task.FromResult<
object
>(
null
);
}
/// <summary>
/// 验证token的有效性
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public
override
Task ValidateIdentity(OAuthValidateIdentityContext context)
{
return
base
.ValidateIdentity(context);
}
}
- LiveVideoChat:聊天中间件(核心)
1、类标注Authorize标记是否需要token访问
2、类标注HubName给集线器起名,客户端在调用的时候会用到
3、方法标注HubMethodName供客户端脚本调用
4、熟悉分组广播,全部广播,单一广播
using
System;
using
System.Collections.Concurrent;
using
System.Collections.Generic;
using
System.Linq;
using
System.Security.Claims;
using
System.Threading;
using
System.Threading.Tasks;
using
System.Web;
using
Brand.Model;
using
Brand.Service;
using
Ilikexx.Framework;
using
Ilikexx.Framework.Web.Mvc;
using
Microsoft.AspNet.SignalR;
using
Microsoft.AspNet.SignalR.Hubs;
using
Microsoft.AspNet.SignalR.Infrastructure;
using
Newtonsoft.Json.Linq;
namespace
Brand.Api.Controllers.SignalR
{
/// <summary>
/// 视频聊天模块
/// </summary>
[HubName(
"LiveVideoChatService"
)]
[Authorize]
public
class
LiveVideoChat : BaseHub
{
/// <summary>
/// 在线用户类,按各个房间存储
/// </summary>
public
static
ConcurrentDictionary<
string
, List<Dictionary<
string
, User>>> OnLineUsers =
new
ConcurrentDictionary<
string
, List<Dictionary<
string
, User>>>();
private
UserService userService;
/// <summary>
///
/// </summary>
public
LiveVideoChat()
{
userService =
new
UserService();
}
/// <summary>
/// 获取调用者的用户信息
/// </summary>
public
JObject CallerUserInfo
{
get
{
JObject data =
new
JObject();
User user =
null
;
List<Dictionary<
string
, User>> listUser;
OnLineUsers.TryGetValue(RoomId,
out
listUser);
if
(listUser !=
null
)
{
Dictionary<
string
, User> mapUser = listUser.Find(x =>
{
return
x.ContainsKey(Context.ConnectionId);
});
if
(mapUser !=
null
)
{
mapUser.TryGetValue(Context.ConnectionId,
out
user);
data.Add(
"Id"
, user.Id);
data.Add(
"NickName"
, user.NickName);
data.Add(
"HeadUrl"
, user.HeadUrl);
}
}
return
data;
}
}
/// <summary>
/// 重写连接
/// </summary>
/// <returns></returns>
public
override
Task OnConnected()
{
Connected();
return
base
.OnConnected();
}
/// <summary>
/// 重新链接
/// </summary>
/// <returns></returns>
public
override
Task OnReconnected()
{
Connected();
return
base
.OnReconnected();
}
/// <summary>
/// 重写断开连接
/// </summary>
/// <param name="stopCalled"></param>
/// <returns></returns>
public
override
Task OnDisconnected(
bool
stopCalled)
{
User user =
null
;
List<Dictionary<
string
, User>> listUser;
OnLineUsers.TryGetValue(RoomId,
out
listUser);
if
(listUser !=
null
)
{
Dictionary<
string
, User> mapUser = listUser.Find(x =>
{
return
x.ContainsKey(Context.ConnectionId);
});
if
(mapUser !=
null
)
{
mapUser.TryGetValue(Context.ConnectionId,
out
user);
listUser.Remove(mapUser);
OnLineUsers.TryAdd(RoomId, listUser);
}
}
//当前离线移除房间
Groups.Remove(Context.ConnectionId, RoomId);
SendToGroup(0);
return
base
.OnDisconnected(stopCalled);
}
/// <summary>
/// 连接上的处理
/// </summary>
private
void
Connected()
{
//房间号
long
Id = cvt.ToLong(RoomId);
User user = userService.GetModel(UserId);
List<Dictionary<
string
, User>> listUser;
OnLineUsers.TryGetValue(RoomId,
out
listUser);
if
(listUser ==
null
)
{
listUser =
new
List<Dictionary<
string
, User>>();
Dictionary<
string
, User> mapUser =
new
Dictionary<
string
, User>();
mapUser.Add(Context.ConnectionId, user);
listUser.Add(mapUser);
OnLineUsers.TryAdd(RoomId, listUser);
Groups.Add(Context.ConnectionId, RoomId);
SendToGroup(1);
}
else
{
Dictionary<
string
, User> mapUser = listUser.Find(x =>
{
return
x.ContainsKey(Context.ConnectionId);
});
//不等于null 说明是重新连接的(重新连接不等于要退出后才连接)
if
(mapUser ==
null
)
{
mapUser =
new
Dictionary<
string
, User>();
mapUser.Add(Context.ConnectionId, user);
listUser.Add(mapUser);
OnLineUsers.TryAdd(RoomId, listUser);
Groups.Add(Context.ConnectionId, RoomId);
SendToGroup(1);
}
}
}
/// <summary>
/// 给房间内的所有用户发消息
/// </summary>
/// <param name="JoinOrExit">0=退出 1=加入</param>
private
void
SendToGroup(
int
JoinOrExit)
{
int
totalCount = 0;
List<Dictionary<
string
, User>> listUser;
OnLineUsers.TryGetValue(RoomId,
out
listUser);
if
(listUser !=
null
)
{
totalCount = listUser.Count;
}
resultMessage.data =
new
{
TotalCount = totalCount,
User = CallerUserInfo
};
resultMessage.ret = 0;
#region 特别注意不用 Clients.Group(RoomId) 可能是刚连接(连接后就可以使用)的时候,调用者加入到组会有延迟,所以分二条发送
if
(JoinOrExit == 1)
{
//给调用者(登录或退出者)发一条
Clients.Caller.JoinRoom(resultMessage);
//所在房间,要排除调用者
Clients.Group(RoomId, Context.ConnectionId).JoinRoom(resultMessage);
}
else
{
//给调用者(登录或退出者)发一条
Clients.Caller.ExitRoom(resultMessage);
//所在房间,要排除调用者
Clients.Group(RoomId, Context.ConnectionId).ExitRoom(resultMessage);
}
#endregion
}
#region 提供给JS事件调用的方法
/// <summary>
/// 发送【文本】消息给当前房间
/// </summary>
/// <param name="message"></param>
[HubMethodName(
"SendMsgText"
)]
public
void
SendMsgText(
string
message)
{
resultMessage.ret = 0;
resultMessage.data =
new
{
Content = message,
User = CallerUserInfo
};
Clients.Group(RoomId, Context.ConnectionId).ReceiveMsgText(resultMessage);
}
/// <summary>
/// 发送【图片】消息给当前房间
/// </summary>
/// <param name="message"></param>
[HubMethodName(
"SendMsgImg"
)]
public
void
SendMsgImg(
string
message)
{
resultMessage.ret = 0;
resultMessage.data =
new
{
Content = message,
User = CallerUserInfo
};
Clients.Group(RoomId, Context.ConnectionId).ReceiveMsgImg(resultMessage);
}
#endregion
}
}
- BaseHub:继承 Hub,封装常用方法和属性
using
Ilikexx.Framework;
using
Ilikexx.Framework.Web.Mvc;
using
Microsoft.AspNet.SignalR;
using
Newtonsoft.Json;
using
Newtonsoft.Json.Linq;
using
System;
using
System.Collections.Generic;
using
System.Linq;
using
System.Net.Http;
using
System.Security.Claims;
using
System.Text;
using
System.Web;
using
System.Web.Http;
using
System.Web.Mvc;
namespace
Brand.Api.Controllers
{
/// <summary>
///
/// </summary>
public
partial
class
BaseHub : Hub
{
/// <summary>
///
/// </summary>
public
ResultMessage resultMessage;
/// <summary>
///
/// </summary>
public
BaseHub()
{
resultMessage =
new
ResultMessage();
}
/// <summary>
/// 获取房间号
/// </summary>
public
string
RoomId
{
get
{
return
Context.QueryString[
"Id"
];
}
}
/// <summary>
/// 转为JSON串
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
protected
string
toJsonString(
object
obj)
{
return
JsonConvert.SerializeObject(obj, Newtonsoft.Json.Formatting.None);
}
/// <summary>
/// 获取所请求的用户ID
/// </summary>
protected
long
UserId
{
get
{
var identity = System.Web.HttpContext.Current.User.Identity
as
ClaimsIdentity;
//因为保存的是userid,当然也可以用其他方式
return
long
.Parse(identity.Name);
}
}
}
}
WEB端代码
客户端/移动端脚本
使用Requirejs引入Signalr脚本
require(["jquery"
,
"ui"
,
"lib/jquery.signalR-2.2.2"
], function ($, ui) {
});
- 开始使用:
1、$.hubConnection创建集线器连接,地址LiveVideoChat要跟服务端的一致,若有参数可以在qs中添加(token,房间号等)
2、createHubProxy创建服务代理LiveVideoChatService也要与服务端集线器类起名(服务名)一致
3、接收服务器方法,其中JoinRoom为服务端委托调用名称,data为回调的数据(目前都统一跟接口返回格式一致为JSON串,{ret:0,data:{},msg:""}) self.connHusService
.on(
"JoinRoom"
, function (data) {
});
4、调用服务器方法,其中SendMsgText为服务端的方法, $msg.val()为发送的数据。要注意若有多个参数要与服务端一致
self.connHusService.invoke("SendMsgText"
, $msg.val())
.done(function () {
var txtResponse = $(
"#txtResponse"
)
txtResponse.append(
"OKOK"
);
})
.fail(function (e) {
alert(e);
});
/****************************************************************************************************
注释:脚本模板示例
****************************************************************************************************/
require([
"jquery"
,
"ui"
,
"lib/jquery.signalR-2.2.2"
], function ($, ui) {
var self = {
$wrap: $(
"body"
),
tpl: '<div id=
"txtResponse"
></div>\
<div style=
"position:absolute;bottom:50px;width:100%"
><input type=
"text"
name=
"msg"
style=
"width:80%;border:1px solid"
/><a href=
"javasrcipt:void(0);"
class
=
"js_send"
style=
"background:green;color:white;padding:10px"
>发送</a></div>',
init: function () {
/// <summary>
/// 初始化
/// </summary>
var size = ui.utils.getViewPort();
self.$wrap.append(ui.render(self.tpl, {
play_width: size.width,
play_height: size.width / 4 * 3
}));
self.chatInit();
//绑定事件
self.bind();
},
bind: function () {
self.$wrap.on(
"click"
,
".js_send"
, function () {
var $msg = self.$wrap.find(
"[name=msg]"
);
//客户端代理调用服务端方法
self.connHusService.invoke(
"SendMsgText"
, $msg.val())
.done(function () {
var txtResponse = $(
"#txtResponse"
)
txtResponse.append(
"OKOK"
);
})
.fail(function (e) {
alert(e);
});
});
},
chatInit: function () {
//创建集线器连接
self.connHus = $.hubConnection(Ilikexx.apiUrl +
"LiveVideoChat"
);
//添加请求参数
self.connHus.qs = {
access_token: Ilikexx.token,
Id: 100
}
//开启日志记录
//self.connHus.logging = true;
// 获取代理
self.connHusService = self.connHus.createHubProxy(
"LiveVideoChatService"
);
// 设置state的值
// self.connHusService.state.ClientType = "HubNonAutoProxy";
// 客户端监听服务端发送的方法
self.connHusService
.on(
"JoinRoom"
, function (data) {
console.log(data)
var txtResponse = $(
"#txtResponse"
)
txtResponse.append(ui.render(
"<div>{{User.NickName}}进入了房间,总人数:{{TotalCount}}</div>"
, data.data));
})
.on(
"ExitRoom"
, function (data) {
console.log(data)
var txtResponse = $(
"#txtResponse"
)
txtResponse.append(ui.render(
"<div>{{User.NickName}}退出了房间,总人数:{{TotalCount}}</div>"
, data.data));
})
.on(
"ReceiveMsgText"
, function (data) {
console.log(data)
var txtResponse = $(
"#txtResponse"
)
txtResponse.append(ui.render(
'<div><img src="{{User.HeadUrl}}" />{{User.NickName}},说:{{Content}}</div>'
, data.data));
})
;
self.connHus.disconnected(function (e, conn) {
console.log(conn)
console.log(
'Wdisconnected。。。。。'
);
//几秒进行重连
// 开启连接
self.connHus.start()
.done(function () {
console.log(
"Hus已连接服务器OK"
);
})
.fail(function () {
console.log(
"Hus已连接服务器失败"
);
});
});
// 开启连接
self.connHus.start()
.done(function () {
console.log(
"Hus已连接服务器OK"
);
})
.fail(function () {
console.log(
"Hus已连接服务器失败"
);
});
},
render: function () {
/// <summary>
/// 渲染数据
/// </summary>
}
};
self.init();
});
--------------------- 作者:qq_964878912 来源:CSDN 原文:https://blog.csdn.net/qq_18798917/article/details/53897586 版权声明:本文为博主原创文章,转载请附上博文链接!
利用SignalR实现实时聊天的更多相关文章
- SignalR实现网页实时聊天功能
SignalR是利用html5 sokit方式实现网页的实时性,在客户端不支持html5的情况下通过轮询实现 实现原理是客户端发送的消息先去服务器,然后服务器根据需要将消息广播到需要接收信息的客户群. ...
- SignalR入门一、通过 SignalR 2 进行实时聊天
一:什么是signalR Asp.net SignalR是微软为实现实时通信的一个类库.一般情况下,signalR会使用JavaScript的长轮询(long polling)的方式来实现客户端和服务 ...
- 使用SignalR+Asp.net创建实时聊天应用程序
一.概述: 使用 ASP.NET 那么 SignalR 2 创建一个实时聊天应用程序.将 SignalR 添加 MVC 5 应用程序中,并创建聊天视图发送并显示消息. 在Demo中,将学习Signal ...
- 使用signalr实现网页和微信公众号实时聊天(上)
最近项目中需要实现客户在公众号中和客服(客服使用后台网站系统)进行实时聊天的功能.折腾了一段时间,实现了这个功能.现在将过程记录下,以便有相同需求的同行可以参考,也是自己做个总结.这篇是上,用手机编辑 ...
- SignalR代理对象异常:Uncaught TypeError: Cannot read property 'client' of undefined 推出的结论 SignalR 简单示例 通过三个DEMO学会SignalR的三种实现方式 SignalR推送框架两个项目永久连接通讯使用 SignalR 集线器简单实例2 用SignalR创建实时永久长连接异步网络应用程序
SignalR代理对象异常:Uncaught TypeError: Cannot read property 'client' of undefined 推出的结论 异常汇总:http://www ...
- Asp.net SignalR 让实时通讯变得如此简单
巡更项目中,需要发送实时消息,以及需要任务开始提醒,于是便有机会接触到SignalR,在使用过程中,发现用SignalR实现通信非常简单,下面我思明将从三个方面分享一下: 一.SignalR是什么 A ...
- 网页实时聊天之PHP实现websocket
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...
- MVC5中使用SignalR2.0实现实时聊天室
原文 MVC5中使用SignalR2.0实现实时聊天室 有时候需要浏览器和服务端保持实时的通讯(比如在线聊天),SignalR的出现让这一切变得非常简单.它能够让服务端向客户端实时的推送消息.如果用户 ...
- vue+websocket+express+mongodb实战项目(实时聊天)(二)
原项目地址:[ vue+websocket+express+mongodb实战项目(实时聊天)(一)][http://blog.csdn.net/blueblueskyhua/article/deta ...
随机推荐
- java.lang.NoClassDefFoundError: com/sun/tools/javac/processing/JavacProcessingEnvironment
最近项目用到了java程序动态编译java源文件,运行程序一直报错,提示错误如下: Can't initialize javac processor due to (most likely) a cl ...
- C# 操作 Excel 文件(.xls 或 .xlsx)
在.net中,常用的操作excel文件的方式,有三种: OLE DB的形式, 第三方框架NPOI, Office组件. 总结: 通过对比,在读取大数据量的excel文件,建议用OLE DB的形式,把e ...
- mac mysql 编码配置
mac mysql 编码配置 (mysql目录下没有my.cnf) 想要修改编码发现自己的/usr/local/mysql/support-files里面根本没有my.cnf 安装方式是去mysql官 ...
- 长春理工大学第十四届程序设计竞赛(重现赛)I.Fate Grand Order
链接:https://ac.nowcoder.com/acm/contest/912/I 题意: Fate Grand Order是型月社发行的角色扮演类手机游戏,是著名的氪金抽卡"垃圾&q ...
- bzoj3159决战 码农题 树剖套splay
最近沉迷码农题无法自拔 首先有一个暴力的想法:对于每个重链维护一个splay,需要翻转的连起来,翻转,接回去 然后发现这样没问题... 一条链只能跨log个重链,也就只有log个splay的子树参与重 ...
- hdu5709Claris Loves Painting主席树 奇妙的DFS序
先不考虑层数限制 一棵树上每个点有个颜色,问一棵子树的颜色数 感觉简单多了是吧 考虑每个点的贡献:自己到根的路径上的一个包含自己的连续段 观察最顶端的点的父亲: 它满足有了额外的同色孩子(咦) 这一条 ...
- ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面”
ExceptionHandlerMiddleware中间件如何呈现“定制化错误页面” DeveloperExceptionPageMiddleware中间件利用呈现出来的错误页面实现抛出异常和当前请求 ...
- node.js安装Oracledb指导文档
https://community.oracle.com/docs/DOC-931127
- webpack.config.js====插件html-webpack-plugin
1. 安装 cnpm install html-webpack-plugin --save-dev 2. webpack.config.js中使用 const htmlWebpackPlugin = ...
- 解决resteasy上传表单文件名乱码
Dubbo在2.6版本后合并了dubbox的resteasy代码后,可以支持rest风格的接口发布,但是在使用form表单上传文件的时候,获取的文件名称是乱码. 下面通过对源码分析一下原因,并提供一种 ...