2013-08-12 9 views
6

Zainspirował mnie film "Scaling the Real-time Web with ASP.NET SignalR" w dziale 56 min i 11 sek.Azure, SignalR i Web Api nie wysyłają wiadomości do klienta

Wyobraź sobie internetowego klienta czatowego używającego SignalR do komunikacji z serwerem. Gdy klient łączy się, jego informacje o punkcie końcowym są przechowywane w tabeli Azure.

Klient czatu może wysłać wiadomość do innego klienta czatu przez SignalR, który wyszukuje punkt końcowy interesującego klienta docelowego (może w innej instancji), a następnie za pomocą interfejsu Web API wysyła wiadomość do drugiej instancji do klienta za pośrednictwem SignalR.

Aby wykazać, że mam uploaded a sample application to github.

Wszystko to działa, gdy istnieje jedna instancja platformy Azure. Jeśli jednak istnieje wiele WIELKICH lazurowych instancji, to ostatnie wywołanie SignalR z serwera do klienta po cichu się nie udaje. Jest tak, jak kod dynamiczny po prostu nie istnieje lub znika z "złego" wątku lub wiadomość została w jakiś sposób wysłana do niewłaściwej instancji lub właśnie popełniłem błąd dopingu.

Wszelkie pomysły będą mile widziane.

strona internetowa jest z tego

<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> 

i JS jest

<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> 

Piasta jest następujący. (Mam małą klasę pamięci do przechowywania informacji o punkcie końcowym w tabeli Azure). Zwróć uwagę na metodę statyczną SendMessageToClient. To ostatecznie kończy się niepowodzeniem. Nazywa z klasy Web API (poniżej)

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); 
    } 
} 

Wreszcie ChateWebApiController jest to

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); 
    } 

} 
+1

Czy obejrzałeś wbudowaną obsługę skalowania? http://www.asp.net/signalr/overview/performance-and-scaling/scaleout-in-signalr –

+0

Tak. Moje potrzeby w zakresie skalowania będą łatwo przekraczać ograniczenia prędkości w technologiach kasetowych. Patrz 47 min 45 s. Będę miał WIELU wiadomości na sekundę. Interesują mnie niestandardowe wzorce od 54 min 30 s, a szczególnie 56 min i 11 sec. – DJA

Odpowiedz

7

Po pierwsze, w związku z brakiem jakiegokolwiek wglądu społeczności w ten problem poświęciłem prawdopodobnie trochę zbyt wiele czasu, aby dojść do sedna. Spodziewam się, że Microsoft opublikuje pewne wskazówki dotyczące tych kwestii w nadchodzących miesiącach, ale do tej pory jesteśmy w dużej mierze sami.

Odpowiedź na ten problem jest niezwykle złożona, ale ma sens, gdy zrozumiesz, jak SignalR działa pod maską. Przepraszam za długą odpowiedź, ale jest to konieczne, aby dać temu problemowi energię, na którą zasługuje.

To rozwiązanie odnosi się tylko do komunikacji z wieloma instancjami Azure i SignalR.Jeśli nie korzystasz z platformy Azure (np. Windows Server), prawdopodobnie nie będzie ona obowiązywać Ciebie lub jeśli planujesz uruchomić tylko jedną instancję Azure, ponownie nie będzie to miało zastosowania. Jest to niezbędne podczas oglądania http://channel9.msdn.com/Events/Build/2013/3-502, zwłaszcza od 43min 14 s do końca.

Zaczynamy ...

Jeśli jesteś zapoznania się z boku pudełka "można byłoby prowadzić do przekonania, że ​​SignalR połączony Azure byłoby korzystania WebSockets. Ułatwi nam to życie, ponieważ pojedyncze połączenie z otwartym gniazdem między klientem a Azure będzie z natury stale związane z pojedynczą instancją platformy Azure i cała komunikacja może przepływać przez ten kanał.

Jeśli w to uwierzysz, popełnisz błąd.

