最近在本身的項目中實踐了SignalR的使用,asp.net core 2.1版本的時候創建了對SignalR的支持,SignalR的可以使用Web Socket, Server Sent Events 和 Long Polling做爲底層傳輸方式.SignalR基於這三種技術構建, 抽象於它們之上, 它讓你更好的關注業務問題而不是底層傳輸技術問題.它分爲了客戶端和服務端,服務端支持到asp.net core和asp.net,客戶端分語言支持java、javascript、C#等。javascript
SignalR會優先使用websocket鏈接,瀏覽器由於版本問題沒辦法支持的話會使用回落機制,轉用SSE或者長輪詢的方式來進行。SignalR將上述機制進行封裝,採用了一致的API進行編程,使得開發人員沒必要糾結與技術細節的選型,提升了編程效率。java
SignalR採用RPC(remote procedure call)的編程範式來進行客戶端和服務端之間的調用。web
SignalR利用底層傳輸來讓服務器能夠調用客戶端的方法, 反之亦然, 這些方法能夠帶參數, 參數也能夠是複雜對象, SignalR負責序列化和反序列化.編程
Hub是SignalR的一個組件, 它運行在ASP.NET Core應用裏. 因此它是服務器端的一個類.後端
Hub使用RPC接受從客戶端發來的消息, 也能把消息發送給客戶端. 因此它就是一個通訊用的Hub.api
在ASP.NET Core裏, 本身建立的Hub類須要繼承於基類Hub.瀏覽器
在Hub類裏面, 咱們就能夠調用全部客戶端上的方法了. 一樣客戶端也能夠調用Hub類裏的方法.服務器
以前說過方法調用的時候能夠傳遞複雜參數, SignalR能夠將參數序列化和反序列化. 這些參數被序列化的格式叫作Hub 協議, 因此Hub協議就是一種用來序列化和反序列化的格式.websocket
Hub協議的默認協議是JSON, 還支持另一個協議是MessagePack. MessagePack是二進制格式的, 它比JSON更緊湊, 並且處理起來更簡單快速, 由於它是二進制的.cookie
此外, SignalR也能夠擴展使用其它協議..
隨着系統的運行, 有時您可能須要進行橫向擴展. 就是應用運行在多個服務器上.
這時負載均衡器會保證每一個進來的請求按照必定的邏輯分配到多是不一樣的服務器上.
在使用Web Socket的時候, 沒什麼問題, 由於一旦Web Socket的鏈接創建, 就像在瀏覽器和那個服務器之間打開了隧道同樣, 服務器是不會切換的.
可是若是使用Long Polling, 就可能有問題了, 由於使用Long Polling的狀況下, 每次發送消息都是不一樣的請求, 而每次請求可能會到達不一樣的服務器. 不一樣的服務器可能不知道前一個服務器通訊的內容, 這就會形成問題.
針對這個問題, 咱們須要使用Sticky Sessions (粘性會話).
Sticky Sessions 貌似有不少中實現方式, 可是主要是下面要介紹的這種方式.
做爲第一次請求的響應的一部分, 負載均衡器會在瀏覽器裏面設置一個Cookie, 來表示使用過這個服務器. 在後續的請求裏, 負載均衡器讀取Cookie, 而後把請求分配給同一個服務器.
我在項目中使用SignalR主要是用來通知用戶事件建立的結果。
首先你要建立一個HUB:
public class EventMessageHub : Hub<IEventNotification> { }
我就創建了一個空的Hub,它繼承自Hub<T>,帶泛型的Hub基類表示他是一個強類型的Hub,那麼T就是一個接口,這個接口定了了一些發送消息的方法,個人IEventNotification的定義以下:
public interface IEventNotification { Task Notify(string principal,string time, string message); }
我在裏面定義了一個方法,用於發送消息到客戶端
定義好Hub後你要作的是爲這個Hub建立一個URL,使得客戶端能夠訪問獲得這個Hub,只有訪問到這個Hub才能執行遠程的從客戶端調用服務端的代碼。配置以下:
①首先在StartUp方法中的ConfigureServices方法中配置SignalR的服務:
//signalR services.AddSignalR();
②而後在Configure方法中配置SignalR的中間件:
app.UseSignalR(route => { route.MapHub<EventMessageHub>("/eventMessage"); });
這樣,就把這個Hub配置好,你就能夠從服務端發送消息給客戶端了。
可是SignalR還須要有認證的過程,SignalR在asp.net core中使用的認證是分開的,也就是說,asp.net core自己作好認證後,SignalR是不會識別這個認證的,須要單獨爲SignalR配置認證。
SignalR中有一個接口,是IUserIdProvider,這個接口用於標誌當前的用戶ID,他的定義以下:
public interface IUserIdProvider { // // 摘要: // Gets the user ID for the specified connection. // // 參數: // connection: // The connection to get the user ID for. // // 返回結果: // The user ID for the specified connection. string GetUserId(HubConnectionContext connection); }
可見該接口包含一個方法GetUserId,該方法用於標誌當前用戶。
我給了一個該接口的實現:
public class SignalRUserIdProvider : IUserIdProvider { public string GetUserId(HubConnectionContext connection) { //tokenvalidationparameter中配置的RoleClaimType和NameClaimType在這裏不起做用,要用原始的claim var orgIdentifier = connection.User.FindFirst("orgIdentifier")?.Value; if (string.IsNullOrEmpty(orgIdentifier)) { return null; } return orgIdentifier; } }
connection實參上有一個User屬性,這個屬性的類型是ClaimsPrincipal,這個類型屬於asp.net core認證框架下的基本概念,你能夠上網查,無論你用什麼類型的認證方式,最後識別用戶都要轉換成這個ClaimsPrincipal.
那想要獲取這個User屬性的值的話就要進行一些配置。個人項目是先後端分離,採用的是jwt token bearer的認證方式,因此,在這裏我只講這種類型的,等到用mvc開發使用cookie的時候再回來補充。
有一個http的請求頭是Authorization(受權的意思,我很奇怪這個頭的名字爲何不是Authentication),該請求頭標誌了認證的類型,好比Basic,或Bearer,格式就是Authorization Bearer XXXXjhhhhhX........這樣子。那咱們進行認證受權的時候就要配置這個頭,請求到達了服務端後服務端就會解析這個頭,而後識別發送該請求的客戶,下面是我服務端配置的認證的一些代碼:
private static void AddJwt(IServiceCollection services, IConfiguration config) {
//添加認證類型 services.AddAuthentication(option => { option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(option => { option.TokenValidationParameters = new TokenValidationParameters { //RoleClaimType和NameClaimType的做用是將自定義的claim轉化成標準的claim RoleClaimType = "orgRole", NameClaimType = "orgIdentifier", //ensure the token was issued by a trusted authorization server (default value true) ValidateIssuer = true, ValidIssuer = configSection.GetSection("Issuer").Value, //ensure the token audience matches our audience value(default value true) ValidateAudience = true, ValidAudience = configSection.GetSection("Audience").Value, ValidateIssuerSigningKey = true, //specify the key used to sign the token IssuerSigningKey = signingKey, RequireSignedTokens = true, //ensure the token hasn't expired ValidateLifetime = true, RequireExpirationTime = true, //clock skew compensates fro server time drift. we recommend 5 minutes or less ClockSkew = TimeSpan.FromMinutes(5), }; //signalr須要這個配置 option.Events = new JwtBearerEvents() { OnMessageReceived = context => { // If the request is for our hub... var path = context.HttpContext.Request.Path; if (path.StartsWithSegments("/eventMessage")) { var accessToken = context.Request.Query["access_token"]; // Read the token out of the query string context.Token = accessToken; } return Task.CompletedTask; } }; }); }
上面展現了我項目中配置的認證方式,前面的TokenValidationParameters主要用於配置asp.net core服務端認證用戶的方式,後面的option.Events = new JwtBearerEvents()。。。。這個就是用來配置SignalR發送請求的時候是不會攜帶http header頭的,因此你不能使用Authorization Bearer xxxooooxxx....這種方式來對服務端進行認證,SignalR採用的都是這樣的:
WebSocket connected to ws://localhost:5003/eventMessage?id=RydCi063Wh0kZLzivQLG-w&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdJZCI6ImU1MjY0ODRiLWNhYjUtNGU1Yy04MTBiLWEwYzQ3MDljYmU3ZCIsImp0aSI6IjE3ZmM2YzY3LTM0NTMtNDA0Yy05MWQ1LWI0MDZjZTZmYzc0MyIsImlhdCI6MTU1NjYxMzg1Mywib3JnUm9sZSI6InNlY29uZGFyeWFkbWluIiwib3JnTmFtZSI6IuS4reWbvemTtuihjOWMheWktOWIhuihjOS_oeaBr-enkeaKgOmDqCIsIm9yZ0lkZW50aWZpZXIiOiJBNDY0MCIsIm9yZzIiOiJCMzQyNiIsIm1hbmFnZW1lbnRMaW5lSWQiOiI3NWIzZjNjMS0zMTU3LTRjMjMtYjc0Ni0yMjhiNDY3OWUwNjQiLCJuYmYiOjE1NTY2MTM4NTMsImV4cCI6MTU1NjYxNTY1MywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo0MjAwIn0.pmRqvqNGXATprIhmDTCpMGJ-tutgdv4V6GZ3GzMwo3s.
從上面能夠看到token是被放到了url的查詢字段中的,option.TokenValidationParameters = new TokenValidationParameters{.........}這種方式只適用於http的Authorization頭部攜帶token的方式,因此你就要爲你的SignalR設置一套程序來讓他可以識別當前用戶,也就是認證的過程。上面的代碼中的OnMessageReceived 就是要幹這個事兒。
也能夠看出,jwt token自己就至關因而明文保存,只不過是用base64編碼的,你隨便把一個jwt token粘貼到一個支持base64 decode的網站上面均可以獲取這裏面的詳細信息。
當咱們作好這一步後,咱們就能夠識別當前用戶,在個人項目中我要向特定的用戶發送消息:
await _hubContext.Clients.User(notification.TargetOrgIdentifier).Notify(notification.RequestOrgNam, DateTime.Now.ToLocalTime().ToString(), $"發起了{notification.ToString()}");
稍微說明一下這個代碼的意圖:
_hubContext.Clients.User(string userId)這個方法接受一個UserId,用於標誌是哪個客戶,這個UserId咱們在前面的IUserIdProvider就已經配置好,只要這個UserId對應的客戶端鏈接上Hub,那麼咱們就能給這個客戶端發送消息。Notify是咱們使用強類型的Hub<T>中T接口定義的方法,使用強類型的Hub的好處就是你不用硬編碼一個方法,這樣就不會由於書寫硬編碼產生錯誤。
下面在說一下不使用強類型Hub來編程Hub
public class ChatHub : Hub { public async Task SendMessageAsync(string userId, string message) { await this.Clients.User(userId).SendAsync("ReciveMessage", message); } }
我定義了一個ChatHub的hub,這個hub裏面的SendMessageAsync是一種硬編碼的寫法,SendMessageAsync能夠被客戶端調用,而方法中的Clients.User(userId).SendAsync(.....方法自己能夠做爲一個事件在客戶端進行監聽。好比說JavaScript客戶端能夠寫這樣的代碼來監遵從ChatHub發送過來的消息:
async connect() { this.connection = new HubConnectionBuilder() .withUrl(`${environment.api_url}/eventMessage`, { accessTokenFactory: this.jwtHelper.tokenGetter }) .build();//①首先建立一個遠程服務器的鏈接,使用的是建造者模式來建立 this.connection.on('ReciveMessage', (principal: string, time: string, message: string) => { this.messageList.push({ principal, time, message }); });//②而後讓這個鏈接來監聽服務端傳回來的事件 await this.connection.start();//③配置好之後就啓動這個鏈接 }
上面的代碼使用TypeScript書寫,connection是類中的一個私有字段,類型就是HubConnection。
這就是我使用SignalR的過程。後續使用過程當中再補充吧。