web-dev-qa-db-ja.com

.Net CoreSignalR-接続タイムアウト-ハートビートタイマー-接続状態変更処理

前もって明確にするために、この質問は.Net Core SignalRに関するものであり、以前のバージョンではありません。

新しいSignalRには、IISの背後にあるWebSocketに問題があります(Chrome/Win7/IIS Expressで動作させることができません)。代わりに、Server Sent Events(SSE)を使用しています。ただし、問題は、約2分後にタイムアウトすると、接続状態が2から3になることです。自動再接続が削除されました(以前のバージョンでは、とにかくうまく機能していなかったようです)。

クライアントがタイムアウトするのを防ぐために、ハートビートタイマーを今すぐ実装したいと思います。30秒ごとのティックでうまくいく可能性があります。

11月10日更新

これで、サーバー側のHeartbeatを実装できました。これは、基本的にRicardo Peresの https://weblogs.asp.net/ricardoperes/signalr-in-asp-net-core から取得したものです。

  • startup.csで、public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)に追加します
_    app.UseSignalR(routes =>  
    {  
        routes.MapHub<TheHubClass>("signalr");  
    });

    TimerCallback SignalRHeartBeat = async (x) => {   
    await serviceProvider.GetService<IHubContext<TheHubClass>>().Clients.All.InvokeAsync("Heartbeat", DateTime.Now); };
    var timer = new Timer(SignalRHeartBeat).Change(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(30));            
_
  • HubClass

HubClassには、public async Task HeartBeat(DateTime now) => await Clients.All.InvokeAsync("Heartbeat", now);を追加しました

明らかに、タイマー、送信されるデータ(DateTimeを送信するだけです)、およびクライアントメソッド名の両方が異なる可能性があります。

.Net Core2.1以降を更新します

以下のコメントを参照してください。タイマーコールバックは使用しないでください。これを行うために、IHostedService(または抽象BackgroundService)を実装しました。

_public class HeartBeat : BackgroundService
{
    private readonly IHubContext<SignalRHub> _hubContext;
    public HeartBeat(IHubContext<SignalRHub> hubContext)
    {
        _hubContext = hubContext;
    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _hubContext.Clients.All.SendAsync("Heartbeat", DateTime.Now, stoppingToken);
            await Task.Delay(30000, stoppingToken);
        }            
    }
}
_

スタートアップクラスで、services.AddSignalR();の後にワイヤリングします。

_services.AddHostedService<HeartBeat>();
_
  • クライアント
_    var connection = new signalR.HubConnection("/signalr", { transport: signalR.TransportType.ServerSentEvents });
    connection.on("Heartbeat", serverTime => { console.log(serverTime); });
_

最初の質問の残りの部分

残っているのは、クライアントを適切に再接続する方法です。 IOが一時停止された後(ブラウザのコンピュータがスリープ状態になった、接続が失われた、Wi-Fiが変更されたなど)

少なくとも接続が切断されるまで、正しく機能するクライアント側のハートビートを実装しました。

  • ハブクラス:public async Task HeartBeatTock() => await Task.CompletedTask;
  • クライアント:

    var heartBeatTockTimer; function sendHeartBeatTock(){connection.invoke( "HeartBeatTock"); } connection.start()。then(args => {heartBeatTockTimer = setInterval(sendHeartBeatTock、10000);});

ブラウザが一時停止した後IOたとえば、invokeメソッドは例外をスローします-非同期であるため、単純なtry/catchではキャッチできません。HeartBeatTockに対して実行しようとしたのは何かでしたlike(擬似コード):

_function sendHeartBeatTock
    try connection.invoke("HeartbeatTock)
    catch exception
         try connection.stop()
         catch exception (and ignore it)
         finally
              connection = new HubConnection().start()
    repeat try connection.invoke("HeartbeatTock")
    catch exception
        log("restart did not work")
        clearInterval(heartBeatTockTimer)
        informUserToRefreshBrowser()
_

現在、これはいくつかの理由で機能しません。 invokeは、非同期で実行されているため、コードブロックの実行後に例外をスローします。 .catch()メソッドを公開しているように見えますが、そこで自分の考えを適切に実装する方法がわかりません。もう1つの理由は、新しい接続を開始するには、「connection.on( "send" ...)」のようなすべてのサーバー呼び出しを再実装する必要があるためです。これはばかげているように見えます。

再接続するクライアントを適切に実装する方法に関するヒントをいただければ幸いです。

8
ExternalUse

私は今、実用的な解決策を持っています(これまでのところChromeとFFでテスト済み))。このようなもので、私はここに私の解決策を投稿しています:

ハートビート-「ティック」メッセージ(サーバーがクライアントに定期的にpingを実行する)は、上記の質問で説明されています。クライアント(「Tock」部分)には、次のものがあります。

  • コールバックメソッド(connection.on())を繰り返すことができるように、接続を登録する関数。そうしないと、「新しいHubConnection」を再起動しただけで失われます。
  • tockTimerを登録する関数
  • そして実際にTockpingを送信する関数

Tockメソッドは、送信時にエラーをキャッチし、新しい接続を開始しようとします。タイマーが動作し続けるので、新しい接続を登録してから、ただ座って次の呼び出しを待ちます。クライアントをまとめる:

// keeps the connection object
var connection = null;
// stores the ID from SetInterval
var heartBeatTockTimer = 0;
// how often should I "tock" the server
var heartBeatTockTimerSeconds = 10;
// how often should I retry after connection loss?
var maxRetryAttempt = 5;
// the retry should wait less long then the TockTimer, or calls may overlap
var retryWaitSeconds = heartBeatTockTimerSeconds / 2;
// how many retry attempts did we have?
var currentRetryAttempt = 0;
// helper function to wait a few seconds
$.wait = function(miliseconds) {
    var defer = $.Deferred();
    setTimeout(function() { defer.resolve(); }, miliseconds);
    return defer;
};
// first routine start of the connection
registerSignalRConnection();
function registerSignalRConnection() {
    ++currentRetryAttempt;
    if (currentRetryAttempt > maxRetryAttempt) {
        console.log("Clearing registerHeartBeatTockTimer");
        clearInterval(heartBeatTockTimer);
        heartBeatTockTimer = 0;
        throw "Retry attempts exceeded.";
    }
    if (connection !== null) {
        console.log("registerSignalRConnection was not null", connection);
        connection.stop().catch(err => console.log(err));
    }
    console.log("Creating new connection");
    connection = new signalR.HubConnection("/signalr", { transport: signalR.TransportType.ServerSentEvents });
    connection.on("Heartbeat", serverTime => { console.log(serverTime); });
    connection.start().then(() => {
        console.log("Connection started, starting timer.");
        registerHeartBeatTockTimer();
    }).catch(exception => {
        console.log("Error connecting", exception, connection);
    });
}
function registerHeartBeatTockTimer() {
    // make sure we're registered only once
    if (heartBeatTockTimer !== 0) return;
    console.log("Registering registerHeartBeatTockTimer");
    if (connection !== null)
        heartBeatTockTimer = setInterval(sendHeartBeatTock, heartBeatTockTimerSeconds * 1000);
    else
        console.log("Connection didn't allow registry");
}

function sendHeartBeatTock() {
    console.log("Standard attempt HeartBeatTock");
    connection.invoke("HeartBeatTock").then(() => { 
         console.log("HeartbeatTock worked.") })
         .catch(err => {
             console.log("HeartbeatTock Standard Error", err);
             $.wait(retryWaitSeconds * 1000).then(function() {
                 console.log("executing attempt #" + currentRetryAttempt.toString());
             registerSignalRConnection();
         });
         console.log("Current retry attempt: ", currentRetryAttempt);
     });
 }
1
ExternalUse

これは、IISの背後でSignalR Coreを実行している場合の 問題 です。 IISは2分後にアイドル状態の接続を閉じます。長期計画では、キープアライブメッセージを追加します。これにより、副作用としてIIS接続。今のところ問題を回避するには、次のことができます。

  • クライアントに定期的にメッセージを送信する
  • IIS as described hereでアイドルタイムアウト設定を変更します
  • クライアント側で接続が閉じられた場合は、接続を再開します
  • 別のトランスポートを使用する(たとえば、IISの背後にあるWin7/Win2008 R2ではwebSocketを使用できないため、長いポーリング)
2
Pawel

ExternalUseの answer ..に基づくクライアントバージョン.

import * as signalR from '@aspnet/signalr'
import _ from 'lodash'

var connection = null;
var sendHandlers = [];
var addListener = f => sendHandlers.Push(f);

function registerSignalRConnection() {

    if (connection !== null) {
        connection.stop().catch(err => console.log(err));
    }

    connection = new signalR.HubConnectionBuilder()
        .withUrl('myHub')
        .build();

    connection.on("Heartbeat", serverTime =>
        console.log("Server heartbeat: " + serverTime));

    connection.on("Send", data =>
        _.each(sendHandlers, value => value(data)));

    connection.start()
        .catch(exception =>
            console.log("Error connecting", exception, connection));
}

registerSignalRConnection();

setInterval(() =>
    connection.invoke("HeartBeatTock")
        .then(() => console.log("Client heatbeat."))
        .catch(err => {    
            registerSignalRConnection();
        }), 10 * 1000);

export { addListener };
1
Ian Warburton