Implementacja internetowego interfejsu API

Starannie zaprojektowany internetowy interfejs API RESTful definiuje zasoby, relacje i schematy nawigacji, które są dostępne dla aplikacji klienckich. Podczas implementowania i wdrażania internetowego interfejsu API większe znaczenie mają fizyczne wymagania środowiska hostującego internetowy interfejs API oraz konstrukcja tego interfejsu niż struktura logiczna danych. Te wskazówki koncentrują się na najlepszych rozwiązaniach dotyczących implementowania internetowego interfejsu API i publikowania go w celu udostępnienia go aplikacjom klienckim. Aby uzyskać szczegółowe informacje na temat projektowania internetowego interfejsu API, zobacz Projektowanie internetowego interfejsu API.

Przetwarzanie żądań

Podczas implementowania kodu przeznaczonego do obsługi żądań należy wziąć pod uwagę poniższe kwestie.

Akcje GET, PUT, DELETE, HEAD i PATCH powinny być idempotentne

Kod, który implementuje te żądania, nie powinien powodować żadnych efektów ubocznych. Żądanie powtórzone względem tego samego zasobu powinno dać w wyniku ten sam stan. Na przykład wysłanie wielu żądań DELETE do tego samego identyfikatora URI powinno mieć taki sam efekt, chociaż kod stanu HTTP w komunikatach odpowiedzi może być inny. Pierwsze żądanie DELETE może zwrócić kod stanu 204 (Brak zawartości), podczas gdy kolejne żądanie DELETE może zwrócić kod 404 (Nie znaleziono).

Uwaga

Artykuł Idempotency Patterns on Jonathan Oliver's blog (Wzorce idempotentności) zawiera omówienie idempotentności i sposobu ich powiązania z operacjami zarządzania danymi.

Akcja POST, która tworzy nowe zasoby, nie powinna mieć niepowiązanych efektów ubocznych

Jeśli żądanie POST ma na celu utworzenie nowego zasobu, skutki żądania powinny być ograniczone do nowego zasobu (i ewentualnie wszelkich bezpośrednio powiązanych zasobów, jeśli istnieje jakiś związek). Na przykład w systemie handlu elektronicznego żądanie POST, które tworzy nowe zamówienie dla klienta, może również zmienić poziomy zapasów i wygenerować informacje rozliczeniowe, ale nie powinno modyfikować informacji niezwiązanych bezpośrednio z zamówieniem lub mieć inne skutki uboczne dla ogólnego stanu systemu.

Unikanie implementowania operacji POST, PUT i DELETE z dużą liczbą żądań

Obsługa żądań POST, PUT i DELETE w kolekcjach zasobów. Żądanie POST może zawierać szczegóły dotyczące wielu nowych zasobów i dodawać je wszystkie do tej samej kolekcji, żądanie PUT może zamieniać cały zestaw zasobów w kolekcji, a żądanie DELETE może usuwać całą kolekcję.

Dzięki obsłudze protokołu OData w internetowym interfejsie ASP.NET API 2 można dzielić żądania na partie. Aplikacja kliencka może spakować kilka żądań internetowego interfejsu API i wysłać je do serwera jako pojedyncze żądanie HTTP, po czym odebrać jedną odpowiedź HTTP zawierającą odpowiedzi na poszczególne żądania. Aby uzyskać więcej informacji, zobacz Introducing batch support in Web API and Web API OData (Wprowadzenie obsługi wsadowej w internetowym interfejsie API i internetowym interfejsie API OData).

Wysyłanie odpowiedzi zgodnie ze specyfikacją protokołu HTTP

Internetowy interfejs API musi zwracać komunikaty zawierające prawidłowy kod stanu protokołu HTTP, aby umożliwić klientowi określenie sposobu obsługi wyniku, odpowiednie nagłówki HTTP, aby umożliwić klientowi poznanie charakteru wyniku, oraz poprawnie sformatowaną treść pozwalającą klientowi przeanalizować wynik.

Na przykład operacja POST powinna zwrócić kod stanu 201 (Utworzono), a komunikat odpowiedzi powinien zawierać w nagłówku Location identyfikator URI nowo utworzonego zasobu.

Zapewnienie obsługi negocjowania zawartości

Treść komunikatu odpowiedzi może zawierać dane w różnych formatach. Na przykład żądanie HTTP GET może zwracać dane w formacie JSON lub XML. Żądanie przesłane przez klienta może obejmować nagłówek Accept określający obsługiwane przez niego formaty danych. Te formaty są określone jako typy nośnika. Na przykład klient, który wystawia żądanie GET, które pobiera obraz, może określić nagłówek Accept z listą typów multimediów, które klient może obsłużyć, takich jak image/jpeg, image/gif, image/png. Kiedy internetowy interfejs API zwraca wynik, powinien sformatować dane przy użyciu jednego z tych typów nośnika i określić format w nagłówku Content-Type odpowiedzi.

Jeśli klient nie ma określonego nagłówka Accept, wówczas użyj rozsądnego formatu domyślnego dla treści odpowiedzi. Na przykład dla struktury internetowego interfejsu API ASP.NET domyślnym formatem danych tekstowych jest JSON.

Podejście charakterystyczne dla architektury HATEOAS umożliwia klientowi nawigowanie i odnajdywanie zasobów od pierwszego punktu początkowego. Odbywa się to z wykorzystaniem linków zawierających identyfikatory URI. Kiedy klient wysyła żądanie HTTP GET w celu uzyskania zasobu, odpowiedź powinna zawierać identyfikatory URI. Pozwalają one aplikacji klienckiej szybko zlokalizować wszelkie bezpośrednio powiązane zasoby. Klient może na przykład złożyć wiele zamówień za pośrednictwem internetowego interfejsu API do obsługi rozwiązania z zakresu handlu elektronicznego. Kiedy aplikacja kliencka pobiera szczegóły dotyczące danego klienta, odpowiedź powinna zawierać linki, które umożliwiają aplikacji klienckiej wysłanie żądań HTTP GET w celu pobrania tych zamówień. Linki w stylu HATEOAS powinny opisywać pozostałe operacje (POST, PUT, DELETE itd.) obsługiwane przez poszczególne połączone zasoby oraz odpowiedni identyfikator URI umożliwiający wykonanie każdego żądania. To podejście zostało szczegółowo opisane w temacie Projektowanie interfejsu API.

Aktualnie nie istnieją standardy, które określają sposób implementacji architektury HATEOAS, ale poniższy przykład przedstawia jedno z możliwych podejść. W tym przykładzie żądanie HTTP GET, które znajduje szczegóły dla klienta, zwraca odpowiedź zawierającą linki HATEOAS odwołujące się do zamówień dla tego klienta:

GET https://adventure-works.com/customers/2 HTTP/1.1
Accept: text/json
...
HTTP/1.1 200 OK
...
Content-Type: application/json; charset=utf-8
...
Content-Length: ...
{"CustomerID":2,"CustomerName":"Bert","Links":[
    {"rel":"self",
    "href":"https://adventure-works.com/customers/2",
    "action":"GET",
    "types":["text/xml","application/json"]},
    {"rel":"self",
    "href":"https://adventure-works.com/customers/2",
    "action":"PUT",
    "types":["application/x-www-form-urlencoded"]},
    {"rel":"self",
    "href":"https://adventure-works.com/customers/2",
    "action":"DELETE",
    "types":[]},
    {"rel":"orders",
    "href":"https://adventure-works.com/customers/2/orders",
    "action":"GET",
    "types":["text/xml","application/json"]},
    {"rel":"orders",
    "href":"https://adventure-works.com/customers/2/orders",
    "action":"POST",
    "types":["application/x-www-form-urlencoded"]}
]}

W tym przykładzie dane klienta są reprezentowane przez klasę Customer pokazaną w poniższym fragmencie kodu. Linki HATEOAS znajdują się we właściwości kolekcji Links:

public class Customer
{
    public int CustomerID { get; set; }
    public string CustomerName { get; set; }
    public List<Link> Links { get; set; }
    ...
}

public class Link
{
    public string Rel { get; set; }
    public string Href { get; set; }
    public string Action { get; set; }
    public string [] Types { get; set; }
}

Operacja HTTP GET pobiera dane klienta z magazynu i tworzy obiekt Customer, a następnie wypełnia kolekcję Links. Wynikiem jest komunikat odpowiedzi w formacie JSON. Każdy link zawiera następujące pola:

  • Relacja (Rel) między zwracanym obiektem a obiektem opisanym przez łącze. W tym przypadku self wskazuje, że łącze jest odwołaniem z powrotem do samego obiektu (podobnie jak this wskaźnik w wielu językach obiektowych) i orders jest nazwą kolekcji zawierającej powiązane informacje o kolejności.
  • Hiperlink (Href) do obiektu opisywanego przez link w formie identyfikatora URI.
  • Typ żądania HTTP (Action), które może zostać wysłane do tego identyfikatora URI.
  • Format dowolnych danych (Types), który powinien zostać podany w żądaniu HTTP lub może zostać zwrócony w odpowiedzi — w zależności od typu żądania.

