最近用.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(4);//服务端发保持连接请求到客户端间隔,默认15秒,改成2分钟,网页需跟着设置connection.serverTimeoutInMilliseconds = 24e4;即4分钟                options.KeepAliveInterval = TimeSpan.FromMinutes(2);            });

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

1
connection.keepAliveIntervalInMilliseconds = 12e4;
1
connection.serverTimeoutInMilliseconds = 24e4;<br>  注入了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 = 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><scriptsrc="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 = 24e4connection.keepAliveIntervalInMilliseconds = 12e4Vue.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实现实时通信相关推荐

  1. 兼容 .NET Core3.0, Natasha 框架实现 隔离域与热编译操作

    关于 Natasha    动态构建已经成为了封装者们的家常便饭,从现有的开发趋势来看,普通反射性能之低,会迫使开发者转向EMIT/表达式树等构建方式,但是无论是EMIT还是表达式树,都会依赖于反射的 ...

  2. 【A】兼容Core3.0后 Natasha 的隔离域与热编译操作。

    文章转载授权级别:A         预计阅读时间:15分钟 一.  2.0预览版本增加了哪些功能 大部分为底层的升级优化,例如: 引擎兼容 Core3.0 优化编译流程,增加编译前语法检测及日志,统 ...

  3. net应用程序中发生了未经处理的异常怎么办_介绍一些在.NET Core 3.0中引入的诊断改进工具...

    编者按:即使.NET Core3.1.5已经发布,在进行.NET Core的性能诊断时,我们有时依然不知该从何处下手,那这篇介绍.NET Core3.0中引入的诊断工具,或许能为我们提供参考. 在.N ...

  4. vs2019中如何创建qt项目_VS2019创建新项目居然没有.NET Core3.0的模板?

    今天是个值得欢喜的日子,因为VS2019在今天正式发布了.作为微软粉,我已经用了一段时间的VS2019 RC版本了.但是,今天有很多小伙伴在我的<ASP.NET Core 3.0 上的gRPC服 ...

  5. .NET Core3.0+ WebApi dynamic传参中带有ValueKind属性处理方法

    背景:在.NET Core3.0+框架下编写WebApi并且使用dynamic类型传参,后台接收到的参数中会包含一个ValueKind的属性,包含ValueKind属性会影响取参的便捷性 导致原因:. ...

  6. 在 Asp.NET MVC 中使用 SignalR 实现推送功能

    一,简介 Signal 是微软支持的一个运行在 Dot NET 平台上的 html websocket 框架.它出现的主要目的是实现服务器主动推送(Push)消息到客户端页面,这样客户端就不必重新发送 ...

  7. linux安装.net core3.0,树莓派4安装net core3.0环境

    树莓派4官方系统是32系统,所以需要安装arm32版本的net core sk和runtime 1,首先创建一个文件夹 dotnet-arm32 sudo mkdir dotnet arm32 2,下 ...

  8. .NET Core 3.0 中的新变化

    译者:楚人Leo 译文:http://www.cnblogs.com/leolion/p/10585834.html 原文:https://msdn.microsoft.com/en-us/magaz ...

  9. EF Core3.0+ 通过拦截器实现读写分离与SQL日志记录

    前言 本文主要是讲解EF Core3.0+ 通过拦截器实现读写分离与SQL日志记录 注意拦截器只有EF Core3.0+ 支持,2.1请考虑上下文工厂的形式实现. 说点题外话.. 一晃又大半年没更新技 ...

最新文章

  1. 首次公开!深度学习在知识图谱构建中的应用
  2. u-boot移植初步尝试-tiny4412
  3. 人工智能抢80万工人的饭碗,真的会失业吗?但是有新的转机
  4. JavaWeb学习总结(二)——Tomcat服务器学习和使用(一)
  5. 多个线程直接按对数据进行操作容易引发线程安全问题
  6. 统计学习方法第四章课后习题(转载+重新排版+自己解读)
  7. E. Jamie and Tree(树链剖分 + 线段树)
  8. AutoX披露无人车云代驾系统:夜晚远程操控车队也easy
  9. 关于树的前序遍历,中序遍历,后序遍历的相互转化(含代码实现)
  10. JS操作JSON数据交换
  11. DDoS攻击重大历史事件
  12. 中国大学MOOC 视频字幕获取及处理方法
  13. 【转载】最全的计算广告资料,广告算法工程师入门
  14. 宋浩概率论与数理统计-第一章-笔记
  15. JavaScript---BOM基础
  16. 计算机最快接口速度,实测:USB3.1究竟比USB3.0接口快多少?
  17. c语言检测网络连接,C++ 网络连通性检测的实现方法
  18. C语言中如何定义字符串?
  19. uni-app 点击图片放大预览
  20. python收益风险点图_使用python matploblib库绘制准确率,损失率折线图

热门文章

  1. C# static readonly 与 const 的区别
  2. 大厂高级前端面试题答案
  3. 如何在ABAP Netweaver和CloudFoundry里记录并查看日志 1
  4. UITableView的使用及代理方法
  5. ASP.NET Core中的依赖注入(4): 构造函数的选择与服务生命周期管理
  6. 你们是不是对QQ总在后台莫名其妙更新一些东西很反感
  7. 设计模式之Builder
  8. 可能是.NET领域性能最好的对象映射框架——Mapster
  9. 如何在 .NETCore 中修改 QueryString ?
  10. 使用自定义卫语句写出更简洁的代码