在.net core3.0中使用SignalR实现实时通信

  最近用.net core3.0重构网站,老大想做个站内信功能,就是有些耗时的后台任务的结果需要推送给用户。一开始我想简单点,客户端每隔1分钟调用一下我的接口,看看是不是有新消息,有的话就告诉用户有新推送,但老大不干了,他就是要实时通信,于是我只好上SignalR了。

  说干就干,首先去Nuget搜索640?wx_fmt=png

 

 

 但是只有Common是有3.0版本的,后来发现我需要的是Microsoft.AspNetCore.SignalR.Core,然而这个停更的状态?于是我一脸蒙蔽,捣鼓了一阵发现,原来.net core的SDK已经内置了Microsoft.AspNetCore.SignalR.Core,640?wx_fmt=png,右键项目,打开C:\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\3.0.0\ref\netcoreapp3.0 文件夹搜索SignalR,添加引用即可。

 

 

  接下来注入SignalR,如下代码:

640?wx_fmt=gif

            //注入SignalR实时通讯,默认用json传输
services.AddSignalR(options =>
{
//客户端发保持连接请求到服务端最长间隔,默认30秒,改成4分钟,网页需跟着设置connection.keepAliveIntervalInMilliseconds = 12e4;即2分钟
options.ClientTimeoutInterval = TimeSpan.FromMinutes(4);
//服务端发保持连接请求到客户端间隔,默认15秒,改成2分钟,网页需跟着设置connection.serverTimeoutInMilliseconds = 24e4;即4分钟
options.KeepAliveInterval = TimeSpan.FromMinutes(2);
});

640?wx_fmt=gif

 

  这个解释一下,SignalR默认是用Json传输的,但是还有另外一种更短小精悍的传输方式MessagePack,用这个的话性能会稍微高点,但是需要另外引入一个DLL,JAVA端调用的话也是暂时不支持的。但是我其实是不需要这点性能的,所以我就用默认的json好了。另外有个概念,就是实时通信,其实是需要发“心跳包”的,就是双方都需要确定对方还在不在,若挂掉的话我好重连或者把你干掉啊,所以就有了两个参数,一个是发心跳包的间隔时间,另一个就是等待对方心跳包的最长等待时间。一般等待的时间设置成发心跳包的间隔时间的两倍即可,默认KeepAliveInterval是15秒,ClientTimeoutInterval是30秒,我觉得不需要这么频繁的确认对方“死掉”了没,所以我改成2分钟发一次心跳包,最长等待对方的心跳包时间是4分钟,对应的客户端就得设置

1
connection.keepAliveIntervalInMilliseconds = 12e4;
1
connection.serverTimeoutInMilliseconds = 24e4;<br>  注入了SignalR之后,接下来需要使用WebSocket和SignalR,对应代码如下:

640?wx_fmt=gif

            //添加WebSocket支持,SignalR优先使用WebSocket传输
app.UseWebSockets();
//app.UseWebSockets(new WebSocketOptions
//{
// //发送保持连接请求的时间间隔,默认2分钟
// KeepAliveInterval = TimeSpan.FromMinutes(2)
//});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<MessageHub>("/msg");
});

640?wx_fmt=gif

这里提醒一下,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 = 1, int pageSize = 10)
{
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必须和后端的配置保持一致,不然分分钟出现下面异常:

640?wx_fmt=png

 

 这是因为你没有在我规定的时间内向我发送“心跳包”,所以我认为你已经“阵亡”了,为了避免不必要的傻傻连接,我停止了连接。另外需要说的是重连机制,有多种重连机制,这里我选择每隔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

640?wx_fmt=gif

接下来就可以用this.$connection 愉快的使用了。

  到这里或许你觉得大功告成了,若没看浏览器的控制台输出,我也是这么认为的,然后控制台出现了红色!:

640?wx_fmt=png

 

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

 640?wx_fmt=png

 

   这个是咋回事呢,咋就用不了WebSocket呢,我的是谷歌浏览器呀,肯定是支持WebSocket的,咋办,只好去群里讨教了,后来大神告诉我,需要在ngnix配置如下红框内的:

640?wx_fmt=png

 

 真是一语惊醒梦中人,我加上如下代码就正常了:

        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;
        }


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/313181.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