Linki HATEOAS przedstawione w przykładowej odpowiedzi HTTP oznaczają, że aplikacja kliencka może wykonywać następujące operacje:

  • Wysłanie żądania HTTP GET do identyfikatora URI https://adventure-works.com/customers/2 w celu pobrania szczegółów dotyczących klienta (ponownie). Dane mogą zostać zwrócone w formacie XML lub JSON.
  • Wysłanie żądania HTTP PUT do identyfikator URI https://adventure-works.com/customers/2 w celu zmodyfikowania szczegółów dotyczących klienta. Nowe dane muszą zostać zawarte w komunikacie odpowiedzi w formacie x-www-form-urlencoded.
  • Wysłanie żądania HTTP DELETE do identyfikatora URI https://adventure-works.com/customers/2 w celu usunięcia klienta. Żądanie nie oczekuje żadnych dodatkowych informacji ani zwracanych danych w treści komunikatu odpowiedzi.
  • Wysłanie żądania HTTP GET do identyfikatora URI https://adventure-works.com/customers/2/orders w celu znalezienia wszystkich zamówień klienta. Dane mogą zostać zwrócone w formacie XML lub JSON.
  • Żądanie HTTP POST do identyfikatora URI https://adventure-works.com/customers/2/orders w celu utworzenia nowego zamówienia dla tego klienta. Dane w komunikacie żądania muszą mieć format x-www-form-urlencoded.

Obsługa wyjątków

Poniższe zagadnienia są przydatne, jeśli operacja zgłosi nieprzechwycony wyjątek.

Przechwytywanie wyjątków i zwracanie klientom zrozumiałej odpowiedzi

Kod, który implementuje operację HTTP, powinien zapewnić kompleksową obsługę wyjątków, nie pozwalając, aby nieprzechwycone wyjątki dostawały się na platformę. Jeśli wyjątek uniemożliwia pomyślne ukończenie operacji, może on zostać uwzględniony w komunikacie odpowiedzi, ale powinien mu towarzyszyć zrozumiały opis błędu, który spowodował wystąpienie wyjątku. Nie wystarczy tylko zwrócić kod stanu 500 w dowolnej sytuacji — wyjątek powinien również obejmować odpowiedni kod stanu HTTP. Jeśli na przykład żądanie użytkownika powoduje aktualizację bazy danych naruszającą określone ograniczenie (np. jest to próba usunięcia klienta, który ma zamówienia oczekujące), wówczas powinien zostać zwrócony stan kodu 409 (Konflikt), a w treści komunikatu powinna zostać opisana przyczyna konfliktu. Jeśli żądania nie można zrealizować z innego powodu, może zostać zwrócony kod stanu 400 (Nieprawidłowe żądanie). Pełną listę kodów stanu HTTP można znaleźć na stronie Definicje kodu stanu w witrynie internetowej W3C.

W przykładowym kodzie są uwzględnione różne warunki i jest zwrócona właściwa odpowiedź.

[HttpDelete]
[Route("customers/{id:int}")]
public IHttpActionResult DeleteCustomer(int id)
{
    try
    {
        // Find the customer to be deleted in the repository
        var customerToDelete = repository.GetCustomer(id);

        // If there is no such customer, return an error response
        // with status code 404 (Not Found)
        if (customerToDelete == null)
        {
            return NotFound();
        }

        // Remove the customer from the repository
        // The DeleteCustomer method returns true if the customer
        // was successfully deleted
        if (repository.DeleteCustomer(id))
        {
            // Return a response message with status code 204 (No Content)
            // To indicate that the operation was successful
            return StatusCode(HttpStatusCode.NoContent);
        }
        else
        {
            // Otherwise return a 400 (Bad Request) error response
            return BadRequest(Strings.CustomerNotDeleted);
        }
    }
    catch
    {
        // If an uncaught exception occurs, return an error response
        // with status code 500 (Internal Server Error)
        return InternalServerError();
    }
}

Napiwek

Nie dołączaj informacji, które mogą być przydatne dla osoby atakującej próbującej przeniknąć przez interfejs API.

Wiele serwerów internetowych przechwytuje sytuacje powodujące występowanie błędów, zanim dotrą do internetowego interfejsu API. Jeśli na przykład skonfigurujesz uwierzytelnianie dla witryny internetowej, a użytkownik nie poda prawidłowych danych uwierzytelniających, serwer internetowy powinien w odpowiedzi podać kod stanu 401 (Brak autoryzacji). Po uwierzytelnieniu klienta kod może wykonać własne testy, aby sprawdzić, czy klient powinien mieć dostęp do żądanego zasobu. Jeśli ta autoryzacja nie powiedzie się, powinien zostać zwrócony kod stanu 403 (Zabronione).

Spójna obsługa wyjątków i rejestrowania informacji o błędach

Aby zapewnić spójną obsługę wyjątków, warto rozważyć zaimplementowanie globalnej strategii obsługi błędów w całym internetowym interfejsie API. Warto również zapewnić rejestrowanie błędów z uwzględnieniem szczegółowych informacji o każdym wyjątku. Rejestr ten może zawierać szczegółowe dane, jeśli nie jest dostępny przez Internet dla klientów.

Rozróżnienie błędów po stronie klienta i błędów po stronie serwera

Protokół HTTP rozróżnia błędy spowodowane przez aplikację kliencką (kody stanu HTTP 4xx) i błędy spowodowane nieprawidłowościami na serwerze (kody stanu HTTP 5xx). W komunikatach stanowiących odpowiedź na błąd należy przestrzegać tej konwencji.

Optymalizowanie dostępu do danych po stronie klienta

W środowisku rozproszonym (np. obejmującym serwer internetowy i aplikacje klienckie) sieć stanowi częste źródło problemów. Może ona stanowić wąskie gardło zwłaszcza wtedy, gdy aplikacja kliencka często wysyła żądania lub odbiera dane. W związku z tym należy dążyć do zminimalizowania ruchu w sieci. Podczas implementowania kodu służącego do pobrania i przechowywania danych warto wziąć pod uwagę następujące kwestie:

Obsługa przechowywania w pamięci podręcznej po stronie klienta

Protokół HTTP 1.1 obsługuje przechowywanie w pamięci podręcznej na klientach i serwerach pośrednich. Żądanie jest kierowane przy użyciu nagłówka Cache-Control. Jeśli aplikacja kliencka wysyła żądanie HTTP GET do internetowego interfejsu API, odpowiedź może obejmować nagłówek Cache-Control, który wskazuje, czy dane w treści odpowiedzi można bezpiecznie przechowywać w pamięci podręcznej na kliencie lub serwerze pośrednim, przez który wiedzie trasa żądania. Nagłówek informuje również o czasie pozostałym do wygaśnięcia żądania i uznania go za przestarzałe.

Poniższy przykład przedstawia żądanie HTTP GET i powiązaną z nim odpowiedź, która zawiera nagłówek Cache-Control:

GET https://adventure-works.com/orders/2 HTTP/1.1
HTTP/1.1 200 OK
...
Cache-Control: max-age=600, private
Content-Type: text/json; charset=utf-8
Content-Length: ...
{"orderID":2,"productID":4,"quantity":2,"orderValue":10.00}

W tym przykładzie nagłówek Cache-Control informuje, że zwrócone dane powinny wygasnąć po 600 sekundach, są przeznaczone tylko dla jednego klienta i nie mogą być przechowywane w udostępnionej pamięci podręcznej używanej przez innych klientów (są oznaczone jako private). W nagłówku Cache-Control można też zdefiniować dla danych dyrektywę public, a nie private. Wówczas dane mogą być przechowywane w udostępnionej pamięci podręcznej. Można także określić dyrektywę no-store, co oznacza, że danych nie można przechowywać na kliencie. Poniższy przykładowy kod przedstawia sposób utworzenia nagłówka Cache-Control w komunikacie odpowiedzi:

public class OrdersController : ApiController
{
    ...
    [Route("api/orders/{id:int:min(0)}")]
    [HttpGet]
    public IHttpActionResult FindOrderByID(int id)
    {
        // Find the matching order
        Order order = ...;
        ...
        // Create a Cache-Control header for the response
        var cacheControlHeader = new CacheControlHeaderValue();
        cacheControlHeader.Private = true;
        cacheControlHeader.MaxAge = new TimeSpan(0, 10, 0);
        ...

        // Return a response message containing the order and the cache control header
        OkResultWithCaching<Order> response = new OkResultWithCaching<Order>(order, this)
        {
            CacheControlHeader = cacheControlHeader
        };
        return response;
    }
    ...
}

Ten kod używa niestandardowej IHttpActionResult klasy o nazwie OkResultWithCaching. Ta klasa umożliwia kontrolerowi ustawienie zawartości nagłówka pamięci podręcznej:

public class OkResultWithCaching<T> : OkNegotiatedContentResult<T>
{
    public OkResultWithCaching(T content, ApiController controller)
        : base(content, controller) { }

    public OkResultWithCaching(T content, IContentNegotiator contentNegotiator, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters)
        : base(content, contentNegotiator, request, formatters) { }

    public CacheControlHeaderValue CacheControlHeader { get; set; }
    public EntityTagHeaderValue ETag { get; set; }

    public override async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        HttpResponseMessage response;
        try
        {
            response = await base.ExecuteAsync(cancellationToken);
            response.Headers.CacheControl = this.CacheControlHeader;
            response.Headers.ETag = ETag;
        }
        catch (OperationCanceledException)
        {
            response = new HttpResponseMessage(HttpStatusCode.Conflict) {ReasonPhrase = "Operation was cancelled"};
        }
        return response;
    }
}

Uwaga

