最近用.net core3.0重构网站,老大想作个站内信功能,就是有些耗时的后台任务的结果须要推送给用户。一开始我想简单点,客户端每隔1分钟调用一下个人接口,看看是否是有新消息,有的话就告诉用户有新推送,但老大不干了,他就是要实时通讯,因而我只好上SignalR了。javascript
说干就干,首先去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,添加引用便可。vue
接下来注入SignalR,以下代码:java
//注入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分钟,对应的客户端就得设置npm
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,再不行就用轮询的方式,也就是我最开始想的那种办法。json
另外得说一下的是假如前端调用的话,他是须要测试的,这时候其实须要跨域访问,否则每次打包好放到服务器再测这个实时通讯的话有点麻烦。添加跨域的代码以下:后端
#if DEBUG //注入跨域 services.AddCors(option => option.AddPolicy("cors", policy => policy.AllowAnyHeader().AllowAnyMethod().AllowCredentials() .WithOrigins("http://localhost:8001", "http://localhost:8000", "http://localhost:8002"))); #endif
而后加上以下代码便可。api
#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必须和后端的配置保持一致,否则分分钟出现下面异常:
这是由于你没有在我规定的时间内向我发送“心跳包”,因此我认为你已经“阵亡”了,为了不没必要要的傻傻链接,我中止了链接。另外须要说的是重连机制,有多种重连机制,这里我选择每隔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; }