在.net core3.0中使用SignalR实现实时通信
最近用.net core3.0重构网站,老大想做个站内信功能,就是有些耗时的后台任务的结果需要推送给用户。一开始我想简单点,客户端每隔1分钟调用一下我的接口,看看是不是有新消息,有的话就告诉用户有新推送,但老大不干了,他就是要实时通信,于是我只好上SignalR了。
说干就干,首先去Nuget搜索
但是只有Common是有3.0版本的,后来发现我需要的是Microsoft.AspNetCore.SignalR.Core,然而这个停更的状态?于是我一脸蒙蔽,捣鼓了一阵发现,原来.net core的SDK已经内置了Microsoft.AspNetCore.SignalR.Core,
,右键项目,打开C:\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\3.0.0\ref\netcoreapp3.0 文件夹搜索SignalR,添加引用即可。
接下来注入SignalR,如下代码:
//注入SignalR实时通讯,默认用json传输
services.AddSignalR(options =>
{
//客户端发保持连接请求到服务端最长间隔,默认30秒,改成4分钟,网页需跟着设置connection.keepAliveIntervalInMilliseconds = 12e4;即2分钟
options.ClientTimeoutInterval = TimeSpan.FromMinutes();
//服务端发保持连接请求到客户端间隔,默认15秒,改成2分钟,网页需跟着设置connection.serverTimeoutInMilliseconds = 24e4;即4分钟
options.KeepAliveInterval = TimeSpan.FromMinutes();
});
这个解释一下,SignalR默认是用Json传输的,但是还有另外一种更短小精悍的传输方式MessagePack,用这个的话性能会稍微高点,但是需要另外引入一个DLL,JAVA端调用的话也是暂时不支持的。但是我其实是不需要这点性能的,所以我就用默认的json好了。另外有个概念,就是实时通信,其实是需要发“心跳包”的,就是双方都需要确定对方还在不在,若挂掉的话我好重连或者把你干掉啊,所以就有了两个参数,一个是发心跳包的间隔时间,另一个就是等待对方心跳包的最长等待时间。一般等待的时间设置成发心跳包的间隔时间的两倍即可,默认KeepAliveInterval是15秒,ClientTimeoutInterval是30秒,我觉得不需要这么频繁的确认对方“死掉”了没,所以我改成2分钟发一次心跳包,最长等待对方的心跳包时间是4分钟,对应的客户端就得设置
connection.keepAliveIntervalInMilliseconds = 12e4;
connection.serverTimeoutInMilliseconds = 24e4;
注入了SignalR之后,接下来需要使用WebSocket和SignalR,对应代码如下:
//添加WebSocket支持,SignalR优先使用WebSocket传输
app.UseWebSockets();
//app.UseWebSockets(new WebSocketOptions
//{
// //发送保持连接请求的时间间隔,默认2分钟
// KeepAliveInterval = TimeSpan.FromMinutes(2)
//});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<MessageHub>("/msg");
});
这里提醒一下,WebSocket只是实现SignalR实时通信的一种手段,若这个走不通的情况下,他还可以降级使用SSE,再不行就用轮询的方式,也就是我最开始想的那种办法。
另外得说一下的是假如前端调用的话,他是需要测试的,这时候其实需要跨域访问,不然每次打包好放到服务器再测这个实时通信的话有点麻烦。添加跨域的代码如下:
#if DEBUG
//注入跨域
services.AddCors(option => option.AddPolicy("cors",
policy => policy.AllowAnyHeader().AllowAnyMethod().AllowCredentials()
.WithOrigins("http://localhost:8001", "http://localhost:8000", "http://localhost:8002")));
#endif
然后加上如下代码即可。
#if DEBUG
//允许跨域,不支持向所有域名开放了,会有错误提示
app.UseCors("cors");
#endif
好了,可以开始动工了。创建一个MessageHub:
public class MessageHub : Hub
{
private readonly IUidClient _uidClient; public MessageHub(IUidClient uidClient)
{
_uidClient = uidClient;
} public override async Task OnConnectedAsync()
{
var user = await _uidClient.GetLoginUser();
//将同一个人的连接ID绑定到同一个分组,推送时就推送给这个分组
await Groups.AddToGroupAsync(Context.ConnectionId, user.Account);
}
}
由于每次连接的连接ID不同,所以最好把他和登录用户的用户ID绑定起来,推送时直接推给绑定的这个用户ID即可,做法可以直接把连接ID和登录用户ID绑定起来,把这个用户ID作为一个分组ID。
然后使用时就如下:
public class MessageService : BaseService<Message, ObjectId>, IMessageService
{
private readonly IUidClient _uidClient;
private readonly IHubContext<MessageHub> _messageHub; public MessageService(IMessageRepository repository, IUidClient uidClient, IHubContext<MessageHub> messageHub) : base(repository)
{
_uidClient = uidClient;
_messageHub = messageHub;
} /// <summary>
/// 添加并推送站内信
/// </summary>
/// <param name="dto"></param>
/// <returns></returns>
public async Task Add(MessageDTO dto)
{
var now = DateTime.Now; var log = new Message
{
Id = ObjectId.GenerateNewId(now),
CreateTime = now,
Name = dto.Name,
Detail = dto.Detail,
ToUser = dto.ToUser,
Type = dto.Type
}; var push = new PushMessageDTO
{
Id = log.Id.ToString(),
Name = log.Name,
Detail = log.Detail,
Type = log.Type,
ToUser = log.ToUser,
CreateTime = now
}; await Repository.Insert(log);
//推送站内信
await _messageHub.Clients.Groups(dto.ToUser).SendAsync("newmsg", push);
//推送未读条数
await SendUnreadCount(dto.ToUser); if (dto.PushCorpWeixin)
{
const string content = @"<font color='blue'>{0}</font>
<font color='comment'>{1}</font>
系统:**CMS**
站内信ID:<font color='info'>{2}</font>
详情:<font color='comment'>{3}</font>"; //把站内信推送到企业微信
await _uidClient.SendMarkdown(new CorpSendTextDto
{
touser = dto.ToUser,
content = string.Format(content, dto.Name, now, log.Id, dto.Detail)
});
}
} /// <summary>
/// 获取本人的站内信列表
/// </summary>
/// <param name="name">标题</param>
/// <param name="detail">详情</param>
/// <param name="unread">只显示未读</param>
/// <param name="type">类型</param>
/// <param name="createStart">创建起始时间</param>
/// <param name="createEnd">创建结束时间</param>
/// <param name="pageIndex">当前页</param>
/// <param name="pageSize">每页个数</param>
/// <returns></returns>
public async Task<PagedData<PushMessageDTO>> GetMyMessage(string name, string detail, bool unread = false, EnumMessageType? type = null, DateTime? createStart = null, DateTime? createEnd = null, int pageIndex = , int pageSize = )
{
var user = await _uidClient.GetLoginUser();
Expression<Func<Message, bool>> exp = o => o.ToUser == user.Account; if (unread)
{
exp = exp.And(o => o.ReadTime == null);
} if (!string.IsNullOrEmpty(name))
{
exp = exp.And(o => o.Name.Contains(name));
} if (!string.IsNullOrEmpty(detail))
{
exp = exp.And(o => o.Detail.Contains(detail));
} if (type != null)
{
exp = exp.And(o => o.Type == type.Value);
} if (createStart != null)
{
exp.And(o => o.CreateTime >= createStart.Value);
} if (createEnd != null)
{
exp.And(o => o.CreateTime < createEnd.Value);
} return await Repository.FindPageObjectList(exp, o => o.Id, true, pageIndex,
pageSize, o => new PushMessageDTO
{
Id = o.Id.ToString(),
CreateTime = o.CreateTime,
Detail = o.Detail,
Name = o.Name,
ToUser = o.ToUser,
Type = o.Type,
ReadTime = o.ReadTime
});
} /// <summary>
/// 设置已读
/// </summary>
/// <param name="id">站内信ID</param>
/// <returns></returns>
public async Task Read(ObjectId id)
{
var msg = await Repository.First(id); if (msg == null)
{
throw new CmsException(EnumStatusCode.ArgumentOutOfRange, "不存在此站内信");
} if (msg.ReadTime != null)
{
//已读的不再更新读取时间
return;
} msg.ReadTime = DateTime.Now;
await Repository.Update(msg, "ReadTime");
await SendUnreadCount(msg.ToUser);
} /// <summary>
/// 设置本人全部已读
/// </summary>
/// <returns></returns>
public async Task ReadAll()
{
var user = await _uidClient.GetLoginUser(); await Repository.UpdateMany(o => o.ToUser == user.Account && o.ReadTime == null, o => new Message
{
ReadTime = DateTime.Now
}); await SendUnreadCount(user.Account);
} /// <summary>
/// 获取本人未读条数
/// </summary>
/// <returns></returns>
public async Task<int> GetUnreadCount()
{
var user = await _uidClient.GetLoginUser();
return await Repository.Count(o => o.ToUser == user.Account && o.ReadTime == null);
} /// <summary>
/// 推送未读数到前端
/// </summary>
/// <returns></returns>
private async Task SendUnreadCount(string account)
{
var count = await Repository.Count(o => o.ToUser == account && o.ReadTime == null);
await _messageHub.Clients.Groups(account).SendAsync("unread", count);
}
}
IHubContext<MessageHub>可以直接注入并且使用,然后调用_messageHub.Clients.Groups(account).SendAsync即可推送。接下来就简单了,在MessageController里把这些接口暴露出去,通过HTTP请求添加站内信,或者直接内部调用添加站内信接口,就可以添加站内信并且推送给前端页面了,当然除了站内信,我们还可以做得更多,比如比较重要的顺便也推送到第三方app,比如企业微信或钉钉,这样你还会怕错过重要信息?
接下来到了客户端了,客户端只说网页端的,代码如下:
<body>
<div class="container">
<input type="button" id="getValues" value="Send" />
<ul id="discussion"></ul>
</div>
<script
src="https://cdn.jsdelivr.net/npm/@microsoft/signalr@3.0.0-preview7.19365.7/dist/browser/signalr.min.js"></script> <script type="text/javascript">
var connection = new signalR.HubConnectionBuilder()
.withUrl("/message")
.build();
connection.serverTimeoutInMilliseconds = 24e4;
connection.keepAliveIntervalInMilliseconds = 12e4; var button = document.getElementById("getValues"); connection.on('newmsg', (value) => {
var liElement = document.createElement('li');
liElement.innerHTML = 'Someone caled a controller method with value: ' + value;
document.getElementById('discussion').appendChild(liElement);
}); button.addEventListener("click", event => {
fetch("api/message/sendtest")
.then(function (data) {
console.log(data);
})
.catch(function (error) {
console.log(err);
}); }); var connection = new signalR.HubConnectionBuilder()
.withUrl("/message")
.build(); connection.on('newmsg', (value) => {
console.log(value);
}); connection.start();
</script>
</body>
上面的代码还是需要解释下的,serverTimeoutInMilliseconds和keepAliveIntervalInMilliseconds必须和后端的配置保持一致,不然分分钟出现下面异常:

这是因为你没有在我规定的时间内向我发送“心跳包”,所以我认为你已经“阵亡”了,为了避免不必要的傻傻连接,我停止了连接。另外需要说的是重连机制,有多种重连机制,这里我选择每隔10秒重连一次,因为我觉得需要重连,那一般是因为服务器挂了,既然挂了,那我每隔10秒重连也是不会浪费服务器性能的,浪费的是浏览器的性能,客户端的就算了,忽略不计。自动重连代码如下:
async function start() {
try {
await connection.start();
console.log(connection)
} catch (err) {
console.log(err);
setTimeout(() => start(), 1e4);
}
};
connection.onclose(async () => {
await start();
});
start();
当然还有其他很多重连的方案,可以去官网看看。
当然若你的客户端是用vue写的话,写法会有些不同,如下:
import '../../public/signalR.js'
const wsUrl = process.env.NODE_ENV === 'production' ? '/msg' :'http://xxx.net/msg'
var connection = new signalR.HubConnectionBuilder().withUrl(wsUrl).build()
connection.serverTimeoutInMilliseconds = 24e4
connection.keepAliveIntervalInMilliseconds = 12e4
Vue.prototype.$connection = connection
接下来就可以用this.$connection 愉快的使用了。
到这里或许你觉得大功告成了,若没看浏览器的控制台输出,我也是这么认为的,然后控制台出现了红色!:

虽然出现了这个红色,但是依然可以正常使用,只是降级了,不使用WebSocket了,心跳包变成了一个个的post请求,如下图:

这个是咋回事呢,咋就用不了WebSocket呢,我的是谷歌浏览器呀,肯定是支持WebSocket的,咋办,只好去群里讨教了,后来大神告诉我,需要在ngnix配置一下下面的就可以了:
location /msg {
proxy_connect_timeout 300;
proxy_read_timeout 300;
proxy_send_timeout 300;
proxy_pass http://xxx.net;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
在.net core3.0中使用SignalR实现实时通信的更多相关文章
- Core3.0中Swagger使用JWT
前言 学习ASP.NETCore,原链接 https://www.cnblogs.com/laozhang-is-phi/p/9511869.html 原教程是Core2.2,后期也升级到了Core3 ...
- ASP.NET Core3.0 中的运行时编译
运行时编译 通过 Razor 文件的运行时编译补充生成时编译. 当 .cshtml 文件的内容发生更改时,ASP.NET Core MVC 将重新编译 Razor 文件 . 通过 Razor 文件的运 ...
- 兼容 .NET Core3.0, Natasha 框架实现 隔离域与热编译操作
关于 Natasha 动态构建已经成为了封装者们的家常便饭,从现有的开发趋势来看,普通反射性能之低,会迫使开发者转向EMIT/表达式树等构建方式,但是无论是EMIT还是表达式树,都会依赖于反射的 ...
- 在Asp.Net Core中配置使用MarkDown富文本编辑器实现图片上传和截图上传(开源代码.net core3.0)
我们的富文本编辑器不能没有图片上传尤其是截图上传,下面我来教大家怎么实现MarkDown富文本编辑器截图上传和图片上传. 1.配置编辑器到html页 <div id="test-edi ...
- 在Asp.Net或.Net Core中配置使用MarkDown富文本编辑器有开源模板代码(代码是.net core3.0版本)
研究如何使用Markdown你们可能要花好几天才能搞定,但是看我的文章或者下载了源码,你搞定一般在10分钟之内.我先给各位介绍下它: Markdown 是一种轻量级标记语言,它允许人们使用易读易写的纯 ...
- 关于.net core 中的signalR组件的使用
SignalR是为了提供更方便的web交互响应式到推送式的解决方案.有了它之后可以实现客户端直接调用服务端的方法并且获得返回值 (客户端可以是各种平台,目前SignalR支持的语言版本有C#.java ...
- 我用VS2012在Nuget中安装Signalr之后报错
我用VS2012在Nuget中安装Signalr之后报错 “/”应用程序中的服务器错误. The following errors occurred while attempting to load ...
- [渣译文] SignalR 2.0 系列:SignalR的服务器广播
英文渣水平,大伙凑合着看吧…… 这是微软官方SignalR 2.0教程Getting Started with ASP.NET SignalR 2.0系列的翻译,这里是第八篇:SignalR的服务器广 ...
- 《ASP.NET SignalR系列》第五课 在MVC中使用SignalR
接着上一篇:<ASP.NET SignalR系列>第四课 SignalR自托管(不用IIS) 一.概述 本教程主要阐释了如何在MVC下使用ASP.NET SignalR. 添加Signal ...
随机推荐
- .Net Core WebApi(二)在Windows服务器上部署
上一篇学习到了如何简单的创建.Net Core Api和Swagger使用,既然写了接口,那么就需要部署到服务器上才能够正式使用.服务器主要用到了两种系统,Windows和Linux,.Net和Win ...
- 痞子衡嵌入式:飞思卡尔i.MX RTyyyy系列MCU硬件那些事(1)- 官方EVK简介
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是飞思卡尔i.MX RTyyyy系列MCU的配套EVK板. 半导体设计厂商发布任何一块MCU芯片新品,一般都会同步推出基于这款MCU的配套 ...
- java、python、MYSQL环境安装
JAVA的环境变量:变量值:%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin; 变量名:JAVA_HOME python的环境变量:变量值: %PY_HOME ...
- 挖穿各大SRC的短信轰炸
今天给大家分享一个短信轰炸绕过的姿势,大疆.百度.腾讯等等src都有用此方法绕过的案例. 给大家看一下 这里就不给大家截图了,在src中提交的截图都没有打码,这里放出来不太方便. 这里就只举出大疆的例 ...
- JDBC对Mysql utf8mb4字符集的处理
写在前面 在开发微信小程序的时候,评论服务模块希望添加上emoji表情,但是emoji表情是4个字节长度的,所以需要进行设置 当前项目是JAVA编写, 使用JDBC连接操作数据库, 如下针对的JDBC ...
- Redis数据库之数据基本管理操作
了解并掌握各种数据类型的命令操作方式,以及各种数据类型值的操作方式.同时,熟练记忆列表.哈希.集合和有序集合等数据类型的常用操作命令.能根据指令格式完成相应的指令操作. ①string数据类型的练习 ...
- Spring框架学习笔记(3)——SpringMVC框架
SpringMVC框架是基于Spring框架,可以让我们更为方便的进行Web的开发,实现前后端分离 思路和原理 我们之前仿照SpringMVC定义了一个自定义MVC框架,两者的思路其实都是一样的. 建 ...
- layui内部定义的function,外部调用时候,提示某函数未定义现象解决方案
1.引入layui.all.js文件 <script type="text/javascript" src="${pageContext.request.conte ...
- poi实现excel的导入导出功能
Java使用poi实现excel的导入导出功能: 工具类ExcelUtil,用于解析和初始化excel的数据:代码如下 package com.raycloud.kmmp.item.service.u ...
- 前端get和post那些事
首先,简单介绍下,get和post请求方法,综合以往笔记,现整理如下: 一.HTTP请求比较: 两种在客户端和服务器端进行请求-响应的方法是:GET和POST. GET - 从指定的资源请求数据 PO ...