Protokół HTTP definiuje również dyrektywę no-cache dla nagłówka Cache-Control. Ta dyrektywa nie znaczy jednak, że danych nie można przechowywać w pamięci podręcznej. Informuje ona, że należy ponownie sprawdzić na serwerze poprawność danych przechowywanych w pamięci podręcznej, zanim zostaną one zwrócone. Dane mogą być przechowywane, ale są sprawdzane przed każdym użyciem, co ma zagwarantować ich aktualność.

Zarządzanie pamięcią podręczną należy do aplikacji klienckiej lub serwera pośredniego. Jeśli jest ono poprawnie zaimplementowane, może oszczędzić przepustowość i zwiększyć wydajność, eliminując konieczność pobierania danych, które zostały już ostatnio pobrane.

Wartość max-age w nagłówku Cache-Control ma charakter orientacyjny. Nie stanowi ona gwarancji, że określone dane nie zmienią się w wyznaczonym okresie. Wartość max-age w internetowym interfejsie API należy określić w zależności od oczekiwanej zmienności danych. Po upływie tego czasu klient powinien usunąć obiekt z pamięci podręcznej.

Uwaga

Większość nowoczesnych przeglądarek internetowych zapewnia przechowywanie danych w pamięci podręcznej po stronie klienta, dodając do żądań odpowiednie nagłówki Cache-Control i sprawdzając nagłówki wyników, zgodnie z opisem. Jednak niektóre starsze przeglądarki nie przechowują w pamięci podręcznej wartości zwracanych z adresu URL, który zawiera ciąg zapytania. Nie stanowi to zwykle problemu w przypadku niestandardowych aplikacji klienckich implementujących własną strategię zarządzania pamięcią podręczną względem omówionego tutaj protokołu.

Niektóre starsze serwery proxy zachowują się w podobny sposób. Mogą zatem nie umożliwiać przechowywania w pamięci podręcznej żądań opartych na adresach URL z ciągami zapytania. Problemy mogą zatem wystąpić w przypadku niestandardowych aplikacji klienckich, które nawiązują połączenie z serwerem internetowym za pośrednictwem takiego serwera proxy.

Optymalizowanie przetwarzania zapytań dzięki podaniu elementu ETags

Jeśli aplikacja kliencka pobiera obiekt, komunikat odpowiedzi może również obejmować element ETag (tag jednostki). Element ETag jest nieprzezroczystym ciągiem wskazującym wersję zasobu; za każdym razem, gdy zasób zmienia element ETag jest również modyfikowany. Element ETag powinien być przechowywany przez aplikację klienta jako część danych w pamięci podręcznej. Poniższy przykład kodu pokazuje, jak dodać element ETag jako część odpowiedzi na żądanie HTTP GET. Ten kod zawiera metodę GetHashCode obiektu umożliwiającą wygenerowanie wartości liczbowej identyfikującej obiekt (w razie potrzeby można zastąpić tę metodę i wygenerować własną wartość skrótu przy użyciu algorytmu takiego jak MD5):

public class OrdersController : ApiController
{
    ...
    public IHttpActionResult FindOrderByID(int id)
    {
        // Find the matching order
        Order order = ...;
        ...

        var hashedOrder = order.GetHashCode();
        string hashedOrderEtag = $"\"{hashedOrder}\"";
        var eTag = new EntityTagHeaderValue(hashedOrderEtag);

        // Return a response message containing the order and the cache control header
        OkResultWithCaching<Order> response = new OkResultWithCaching<Order>(order, this)
        {
            ...,
            ETag = eTag
        };
        return response;
    }
    ...
}

Komunikat odpowiedzi przesłany przez internetowy interfejs API wygląda następująco:

HTTP/1.1 200 OK
...
Cache-Control: max-age=600, private
Content-Type: text/json; charset=utf-8
ETag: "2147483648"
Content-Length: ...
{"orderID":2,"productID":4,"quantity":2,"orderValue":10.00}

Napiwek

Ze względów bezpieczeństwa nie zezwalaj na buforowanie danych poufnych ani danych zwracanych za pośrednictwem uwierzytelnionego połączenia (HTTPS).

Aplikacja kliencka może w dowolnym momencie wysłać kolejne żądanie GET w celu pobrania tego samego zasobu. Jeśli zasób zmienił się (ma inny element ETag), wersja przechowywana w pamięci podręcznej powinna zostać odrzucona, a jej miejsce powinna zająć nowa wersja. Jeśli zasób jest duży i jego przesłanie z powrotem do klienta wymaga znacznej przepustowości, żądania ponownego pobrania tych samych danych mogą być obsługiwane mało wydajnie. Aby temu zaradzić, warto zapewnić w internetowym interfejsie API obsługę następującego procesu optymalizacji żądań GET zdefiniowanego w protokole HTTP:

  • Klient tworzy żądanie GET zawierające element ETag odpowiadający przechowywanej obecnie w pamięci podręcznej wersji zasobu, do którego odwołuje się nagłówek HTTP If-None-Match:

    GET https://adventure-works.com/orders/2 HTTP/1.1
    If-None-Match: "2147483648"
    
  • Operacja GET w internetowym interfejsie API pobiera bieżący element ETag dla żądanych danych (zamówienie 2 w powyższym przykładzie) i porównuje go z wartością w nagłówku If-None-Match.

  • Jeśli bieżący element ETag dla żądanych danych jest zgodny z elementem ETag podanym w żądaniu, to znaczy, że zasób nie został zmieniony i internetowy interfejs API powinien zwrócić odpowiedź HTTP z pustą treścią komunikatu i kodem stanu 304 (Nie zmodyfikowano).

  • Jeśli bieżący element ETag dla żądanych danych nie jest zgodny z elementem ETag podanym w żądaniu, to znaczy, że dane zostały zmienione i internetowy interfejs API powinien zwrócić odpowiedź HTTP z nowymi danymi w treści komunikatu i kodem stanu 200 (OK).

  • Jeśli żądane dane już nie istnieją, internetowy interfejs API powinien zwrócić odpowiedź HTTP z kodem stanu 404 (Nie znaleziono).

  • Klient obsługuje pamięć podręczną za pomocą kodu stanu. Jeśli dane nie uległy zmianie (kod stanu 304), obiekt może pozostać w pamięci podręcznej, a aplikacja kliencka powinna w dalszym ciągu używać tej wersji obiektu. Jeśli dane zostały zmienione (kod stanu 200), obiekt w pamięci podręcznej powinien zostać odrzucony i zastąpiony nowym. Jeśli dane nie są już dostępne (kod stanu 404), obiekt powinien zostać usunięty z pamięci podręcznej.

Uwaga

Jeśli nagłówek odpowiedzi zawiera nagłówek Cache-Control z dyrektywą no-store, obiekt zawsze powinien być usuwany z pamięci podręcznej, niezależnie od kodu stanu HTTP.

Poniższy kod przedstawia metodę rozszerzoną FindOrderByID w celu obsługi nagłówka If-None-Match. Jeśli nagłówek If-None-Match zostanie ominięty, określone zamówienie zawsze jest pobierane:

public class OrdersController : ApiController
{
    [Route("api/orders/{id:int:min(0)}")]
    [HttpGet]
    public IHttpActionResult FindOrderByID(int id)
    {
        try
        {
            // Find the matching order
            Order order = ...;

            // If there is no such order then return NotFound
            if (order == null)
            {
                return NotFound();
            }

            // Generate the ETag for the order
            var hashedOrder = order.GetHashCode();
            string hashedOrderEtag = $"\"{hashedOrder}\"";

            // Create the Cache-Control and ETag headers for the response
            IHttpActionResult response;
            var cacheControlHeader = new CacheControlHeaderValue();
            cacheControlHeader.Public = true;
            cacheControlHeader.MaxAge = new TimeSpan(0, 10, 0);
            var eTag = new EntityTagHeaderValue(hashedOrderEtag);

            // Retrieve the If-None-Match header from the request (if it exists)
            var nonMatchEtags = Request.Headers.IfNoneMatch;

            // If there is an ETag in the If-None-Match header and
            // this ETag matches that of the order just retrieved,
            // then create a Not Modified response message
            if (nonMatchEtags.Count > 0 &&
                String.CompareOrdinal(nonMatchEtags.First().Tag, hashedOrderEtag) == 0)
            {
                response = new EmptyResultWithCaching()
                {
                    StatusCode = HttpStatusCode.NotModified,
                    CacheControlHeader = cacheControlHeader,
                    ETag = eTag
                };
            }
            // Otherwise create a response message that contains the order details
            else
            {
                response = new OkResultWithCaching<Order>(order, this)
                {
                    CacheControlHeader = cacheControlHeader,
                    ETag = eTag
                };
            }

            return response;
        }
        catch
        {
            return InternalServerError();
        }
    }
...
}

Ten przykład zawiera dodatkową klasę niestandardową IHttpActionResult o nazwie EmptyResultWithCaching. Ta klasa stanowi po prostu opakowanie obiektu HttpResponseMessage, który nie zawiera treści odpowiedzi:

public class EmptyResultWithCaching : IHttpActionResult
{
    public CacheControlHeaderValue CacheControlHeader { get; set; }
    public EntityTagHeaderValue ETag { get; set; }
    public HttpStatusCode StatusCode { get; set; }
    public Uri Location { get; set; }

    public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        HttpResponseMessage response = new HttpResponseMessage(StatusCode);
        response.Headers.CacheControl = this.CacheControlHeader;
        response.Headers.ETag = this.ETag;
        response.Headers.Location = this.Location;
        return response;
    }
}