W obecnej wersji SignalR przeciwko Azure nie korzysta z WebSockets. (Jest to udokumentowane na http://www.asp.net/signalr/overview/getting-started/supported-platforms) IE10, ponieważ klient użyje "Forever Frame" - nieco źle zdefiniowanego i egzotycznego użycia osadzonych elementów iframe. Czytanie doskonałego ebooka znalezionego pod numerem http://campusmvp.net/signalr-ebook sugeruje, że utrzymuje on połączenie "na zawsze" otwarte dla serwera. Nie dotyczy to całkowicie. Korzystanie z Fiddlera pokazuje, że otwiera ono połączenie HTTP za każdym razem, gdy klient musi komunikować się z serwerem, chociaż początkowa komunikacja (która powoduje wywołanie metody OnConnect) pozostaje trwale otwarta. Adres URL będzie tego formatu/signalr/connect? Transport = foreverFrame & connectionToken = Zobaczysz, że ikona w Skrzypku jest skierowaną w dół zieloną strzałką, co oznacza "pobieranie".

Wiemy, że platforma Azure korzysta z systemu równoważenia obciążenia. Biorąc pod uwagę, że na zawsze ramka ustanowi nowe połączenie za każdym razem, gdy będzie musiała wysłać wiadomość do serwera, to w jaki sposób system równoważący obciążenie wie, aby zawsze wysyłać wiadomość z powrotem do instancji Azure, która była odpowiedzialna za ustanowienie strony serwerowej połączenia SignalR ? Odpowiedź ... nie ma; i w zależności od zastosowania może to być problem lub nie. Jeśli wiadomość na konsolę Azure musi zostać po prostu nagrana lub podjęta inna czynność, nie czytaj dalej. Nie masz problemu. Twoja metoda po stronie serwera zostanie wywołana i wykonasz akcję; prosty.

Jeśli jednak wiadomość musi zostać odesłana do klienta za pośrednictwem SignalR lub wysłana do innego klienta (np. Aplikacja do czatu), to masz znacznie więcej pracy do wykonania. Które z wielu wystąpień może rzeczywiście wysłać wiadomość? Jak go znaleźć? Jak uzyskać wiadomość do tej drugiej instancji?

Aby pokazać, jak wszystkie te aspekty wchodzą w interakcje, napisałem aplikację demonstracyjną, którą można znaleźć pod adresem https://github.com/daveapsgithub/AzureSignalRInteration Aplikacja zawiera wiele szczegółów na swojej stronie internetowej, ale w skrócie, jeśli ją uruchomisz, łatwo to zauważysz jedyną instancją, która pomyślnie wyśle ​​wiadomość z powrotem do klienta, jest instancja, w której odbierana jest metoda "OnConnect". Próba wysłania wiadomości do klienta w dowolnej innej instancji zakończy się niepowodzeniem.

Pokazuje również, że moduł równoważenia obciążenia przekazuje komunikaty do różnych instancji, a próba odpowiedzi na każde wystąpienie, które nie jest instancją "OnConnected", zakończy się niepowodzeniem. Na szczęście, niezależnie od instancji, która otrzymuje komunikat, identyfikator połączenia SignalR pozostaje taki sam dla tego klienta. (jak można się spodziewać)

Z myślą o tych lekcjach ponownie zapoznałem się z oryginalnym pytaniem i zaktualizowałem projekt, który można znaleźć pod adresem https://github.com/daveapsgithub/AzureSignalRWebApi2 Obsługa pamięci podręcznej Azure Table jest teraz nieco bardziej złożona. Ponieważ metody OnConnected nie można podać żadnych parametrów, musimy przechowywać identyfikator połączenia SignalR i punkt końcowy WebApi w pamięci tabeli Azure początkowo po wywołaniu OnConnected.Następnie, gdy każdy klient następnie "rejestruje się" jako albo identyfikator klienta "A", albo identyfikator klienta "B", to wywołanie to musi wyszukać pamięć podręczną Azure Table dla tego identyfikatora połączenia i odpowiednio ustawić identyfikator klienta.

Gdy A wysyła wiadomość do B, nie wiemy, na którą instancję pojawia się wiadomość. Ale to już nie jest problem, ponieważ po prostu szukamy punktu końcowego "B", wykonujemy wywołanie WebApi, a następnie SignalR może wysłać wiadomość do B.

Istnieją dwie poważne pułapki, które należy świadomy. Jeśli debugujesz i posiadasz punkt przerwania w OnConnected i przechodzisz przez kod, klient prawdopodobnie przerwie i wyśle ​​kolejne żądanie ponownego połączenia (pamiętaj, aby spojrzeć na Skrzypek). Po zakończeniu inspekcji OnConnected zobaczysz, że jest ona wywoływana ponownie jako część żądania ponownego połączenia. Jaki może być problem? Problem polega na tym, że żądanie ponownego połączenia dotyczy innego żądania HTTP, które musiało przejść przez moduł równoważenia obciążenia. Będziesz teraz debugować zupełnie inną instancję z innym punktem końcowym WebApi, który ma zostać zapisany w bazie danych. Ta instancja, mimo że została odebrana za pomocą komunikatu "OnConnected", nie jest instancją "OnConnected". Pierwsze wystąpienie, które odebrało komunikat OnConnected, jest jedyną instancją, która może przekazywać wiadomości do klienta. Podsumowując, nie rób żadnych czasochłonnych czynności w OnConnected (i jeśli musisz wtedy użyć jakiegoś wzorca Async, aby uruchomić go w osobnym wątku, aby OnConnected mógł szybko powrócić).

Po drugie, nie używaj dwóch instancji IE10 do testowania aplikacji SignalR korzystających z tej architektury. Użyj IE i innej przeglądarki. Jeśli otworzysz jedną IE, która ustanowi połączenie SignalR, a następnie otworzysz kolejną IE, połączenie SignalR pierwszej przeglądarki zostanie przerwane i pierwszy IE zacznie używać połączenia SignalR drugiego IE. Trudno w to uwierzyć, ale odnieś się do okien wyjściowych Emulatora Obliczeń, aby zweryfikować ten szaleństwo.

Ponieważ pierwszy SignalR porzucił swoje pierwotne połączenie, jego instancja Azure zostanie również "przeniesiona" do innej instancji, punkt końcowy WebApi nie zostanie zaktualizowany w tabeli Azure, a wszelkie wiadomości, które zostaną do niego wysłane, będą po cichu zawieść.

Zaktualizowałem kod źródłowy zamieszczony jako część oryginalnego pytania, aby wykazać, że działa. Oprócz zmian w klasie magazynu tabel Azure zmiany kodu były niewielkie. Po prostu musimy dodać trochę kodu do metody 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); 
    } 
