7

56 分 11 秒のセクションにあるビデオ「 ASP.NET SignalR を使用したリアルタイム Web のスケーリング」に触発されました。

SignalR を使用してサーバーと通信する Web ベースのチャット クライアントを想像してみてください。クライアントが接続すると、そのエンドポイント情報が Azure テーブルに格納されます。

チャット クライアントは、SignalR を介して別のチャット クライアントにメッセージを送信できます。SignalR は、目的の送信先クライアントのエンド ポイント (別のインスタンス上にある可能性があります) を検索し、Web API を使用して、SignalR を介して他のインスタンスにメッセージをクライアントに送信します。

サンプル アプリケーションを github にアップロードしました。

これはすべて、単一の Azure インスタンスがある場合に機能します。ただし、複数の azure インスタンスがある場合、サーバーからクライアントへの SignalR への最後の呼び出しはサイレントに失敗します。動的コードが存在しないか、「悪い」スレッドから外れているか、メッセージが何らかの形で間違ったインスタンスに送信されたか、または私が間違った間違いを犯したようです。

どんなアイデアでも大歓迎です。

Webページはこれで設定されています

<input type="radio" name='ClientId' value='A' style='width:30px'/>Chat client A</br>
<input type="radio" name='ClientId' value='B' style='width:30px'/>Chat client B</br>
<input type='button' id='register' value='Register' />
<input type='text' id='txtMessage' size='50' /><input type='button' id='send' value='Send' />

<div id='history'>
</div>

そしてJSは

<script type="text/javascript">
    $(function () {

        // Declare a proxy to reference the hub. 
        var chat = $.connection.chatHub;

        chat.client.sendMessageToClient = function (message) {
            $('#history').append("<br/>" + message);
        };


        // Start the connection.
        $.connection.hub.start().done(function () {

            $('#register').click(function () {

                // Call the Send method on the hub. 
                chat.server.register($('input[name=ClientId]:checked', '#myForm').val());

            });


            $('#send').click(function () {

                // Call the Send method on the hub. 
                chat.server.sendMessageToServer($('input[name=ClientId]:checked', '#myForm').val(), $('#txtMessage').val());
            });

        });
    });
</script>

ハブは以下の通り。(エンドポイント情報を Azure テーブルに格納するための小さなストレージ クラスがあります)。静的メソッド SendMessageToClient に注意してください。これは最終的に失敗するものです。Web Api クラス (下記) から呼び出されます。

public class ChatHub : Hub
{
    public void Register(string chatClientId)
    {
        Storage.RegisterChatEndPoint(chatClientId, this.Context.ConnectionId);
    }

    /// <summary>
    /// Receives the message and sends it to the SignalR client.
    /// </summary>
    /// <param name="message">The message.</param>
    /// <param name="connectionId">The connection id.</param>
    public static void SendMessageToClient(string message, string connectionId)
    {
        GlobalHost.ConnectionManager.GetHubContext<ChatHub>().Clients.Client(connectionId).SendMessageToClient(message);

        Debug.WriteLine("Sending a message to the client on SignalR connection id: " + connectionId);
        Debug.WriteLine("Via the Web Api end point: " + RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["WebApi"].IPEndpoint.ToString());

    }


    /// <summary>
    /// Sends the message to other instance.
    /// </summary>
    /// <param name="chatClientId">The chat client id.</param>
    /// <param name="message">The message.</param>
    public void SendMessageToServer(string chatClientId, string message)
    {
        // Get the chatClientId of the destination.
        string otherChatClient = (chatClientId == "A" ? "B" : "A");

        // Find out this other chatClientId's end point
        ChatClientEntity chatClientEntity = Storage.GetChatClientEndpoint(otherChatClient);

        if (chatClientEntity != null)
            ChatWebApiController.SendMessage(chatClientEntity.WebRoleEndPoint, chatClientEntity.SignalRConnectionId, message);
    }
}

最後に ChateWebApiController はこれです

public class ChatWebApiController : ApiController
{
    [HttpGet]
    public void SendMessage(string message, string connectionId)
    {
        //return message;
        ChatHub.SendMessageToClient(message, connectionId);
    }