Napiwek

W tym przykładzie element ETag danych jest generowany przez utworzenie wartości skrótu danych pobranych ze źródła danych. Jeśli element ETag może zostać obliczony w inny sposób, proces może zostać dodatkowo zoptymalizowany — w przypadku zmiany dane muszą wówczas zostać tylko pobrane ze źródła danych. Takie podejście jest szczególnie przydatne, jeśli dane są obszerne lub uzyskanie dostępu do źródła danych może spowodować znaczne opóźnienia (jeśli na przykład źródłem danych jest zdalna baza danych).

Obsługa optymistycznej współbieżności za pomocą elementów Etag

Aby umożliwić aktualizowanie z uwzględnieniem danych przechowywanych wcześniej w pamięci podręcznej, protokół HTTP obsługuje strategię optymistycznej współbieżności. Jeśli po pobraniu i buforowaniu zasobu aplikacja kliencka wyśle żądanie PUT lub DELETE, aby zmienić lub usunąć zasób, powinien zawierać nagłówek If-Match odwołujący się do elementu ETag. Internetowy interfejs API może za pomocą tych informacji ustalić, czy zasób został zmieniony przez innego użytkownika od czasu, kiedy został pobrany, i wysyłać odpowiednią odpowiedź z powrotem do aplikacji klienckiej. Odbywa się to w następujący sposób:

  • Klient tworzy żądanie PUT zawierające nowe szczegóły dotyczące zasobu i element ETag odpowiadający przechowywanej obecnie w pamięci podręcznej wersji zasobu, do którego odwołuje się nagłówek HTTP If-Match. W poniższym przykładzie przedstawiono żądanie PUT, które aktualizuje zamówienie:

    PUT https://adventure-works.com/orders/1 HTTP/1.1
    If-Match: "2282343857"
    Content-Type: application/x-www-form-urlencoded
    Content-Length: ...
    productID=3&quantity=5&orderValue=250
    
  • Operacja PUT w internetowym interfejsie API pobiera bieżący element ETag dla żądanych danych (zamówienie 1 w powyższym przykładzie) i porównuje go z wartością w nagłówku If-Match.

  • Jeśli bieżący element ETag dla żądanych danych jest zgodny z elementem ETag podanym w żądaniu, to znaczy, że zasób nie został zmieniony i internetowy interfejs API powinien wykonać aktualizację oraz zwrócić komunikat z kodem stanu HTTP 204 (Brak zawartości), jeśli operacja przebiegnie pomyślnie. Odpowiedź może zawierać nagłówki Cache-Control i ETag dla zaktualizowanej wersji zasobu. Odpowiedź zawsze powinna zawierać nagłówek Location odwołujący się do identyfikatora URI zasobu, który właśnie został zaktualizowany.

  • Jeśli bieżący element ETag dla żądanych danych nie jest zgodny z elementem ETag podanym w żądaniu, to znaczy, że dane od czasu pobrania zostały zmienione przez innego użytkownika i internetowy interfejs API powinien zwrócić odpowiedź HTTP z pustą treścią komunikatu oraz kodem stanu 412 (Niepowodzenie warunku wstępnego).

  • Jeśli zasób, który ma zostać zaktualizowany, już nie istnieje, internetowy interfejs API powinien zwrócić odpowiedź HTTP z kodem stanu 404 (Nie znaleziono).

  • Klient obsługuje pamięć podręczną za pomocą kodu stanu i nagłówków odpowiedzi. Jeśli dane zostały zaktualizowane (kod stanu 204), obiekt może pozostać w pamięci podręcznej (o ile w nagłówku Cache-Control nie określono dyrektywy no-store), ale element ETag powinien zostać zaktualizowany. Jeśli dane zostały zmienione przez innego użytkownika (kod stanu 412) lub nie znaleziono (kod stanu 404), buforowany obiekt powinien zostać odrzucony.

Następny przykład kodu przedstawia implementację operacji PUT dla kontrolera Orders:

public class OrdersController : ApiController
{
    [HttpPut]
    [Route("api/orders/{id:int}")]
    public IHttpActionResult UpdateExistingOrder(int id, DTOOrder order)
    {
        try
        {
            var baseUri = Constants.GetUriFromConfig();
            var orderToUpdate = this.ordersRepository.GetOrder(id);
            if (orderToUpdate == null)
            {
                return NotFound();
            }

            var hashedOrder = orderToUpdate.GetHashCode();
            string hashedOrderEtag = $"\"{hashedOrder}\"";

            // Retrieve the If-Match header from the request (if it exists)
            var matchEtags = Request.Headers.IfMatch;

            // If there is an ETag in the If-Match header and
            // this ETag matches that of the order just retrieved,
            // or if there is no ETag, then update the Order
            if (((matchEtags.Count > 0 &&
                String.CompareOrdinal(matchEtags.First().Tag, hashedOrderEtag) == 0)) ||
                matchEtags.Count == 0)
            {
                // Modify the order
                orderToUpdate.OrderValue = order.OrderValue;
                orderToUpdate.ProductID = order.ProductID;
                orderToUpdate.Quantity = order.Quantity;

                // Save the order back to the data store
                // ...

                // Create the No Content response with Cache-Control, ETag, and Location headers
                var cacheControlHeader = new CacheControlHeaderValue();
                cacheControlHeader.Private = true;
                cacheControlHeader.MaxAge = new TimeSpan(0, 10, 0);

                hashedOrder = order.GetHashCode();
                hashedOrderEtag = $"\"{hashedOrder}\"";
                var eTag = new EntityTagHeaderValue(hashedOrderEtag);

                var location = new Uri($"{baseUri}/{Constants.ORDERS}/{id}");
                var response = new EmptyResultWithCaching()
                {
                    StatusCode = HttpStatusCode.NoContent,
                    CacheControlHeader = cacheControlHeader,
                    ETag = eTag,
                    Location = location
                };

                return response;
            }

            // Otherwise return a Precondition Failed response
            return StatusCode(HttpStatusCode.PreconditionFailed);
        }
        catch
        {
            return InternalServerError();
        }
    }
    ...
}

Napiwek

Wykorzystanie nagłówka If-Match jest opcjonalne. Jeśli zostanie on pominięty, internetowy interfejs API zawsze będzie podejmował próbę zaktualizowania określonego zamówienia, co może skutkować nieświadomym zastąpieniem aktualizacji wprowadzonej przez innego użytkownika. Aby uniknąć problemów z powodu utraty aktualizacji, należy zawsze uwzględnić nagłówek If-Match.

Obsługa dużych żądań i odpowiedzi

Czasami aplikacja kliencka musi wysyłać żądania wysyłające lub odbierające dane, które mogą mieć rozmiar kilku megabajtów (lub większych). Oczekiwanie na przetransmitowanie takiej ilości danych może sprawić, że aplikacja kliencka przestanie odpowiadać. W przypadku obsługi żądań obejmujących dużą ilość danych warto wziąć pod uwagę następujące kwestie:

Optymalizowanie żądań i odpowiedzi dotyczących dużych obiektów

Niektóre zasoby mogą być dużymi obiektami lub zawierać duże pola, takie jak obrazy graficzne lub inne typy danych binarnych. Internetowy interfejs API powinien obsługiwać przesyłanie strumieniowe, co optymalizuje przekazywanie i pobieranie takich zasobów.

Protokół HTTP udostępnia mechanizm kodowania fragmentarycznego transferu, który umożliwia przesyłanie strumieniowe dużych obiektów danych z powrotem do klienta. Kiedy klient wysyła żądanie HTTP GET dotyczące dużego obiektu, internetowy interfejs API może wysłać odpowiedź w segmentowanych fragmentach za pośrednictwem połączenia HTTP. Długość danych w odpowiedzi może nie być początkowo znana (może być generowana), dlatego serwer hostujący internetowy interfejs API powinien wysłać komunikat odpowiedzi z każdym fragmentem określającym Transfer-Encoding: Chunked nagłówek, a nie nagłówek Content-Length. Aplikacja kliencka może odebrać poszczególne fragmenty i utworzyć z nich pełną odpowiedź. Transfer danych jest ukończony, kiedy serwer odsyła ostatni fragment o rozmiarze zero.

Pojedyncze żądanie mogłoby spowodować zwrócenie wielkiego obiektu, który zajmuje dużo zasobów. Jeśli podczas procesu przesyłania strumieniowego internetowy interfejs API ustali, że ilość danych w żądaniu przekroczyła dopuszczalne granice, może przerwać operację i zwrócić komunikat odpowiedzi z kodem stanu 413 (Zbyt duża jednostka żądania).

Rozmiar dużych obiektów przesyłanych przez sieć przy użyciu kompresji HTTP można zminimalizować. Takie podejście pozwala zmniejszyć ruch sieciowy i związane z nim opóźnienie w sieci, jednak wymaga to dodatkowego przetwarzania po stronie klienta i serwera hostującego internetowy interfejs API. Na przykład aplikacja kliencka, która oczekuje odbierania skompresowanych danych, może zawierać Accept-Encoding: gzip nagłówek żądania (można również określić inne algorytmy kompresji danych). Jeśli serwer obsługuje kompresję, powinien odpowiadać zawartości przechowywanej w formacie gzip w treści komunikatu i nagłówku Content-Encoding: gzip odpowiedzi.