+0

Uratowałeś mi tu tyłek po dniu wyciągania włosów. Dziękuję Ci. – SB2055

1

Jak skomentował to na pewno warto rozważyć supported scale out solutions

Wydawałoby się, biorąc pod uwagę korzystanie Azure, że najbardziej odpowiednie będą wartości Azure Service Bus Scaleout.

Czy w jednym z tych wywołań metod dynamicznych może występować literówka? W poniższej metodzie, czy klient nie powinien wywoływać wielbłąda w obudowie?

GlobalHost.ConnectionManager.GetHubContext<ChatHub>().Clients 
.Client(connectionId).sendMessageToClient(message); 
+0

Zobacz mój poprzedni komentarz. To będzie zbyt wolne dla mojej aplikacji. Częstotliwość wiadomości przekroczy zalecenia dotyczące magistrali serwisowej. – DJA

+0

Czy mógłbyś całkowicie rozważyć inne podejście: próbuj robić odłamki. to znaczy, upewnij się, że wszystkie rozmowy są hostowane na jednym serwerze, więc czat byłby odizolowany na jednym serwerze, ale można skalować wiele rozmów na wiele serwerów? – dove

+0

Moje wymagania dokładnie odpowiadają proponowanemu niestandardowemu wzorowi skalowania opisanemu przez Damiena Edwardsa w 56 min 11 sek. Chcę rozwiązać problem, który został zadany. Sharding nie będzie odpowiednim rozwiązaniem dla mojego problemu. – DJA

Powiązane problemy