在.net core3.0中使用SignalR实现实时通信
最近用.net core3.0重構(gòu)網(wǎng)站,老大想做個(gè)站內(nèi)信功能,就是有些耗時(shí)的后臺(tái)任務(wù)的結(jié)果需要推送給用戶。一開(kāi)始我想簡(jiǎn)單點(diǎn),客戶端每隔1分鐘調(diào)用一下我的接口,看看是不是有新消息,有的話就告訴用戶有新推送,但老大不干了,他就是要實(shí)時(shí)通信,于是我只好上SignalR了。
說(shuō)干就干,首先去Nuget搜索
?
?
?但是只有Common是有3.0版本的,后來(lái)發(fā)現(xiàn)我需要的是Microsoft.AspNetCore.SignalR.Core,然而這個(gè)停更的狀態(tài)?于是我一臉蒙蔽,搗鼓了一陣發(fā)現(xiàn),原來(lái).net core的SDK已經(jīng)內(nèi)置了Microsoft.AspNetCore.SignalR.Core,,右鍵項(xiàng)目,打開(kāi)C:\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\3.0.0\ref\netcoreapp3.0 文件夾搜索SignalR,添加引用即可。
?
?
接下來(lái)注入SignalR,如下代碼:
//注入SignalR實(shí)時(shí)通訊,默認(rèn)用json傳輸
services.AddSignalR(options =>
{
//客戶端發(fā)保持連接請(qǐng)求到服務(wù)端最長(zhǎng)間隔,默認(rèn)30秒,改成4分鐘,網(wǎng)頁(yè)需跟著設(shè)置connection.keepAliveIntervalInMilliseconds = 12e4;即2分鐘
options.ClientTimeoutInterval = TimeSpan.FromMinutes(4);
//服務(wù)端發(fā)保持連接請(qǐng)求到客戶端間隔,默認(rèn)15秒,改成2分鐘,網(wǎng)頁(yè)需跟著設(shè)置connection.serverTimeoutInMilliseconds = 24e4;即4分鐘
options.KeepAliveInterval = TimeSpan.FromMinutes(2);
});
?
這個(gè)解釋一下,SignalR默認(rèn)是用Json傳輸?shù)?#xff0c;但是還有另外一種更短小精悍的傳輸方式MessagePack,用這個(gè)的話性能會(huì)稍微高點(diǎn),但是需要另外引入一個(gè)DLL,JAVA端調(diào)用的話也是暫時(shí)不支持的。但是我其實(shí)是不需要這點(diǎn)性能的,所以我就用默認(rèn)的json好了。另外有個(gè)概念,就是實(shí)時(shí)通信,其實(shí)是需要發(fā)“心跳包”的,就是雙方都需要確定對(duì)方還在不在,若掛掉的話我好重連或者把你干掉啊,所以就有了兩個(gè)參數(shù),一個(gè)是發(fā)心跳包的間隔時(shí)間,另一個(gè)就是等待對(duì)方心跳包的最長(zhǎng)等待時(shí)間。一般等待的時(shí)間設(shè)置成發(fā)心跳包的間隔時(shí)間的兩倍即可,默認(rèn)KeepAliveInterval是15秒,ClientTimeoutInterval是30秒,我覺(jué)得不需要這么頻繁的確認(rèn)對(duì)方“死掉”了沒(méi),所以我改成2分鐘發(fā)一次心跳包,最長(zhǎng)等待對(duì)方的心跳包時(shí)間是4分鐘,對(duì)應(yīng)的客戶端就得設(shè)置
| 1 | connection.keepAliveIntervalInMilliseconds = 12e4; |
| 1 | connection.serverTimeoutInMilliseconds = 24e4;<br> 注入了SignalR之后,接下來(lái)需要使用WebSocket和SignalR,對(duì)應(yīng)代碼如下: |
//添加WebSocket支持,SignalR優(yōu)先使用WebSocket傳輸
app.UseWebSockets();
//app.UseWebSockets(new WebSocketOptions
//{
// //發(fā)送保持連接請(qǐng)求的時(shí)間間隔,默認(rèn)2分鐘
// KeepAliveInterval = TimeSpan.FromMinutes(2)
//});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<MessageHub>("/msg");
});
這里提醒一下,WebSocket只是實(shí)現(xiàn)SignalR實(shí)時(shí)通信的一種手段,若這個(gè)走不通的情況下,他還可以降級(jí)使用SSE,再不行就用輪詢的方式,也就是我最開(kāi)始想的那種辦法。
另外得說(shuō)一下的是假如前端調(diào)用的話,他是需要測(cè)試的,這時(shí)候其實(shí)需要跨域訪問(wèn),不然每次打包好放到服務(wù)器再測(cè)這個(gè)實(shí)時(shí)通信的話有點(diǎn)麻煩。添加跨域的代碼如下:
#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//允許跨域,不支持向所有域名開(kāi)放了,會(huì)有錯(cuò)誤提示
app.UseCors("cors");
#endif
好了,可以開(kāi)始動(dòng)工了。創(chuàng)建一個(gè)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();
//將同一個(gè)人的連接ID綁定到同一個(gè)分組,推送時(shí)就推送給這個(gè)分組
await Groups.AddToGroupAsync(Context.ConnectionId, user.Account);
}
}
由于每次連接的連接ID不同,所以最好把他和登錄用戶的用戶ID綁定起來(lái),推送時(shí)直接推給綁定的這個(gè)用戶ID即可,做法可以直接把連接ID和登錄用戶ID綁定起來(lái),把這個(gè)用戶ID作為一個(gè)分組ID。
然后使用時(shí)就如下:
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>
/// 添加并推送站內(nèi)信
/// </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);
//推送站內(nèi)信
await _messageHub.Clients.Groups(dto.ToUser).SendAsync("newmsg", push);
//推送未讀條數(shù)
await SendUnreadCount(dto.ToUser);
if (dto.PushCorpWeixin)
{
const string content = @"<font color='blue'>{0}</font>
<font color='comment'>{1}</font>
系統(tǒng):**CMS**
站內(nèi)信ID:<font color='info'>{2}</font>
詳情:<font color='comment'>{3}</font>";
//把站內(nèi)信推送到企業(yè)微信
await _uidClient.SendMarkdown(new CorpSendTextDto
{
touser = dto.ToUser,
content = string.Format(content, dto.Name, now, log.Id, dto.Detail)
});
}
}
/// <summary>
/// 獲取本人的站內(nèi)信列表
/// </summary>
/// <param name="name">標(biāo)題</param>
/// <param name="detail">詳情</param>
/// <param name="unread">只顯示未讀</param>
/// <param name="type">類型</param>
/// <param name="createStart">創(chuàng)建起始時(shí)間</param>
/// <param name="createEnd">創(chuàng)建結(jié)束時(shí)間</param>
/// <param name="pageIndex">當(dāng)前頁(yè)</param>
/// <param name="pageSize">每頁(yè)個(gè)數(shù)</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>
/// 設(shè)置已讀
/// </summary>
/// <param name="id">站內(nèi)信ID</param>
/// <returns></returns>
public async Task Read(ObjectId id)
{
var msg = await Repository.First(id);
if (msg == null)
{
throw new CmsException(EnumStatusCode.ArgumentOutOfRange, "不存在此站內(nèi)信");
}
if (msg.ReadTime != null)
{
//已讀的不再更新讀取時(shí)間
return;
}
msg.ReadTime = DateTime.Now;
await Repository.Update(msg, "ReadTime");
await SendUnreadCount(msg.ToUser);
}
/// <summary>
/// 設(shè)置本人全部已讀
/// </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>
/// 獲取本人未讀條數(shù)
/// </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>
/// 推送未讀數(shù)到前端
/// </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>可以直接注入并且使用,然后調(diào)用_messageHub.Clients.Groups(account).SendAsync即可推送。接下來(lái)就簡(jiǎn)單了,在MessageController里把這些接口暴露出去,通過(guò)HTTP請(qǐng)求添加站內(nèi)信,或者直接內(nèi)部調(diào)用添加站內(nèi)信接口,就可以添加站內(nèi)信并且推送給前端頁(yè)面了,當(dāng)然除了站內(nèi)信,我們還可以做得更多,比如比較重要的順便也推送到第三方app,比如企業(yè)微信或釘釘,這樣你還會(huì)怕錯(cuò)過(guò)重要信息?
接下來(lái)到了客戶端了,客戶端只說(shuō)網(wǎng)頁(yè)端的,代碼如下:<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必須和后端的配置保持一致,不然分分鐘出現(xiàn)下面異常:
?
?這是因?yàn)槟銢](méi)有在我規(guī)定的時(shí)間內(nèi)向我發(fā)送“心跳包”,所以我認(rèn)為你已經(jīng)“陣亡”了,為了避免不必要的傻傻連接,我停止了連接。另外需要說(shuō)的是重連機(jī)制,有多種重連機(jī)制,這里我選擇每隔10秒重連一次,因?yàn)槲矣X(jué)得需要重連,那一般是因?yàn)榉?wù)器掛了,既然掛了,那我每隔10秒重連也是不會(huì)浪費(fèi)服務(wù)器性能的,浪費(fèi)的是瀏覽器的性能,客戶端的就算了,忽略不計(jì)。自動(dòng)重連代碼如下:
async function start() {try {
await connection.start();
console.log(connection)
} catch (err) {
console.log(err);
setTimeout(() => start(), 1e4);
}
};
connection.onclose(async () => {
await start();
});
start();
當(dāng)然還有其他很多重連的方案,可以去官網(wǎng)看看。
當(dāng)然若你的客戶端是用vue寫的話,寫法會(huì)有些不同,如下:
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
接下來(lái)就可以用this.$connection 愉快的使用了。
到這里或許你覺(jué)得大功告成了,若沒(méi)看瀏覽器的控制臺(tái)輸出,我也是這么認(rèn)為的,然后控制臺(tái)出現(xiàn)了紅色!:?
?雖然出現(xiàn)了這個(gè)紅色,但是依然可以正常使用,只是降級(jí)了,不使用WebSocket了,心跳包變成了一個(gè)個(gè)的post請(qǐng)求,如下圖:
?
?
? 這個(gè)是咋回事呢,咋就用不了WebSocket呢,我的是谷歌瀏覽器呀,肯定是支持WebSocket的,咋辦,只好去群里討教了,后來(lái)大神告訴我,需要在ngnix配置如下紅框內(nèi)的:
?
?真是一語(yǔ)驚醒夢(mèng)中人,我加上如下代碼就正常了:
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;
??????? }
總結(jié)
以上是生活随笔為你收集整理的在.net core3.0中使用SignalR实现实时通信的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: C# 8.0 的默认接口方法
- 下一篇: 活动最后72小时:购书优惠劵,折后再折,