Kompresję kodowaną można łączyć z przesyłaniem strumieniowym. Najpierw (przed przesyłaniem strumieniowym) należy skompresować dane i określić w nagłówkach komunikatu kodowanie zawartości w formacie gzip oraz kodowanie fragmentaryczne transferu. Warto zauważyć, że niektóre serwery internetowe (takie jak Internet Information Server) można skonfigurować, aby automatycznie kompresowały odpowiedzi HTTP niezależnie od tego, czy internetowy interfejs API kompresuje dane, czy nie.

Implementowanie częściowych odpowiedzi dla klientów, którzy nie obsługują operacji asynchronicznych

Alternatywą dla asynchronicznego przesyłania strumieniowego jest jawne zażądanie przez aplikację kliencką, aby dane dużych obiektów przekazywane były we fragmentach, czyli jako odpowiedzi częściowe. Aplikacja kliencka wysyła żądanie HTTP HEAD, aby uzyskać informacje o obiekcie. Jeśli internetowy interfejs API obsługuje częściowe odpowiedzi, powinien odpowiadać na żądanie HEAD z komunikatem odpowiedzi zawierającym Accept-Ranges nagłówek i Content-Length nagłówek wskazujący całkowity rozmiar obiektu, ale treść komunikatu powinna być pusta. Korzystając z tych informacji, aplikacja kliencka może utworzyć serię żądań GET, które określają zakres bajtów do odebrania. Internetowy interfejs API powinien zwrócić komunikat odpowiedzi ze stanem HTTP 206 (zawartość częściowa), nagłówkiem Content-Length, który określa rzeczywistą ilość danych zawartych w treści komunikatu odpowiedzi oraz nagłówek Content-Range wskazujący, która część (na przykład bajty 4000 do 8000) obiektu, który reprezentuje te dane.

Żądania HTTP HEAD i częściowe odpowiedzi zostały szczegółowo opisane w temacie Projektowanie interfejsu API.

Unikanie wysyłania zbędnych komunikatów o stanie „100 — kontynuuj” w aplikacjach klienckich

Aplikacja kliencka, za pomocą której ma zostać wysłana na serwer duża ilość danych, może najpierw sprawdzić, czy serwer zaakceptuje takie żądanie. Przed wysłaniem danych aplikacja kliencka można przesłać żądanie HTTP z nagłówkiem Expect: 100-Continue, nagłówkiem Content-Length wskazującym rozmiar danych oraz pustą treścią komunikatu. Jeśli serwer jest gotowy obsłużyć żądanie, powinien odpowiedzieć za pomocą komunikatu zawierającego stan HTTP 100 (Kontynuuj). Aplikacja kliencka może wówczas przystąpić do działania i wysłać kompletne żądanie — z danymi w treści komunikatu.

Jeśli hostujesz usługę przy użyciu usług IIS, sterownik HTTP.sys automatycznie wykrywa i obsługuje nagłówki Oczekiwano: 100-Kontynuuj przed przekazaniem żądań do aplikacji internetowej. Dlatego jest mało prawdopodobne, że nagłówki te pojawią się w kodzie aplikacji. Możesz założyć, że usługi IIS już odfiltrowały komunikaty, które nie są odpowiednie lub są za duże.

Jeśli tworzysz aplikacje klienckie przy użyciu programu .NET Framework, wszystkie komunikaty POST i PUT będą domyślnie wysyłać komunikaty z nagłówkami Expect: 100-Continue. Podobnie jak w przypadku operacji po stronie serwerów, proces jest obsługiwany w sposób niewidoczny przez program .NET Framework. Jednak w wyniku tego procesu każde żądanie POST i PUT powoduje dwukrotne przesłanie danych na serwer i z powrotem — nawet w przypadku małych żądań. Jeśli aplikacja nie wysyła żądań z dużą ilością danych, możesz wyłączyć tę funkcję za pomocą klasy ServicePointManager w celu utworzenia obiektów ServicePoint w aplikacji klienckiej. Obiekt ServicePoint obsługuje połączenia, które klient nawiązuje z serwerem, korzystając z fragmentów identyfikatorów URI dotyczących schematu i hosta określających zasoby na serwerze. Następnie można ustawić dla właściwości Expect100Continue obiektu ServicePoint wartość false. Wszystkie kolejne żądania POST i PUT wysłane przez klienta za pomocą identyfikatora URI, który odpowiada fragmentom dotyczącym schematu i hosta obiektu ServicePoint, zostaną wysłane bez nagłówków Expect: 100-Continue. Poniższy kod przedstawia sposób skonfigurowania obiektu ServicePoint, który konfiguruje wszystkie żądania wysyłane do identyfikatorów URI ze schematem http i hostem www.contoso.com.

Uri uri = new Uri("https://www.contoso.com/");
ServicePoint sp = ServicePointManager.FindServicePoint(uri);
sp.Expect100Continue = false;

Można również ustawić właściwość statyczną Expect100Continue klasy, aby określić domyślną wartość tej właściwości dla wszystkich następnie utworzonych obiektów programu ServicePoint.ServicePointManager

Obsługa dzielenia na strony w przypadku żądań, które mogą zwrócić dużą liczbę obiektów

Jeśli kolekcja zawiera dużą liczbę zasobów, wysłanie żądania GET do odpowiedniego identyfikatora URI może spowodować intensywne przetwarzanie na serwerze hostującym internetowy interfejs API. To skutkuje obniżeniem wydajności i wygenerowaniem intensywnego ruchu sieciowego, co przekłada się na większe opóźnienia.

Aby zapewnić sprawne działanie w takich sytuacjach, internetowy interfejs API powinien obsługiwać ciągi zapytań, które umożliwiają aplikacji klienckiej doprecyzowanie żądań pobrania danych tak, aby dane miały postać łatwiejszych w obsłudze, odrębnych bloków (lub stron). Poniższy kod przedstawia metodę GetAllOrders w kontrolerze Orders . Ta metoda umożliwia pobranie szczegółów zamówienia. Jeśli ta metoda jest nieograniczona, może zwrócić dużą ilość danych. Parametry limit i offset mają na celu zmniejszenie ilości danych. Ich zadaniem jest utworzenie mniejszego podzestawu — w tym przypadku domyślnie jest to tylko 10 pierwszych zamówień:

public class OrdersController : ApiController
{
    ...
    [Route("api/orders")]
    [HttpGet]
    public IEnumerable<Order> GetAllOrders(int limit=10, int offset=0)
    {
        // Find the number of orders specified by the limit parameter
        // starting with the order specified by the offset parameter
        var orders = ...
        return orders;
    }
    ...
}

Aplikacja kliencka może wysłać żądanie pobrania 30 zamówień, rozpoczynając od przesunięcia 50 i korzystając z identyfikatora URI https://www.adventure-works.com/api/orders?limit=30&offset=50.

Napiwek

Umożliwienie aplikacjom klienckim określania ciągów zapytania, których wynikiem jest identyfikator URI mający ponad 2000 znaków, nie jest zalecane. Wielu klientów internetowych i serwerów nie może obsługiwać tak długich identyfikatorów URI.

Zapewnienie dostępności, skalowalności i krótkiego czasu odpowiedzi

Ten sam internetowy interfejs API może być używany przez wiele aplikacji klienckich działających w dowolnym miejscu na świecie. Internetowy interfejs API musi zatem być tak zaimplementowany, aby zapewniał krótki czas odpowiedzi przy dużym obciążeniu, skalowalność umożliwiającą działanie przy różnych obciążeniach i dostępność dla klientów wykonujących newralgiczne operacje biznesowe. Poniższe informacje są pomocne podczas pracy nad spełnieniem tych wymogów:

Zapewnienie asynchronicznej obsługi długotrwałych żądań

Żądanie, którego przetwarzanie może zająć dużo czasu, powinno być wykonywane bez blokowania klienta, który przesłał żądanie. Internetowy interfejs API może wykonać niektóre wstępne kontrole w celu zweryfikowania żądania, zainicjować oddzielne zadanie w celu wykonania pracy, a następnie zwrócić komunikat odpowiedzi z kodem HTTP 202 (Zaakceptowano). Zadanie można uruchomić asynchronicznie w ramach przetwarzania przez internetowy interfejs API lub można je wykonać w tle, odciążając zasoby.

Internetowy interfejs API powinien również udostępniać mechanizm zwracania wyników przetwarzania do aplikacji klienckiej. Można to zrobić, korzystając z mechanizmu sondowania aplikacji klienckich, który okresowo sprawdza, czy przetwarzanie zostało zakończone, i uzyskuje wynik. Innym sposobem jest włączenie wysyłania przez internetowy interfejs API powiadomień informujących o zakończeniu operacji.