活动最后72小时:购书优惠劵,折后再折,赶紧来抢啊

1024程序员节当当网计算机图书每满100减50&#xff01;满200减100&#xff01;满300-150&#xff01;机械工业出版社华章公司联合当当网特意为【DotNET技术圈】用户申请了一批可与满减叠加使用的“满200减30”的图书优惠码&#xff0c;优惠码使用后相当于&#xff1a;400减230 …

C# 8 新特性 - using 声明

using语句块 尽管.NET Core运行时有垃圾收集器&#xff08;GC&#xff09;来负责内存清理工作&#xff0c;但是我们还是要自己确保当非托管资源不再使用的时候应该被清理掉。以前针对实现了IDisposable接口的对象&#xff0c;我们经常会使用using 语句块来这样做&#xff1a; 这…

.Net Core3.0依赖注入DI

构建ASP.NET Core应用程序的时候&#xff0c;依赖注入已成为了.NET Core的核心&#xff0c;这篇文章&#xff0c;我们理一理依赖注入的使用方法。不使用依赖注入首先&#xff0c;我们创建一个ASP.NET Core Mvc项目&#xff0c;定义个表达的爱服务接口&#xff0c;中国小伙类实现…

.Net轻量状态机Stateless

很多业务系统开发中&#xff0c;不可避免的会出现状态变化&#xff0c;通常采用的情形可能是使用工作流去完成&#xff0c;但是对于简单场景下&#xff0c;用工作流有点大财小用感觉&#xff0c;比如订单业务中&#xff0c;订单状态的变更&#xff0c;涉及到的状态量不是很多&a…

Asp.net Core全局异常监控和记录日志

前言系统异常监控可以说是重中之重&#xff0c;系统不可能一直运行良好&#xff0c;开发和运维也不可能24小时盯着系统&#xff0c;系统抛异常后我们应当在第一时间收到异常信息。在Asp.net Core里我使用拦截器和中间件两种方式来监控异常。全局异常监控的数据最好还是写入数据…

SiteServer CMS 新版本 V6.13(2019年11月1日发布)

欢迎来到 SiteServer CMS V6.13 版本&#xff0c;经过两个月的连续迭代开发&#xff0c;V6.13版本新增了几项重要功能&#xff0c;我们希望你会喜欢&#xff0c;一些关键的亮点包括&#xff1a;。新增功能及BUG 修复经过两个月的连续迭代开发&#xff0c;V6.13 版本新增了部分功…

CUDA的global内存访问的问题

http://blog.csdn.net/OpenHero/article/details/3520578 关于CUDA的global内存访问的问题&#xff0c;怎么是访问的冲突&#xff0c;怎样才能更好的访问内存&#xff0c;达到更高的速度。下面先看几张图&#xff0c;这些图都是CUDA编程手册上的图&#xff0c;然后分别对这些…

C# 8 新特性 - 异步流 Asynchronous Streams

异步流 Asynchronous Streams例子 这是一个很简单的控制台程序。它有一个NumberFactory&#xff0c;它可以根据传递的参数来产生一串数字&#xff08;IEnumerable<int>&#xff09;。然后在这个程序中把每个数字都打印出来&#xff0c;同时在前边显示出当前的线程ID。 这…

__syncthreads()

http://www.cnblogs.com/dwdxdy/p/3215136.html __syncthreads()是cuda的内建函数&#xff0c;用于块内线程通信. __syncthreads() is you garden variety thread barrier. Any thread reaching the barrier waits until all of the other threads in that block also reach i…

互联网50周年!这有它的一张“出生证明”

2019 年 10 月 29 日是互联网的 50 周年&#xff0c;50 年前(1969 年 10 月 29 日)&#xff0c;加州大学洛杉矶分校的计算机将一个只有两个字母(LO)的数据包发送到斯坦福研究所的计算机上&#xff0c;这是互联网史上的第一个数据包&#xff0c;从此开启互联网时代的第一步。 当…

