asp.Netcoreリアルタイムライブラリ:SignalR(1)
16792 ワード
SignalRの基本概念
前言
最近自分のプロジェクトでSignalRの使用を実践しました.asp.Netcore 2.1のバージョンの時にSignalRに対するサポートを創立して、SignalRの使用可能なWeb Socket、Server Sent EventsとLong Pollingは下層の伝送方式とします.SignalRはこの3つの技術に基づいて構築され、それらの上に抽象化され、下位伝送技術の問題ではなくビジネス問題にもっと注目することができます.クライアントとサービス側に分かれ、サービス側はaspにサポートする.Netcoreとasp.Netは、クライアント言語別でjava、javascript、C#などをサポートします.
SignalRはwebsocket接続を優先し、ブラウザはバージョンの問題でサポートできない場合はフォールバックメカニズムを使用し、SSEやロングポーリング方式に移行します.SignalRは上記のメカニズムをカプセル化し,一貫したAPIを用いてプログラミングを行い,開発者が技術的詳細の選定にこだわる必要がなく,プログラミング効率を向上させた.
SignalRはRPC(remote procedure call)のプログラミングパターンを用いてクライアントとサービス間の呼び出しを行う.
SignalRは、下位層の伝送を利用するサーバがクライアントを呼び出すことができる方法であり、逆に、これらの方法はパラメータを持つことができ、パラメータは複雑なオブジェクトであってもよく、SignalRはシーケンス化と逆シーケンス化を担当する.
HUB
HubはSignalRのコンポーネントであり、ASP.で動作する.NET Coreアプリでサーバ側のクラスです
HubはRPCを用いるクライアントからのメッセージを受信し、クライアントにもメッセージを送信することができる.だからそれは信用できるHubです.
ASP.NET Coreでは、自分が作成したHubクラスをベースクラスHubに継承する必要がある.
Hubクラスでは、すべてのクライアント上のメソッドを呼び出すことができます.同様にクライアントはHubクラスのメソッドを呼び出すこともできる.
メソッド呼び出し時に複雑なパラメータを渡すことができる、SignalRはパラメータをシーケンス化および逆シーケンス化することができる.これらのパラメータがシーケンス化するフォーマットをHubプロトコルと呼ぶので、Hubプロトコルはシーケンス化と逆シーケンス化のためのフォーマットである.
HubプロトコルのデフォルトプロトコルはJSONであり、もう一つのプロトコルはMessagePackであることをサポートしている.MessagePackはバイナリフォーマットで、JSONよりコンパクトで、バイナリなので処理が簡単です.
また、SignalRは他のプロトコルを拡張することもできる.
よこほうこうかくちょう
システムの稼働に伴い、横方向の拡張が必要になる場合があります.アプリケーションが複数のサーバ上で実行する.
このとき、負荷イコライザは、入る要求ごとに一定の論理で異なるサーバに割り当てられることを保証する.
Web Socketを使用する場合、問題はありません.Web Socketの接続が確立すると、ブラウザとそのサーバの間にトンネルが開いたように、サーバは切り替えられないからです.
ただし、Long Pollingを使用すると、Long Pollingを使用する場合、メッセージを送信するたびに異なるリクエストとなり、リクエストごとに異なるサーバに到達する可能性があるため、問題が発生する可能性がある.異なるサーバが前のサーバの通信内容を知らない場合がある、これは問題を引き起こす.
この問題に対して、Sticky Sessions(スティッキーセッション)を使用する必要があります.
Sticky Sessionsは多くの中で実現方式があるようだが、主に以下に紹介するこの方式である.
最初のリクエストの応答の一部として、負荷イコライザはブラウザにCookieを設定、このサーバを使用したことを示す.その後の要求では、負荷イコライザがCookieを読み出し、同じサーバに要求を割り当てる.
ASP.NET CoreでのSignalRの使用
私がプロジェクトでSignalRを使用するのは、主にユーザーイベントの作成結果を通知するために使用されます.
まずHUBを作成します.public class EventMessageHub : Hub
{
}
私は空のHubを創立して、それはHubから受け継いで、汎型のHubベースクラスを持って彼が強いタイプのHubであることを表して、Tは1つのインタフェースで、このインタフェースはいくつかのメッセージを送信する方法を決めて、私のIEventNotificationの定義は以下の通りです:public interface IEventNotification
{
Task Notify(string principal,string time, string message);
}
クライアントにメッセージを送信する方法を定義しました
Hubを定義したら、クライアントがこのHubにアクセスできるようにするURLを作成し、このHubにアクセスしてこそ、クライアントからサービス側を呼び出すコードをリモートで実行することができます.次のように構成されています.
①まず、StartUpメソッドのConfigureServicesメソッドでSignalRのサービスを構成します.//signalR
services.AddSignalR();
②次に、コンフィギュレーション方式でSignalRのミドルウェアを構成する.app.UseSignalR(route =>
{
route.MapHub("/eventMessage");
});
これにより、このHubを構成することで、サービス側からクライアントにメッセージを送信することができます.
しかしSignalRには認証のプロセスが必要であり、SignalRはaspにある.Netcoreで使用する認証は別々、すなわちasp.Netcore自体が認証されている場合、SignalRはこの認証を認識しません.SignalRの認証を個別に構成する必要があります.
SignalRには、現在のユーザーIDを示すIUserIdProviderというインタフェースがあります.彼の定義は以下の通りです. 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;
}
}
接続実パラメータには、ClaimsPrincipalというタイプのUser属性があり、このタイプはaspに属する.Netcore認証の枠組みの下の基本的な概念は、インターネットで調べることができます.どんなタイプの認証方式を使っても、最後にユーザーを識別するにはこのClaimsPrincipalに変換しなければなりません.
では、このUserプロパティの値を取得するには、いくつかの構成が必要です.私のプロジェクトは前後端分離で、jwt token bearerの認証方式を採用しているので、ここではこのタイプだけを話して、mvcでクッキーを開発してから補充します.
httpのリクエストヘッダはAuthorization(ライセンスという意味で、このヘッダの名前がなぜAuthenticationではないのか不思議です)で、BasicやBearerなどの認証のタイプを示すリクエストヘッダがあります.フォーマットはAuthorization Bearer XXXXjhhhhhhhhhXです.そうですか.では、認証権限を取得するときにこのヘッダを構成します.リクエストがサービス側に到着すると、サービス側はこのヘッダを解析し、リクエストを送信したお客様を識別します.次に、私のサービス側構成の認証のコードをいくつか示します. 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を構成するために使用されています.Netcoreサービス側がユーザを認証する方式、後のoption.Events = new JwtBearerEvents()....これはSignalRがリクエストを送信するときにhttpヘッダを携帯しないように構成するためのものです.Authorization Bearer xxooooxxxは使用できません.このようにしてサービス側を認証し、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自体は明文保存に相当し、base 64で符号化されているだけで、base 64 decodeをサポートするサイトにjwt tokenを貼り付けると詳細が得られることもわかります.
このステップを完了すると、現在のユーザーを識別できます.私のプロジェクトで特定のユーザーにメッセージを送信します.
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インタフェースを使用して定義する方法で、強いタイプの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は1種のハードコーディングの書き方で、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();//③
}
上のコードはTypeスクリプトで書かれています.connectionはクラスのプライベートフィールドで、タイプはHubConnectionです.
これが私がSignalRを使う過程です.あとは使用中に補充しましょう.
転載先:https://www.cnblogs.com/pangjianxin/p/10797531.html
public class EventMessageHub : Hub
{
}
public interface IEventNotification
{
Task Notify(string principal,string time, string message);
}
//signalR
services.AddSignalR();
app.UseSignalR(route =>
{
route.MapHub("/eventMessage");
});
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);
}
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;
}
}
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;
}
};
});
}
WebSocket connected to ws://localhost:5003/eventMessage?id=RydCi063Wh0kZLzivQLG-w&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdJZCI6ImU1MjY0ODRiLWNhYjUtNGU1Yy04MTBiLWEwYzQ3MDljYmU3ZCIsImp0aSI6IjE3ZmM2YzY3LTM0NTMtNDA0Yy05MWQ1LWI0MDZjZTZmYzc0MyIsImlhdCI6MTU1NjYxMzg1Mywib3JnUm9sZSI6InNlY29uZGFyeWFkbWluIiwib3JnTmFtZSI6IuS4reWbvemTtuihjOWMheWktOWIhuihjOS_oeaBr-enkeaKgOmDqCIsIm9yZ0lkZW50aWZpZXIiOiJBNDY0MCIsIm9yZzIiOiJCMzQyNiIsIm1hbmFnZW1lbnRMaW5lSWQiOiI3NWIzZjNjMS0zMTU3LTRjMjMtYjc0Ni0yMjhiNDY3OWUwNjQiLCJuYmYiOjE1NTY2MTM4NTMsImV4cCI6MTU1NjYxNTY1MywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo0MjAwIn0.pmRqvqNGXATprIhmDTCpMGJ-tutgdv4V6GZ3GzMwo3s.
await _hubContext.Clients.User(notification.TargetOrgIdentifier).Notify(notification.RequestOrgNam,
DateTime.Now.ToLocalTime().ToString(),
$" {notification.ToString()}");
public class ChatHub : Hub
{
public async Task SendMessageAsync(string userId, string message)
{
await this.Clients.User(userId).SendAsync("ReciveMessage", message);
}
}
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();//③
}