Można zaimplementować prosty mechanizm sondowania dzięki udostępnieniu identyfikatora URI polling, który działa jak zasób wirtualny. Operacja ta wygląda następująco:

  1. Aplikacja kliencka wysyła żądanie początkowe do internetowego interfejsu API.
  2. Internetowy interfejs API przechowuje informacje o żądaniu w tabeli przechowywanej w usłudze Azure Table Storage lub Microsoft Azure Cache i generuje unikatowy klucz dla tego wpisu, prawdopodobnie w postaci identyfikatora GUID. Alternatywnie komunikat zawierający informacje o żądaniu i unikatowy klucz można również wysłać za pośrednictwem usługi Azure Service Bus .
  3. Internetowy interfejs API inicjuje przetwarzanie jako oddzielne zadanie lub bibliotekę, na przykład Hangfire. Internetowy interfejs API rejestruje stan zadania w tabeli jako Uruchomiono.
    • Jeśli używasz usługi Azure Service Bus, przetwarzanie komunikatów będzie wykonywane oddzielnie od interfejsu API, na przykład przy użyciu usługi Azure Functions lub usługi AKS.
  4. Internetowy interfejs API zwraca komunikat odpowiedzi z kodem stanu HTTP 202 (Zaakceptowane) i identyfikatorem URI zawierającym wygenerowany unikatowy klucz — taki jak /polling/{guid}.
  5. Po zakończeniu zadania internetowy interfejs API przechowuje wyniki w tabeli i ustawia stan zadania na Ukończono. Należy pamiętać, że jeśli zadanie nie powiedzie się, internetowy interfejs API może również zapisać informacje o awarii i ustawić stan Niepowodzenie.
    • Rozważ zastosowanie technik ponawiania prób, aby rozwiązać problemy z błędami przejściowymi.
  6. Kiedy zadanie jest uruchomione, klient może kontynuować własne przetwarzanie. Może okresowo wysyłać żądanie do otrzymanego wcześniej identyfikatora URI.
  7. Internetowy interfejs API w identyfikatorze URI wykonuje zapytanie o stan odpowiedniego zadania w tabeli i zwraca komunikat odpowiedzi z kodem stanu HTTP 200 (OK) zawierającym ten stan (Uruchomiono, Ukończono lub Niepowodzenie). Jeśli zadanie zostało ukończone lub nie powiodło się, komunikat odpowiedzi może również obejmować wyniki przetwarzania lub wszelkie dostępne informacje dotyczące przyczyny niepowodzenia.
    • Jeśli długotrwały proces ma więcej stanów pośrednich, lepiej jest użyć biblioteki obsługującej wzorzec sagi, na przykład NServiceBus lub MassTransit.

Opcje implementowania powiadomień obejmują:

  • Wypychanie asynchronicznych odpowiedzi do aplikacji klienckich przy użyciu centrum powiadomień. Aby uzyskać więcej informacji, zobacz Wysyłanie powiadomień do określonych użytkowników przy użyciu usługi Azure Notification Hubs.
  • Zastosowanie modelu Comet w celu zachowania stałego połączenia sieciowego między klientem i serwerem hostującym internetowy interfejs API, a także użycie tego połączenia do wypychania komunikatów z serwera z powrotem do klienta. Przykładowe rozwiązanie można znaleźć w artykule w magazynie MSDN Building a Simple Comet Application in the Microsoft .NET Framework (Tworzenie prostej aplikacji Comet w programie Microsoft .NET Framework).
  • Używanie usługi SignalR do wypychania danych w czasie rzeczywistym z serwera internetowego do klienta za pośrednictwem trwałego połączenia sieciowego. Usługa SignalR jest dostępna dla aplikacji internetowych programu ASP.NET jako pakiet NuGet. Więcej informacji na ten temat można znaleźć w witrynie internetowej ASP.NET SignalR.

Zagwarantowanie bezstanowości każdego żądania

Każde żądanie powinno być uznane za niepodzielne. Nie powinny istnieć zależności między żądaniem wysłanym przez aplikację kliencką i jakimikolwiek kolejnymi żądaniami przesłanymi przez tego samego klienta. To rozwiązanie pomaga w zapewnieniu skalowalności — wystąpienia usługi internetowej można wdrożyć na wielu serwerach. Żądania klientów można skierować do dowolnego z tych wystąpień, a wyniki zawsze powinny być takie same. Z podobnego powodu przyczynia się to także do zwiększenia dostępności. Jeśli wystąpi błąd serwera internetowego, żądania mogą zostać skierowane do innego wystąpienia (przy użyciu usługi Azure Traffic Manager). W tym czasie można ponownie uruchomić serwer bez wpływu na aplikacje klienckie.

Śledzenie klientów i implementowanie ograniczania przepustowości w celu zmniejszenia prawdopodobieństwa ataków typu DoS

Jeśli określony klient wysyła dużą liczbę żądań w danym okresie, może zająć całą usługę, co obniży wydajność obsługi pozostałych klientów. Aby temu zapobiec, internetowy interfejs API może monitorować wywołania z aplikacji klienckich, śledząc adres IP wszystkich żądań przychodzących lub rejestrując każde uzyskanie dostępu uwierzytelnionego. Korzystając z tych informacji, można ograniczyć dostęp do zasobów. Jeśli klient przekroczy zdefiniowany limit, internetowy interfejs API może zwrócić komunikat odpowiedzi ze stanem 503 (Usługa niedostępna) i dołączyć nagłówek Retry-After, który określa, kiedy klient może wysłać następne żądanie bez ryzyka odrzucenia. Ta strategia może pomóc zmniejszyć prawdopodobieństwo ataku typu "odmowa usługi" (DoS) z zestawu klientów, którzy utkną w martwym punkcie systemu.

Staranne zarządzanie trwałymi połączeniami HTTP

Protokół HTTP obsługuje trwałe połączenia HTTP, jeśli są one dostępne. Specyfikacja HTTP 1.0 dodała nagłówek Połączenie ion:Keep-Alive, który umożliwia aplikacji klienckiej wskazanie serwerowi, że może używać tego samego połączenia do wysyłania kolejnych żądań, a nie otwierania nowych. Połączenie zostaje zamknięte automatycznie, jeśli klient nie użyje ponownie połączenia w czasie zdefiniowanym przez hosta. To zachowanie jest domyślne w przypadku protokołu HTTP 1.1 używanego przez usługi platformy Azure, a zatem w komunikatach nie trzeba uwzględniać nagłówków Keep-Alive.

Zachowanie otwartego połączenia może pomóc w skróceniu czasu odpowiedzi, ponieważ zmniejsza opóźnienie i przeciążenie sieci. Jednak utrzymywanie zbędnych połączeń otwartych przez czas dłuższy niż jest to konieczne może niekorzystnie wpływać na skalowalność, ponieważ utrudnia pozostałym klientom jednoczesne nawiązanie połączenia. W przypadku aplikacji klienckich działających na urządzeniu przenośnym może to również wpływać na czas pracy baterii. Jeśli aplikacja wysyła do serwera tylko okazjonalne żądania, zachowanie otwartego połączenia może spowodować szybsze zużycie baterii. Aby upewnić się, że połączenie z użyciem protokołu HTTP 1.1 nie jest trwałe, klient może umieścić w komunikatach nagłówek Connection:Close, co przesłoni zachowanie domyślne. Podobnie, jeśli serwer obsługuje bardzo dużą liczbę klientów, może uwzględnić w komunikatach odpowiedzi nagłówek Connection:Close, co powinno zamknąć połączenie, oszczędzając zasoby serwera.

Uwaga

Trwałe połączenia HTTP są funkcją opcjonalną, która pozwala zmniejszyć obciążenie sieci wynikające z wielokrotnego ustanawiania kanału komunikacyjnego. Ani internetowy interfejs API, ani aplikacja kliencka nie powinna zależeć od dostępności trwałego połączenia HTTP. Nie używaj trwałych połączeń HTTP do implementowania systemów powiadomień w stylu comet; Zamiast tego należy użyć gniazd (lub gniazd internetowych, jeśli są dostępne) w warstwie TCP. Należy też pamiętać, że nagłówki Keep-Alive mają ograniczone zastosowanie, jeśli aplikacja kliencka komunikuje się z serwerem za pośrednictwem serwera proxy. W takiej sytuacji trwałe jest tylko połączenie z klientem i serwerem proxy.

Publikowanie internetowego interfejsu API i zarządzanie nim

Aby można było udostępnić internetowy interfejs API aplikacjom klienckim, interfejs ten musi zostać wdrożony w środowisku hosta. Środowiskiem tym jest zwykle serwer internetowy, chociaż może to być inny rodzaj procesu hosta. Przed opublikowaniem internetowego interfejsu API warto uwzględnić następujące kwestie:

  • Wszystkie żądania muszą być uwierzytelnione i autoryzowane. Musi także zostać wymuszony odpowiedni poziom kontroli dostępu.
  • Komercyjny internetowy interfejs API może podlegać różnym wymogom dotyczącym gwarantowanej jakości w zakresie czasu odpowiedzi. Ważne jest, aby zapewnić skalowalność środowiska hosta, jeśli obciążenie może się znacznie różnić w czasie.
  • W przypadku monetyzacji może być konieczne mierzenie żądań.
  • W przypadku określonych klientów, którzy wyczerpali swój przydział, może być konieczne regulowanie przepływu ruchu w internetowym interfejsie API oraz wdrożenie ograniczania przepustowości.
  • Wymogi wynikające z przepisów mogą narzucać konieczność rejestrowania i prowadzenia inspekcji wszystkich żądań i odpowiedzi.
  • Aby zapewnić dostępność, może być konieczne monitorowanie kondycji serwera hostujacego internetowy interfejs API i ponowne jego uruchomienie (w razie potrzeby).