Eltwise_layer简介

http://www.voidcn.com/blog/thy_2014/article/p-6117416.html common_layer&#xff1a; ArgMaxLayer类&#xff1b; ConcatLayer类&#xff1a; EltwiseLayer类&#xff1b; FlattenLayer类&#xff1b; InnerProductLayer类&#xff1b; MVNLayer类&#xff1b; SilenceLaye…

PowerBI 秒级实时大屏展示方案 全面助力双十一

双十一来了&#xff0c;你准备好了吗&#xff1f;不管你是否准备完毕&#xff0c;我们带来了全网首发的 PowerBI 秒级实时大屏展示方案&#xff0c;你可以直接用来展示双十一的实时状况。我们一步步来说明这个套件模板教程。真实效果功能如下&#xff1a;全实时展示 双十一 当天…

优化 .net core 应用的 dockerfile

优化 .net core 应用的 dockerfileIntro在给 .net core 应用的写 dockerfile 的时候一直有个苦恼&#xff0c;就是如果有很多个项目&#xff0c;在 dockerfile 里写起来就会很繁琐&#xff0c;有很多项目文件要 copy&#xff0c;dockerfile 还不支持直接批量复制项目文件&#…

C# 8 新特性 - 静态本地方法

从C# 8 开始&#xff0c;本地方法就可以是静态的了。 与其他的本地方法不同&#xff0c;静态的本地方法无法捕获任何本地状态量。 直接看例子&#xff1a; 这段代码里有两个本地方法&#xff0c;他们分别对实例的一个字段和方法里的一个本地变量进行了修改操作&#xff0c;也就…

​.NET手撸2048小游戏

前言2048是一款益智小游戏&#xff0c;得益于其规则简单&#xff0c;又和 2的倍数有关&#xff0c;因此广为人知&#xff0c;特别是广受程序员的喜爱。本文将再次使用我自制的“准游戏引擎” FlysEngine&#xff0c;从空白窗口开始&#xff0c;演示如何“手撸” 2048小游戏&…

自行实现高性能MVC

wcf虽然功能多、扩展性强但是也面临配置忒多&#xff0c;而且restful的功能相当怪异&#xff0c;并且目前没法移植。asp.net core虽然支持webapi&#xff0c;但是功能也相对繁多、配置复杂。就没有一个能让码农们安安心心的写webapi&#xff0c;无需考虑性能、配置、甚至根据问…

caffe matio问题

http://blog.csdn.net/houqiqi/article/details/46469981 注&#xff1a;如果指令行模式实在解决不了/lib/libcaffe.so: undefined reference to Mat_VarReadDataLinear问题&#xff0c;可以尝试在QT下进行训练和测试。 1&#xff0c; 下载matio(http://sourceforge.NET/pro…

技术管理者怎样跳出“泥潭”

近几年面试了不少新人&#xff0c;当问到职业规划时&#xff0c;大多都会说先积累技术&#xff0c;然后往架构师的方向发展。这可能是技术人的一个特质&#xff0c;喜欢跟机器相处&#xff0c;沉浸在代码之中&#xff0c;而不喜欢跟人打交道。现实的情况是&#xff0c;一些中小…

你或许以为你不需要领域驱动设计

作者&#xff1a;邹溪源&#xff0c;长沙资深互联网从业者&#xff0c;架构师社区合伙人&#xff01;一犹记得刚刚参加工作时&#xff0c;是地图厂商四维图新集团旗下的一家子公司&#xff0c;主要从事规划测绘相关软件研发的公司。当时我的项目是为勘测设计院提供相对应的应用…

redis为什么这么火该怎么用

最近一些人在介绍方案时&#xff0c;经常会出现redis这个词&#xff0c;于是很多小伙伴百度完redis也就觉得它是一个缓存&#xff0c;然后项目里面把数据丢进去完事&#xff0c;甚至有例如将实体属性拆分塞进redis hash里面的奇怪用法等等&#xff01;原因是什么呢&#xff1f;…