· Ten artykuł pochodzi z serii przygotowań do egzaminu 70-503: Windows Communication Foundation.
Wiemy już, że WCF może tworzyć oddzielne instancje klasy serwisu dla poszczególnych wywołań, dla poszczególnych sesji lub używać tylko jednej instancji do obsłużenia wszystkich klientów i ich wywołań.
Dzisiaj dowiemy się jak zarządzać poszczególnymi instancjami klasy serwisu.
Zabezpieczanie serwisu
W rzeczywistym świecie głównym problemem są ataki typu “Odmowa usługi” (ang. Denial of service). Ataki te powodują wyczerpanie się zasobów serwisu tak by go zablokować. WCF udostępnia kilka parametrów, którymi możemy kontrolować dostępność naszego serwisu.
Dławienie
Dławienie (ang. Throttling) po pierwsze zabezpiecza serwis przed przeciążeniem pod wpływem zasypywania wiadomościami, po drugie umożliwia wyrównanie obciążenia serwisu (i całego serwera). W obu przypadkach głównym celem jest ograniczenie ilości przetwarzanych wiadomości w jednostce czasu. Domyślne ustawienia WCF’a nie przewidują dławienia w ogóle. Jeśli włączymy dławienie, WCF sprawdza odpowiednie liczniki przy każdej nadchodzącej wiadomości. Jeśli liczniki zostały przekroczone wiadomości są zbierane do kolejki. Gdy wartości liczników spadną do akceptowalnej wartości, wiadomości z kolejki zostaną przekazane do serwisu.
Do ustawienia dławienia mamy dostępne trzy ustawienia w klasie ServiceThrottlingBehavior:
- MaxConcurrentCalls – liczba jednoczesnych wywołań które serwis zaakceptuje, domyślnie 16,
- MaxConcurrentSessions – maksymalna liczba kanałów wymagających obsługi sesji, które serwis obsłuży, domyślnie 10, każda kolejna próba utworzenia kanału zostanie zakończona wyjątkiem TimeoutException, to ustawienie nie dotyczy bindingów nie obsługujących sesji np. basicHttpBinding,
- MaxConcurrentInstances – maksymalna liczba instancji obiektów implementujących serwis, domyślnie 32, w trybie “per call” wartość jest tym samym co MaxConcurrentCalls ponieważ każde wywołanie ma własną instancję, w trybie “per session”, wartość jest tym samym co MaxConcurrentSessions, dla singletona, ponieważ zawsze jest tylko jedna instancja obiektu obsługującego serwis, wartość ma znaczenie tylko gdy jest użyty IInstanceContextProvider.
Poniżej przykłady konfiguracji dławienia:
1: <behaviors>
2: <serviceBehaviors>
3: <behavior name="throttlingBehavior">
4: <serviceThrottling maxConcurrentCalls="10"
5: maxConcurrentInstances="10"
6: maxConcurrentSessions="5"/>
7: </behavior>
8: </serviceBehaviors>
9: </behaviors>
1: ServiceHost host = new ServiceHost( typeof(UpdateService),
2: new Uri("http://localhost:8080/UpdateService"));
3: host.AddServiceEndpoint( "IUpdateService",
4: new WSHttpBinding(), String.Empty);
5: ServiceThrottlingBehavior throttlingBehavior = new ServiceThrottlingBehavior();
6: throttlingBehavior.MaxConcurrentCalls = 10;
7: throttlingBehavior.MaxConcurrentInstances = 10;
8: throttlingBehavior.MaxConcurrentSessions = 5;
9: host.Description.Behaviors.Add(throttlingBehavior);
10: host.Open();
Tak jak zostało wspomniane, przekroczenie limitów spowoduje wyrzucenie wyjątku TimeoutException u klienta. Ponieważ w każdym przypadku jest to ten sam wyjątek, trzeba przyjrzeć się dokładnie miejscu jego wystąpienia. Jeśli problem dotyczy liczby jednoczesnych sesji (MaxConcurrentSessions) wyjątek będzie wyrzucany przy wywołaniu metody SendPreamble. Jeśli wyjątek jest wyrzucany przy metodzie Send, bardziej prawdopodobnym jest problem z maksymalną liczbą wywołań (MaxConcurrentCalls).
Jeszcze mała uwaga. Dobrze jest konfigurować dławienie z poziomu kodu. Konfigurowanie dławienia w pliku konfiguracyjnym umożliwia administratorowi serwera ustawienie tych opcji np. dopiero gdy będzie tego potrzebował.
Odczytywanie ustawień dławienia
Istnieje możliwość odczytania (ale nie aktualizacji) aktualnych ustawień dławienia po uruchomieniu serwisu.
Klasa ServiceHost udostępnia kolekcję dyspozytorów (ang. dispatchers) w właściwości ChannelDispatchers. Jest to kolekcja obiektów typu ChannelDispatcher. Każdy z nich ma właściwość ServiceThrottle. Przez tą właściwość mamy dostęp do ustawień dławienia, w tym MaxConcurrentCalls, MaxConcurrentInstances i MaxConcurrentSessions. Poniżej przykład kodu:
1: ChannelDispatcher dispatcher =
2: OperationContext.Current.Host.ChannelDispatchers[0] as ChannelDispatcher;
3: ServiceThrottle throttle = dispatcher.ServiceThrottle;
4: Trace.WriteLine(String.Format("MaxConcurrentCalls = {0}",
5: throttle.MaxConcurrentCalls));
6: Trace.WriteLine(String.Format("MaxConcurrentSessions = {0}",
7: throttle.MaxConcurrentSessions));
8: Trace.WriteLine(String.Format("MaxConcurrentInstances = {0}",
9: throttle.MaxConcurrentInstances));
Limity
WCF umożliwia ustawienie limitu (ang. quota) na ilość pamięci wykorzystywanej przez host serwisu i implementujące go obiekty. Zabezpiecza to przed nadmiernym alokowaniem pamięci. Każde kolejne wywołania które spowodują brak możliwości zaalokowania pamięci do obsługi tych wywołań zakończą sie wyjątkiem OutOfMemoryException lub StackOverflowException.
Jeśli ustawimy limity to po przekroczeniu limitu będzie wyrzucany wyjątek QuotaExceededException a wiadomość jest po prostu pomijana i serwis przechodzi do obsługi kolejnej.
Mamy kilka dostępnych limitów:
MaxReceivedMessageSize
– maksymalna wielkość wiadomości, domyślnie 65536 bajtów
<bindings>
<netTcpBinding>
<binding name="netTcp"
maxReceivedMessageSize="128000" />
</netTcpBinding>
</bindings>
NetTcpBinding binding = new NetTcpBinding();
binding.MaxReceivedMessageSize = 128000;
ServiceHost host = new ServiceHost( typeof(UpdateService),
new Uri("net.tcp://localhost:1234/UpdateService"));
host.AddServiceEndpoint( "IUpdateService",
binding, String.Empty);
host.Open();
ReaderQuotas
– limity złożoności wiadomości,
Obiekt ReadeQuotas ma kilka właściwości, które umożliwiają określenie maksymalnej złożoności wiadomości:
- MaxDepth – maksymalna złożoność (głębokość zagnieżdżenia XML’a reprezentującego wiadomość), domyślnie 32
- MaxStringContentLength – maksymalna długość wartości typu string w wiadomości (wartości lub atrybutu, lub tekstu wewnątrz znacznika), domyślnie 8192 bajty,
- MaxArrayLength – maksymalna liczba elementów w pojedynczej tablicy, domyślnie 16384,
- MaxBytesPerRead – maksymalna liczba bajtów zwracanych przy metodzie Read podczas przetwarzania wiadomości, domyślnie 4096 bajtów,
- MaxNameTableCharCount – maksymalna liczba znaków w nazwie tablicy, domyślnie 16384 znaków.
Może ograniczenie liczby zwracanych bajtów w metodzie Read wydaje się dziwne, ale służy to zabezpieczeniu przeciw atakom DoS, których wiadomość zawiera bardzo długie rozpoczynające tagi w wiadomości. Ponieważ do przetworzenia konieczne jest wczytanie całego taga do pamięci, ograniczenie tego zapobiega przepełnieniom.
Rozgraniczenia operacji
Teoretycznie obsługa sesji gwarantuje nam, że wiadomo które wiadomości przychodzą od którego klienta. Sesja umożliwia też zapamiętywanie stanu pomiędzy poszczególnymi wywołaniami. A czy mamy gwarancję że wywołania przyjdą w odpowiedniej kolejności? Nie! Wystarczy że dwie metody będą wywoływane w różnych wątkach i już mamy zachwianie kolejności. Protokół HTTP w ogóle nie gwarantuje kolejności przesyłanych wiadomości.
WCF udostępnia mechanizm oznaczania, które metody nie mogą być wywoływane jako pierwsze lub jako ostatnie przez ustawienie właściwości IsInitiating i IsTerminating w atrybucie OperationContract.
Jeśli IsInitiating jest ustawiony na true w momencie gdy sesja nie została utworzona,sesja jest tworzona. Jeśli sesja już istnieje, metoda jest wywoływana w ramach tej sesji.
Jeśli IsTerminating jest ustawiony na true, gdy metoda jest wywoływana, sesja jest kończona. Nie oznacza to tego samego co zakończenie połączenia, klient nadal musi wywołać metodę Close na obiekcie proxy aby zamknąć połączenie. Każde kolejne próby wywoływania metod serwisu po zakończeniu sesji będą generować wyjątki InvalidOperationException.
Domyślne wartości dla tych właściwości to true dla IsInitiating i false dla IsTerminating.
Poniżej przykład kontraktu z odpowiednimi wartościami tych atrybutów:
[ServiceContract(SessionMode = SessionMode.Required)]
public interface IProcessOrders
{
[OperationContract]
void InitializeOrder(int customerId);
[OperationContract(IsInitiating=false)]
void AddOrderLine(string productId, int quantity);
[OperationContract(IsInitiating=false)]
double GetOrderTotal();
[OperationContract(IsInitiating=false, IsTerminating=true)]
bool SubmitOrder();
}
Z powyższych czterech metod tylko InitializeOrder może rozpocząć sesję, więc każda inna metoda wywołana przed nią spowoduje wyrzucenie wyjątku OperationException. Wywołanie metody SubmitOrder spowoduje zakończenie sesji, żadna inna metoda nie może już być wywołana w tym połączeniu.
Aby użyć tych właściwości serwis musi obsługiwać sesję (przez tryb “per session”, albo “singleton”).
Deaktywowanie instancji
Gdy rozpoczynana jest nowa sesja WCF tworzy nowy kontekst (Context) i w ramach tego kontekstu tworzone są instancje obiektów obsługujące wywołania. WCF pozwala nawet utworzyć kontekst bez instancji. Przy pomocy właściwości ReleaseInstanceMode atrybutu OperationBehavior możemy decydować kiedy instancje mają być zwalniane. Dostępne są następujące opcje:
- BeforeCall – nowa instancja jest tworzona z początkiem wywołania, jeśli instancja już istnieje, jest deaktywowana i wywoływała jest jej metoda Dispose, klient jest w tym czasie zablokowany,
- AfterCall – aktualna instancja jest deaktywowana i zwalniana gdy metoda jest zakończona,
- BeforeAndAfterCall – aktualna instancja jest zwalniana przed wywołaniem, tworzona jest nowa, która zaraz po zakończeniu metody jest zwalniana,
- None – instancja nie jest zwalniana między wywołaniami metod, wartość domyślna.
public class UpdateService : IUpdateService
{
[OperationBehavior(ReleaseInstanceMode=ReleaseInstanceMode.BeforeAndAfterCall)]
public void Update()
{
// Implementation code goes here
}
}
Możemy także zwolnić instancję zaraz po zakończeniu aktualnej metody przez :
OperationContext.Current.InstanceContext.ReleaseServiceInstance();
W kolejnych artykułach opowiemy o obsłudze transakcji.