Warto odłączyć te problemy od problemów technicznych dotyczących implementacji internetowego interfejsu API. Z tego powodu należy rozważyć utworzenie fasady działającej jako oddzielny proces i kierującej żądania do internetowego interfejsu API. Fasada może zapewnić obsługę operacji zarządzania i przekazywania sprawdzonych żądań do internetowego interfejsu API. Użycie fasady wiąże się także z licznymi zaletami w zakresie funkcjonalności, takimi jak:

  • Funkcjonowanie jako punkt integracji wielu internetowych interfejsów API.
  • Przekształcanie komunikatów i tłumaczenie protokołów komunikacyjnych dla klientów utworzonych przy użyciu różnych technologii.
  • Buforowanie żądań i odpowiedzi w celu zmniejszenia obciążenia na serwerze hostującym internetowy interfejs API.

Testowanie internetowego interfejsu API

Internetowy interfejs API powinien zostać przetestowany równie dokładnie jak każdy inny element oprogramowania. Należy rozważyć utworzenie testów jednostkowych w celu zweryfikowania funkcjonalności.

Charakter internetowego interfejsu API przynosi własne dodatkowe wymagania, aby sprawdzić, czy działa prawidłowo. Szczególną uwagę należy zwrócić na następujące aspekty:

  • Przetestuj wszystkie trasy, aby sprawdzić, czy wywołują poprawne operacje. Jeśli nieoczekiwanie zostanie zwrócony kod stanu HTTP 405 (Niedozwolona metoda), to może znaczyć, że wystąpiła niezgodność między trasą i metodami HTTP (GET, POST, PUT, DELETE), które mogą być wysyłane do tej trasy.

    Wysyłanie żądań HTTP do tras, które ich nie obsługują, takich jak przesyłanie żądania POST do określonego zasobu (żądania POST powinny być wysyłane tylko do kolekcji zasobów). W takiej sytuacji jedyną prawidłową odpowiedzią powinien być kod stanu 405 (Niedozwolone).

  • Sprawdź, czy wszystkie trasy są prawidłowo chronione oraz podlegają odpowiedniej kontroli uwierzytelniania i autoryzacji.

    Uwaga

    Za niektóre aspekty zabezpieczeń (np. uwierzytelnianie użytkownika) zwykle odpowiada środowisko hosta, a nie internetowy interfejs API. Mimo to w procesie wdrażania należy także uwzględnić testy zabezpieczeń.

  • Przetestuj obsługę wyjątków w ramach każdej operacji i sprawdź, czy do aplikacji klienckiej jest przekazywana odpowiednia i czytelna odpowiedź HTTP.

  • Sprawdź, czy komunikaty żądań i odpowiedzi są poprawnie sformułowane. Jeśli na przykład żądanie HTTP POST zawiera dane dotyczące nowego zasobu w formacie x-www-form-urlencoded, odpowiadająca mu operacja powinna poprawnie przeanalizować dane, utworzyć zasoby i zwrócić odpowiedź zawierającą szczegóły dotyczące nowego zasobu (w tym poprawny nagłówek Location).

  • Sprawdź poprawność wszystkich linków i identyfikatorów URI w komunikatach odpowiedzi. Na przykład komunikat HTTP POST powinien zwrócić identyfikator URI nowo utworzonego zasobu. Wszystkie linki HATEOAS powinny być prawidłowe.

  • Upewnij się, że każda operacja zwraca prawidłowe kody stanu dla różnych kombinacji danych wejściowych. Na przykład:

    • Jeśli zapytanie zostanie obsłużone pomyślnie, powinien zostać zwrócony kod stanu 200 (OK).
    • Jeśli zasób nie zostanie znaleziony, operacja powinna zwrócić kod stanu HTTP 404 (Nie znaleziono).
    • Jeśli klient wysyła żądanie, które powoduje pomyślne usunięcie zasobu, powinien zostać zwrócony kod stanu 204 (Brak zawartości).
    • Jeśli klient wysyła żądanie, które tworzy nowy zasób, kod stanu powinien mieć wartość 201 (Utworzono).

Zwróć uwagę na nieoczekiwane kody stanu odpowiedzi z zakresu 5xx. Te komunikaty są zwykle zgłaszane przez serwer hosta, aby poinformować, że nie można spełnić prawidłowego żądania.

  • Przetestuj różne kombinacje nagłówków żądań, które może określić aplikacja kliencka, i sprawdź, czy internetowy interfejs API zwraca w komunikacie odpowiedzi oczekiwane informacje.

  • Przetestuj ciągi zapytań. Jeśli operacja może obejmować parametry opcjonalne (takie jak żądania podziału na strony), przetestuj różne kombinacje i kolejność parametrów.

  • Sprawdź, czy operacje asynchroniczne zostały ukończone pomyślnie. Jeśli internetowy interfejs API obsługuje przesyłanie strumieniowe w przypadku żądań zwracających duże obiekty binarne (np. pliki wideo lub audio), upewnij się, że żądania klientów nie są blokowane podczas przesyłania strumieniowego danych. Jeśli internetowy interfejs API implementuje sondowanie dla długotrwałych operacji modyfikacji danych, sprawdź, czy operacje prawidłowo zgłaszają stan podczas ich kontynuowania.

Należy także utworzyć i uruchomić testy wydajności, aby sprawdzić, czy internetowy interfejs API działa odpowiednio przy obciążeniu. Projekt testu sprawdzającego wydajność Internetu i działanie przy obciążeniu można utworzyć za pomocą programu Visual Studio Ultimate.

Korzystanie z usługi Azure API Management

Na platformie Azure rozważ użycie usługi Azure API Management do publikowania internetowego interfejsu API i zarządzania nim. Za pomocą tej funkcji można wygenerować usługę, która działa jak fasada dla co najmniej jednego internetowego interfejsu API. Sama usługa jest skalowalną usługą internetową, którą można utworzyć i skonfigurować przy użyciu witryny Azure Portal. Usługa ta umożliwia opublikowanie internetowego interfejsu API i zarządzanie nim w następujący sposób:

  1. Wdróż internetowy interfejs API w witrynie internetowej, usłudze w chmurze platformy Azure lub na maszynie wirtualnej platformy Azure.

  2. Połącz usługę API Management z internetowym interfejsem API. Żądania wysyłane na adres URL interfejsu API zarządzania są mapowane na identyfikatory URI w internetowym interfejsie API. Dana usługa API Management może kierować żądania do więcej niż jednego internetowego interfejsu API. Dzięki temu można zagregować wiele internetowych interfejsów API w pojedynczą usługę zarządzania. Analogicznie, do danego internetowego interfejsu API może się odwoływać więcej niż jedna usługa API Management, co się przydaje, jeśli chcesz ograniczyć lub podzielić na partycje funkcję dostępną dla różnych aplikacji.

    Uwaga

    Identyfikatory URI w linkach HATEOAS, które są generowane jako część odpowiedzi dla żądań HTTP GET, powinny odwoływać się do adresu URL usługi API Management, a nie serwera internetowego, który hostuje internetowy interfejs API.

  3. Dla każdego internetowego interfejsu API określ operacje HTTP uwidaczniane przez internetowy interfejs API oraz wszystkie parametry opcjonalne, które mogą być danymi wejściowymi operacji. Możesz również określić, czy usługa API Management ma przechowywać w pamięci podręcznej odpowiedź otrzymaną od internetowego interfejsu API w celu zoptymalizowania ponownych żądań dotyczących tych samych danych. Zarejestruj szczegółowe informacje dotyczące odpowiedzi HTTP, które mogą wygenerować poszczególne operacje. Te informacje służą do generowania dokumentacji dla deweloperów, więc powinny być dokładne i kompletne.

    Operacje można definiować ręcznie przy użyciu kreatorów dostarczonych przez witrynę Azure Portal lub zaimportować je z pliku zawierającego definicje w formacie WADL lub Swagger.

  4. Skonfiguruj ustawienia zabezpieczeń dotyczące komunikacji między usługą API Management i serwerem internetowym hostującym internetowy interfejs API. Usługa API Management obecnie obsługuje uwierzytelnianie podstawowe i uwierzytelnianie wzajemne z wykorzystaniem certyfikatów i autoryzacji użytkownika OAuth 2.0.

  5. Tworzenie produktu. Produkt to jednostka publikacji. Do produktu dodaj internetowe interfejsy API, które wcześniej zostały połączone z usługą zarządzania. Po opublikowaniu produktu internetowe interfejsy API zostają udostępnione deweloperom.

    Uwaga

    Przed opublikowaniem produktu możesz również zdefiniować grupy użytkowników, które będą mogły uzyskać dostęp do produktu, oraz dodać użytkowników do tych grup. W ten sposób decydujesz, którzy deweloperzy i aplikacje mogą używać Twojego internetowego interfejsu API. Jeśli dany internetowy interfejs API podlega zatwierdzaniu, deweloper musi wysłać żądanie do administratora produktu, aby uzyskać dostęp do interfejsu. Administrator decyduje o tym, czy dostęp zostanie przyznany. Jeśli zmienią się okoliczności, można zablokować dostęp deweloperom, którzy wcześniej już go uzyskali.

  6. Skonfiguruj zasady dla każdego internetowego interfejsu API. Zasady umożliwiają regulowanie aspektów, takich jak obsługa wywołań międzydomenowych, sposób uwierzytelniania klientów, przezroczysta konwersja między formatami danych XML i JSON, ograniczanie wywołań z danego zakresu adresów IP, limity przydziału użycia i ograniczanie liczby wywołań. Zasady mogą być stosowane globalnie w całym produkcie, w jednym internetowym interfejsie API w produkcie lub dla poszczególnych operacji w internetowym interfejsie API.