    /// <summary>
    /// This calls the method above but on a different instance via Web API
    /// </summary>
    /// <param name="endPoint">The end point.</param>
    /// <param name="connectionId">The connection id.</param>
    /// <param name="message">The message.</param>
    public static void SendMessage(string endPoint, string connectionId, string message)
    {
        HttpClient client = new HttpClient();
        client.BaseAddress = new Uri("http://" + endPoint);

        // Add an Accept header for JSON format.
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        string url = "http://" + endPoint + "/api/ChatWebApi/SendMessage/?Message=" + HttpUtility.UrlEncode(message) + "&ConnectionId=" + connectionId;

        client.GetAsync(url);
    }

}
4

3 に答える 3

8

第一に、この問題に対するコミュニティの洞察がないため、私はこの問題の真相を解明するのに少し時間がかかりすぎた可能性があります。マイクロソフトは今後数か月以内にこれらの問題に関するガイダンスをリリースすると予想していますが、それまではほとんど独力です。

この問題への答えは非常に複雑ですが、SignalR が内部で実際にどのように機能しているかを理解すれば、すべてが理にかなっています。長い回答で申し訳ありませんが、この問題にふさわしいエネルギーを与えるために必要です。

このソリューションは、複数インスタンスの Azure および SignalR 通信にのみ適用されます。Azure (つまり、Windows Server) を使用していない場合は、おそらく当てはまりません。また、Azure のインスタンスを 1 つだけ実行する予定の場合も、これは当てはまりません。これは http://channel9.msdn.com/Events/Build/2013/3-502特に 43 分 14 秒から最後までの視聴に不可欠です。

それでは…</p>

「箱の側面を読む」と、Azure に接続された SignalR が WebSocket を使用していると信じるようになります。これにより、クライアントと Azure の間の単一のオープン ソケット接続が本質的に常に単一の Azure インスタンスにバインドされ、すべての通信がそのチャネルを介して流れる可能性があるため、私たちの生活はシンプルになります。

もしあなたがこれを信じているなら、あなたは間違っているでしょう。

