一、概要
這篇文章將向大家分享最近學習的一種實時通訊框架SignalR。
什么是SignalR?
SignalR是一個.NET Core/.NET Framework的開源實時框架,可使用Long Polling,ServerSent Events和Websocket作為底層傳輸方式。
SignalR基于這三種技術(shù)構(gòu)建,抽象于它們之上,它讓你更好的關(guān)注業(yè)務問題而不是底層傳輸技術(shù)問題。
SignalR這個框架分服務器和客戶端,服務器端支持http://ASP.NET?Core和ASP.NET;而客戶端除了支持瀏覽器的javascript以外,也支持其他類型的客戶端,例如wpf或winfrom桌面應用。
SignalR的作用
SignalR是用來做實時通訊的web應用。
適用場景:
需要從服務器進行高頻率更新的應用。 示例包括游戲、社交網(wǎng)絡、投票、拍賣、地圖和 GPS 應用。
儀表板和監(jiān)視應用。 示例包括公司儀表板、即時銷售更新或旅行警報。
協(xié)作應用。 協(xié)作應用的示例包括白板應用和團隊會議軟件。
需要通知的應用。 社交網(wǎng)絡、電子郵件、聊天、游戲、旅行警報和很多其他應用都需使用通知。Server 主動發(fā)送到 Client 瀏覽器 ←?http://ASP.NET?Core Web Server
無需瀏覽器發(fā)起請求,服務器可主動的向客戶端推送數(shù)據(jù)。
SignalR"底層"實現(xiàn)
SignalR使用了3種“底層”技術(shù)來實現(xiàn)實時Web應用,它分別是Long Polling,ServerSent Events和Websocket.
Polling
Polling是實現(xiàn)實時Web的一種笨方法,它就是通過定期的向服務器發(fā)送請求,來查看服務器的數(shù)據(jù)是否有變化。
如果服務器數(shù)據(jù)沒有變化,那么就返回204 No Content;如果有變化就把最新的數(shù)據(jù)發(fā)送給客戶端
這就是Polling,很簡單,但是比較浪費資源。
SingnalR沒有采用Polling這種技術(shù)。
Long Polling
Long Polling 和 Polling有類似的地方,客戶端都是發(fā)送請求到服務器。但是不同之處是:如果服務器沒有新數(shù)據(jù)要發(fā)給客戶端的話,那么服務器會繼續(xù)保持連接,知道有新的數(shù)據(jù)產(chǎn)生,服務器才把新的數(shù)據(jù)返回給客戶端。
如果請求發(fā)出后一段時間內(nèi)沒有響應,那么請求就回超時。這時,客戶端會再次發(fā)出請求。
ServerSent Events
使用SSE的話,web服務器可以在任何時間把數(shù)據(jù)發(fā)送到瀏覽器,可以稱之為推送。而瀏覽器則會監(jiān)聽進來的信息,這些信息就像流數(shù)據(jù)一樣,這個鏈接也會一直保持開放,直到服務器主動關(guān)閉它。
瀏覽器會使用一個叫做EventSource的對象用來處理傳過來的信息,
缺點:很多瀏覽器都有最大并發(fā)連接數(shù)的限制,只能發(fā)送文本信息并且只是單向通信。
優(yōu)點:使用方式簡單,基于HTTP協(xié)議可自動重連。雖然不支持老的瀏覽器但是很容易進行Polling Fail
Web socket
Web socket是不同于HTTP的另一個TCP協(xié)議。她使得瀏覽器和服務器之間的交互式通信變得可能。使用websocket,消息可以從服務器發(fā)往客戶端,也可以從客戶端發(fā)往服務器,并且沒有HTTP那樣的延遲。信息流沒有完成的時候,TCP Socket通常是保持打開狀態(tài)。
使用現(xiàn)代瀏覽器時,SignalR大部分情況下都會使用web socket,這也是最有效的傳輸方式。
全雙工通信:客戶端和服務器可以同時往對方發(fā)送消息。
并且不受SEE的瀏覽器最大連接數(shù)限制(6個),大部分瀏覽器對websocket連接數(shù)的限制是50個。
消息類型:可以是文本和二進制,web socket也支持流媒體(音頻和視頻)
其實正常的HTTP請求也使用了TCP socket。web socket標準使用了握手機制把用于HTTP的socket升級為使用WS協(xié)議的websocket的socket。
web socket生命周期, 1.HTTP握手 2.通信/數(shù)據(jù)交換 3.關(guān)閉
HTTP握手
每一個websocket開始的時候都是一個簡單的HTTP socket。
客戶端首先發(fā)送一個GET請求到服務器,來請求升級socket。
如果服務器同意的話,這個socket從這時開始就變成了web socket
消息類型
web socket的消息類型可以是文本,二進制。也包括控制類的消息:Ping/Pong和關(guān)閉。
每個消息由一個或多個Frame組成。
SignalR 回落機制
其中web socket僅支持比較現(xiàn)代的瀏覽器,web服務器也不能太老。
而Server Sent Events 情況可能好一點,但是也存在同樣的問題。
所以SignalR采用了回落機制,SignalR有能力去協(xié)商支持的傳輸類型。
瀏覽器使用三種底層技術(shù)是有優(yōu)先級的,1.如果瀏覽器較新則使用web socket 2.如果不支持web socket則降級使用ServerSent Events。3.如果ServerSent Events都不支持則使用Long Polling。
一旦連接建立成功則會一直發(fā)送消息keep live,如果有問題則會拋出異常。
也可以禁用回落機制,只采用一種通信方式也可以。
RPC
RPC(Remote Procedure call)它的優(yōu)點就是可以像調(diào)用本地方法一樣調(diào)用遠程服務。
SignalR采用RPC范式來進行客戶端與服務器之間的通信。
SignalR利用底層傳輸來讓服務器可以調(diào)用客戶端的方法,反之亦然。這些方法可以帶參數(shù),參數(shù)也可以是復雜對象,SignalR負責序列化和反序列化。
HUB
HUB是SignalR的一個組件,它運行在http://ASP.NET?Core應用里。所以它是服務器端的一個類。
HUB使用RPC接收從客戶端發(fā)來的消息,也能把消息發(fā)送給客戶端。所以它就是一個通信用的HUB。
在http://ASP.NET?CORE里,自己創(chuàng)建的HUB類需要繼承于基類HUB。
在HUB類里面,我們就可以調(diào)用所喲客戶端上的方法了。同樣客戶端也可以調(diào)用HUB類里的方法。
之前說過方法調(diào)用的時候可以傳遞復雜參數(shù),SignalR可以將參數(shù)序列化和反序列化。這些參數(shù)被序列化的格式叫做HUB協(xié)議,所以HUB協(xié)議就是一種用來序列化和反序列化的格式。
HUB協(xié)議的默認協(xié)議是JSON,還支持另外一個協(xié)議是MessagePack。MessagePack是二進制格式的。它比JSON更緊湊,而且處理起來更簡單快速,因為它是二進制的。
此外,SignalR也可以擴展使用其他協(xié)議。
橫向擴展
這時負載均衡器會保證每個進來的請求按照一定的邏輯分配到可能是不同服務器上。
在使用web socket的時候,沒什么問題,因為一旦web socket的連接建立,就像在瀏覽器和服務器之間打開了一條隧道,服務器是不會切換的。
但是如果使用Long Polling,就可能是有問題了,因為使用Long Polling的情況下,每次發(fā)送消息都是不同的請求,而每次請求可能會達到不同的服務器。不同的服務器可能不知道前一個服務器通信的內(nèi)容,這就會造成問題。
針對這個問題,我們需要使用Sticky Sessions(粘性會話)。
Sticky Sessions貌似有很多種實現(xiàn)方式,但是主要是下面要介紹的這種方式。
作為第一次請求的響應的一部分,負載均衡器會在瀏覽器里面設置一個Cookie,來表示使用這個服務器。在后續(xù)的請求里,負載均衡器讀取Cookie,然后把請求分配給同一個服務器。
相關(guān)文檔:
開源地址:https://github.com/signalr
官方SignalR介紹:https://docs.microsoft.com/zh-cn/aspnet/signalr/overview/getting-started/introduction-to-signalr
二、詳細內(nèi)容
接下來開始講解如何實戰(zhàn)構(gòu)建這樣的一個應用程序,基礎(chǔ)建項目創(chuàng)建各種文件的步驟我直接跳過了在開發(fā)教程中里有講這里就不做重復操作了。
一.服務端構(gòu)建
(開發(fā)教程)服務端:https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/signalr?view=aspnetcore-5.0&tabs=visual-studio
這里我只展示與教程中不同的部分,源碼我會分享在文章結(jié)尾的群里并會在代碼中寫好注釋方便大家理解。
部分核心源碼展示:
namespace SinganlRDemo.Hubs
{
? ? //Hub也有身份認證,只有認證之后才能響應里面的方法
? ? //[Authorize]
? ? public class ChatHub : Hub
? ? {
? ? ? ? public void Check()
? ? ? ? {
? ? ? ? ? ? //獲取客戶端身份(例:名字)
? ? ? ? ? ? var user = Context.User.Identity.Name;
? ? ? ? }
? ? ? ? public async Task SendMessage(string user, string message)
? ? ? ? {
? ? ? ? ? ? /*
? ? ? ? ? ? * Clients.All代表所有已連接的客戶端
? ? ? ? ? ? *
? ? ? ? ? ? * 第一個入?yún)?,需要調(diào)用的客戶端的方法名稱。具體在SinganlRDesktop庫中MainViewModel類里的108行中體現(xiàn)。
? ? ? ? ? ? * 第二、三個入?yún)⑹潜徽{(diào)用方法需要的參數(shù)。
? ? ? ? ? ? */
? ? ? ? ? ? await Clients.All.SendAsync("ReceiveMessage", user, message);
? ? ? ? }
? ? ? ? public async Task Login(string name)
? ? ? ? {
? ? ? ? ? ? /*
? ? ? ? ? ? * 1.在開發(fā)過程中,會有需要獲取客戶端使用的用戶的用戶名。
? ? ? ? ? ? * Context(Context.ConnectionId)剛好能解決這個問題。Context存在于Hub中。
? ? ? ? ? ? */
? ? ? ? ? ? //2.如果只需要發(fā)送給指定用戶這樣寫即可。
? ? ? ? ? ? //var client = Clients.Client(Context.ConnectionId);
? ? ? ? ? ? //await client.SendAsync("online", $"{ name }in the group.");
? ? ? ? ? ? //3.發(fā)送給所有用戶。
? ? ? ? ? ? await Clients.AllExcept(Context.ConnectionId).SendAsync("online",$"{ name }in the group.");
? ? ? ? ? ? //4.將當前獲取到的用戶添加到分組里和移除出分組
? ? ? ? ? ? //await Groups.AddToGroupAsync(Context.ConnectionId,"JusterGroup");
? ? ? ? ? ? //await Groups.RemoveFromGroupAsync(Context.ConnectionId, "JusterGroup");
? ? ? ? ? ? //對指定分組下的用戶發(fā)送消息
? ? ? ? ? ? await Clients.Group("JusterGroup").SendAsync("online", $"{ name }in the group.");
? ? ? ? }
? ? ? ? public async Task SignOut(string name)
? ? ? ? {
? ? ? ? ? ? await Clients.AllExcept(Context.ConnectionId).SendAsync("online", $"{ name }leave the group.");
? ? ? ? }
? ? }
}
二.客戶端構(gòu)建(WPF)
(開發(fā)教程)客戶端:https://docs.microsoft.com/zh-cn/aspnet/core/signalr/dotnet-client?view=aspnetcore-5.0&tabs=visual-studio
public MainViewModel()
? ? {
? ? ? ? //初始化SignalR的hub,然后指定服務器地址
? ? ? ? connection = new HubConnectionBuilder()
? ? ? ? ? .WithUrl("https://localhost:44394/chathub")
? ? ? ? ? //重連機制
? ? ? ? ? .WithAutomaticReconnect(new RandomRetryPolicy())
? ? ? ? ? .Build();
? ? ? ? //關(guān)閉連接
? ? ? ? connection.Closed += async (error) =>
? ? ? ? {
? ? ? ? ? ? await Task.Delay(new Random().Next(0, 5) * 1000);
? ? ? ? ? ? await connection.StartAsync();
? ? ? ? };
? ? ? ? //重連
? ? ? ? connection.Reconnecting += error =>
? ? ? ? {
? ? ? ? ? ? Debug.Assert(connection.State == HubConnectionState.Reconnecting);
? ? ? ? ? ? // Notify users the connection was lost and the client is reconnecting.
? ? ? ? ? ? // Start queuing or dropping messages.
? ? ? ? ? ? return Task.CompletedTask;
? ? ? ? };
? ? ? ? //接收消息
? ? ? ? connection.On<string, string>("ReceiveMessage", (user, message) =>
? ? ? ? {
? ? ? ? ? ? Application.Current.Dispatcher.Invoke(()=>
? ? ? ? ? ? {
? ? ? ? ? ? ? ? var newMessage = $"{user}: {message}";
? ? ? ? ? ? ? ? TalkMessage += newMessage + "\r\n";
? ? ? ? ? ? });
? ? ? ? });
? ? ? ? //離線、上線通知
? ? ? ? connection.On<string>("online", (message) =>
? ? ? ? {
? ? ? ? ? ? var newMessage = $"{message}";
? ? ? ? ? ? MsgCollection.Add(newMessage);
? ? ? ? });
? ? ? ? connection.StartAsync();
? ? }
? ? /// <summary>
? ? /// 發(fā)送消息給服務器
? ? /// </summary>
? ? /// <param name="user">用戶名</param>
? ? /// <param name="msg">消息內(nèi)容</param>
? ? /// <returns></returns>
? ? public async Task Send(string user,string msg)
? ? {
? ? ? ? try
? ? ? ? {
? ? ? ? ? ? await connection.InvokeAsync("SendMessage",
? ? ? ? ? ? ? ? user, msg);
? ? ? ? }
? ? ? ? catch (Exception ex)
? ? ? ? {
? ? ? ? ? ? MsgCollection.Add(ex.Message);
? ? ? ? }
? ? }
? ? /// <summary>
? ? /// 上線
? ? /// </summary>
? ? /// <returns></returns>
? ? public async Task Login()
? ? {
? ? ? ? _userName = $"Person{ new Random().Next(1, 99999)}";
? ? ? ? await connection.InvokeAsync("Login", _userName);
? ? }
? ? /// <summary>
? ? /// 離線
? ? /// </summary>
? ? /// <returns></returns>
? ? public async Task SignOut()
? ? {
? ? ? ? await connection.InvokeAsync("SignOut", _userName);
? ? ? ? await connection.StopAsync();
? ? }
三、運行效果