Aby uzyskać więcej informacji, zobacz dokumentację usługi API Management.

Napiwek

Platforma Azure udostępnia usługę Azure Traffic Manager, która umożliwia implementowanie trybu failover i równoważenia obciążenia oraz zmniejszenie opóźnień w wielu wystąpieniach witryny internetowej hostowanej w różnych lokalizacjach geograficznych. Usługa Azure Traffic Manager może być używana w połączeniu z usługą API Management: usługa API Management może kierować żądania do wystąpień witryny internetowej za pośrednictwem usługi Azure Traffic Manager. Aby uzyskać więcej informacji, zobacz Metody routingu usługi Traffic Manager.

W tej strukturze, jeśli używasz niestandardowych nazw DNS dla witryn sieci Web, należy skonfigurować odpowiedni rekord CNAME dla każdej witryny sieci Web, aby wskazać nazwę DNS witryny sieci Web usługi Azure Traffic Manager.

Informacje dla deweloperów po stronie klienta

Deweloperzy tworzący aplikacje klienckie zwykle potrzebują informacji na temat sposobu uzyskania dostępu do internetowego interfejsu API oraz dokumentacji dotyczącej parametrów, typów danych, zwracanych typów oraz kodów powrotu opisujących różne żądania i odpowiedzi wysyłane przez usługę internetową i aplikację kliencką.

Tworzenie dokumentacji dotyczącej operacji REST dla internetowego interfejsu API

Usługa Azure API Management obejmuje portal dewelopera opisujący operacje REST udostępniane przez internetowy interfejs API. Po opublikowaniu produkt pojawia się w tym portalu. Deweloperzy mogą za pomocą tego portalu ubiegać się o dostęp. Administrator może zatwierdzić lub odrzucić takie żądanie. W przypadku zatwierdzenia żądania deweloper otrzymuje klucz subskrypcji używany do uwierzytelniania wywołań od tworzonych przez nich aplikacji klienckich. Klucz ten musi zostać dostarczony z każdym wywołaniem internetowego interfejsu API. W przeciwnym razie wywołanie zostanie odrzucone.

Ten portal zapewnia również następujące zasoby:

  • Dokumentacja dotycząca produktu, lista dostępnych operacji, wymagane parametry i różne odpowiedzi, które mogą być zwrócone. Informacje te są generowane na podstawie danych przekazanych w kroku 3 procedury publikowania internetowego interfejsu API dostępnej w sekcji Usługa Microsoft Azure API Management.
  • Fragmenty kodu pokazujące sposób wywołania operacji w kilku językach, takich jak JavaScript, C#, Java, Ruby, Python i PHP.
  • Konsola deweloperów umożliwiająca deweloperom wysyłanie żądań HTTP w ramach testowania poszczególnych operacji w produkcie oraz wyświetlanie wyników.
  • Strona, na której deweloper może raportować wszelkie znalezione problemy.

Witryna Azure Portal umożliwia dostosowanie portalu dla deweloperów w celu zmiany stylu i układu w celu dopasowania do znakowania organizacji.

Implementowanie klienckiego zestawu SDK

Tworzenie aplikacji klienckiej, która wywołuje żądania REST w celu uzyskania dostępu do internetowego interfejsu API, wymaga napisania dużej ilość kodu. Kod ten odpowiada za utworzenie poszczególnych żądań i odpowiednie ich sformatowanie, wysyłanie żądań do serwera hostującego usługę internetową i analizowanie odpowiedzi określających, czy żądanie zakończyło się pomyślnie, czy też się nie powiodło się, a także za wyodrębnienie wszelkich zwróconych danych. Aby uwolnić aplikację kliencką od tych problemów, można przygotować zestaw SDK, który opakowuje interfejs REST i przenosi te szczegóły niskiego poziomu do bardziej funkcjonalnego zestawu metod. Aplikacja kliencka korzysta z tych metod. Metody w sposób przezroczysty konwertują wywołania na żądania REST, a następnie konwertują odpowiedzi na wartości zwracane metody. Jest to typowa technika implementowana przez wiele usług, w tym przez zestaw Azure SDK.

Utworzenie zestawu SDK po stronie klienta jest poważnym przedsięwzięciem, ponieważ implementacja musi zostać spójnie przeprowadzona i dokładnie przetestowana. Znaczną część tego procesu można zmechanizować. Wielu dostawców udostępnia też narzędzia umożliwiające zautomatyzowanie wielu tych zadań.

Monitorowanie internetowego interfejsu API

W zależności od sposobu opublikowania i wdrożenia internetowego interfejsu API można monitorować ten interfejs bezpośrednio lub zbierać informacje dotyczące użycia i kondycji, analizując ruch przechodzący przez usługę API Management.

Monitorowanie bezpośrednie internetowego interfejsu API

Jeśli internetowy interfejs API został zaimplementowany przy użyciu szablonu ASP.NET Web API (jako projekt Interfejs API sieci Web lub jako rola Sieć Web w usłudze w chmurze platformy Azure) oraz programu Visual Studio 2013, możesz zbierać dane dotyczące dostępności, wydajności i użycia, korzystając z usługi Application Insights w programie ASP.NET. Usługa Application Insights to pakiet, który w sposób przezroczysty śledzi i rejestruje informacje dotyczące żądań i odpowiedzi podczas wdrażania internetowego interfejsu API w chmurze. Po zainstalowaniu i skonfigurowaniu pakietu nie trzeba zmieniać kodu w internetowym interfejsie API, aby można było używać tego pakietu. Podczas wdrażania internetowego interfejsu API w witrynie internetowej platformy Azure cały ruch jest badany i zbierane są następujące statystyki:

  • Czas odpowiedzi serwera.
  • Liczba żądań serwera i szczegóły dotyczące poszczególnych żądań.
  • Najwolniej obsłużone żądania w kontekście średniego czasu odpowiedzi.
  • Szczegóły dotyczące żądań zakończonych niepowodzeniem.
  • Liczba sesji zainicjowanych przez różne przeglądarki i agentów użytkownika.
  • Najczęściej wyświetlane strony (szczególnie przydatne w przypadku aplikacji internetowych, a nie internetowych interfejsów API).
  • Różne role użytkownika korzystające z internetowego interfejsu API.

Te dane można wyświetlić w czasie rzeczywistym w witrynie Azure Portal. Można również utworzyć testy internetowe, które monitorują kondycję internetowego interfejsu API. Test internetowy wysyła okresowe żądanie do określonego identyfikatora URI w internetowym interfejsie API i przechwytuje odpowiedź. Można określić definicję pomyślnej odpowiedzi (np. kod stanu HTTP 200), a jeśli żądanie nie zwraca tej odpowiedzi, można skonfigurować alert, który ma zostać wysłany do administratora. W razie niepowodzenia administrator może ponownie uruchomić serwer hostujący internetowy interfejs API.

Aby uzyskać więcej informacji, zobacz Application Insights — wprowadzenie do programu ASP.NET.

Monitorowanie internetowego interfejsu API za pomocą usługi API Management

Jeśli opublikowano internetowy interfejs API przy użyciu usługi API Management, strona usługi API Management w witrynie Azure Portal zawiera pulpit nawigacyjny, który umożliwia wyświetlenie ogólnej wydajności usługi. Strona Analiza umożliwia przejście do szczegółów sposobu używania danego produktu. Ta strona zawiera następujące karty:

  • Użycie. Na tej karcie znajdują się informacje dotyczące liczby wywołań interfejsu API i przepustowości wykorzystanej do obsługi tych wywołań. Szczegóły użycia można filtrować według produktu, interfejsu API i operacji.
  • Kondycja. Ta karta umożliwia wyświetlenie wyniku żądań skierowanych do interfejsu API (zwróconych kodów stanu HTTP), skuteczności zasad przechowywania w pamięci podręcznej, czasu odpowiedzi interfejsu API i czasu odpowiedzi usługi. Dane dotyczące kondycji również można filtrować według produktu, interfejsu API i operacji.
  • Działanie. Ta karta zawiera tekstowe podsumowanie, liczbę pomyślnych wywołań, wywołań zakończonych niepowodzeniem i wywołań zablokowanych, średni czas odpowiedzi oraz czas odpowiedzi dla każdego produktu, internetowego interfejsu API i operacji. Ta strona zawiera również liczbę wywołań pochodzących od każdego dewelopera.
  • Błyskawicznie. Ta karta przedstawia podsumowanie danych dotyczących wydajności. Są to między innymi dane dotyczące deweloperów odpowiedzialnych za większość wywołań interfejsu API oraz produktów, internetowych interfejsów API i operacji, które odebrały te wywołania.

Te informacje umożliwiają namierzenie internetowego interfejsu API lub operacji, która stanowi wąskie gardło, oraz ewentualne przeskalowanie środowiska hosta (dodanie kolejnych serwerów). Można również sprawdzić, czy dowolne aplikacje nie wykorzystują zasobów w nadmiernym stopniu, i zastosować odpowiednie zasady pozwalające ustawić przydziały i ograniczyć liczbę wywołań.

Uwaga

Szczegóły opublikowanego produktu można zmienić. Zmiany zostaną zastosowane natychmiast. Można na przykład dodać lub usunąć określoną operację z internetowego interfejsu API bez konieczności ponownego publikowania produktu, który zawiera internetowy interfejs API.

Następne kroki