現在のリリースでは、Azure に対する SignalR は WebSocket を使用しません。(これはhttp://www.asp.net/signalr/overview/getting-started/supported-platformsで文書化されています) クライアントとしての IE10 は、「Forever Frame」を使用します。埋め込まれた iframe。http://campusmvp.net/signalr-ebookにある優れた電子ブックを読むサーバーへの接続を「永久に」開いたままにすることをお勧めします。これは完全にそうではありません。Fiddler を使用すると、クライアントがサーバーと通信する必要があるたびに HTTP 接続が開かれることがわかりますが、最初の通信 (その結果、OnConnect メソッドが呼び出されます) は永続的に開いたままになります。URL は /signalr/connect?transport=foreverFrame&connectionToken= の形式になります。Fiddler のアイコンは、「ダウンロード」を意味する下向きの緑色の矢印であることがわかります。

Azure がロード バランサーを利用していることはわかっています。サーバーにメッセージを送信する必要があるたびに永久フレームが新しい接続を確立することを考えると、ロード バランサーは、SignalR 接続のサーバー側の確立を担当した Azure インスタンスに常にメッセージを送信することをどのように認識しますか? ? 答えは…違います。アプリケーションによっては、これが問題になる場合とそうでない場合があります。Azure へのメッセージを単に録音するか、その他のアクションを実行する必要がある場合は、これ以上読む必要はありません。問題はありません。サーバー側のメソッドが呼び出され、アクションを実行します。単純。

ただし、メッセージを SignalR 経由でクライアントに送り返すか、別のクライアント (つまり、チャット アプリケーション) にメッセージを送信する必要がある場合は、さらに多くの作業を行う必要があります。複数のインスタンスのうち、実際にメッセージを送信できるのはどれですか? どうすれば見つけられますか?その他のインスタンスへのメッセージをどのように取得できますか?

これらすべての側面がどのように相互作用するかを示すために、 https://github.com/daveapsgithub/AzureSignalRInterationで見つけることができるデモ アプリケーションを作成しました 。クライアントにメッセージを正常に送り返す唯一のインスタンスは、「OnConnect」メソッドが受信されたインスタンスであることがすぐにわかります。他のインスタンスのクライアントにメッセージを送信しようとすると、警告なしで失敗します。

また、ロード バランサーがさまざまなインスタンスにメッセージを送信し、「OnConnected」インスタンス以外のインスタンスで応答しようとすると、警告なしで失敗することも示しています。さいわい、メッセージを受信するインスタンスに関係なく、そのクライアントの SignalR 接続 ID は変わりません。(あなたが期待するように)

これらの教訓を念頭に置いて、元の質問を再検討し、https://github.com/daveapsgithub/AzureSignalRWebApi2にあるプロジェクトを更新しました 。現在、Azure テーブル ストレージの処理は少し複雑になっています。OnConnected メソッドにはパラメーターを指定できないため、OnConnected が呼び出されたときに、最初に SignalR 接続 ID と WebApi エンドポイントを Azure テーブル ストレージに格納する必要があります。その後、各クライアントが自身をクライアント ID 'A' またはクライアント ID 'B' として '登録' すると、この登録呼び出しで、その接続 ID の Azure テーブル ストレージを検索し、クライアント ID を適切に設定する必要があります。

A が B にメッセージを送信するとき、メッセージがどのインスタンスで表示されるかはわかりません。しかし、'B' のエンドポイントを検索し、それに対して WebApi 呼び出しを実行するだけで、SignalR が B にメッセージを送信できるため、これは問題ではありません。

注意が必要な 2 つの大きな落とし穴があります。デバッグ中に OnConnected にブレークポイントがあり、コードをステップ実行すると、クライアントはおそらくタイムアウトになり、後続の再接続要求を送信します (必ず Fiddler を確認してください)。OnConnected の検査が完了すると、再接続要求の一部として再度呼び出されることがわかります。何が問題なのですか?問題は、再接続リクエストが、ロード バランサーを通過する必要のある別の HTTP リクエストにあることです。データベースに保存しようとしている別の WebApi エンドポイントを使用して、まったく別のインスタンスをデバッグします。このインスタンスは、「OnConnected」メッセージを介して受信されましたが、「OnConnected」インスタンスではありません。OnConnected メッセージを受信した最初のインスタンスは、クライアントにメッセージを返すことができる唯一のインスタンスです。したがって、要約すると、OnConnected で時間のかかるアクティビティを実行しないでください (必要な場合は、非同期パターンを使用して別のスレッドで実行し、OnConnected がすばやく戻ることができるようにします)。

次に、IE10 の 2 つのインスタンスを使用して、このアーキテクチャを使用する SignalR アプリケーションをテストしないでください。IE と別のブラウザを使用してください。SignalR 接続を確立する 1 つの IE を開いてから別の IE を開くと、最初のブラウザーの SignalR 接続は破棄され、最初の IE は 2 番目の IE の SignalR 接続の使用を開始します。これは実際には信じがたいことですが、Compute Emulator の出力ウィンドウを参照して、この異常性を確認してください。

最初の SignalR が元の接続を放棄したため、その Azure インスタンスも別のインスタンスに "移動" され、WebApi エンド ポイントは Azure テーブルで更新されず、それに送信されたメッセージは警告なしで失敗します。

元の質問の一部として投稿されたソースコードを更新して、それが機能していることを示しました。Azure テーブル ストレージ クラスへの変更を除けば、コードの変更はマイナーなものでした。Onconnected メソッドにコードを追加するだけです。

public override System.Threading.Tasks.Task OnConnected()
    {
        Storage.RegisterChatEndPoint(this.Context.ConnectionId);
        staticEndPoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["WebApi"].IPEndpoint.ToString();
        staticConnectionId = this.Context.ConnectionId;

        return base.OnConnected();
    }

    public void Register(string chatClientId)
    {
        Storage.RegisterChatClientId(chatClientId, this.Context.ConnectionId);
    }
于 2013-08-30T05:44:42.640 に答える
1

コメントされているように、サポートされているスケールアウト ソリューションを検討する必要があります。

Azure を使用していることを考えると、Azure Service Bus Scaleoutが最も適切であるように思われます。

これらの動的メソッド呼び出しの 1 つにタイプミスがある可能性はありますか? 次の方法では

    public static void SendMessageToClient(string message, string connectionId)
        {
            GlobalHost.ConnectionManager.GetHubContext<ChatHub>().Clients
.Client(connectionId).SendMessageToClient(message);
    .....    
        }

クライアント呼び出しはキャメルケースにするべきではありませんか?

  GlobalHost.ConnectionManager.GetHubContext<ChatHub>().Clients
.Client(connectionId).sendMessageToClient(message);
于 2013-08-13T11:20:13.080 に答える
0

早送りして約 5 年後、Microsoft はhttps://azure.microsoft.com/en-us/blog/azure-signalr-service-a-fully-managed-service-to-add-real-time-functionality/を発表しました。

私はそれをテストしていませんが、理論的には、これらの問題はすべて解消されるはずです。

于 2018-06-05T21:26:37.490 に答える