Azure 最佳实践 - API 的设计与实现(2)

设计良好的REST-API会限制客户端可访问的资源、关系和导航方案。在实现和部署WebAPI时,应考虑到托管WebAPI物理环境的要求以及构造WebAPI的方式,而不是考虑数据的逻辑结构。本文重点介绍如何实现WebAPI,公开给客户端应用程序。有关WebAPI设计的详细信息,请参阅API设计指南。(https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design)


请求处理过程
在处理请求时,应考虑以下几点。
GET、PUT、DELETE、HEAD 和 PATCH 操作应当是幂等的
处理请求的代码不应产生任何副作用。对同一资源重复请求应产生一致的状态。例如,将多个DELETE请求发送到同一URI应产生相同的效果,尽管响应消息中的HTTP状态代码可能有所不同。第一个DELETE请求可能返回状态代码204(无内容),而接下来的DELETE请求可能返回状态代码404(未找到)。
备注
JonathanOliver博客(http://blog.jonathanoliver.com/idempotency-patterns/)中的IdempotencyPatterns(幂等性模式)一文阐述了幂等性质以及它是如何与数据操作相关的。


创建新资源的POST操作不应有副作用
如果POST请求用于创建新资源,则请求的作用应仅限新资源(以及任何直接相关资源,如果有某种关联)。例如,在电子商务系统中创建新客户订单的POST请求可能还会修改库存级别并生成计费信息,但不应修改与订单不相关的信息,也不应对系统的整体状态有任何其他副作用。


避免琐碎的POST、PUT和DELETE操作
支持对资源集合发POST、PUT和DELETE请求。POST请求中可以包含多个新资源的详细信息并将这些资源添加到同一个集合中,PUT请求可以替换集合的整个资源集,DELETE请求可以删除整个集合。
ASP.NET-WebAPI-2中包含的OData支持对批处理请求的支持。客户端应用程序可以打包多个WebAPI请求到单个HTTP请求将其发送给服务器,然后同一个HTTP响应中包含每个请求的处理结果。有关详细信息,请参阅WebAPI和Web-API-OData中的批处理支持简介(http://blogs.msdn.com/b/webdev/archive/2013/11/01/introducing-batch-support-in-web-api-and-web-api-odata.aspx)。


发送响应时需要遵守HTTP规范,必须包含正确HTTP的状态代码(使客户端知道如何处理结果)、相应的HTTP标头(以便客户端了解结果的性质),以及适当格式化的正文(使客户端能够对结果进行分析)。


例如,POST操作应返回状态代码201(已创建),并且应在Location标头中包含新创建资源的URI。


支持内容协商
响应消息的正文可能包含不同格式的数据。例如,GET请求可以返回JSON或XML格式的数据。客户端提交请求时,可以指定它支持的数据格式-Accept标头。这些格式将指定媒体类型。例如,查询图像的GET请求可以指定Accept标头,指定客户端能够处理的媒体类型,如“image/jpeg,image/gif,image/png”。当WebAPI返回结果时,应使用其中一种媒体类型来设置数据格式,并在响应的Content-Type标头
中指定该格式。
如果客户端未指定Accept标头,则对响应正文使用有意义的默认格式。例如,ASP.NET-WebAPI框架对基于文本的数据默认使用JSON格式。


提供支持HATEOAS导航和资源的链接
HATEOAS方法使客户端能够从初始起点导航并发现资源。通过使用包含URI的链接来实现;当客户端发出GET请求来获取资源时,响应应包含能让客户端应用程序快速找到任何直接相关资源的URI。例如,电子商务解决方案的WebAPI中,客户可能下了多个订单。当客户端应用程序检索客户的详细信息时,响应应该包含客户端应用程序能够查询这些订单的链接。此外,HATEOAS样式的链接应描述每个链接所支持的其他操作(POST、PUT、DELETE等)以及用于执行每个请求的URI。API设计中对此方法进行了更详细的介绍。(https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design)
当前没有控制HATEOAS实现的标准,但下面的示例演示了一种可行的方法。在此示例中,用于查找客户详细信息的GET请求返回一个响应,其中包括该客户订单的HATEOAS链接:


GET http://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":"http://adventure-works.com/customers/2",
    "action":"GET",
    "types":["text/xml","application/json"]},
    {"rel":"self",
    "href":"http://adventure-works.com/customers/2",
    "action":"PUT",
    "types":["application/x-www-form-urlencoded"]},
    {"rel":"self",
    "href":"http://adventure-works.com/customers/2",
    "action":"DELETE",
    "types":[]},
    {"rel":"orders",
    "href":"http://adventure-works.com/customers/2/orders",
    "action":"GET",
    "types":["text/xml","application/json"]},
    {"rel":"orders",
    "href":"http://adventure-works.com/customers/2/orders",
    "action":"POST",
    "types":["application/x-www-form-urlencoded"]}
]}



在此示例中,客户数据由以下代码段中的Customer类表示。HATEOAS链接存在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; }
}
GET操作从存储中查询客户数据并构造Customer对象,并填充Links集合。结果的格式为JSON。每个链接包含以下字段:
返回的对象与链接所描述对象之间的关系。在此示例中,“self”指示该链接是返回到对象本身的引用(类似于许多面向对象语言中的this指针),而“orders”则是包含相关订单信息的集合的名称。
描述对象的超链接(Href)。
可发送到URI的HTTP请求类型(Action)。
在HTTP请求中提供或可在响应中返回的数据格式(Types),具体取决于请求的类型。


HTTP响应中所示的HATEOAS链接指示客户端应用程序可执行以下操作:
向URL(http://adventure-works.com/customers/2)发GET请求查询客户的详细信息。数据可以是XML或JSON格式。
向URL发PUT请求修改客户的详细信息。必须在请求消息中以x-www-form-urlencoded格式提供新数据。
向URL发DELETE请求以删除客户。该请求不需要其他任何信息,也不需要在响应消息正文中返回数据。
向http://adventure-works.com/customers/2/orders发GET请求以查找客户的所有订单。数据可以XML或JSON格式。
向URL发PUT请求为此客户创建新订单。必须在请求消息中以x-www-form-urlencoded格式提供数据。


处理异常
如果操作引发未捕捉的异常,请考虑以下几点。


捕捉异常并向客户端返回有意义的响应
处理HTTP请求的服务应提供全面的异常处理,而不是让未捕获的异常返回到框架。如果由于异常而导致无法成功完成操作,则可在响应消息中传回此异常,但它应包括导致异常有意义的描述信息。该异常还应包括相应的HTTP状态码,而不是对于每种情况都只返回状态代码500。例如,如果用户请求触发了违反约束的数据库更新(例如,尝试删除具有未完成订单的客户),则应返回状态代码409(冲突)和指示冲突原因的消息正文。如果某种其他情况导致请求无法完成,则可以返回状态代码400(错误请求)。可以在W3C网站上的状态代码定义(https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)页中找到HTTP状态代码的完整列表。在下例代码中捕获了不同条件的异常。


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

提示
请勿在API中包含对入侵者有用的信息。


许多Web服务器在错误条件到达WebAPI之前,会自行捕获错误条件。例如,如果为网站配置了身份验证,但用户无法提供正确的身份验证信息,则Web服务器应以状态代码401(未经授权)进行响应。客户端经过身份验证后,代码可以执行自己的检查来验证客户端是否能够访问所请求的资源。如果授权失败,则应返回状态代码403(禁止访问)。应一致地处理异常,并记录有关错误的信息。


若要以一致方式处理异常,可考虑在整个WebAPI实现全局错误处理策略。
还应整合所捕获每个异常详细信息的错误日志记录;只要客户端无法通过Web访问,错误日志就可以包含详细的信息。


区分客户端错误和服务器端错误
HTTP协议应区分客户端错误(HTTP 4xx状态代码)和服务器错误(HTTP 5xx状态代码)。请确保在任何错误响应消息中遵守此约定。


优化客户端数据访问
例如,在分布式环境(例如,有Web服务器和客户端应用程序)中,主要问题之一是网络。这可能会成为值得注意的瓶颈问题,尤其当客户端应用程序频繁地发送请求或接收数据时。因此,目标应该是将网络通信量降到最低。在写检索和维护数据的代码时,请考虑以下几点:


支持客户端缓存
HTTP-1.1协议支持在客户端和中间服务器中缓存,请求通过客户端和服务器使用Cache-Control标头进行路由。当客户端应用程序向Web-API发送GET请求时,响应可以包含Cache-Control标头,以指示响应正文中的数据可缓存,多长时间之后失效进而视为过期。下面的示例演示了GET请求和Cache-Control标头的响应:

GET http://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}
在此示例中,Cache-Control标头指定返回的数据在600秒后过期并且只适用于单个客户端,不能在其他客户端的共享缓存中存储(它是专用的)。Cache-Control标头可以指定public(而不是private),在这种情况下,数据可以存储在共享缓存中;它也可以指定no-store,在这种情况下,数据不能由客户端缓存。下面的代码示例显示了如何在响应消息中设定Cache-Control标头:

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;
    }
    ...
}
代码中使用名为OkResultWithCaching的自定义IHttpActionResult类。这个类使控制器可设置缓存标头内容:


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

备注
HTTP协议还为Cache-Control标头定义了no-cache指令。 令人困惑的是,该指令并不意味着“不缓存”而是说“在返回信息之前需要重新验证缓存信息”;数据仍可以缓存,但在每次使用时要检查以确保它仍是最新的。
缓存管理是客户端应用或中间服务器的职责,如果正确的实现,就可以节省带宽和提高性能,因为避免了不必要的查询。Cache-Control标头中的max-age值只是一个建议,并不保证相应数据在指定的时间内不会更改。WebAPI应将max-age设置适当的值,具体取决于数据预期的波动性。到期后,客户端应放弃缓存中的对象。


备注
如上所述,大多数现代Web浏览器通过向请求中添加相应的Cache-Control标头并检查返回结果的标头来支持客户端缓存。但是,某些较旧的浏览器不会缓存查询字符串URL返回的值。不过这对于实现了自己的缓存机制的客户端来说不是问题。
在某些较旧的代理中,不会缓存相同URL。使用这类代理连接Web服务器的自定义客户端可能会有问题。


提供ETag以优化查询的处理
当客户端应用程序检索对象时,响应消息还可以包括ETag(实体标记)。ETag是字符串,它表示资源的版本,每次资源发生更改时,也会修改Etag值。客户端应用程序应将ETag作为缓存的一部分。下面的代码示例演示了如何添加ETag作为GET请求响应的一部分。代码使用对象的GetHashCode方法获取用于对象的哈希码(如有必要,可以使用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;
    }
    ...
}

Web API的响应消息如下所示:

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}

提示
出于安全原因,不允许缓存敏感数据或HTTPS连接返回的数据。
客户端应用随时可以发后续GET请求对资源进行查询,并且如果资源已更改(它具有不同的ETag),应放弃缓存的版本,并将新版本添加到缓存中。如果资源很大并且需要大量的带宽才能传回客户端,则重复提取相同数据的请求可能会效率低下。 为了应对这种情况,HTTP协议定义了以下过程来优化在WebAPI中支持的GET请求:客户端构造GET请求,该请求包含If-None-Match标头中引用资源的当前缓存版本的ETag:

GET http://adventure-works.com/orders/2 HTTP/1.1
If-None-Match: "2147483648"


WebAPI中的GET操作获取所请求数据的当前ETag(上面示例中的订单2),并将其与If-None-Match标头中的值进行比较。
如果所请求数据的ETag与请求提供的ETag匹配,则资源尚未更改,WebAPI应返回HTTP响应,其中包含空的消息正文和状态代码304(未修改)。
如果所请求数据的ETag与请求提供的ETag不匹配,则数据已更改,WebAPI应返回HTTP响应,其中包含带有新数据的消息正文和状态代码200(正常)。
如果所请求的数据不再存在,则Web API应返回状态代码为404(未找到)的HTTP响应。
客户端使用状态码来维护缓存。如果数据尚未更改(状态代码304),则对象可保持在缓存,并且客户端应用应继续使用此版本。如果数据已更改(状态代码200),则应放弃缓存对象,并插入新对象。如果数据不再可用(状态代码404),则应从缓存中删除该对象。


备注
如果响应标头包含Cache-Control标头no-store,则应始终从缓存中删除对象,不用再考虑HTTP状态码。
下面的代码显示了支持If-None-Match标头的FindOrderByID方法。请注意,如果省略If-None-Match标头,将始终检索指定订单:


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


该示例引入了名为EmptyResultWithCaching的自定义IHttpActionResult类。它只是对HttpResponseMessage对象的包装:

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

提示
在此示例中,通过从数据源拿到的数据进行哈希来生成数据的ETag。如果ETag可以某种其他方式计算,则可进一步优化,并且仅在数据已更改时,才需要从数据源中提取数据。如果数据很大或访问数据源会导致显著的延迟(例如,如果数据源是远程数据库),则此方法特别有用。


使用ETag支持乐观并发
为了对以前缓存的数据更新,HTTP协议需要支持乐观并发策略。在提取和缓存资源后,客户端应用程序随后发送PUT或DELETE请求以更改或删除资源时,应包含引用ETag的If-match标头。然后WebAPI可以使用这些信息来确定资源在检索到后是否已被其他用户更改,并将相应响应发送回客户端应用程序,如下所示:
客户端构造PUT请求,该请求包含新资源的详细信息,以及If-Match标头中所引用资源当前缓存版本的ETag。下面的示例演示了用于更新订单的PUT请求:


PUT http://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
WebAPI中的PUT操作获取所请求数据的当前ETag(上面示例中的订单1),并将其与If-Match标头中的值进行比较。
如果所请求数据的当前ETag与请求提供的ETag匹配,则说明资源尚未更改并且WebAPI应执行更新,并在成功的情况下返回包含HTTP状态代码204(无内容)。响应可以包括资源的更新后版本的Cache-Control和ETag标头。响应中应始终包含引用新资源URI的Location标头。
如果所请求数据的当前ETag与请求提供的ETag不匹配,说明数据在提取后已被其他用户更改,WebAPI应返回空消息正文和状态代码412(不满足前提条件)。
如果要更新的资源不再存在,则WebAPI应返回状态代码404(未找到)。
客户端使用状态代码和响应标头来维护缓存。如果数据已更新(状态代码204),则对象可保持在缓存(只要Cache-Control标头未指定no-store),但应更新ETag值。如果数据已被其他用户更改(状态代码412)或未找到(状态代码404),则应放弃缓存的对象。


下一个代码示例显示了Orders controller中PUT操作的实现:


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();
        }
    }
    ...
}
提示
是否使用If-match标头是可选的,如果省略,WebAPI将尝试更新指定订单,可能会覆盖其他用户的更新。若要避免由于丢失更新出现的问题,应始终提供If-Match标头。


处理大型请求和响应
可能会有这样的情况:客户端应用程序需要发送或接收大小为几兆(或更大)的请求。等待传输如此大量的数据可能会导致客户端应用程序停止响应。在需要处理大量数据的请求时,应考虑以下几点:


优化大对象传输的请求和响应
一些资源可能是大型对象或包含大量字段,例如图像或其他类型的二进制数据。WebAPI应支持流式处理以实现这些资源上传和下载的优化。


HTTP协议提供了分块传输编码,用于将大型数据对象流式传回客户端。当客户端发送大对象GET请求时,WebAPI可以在HTTP连接区块中发送回答复。最初答复中的数据长度可能未知(可能会生成它),因此托管WebAPI的服务器应发送包含每个区块的响应消息,这些区块指定Transfer-Encoding:Chunked标头而不是Content-Length标头。客户端应用程序依次拼接每个区块以组合成完整的响应。在数据传输完成后,服务器将发送最后一个区块(大小为零)。


可以想象单个请求使用资源的大型对象。如果在流式处理过程中,WebAPI确定请求中的数据量超过一些可接受的范围,它可以中止操作并返回状态代码413(请求实体太大)的响应消息。可使用HTTP压缩将网络传输的大对象长度降到最低。这个方法可帮助减少网络流量和网络延迟,但代价是需要在客户端和托管WebAPI的服务器上进行额外的处理。例如,需要接收压缩数据的客户端应用程序可以提供Accept-Encoding:gzip请求标头(还可以指定其他数据压缩算法)。如果服务器支持压缩,则应以消息正文中以gzip格式存储内容并指定Content-Encoding:gzip标头。可以将编码压缩和流式处理结合使用;在流式处理之前先压缩,并在消息标头中指定gzip内容编码和chunked传输编码。另外,某些Web服务器(如IIS)可以配置为自动压缩HTTP响应,而不管WebAPI是否已经压缩了数据。


为不支持异步操作的客户端实现部分响应
作为异步流处理的替代方法,客户端应用程序可以区块的方式显式请求大对象数据,称为部分响应。客户端应用程序发送HEAD请求来获取有关对象的信息。如果WebAPI支持部分响应,则应以包含Accept-Ranges标头和Content-Length标头(指示该对象的总大小),但消息正文为空。客户端应用程序可以使用此信息来构造一系列要接收的字节范围的GET请求。WebAPI应返回以下内容的响应消息:HTTP状态206(部分内容)、指定响应消息正文中包含的实际数据量Content-Length标头,以及此数据中表示对象的哪一部分(例如4000到8000个字节)的Content-Range标头。


HTTP HEAD请求和部分响应在API设计中有详细介绍。(https://docs.microsoft.com/en-us/azure/architecture/best-practices/api-design)


客户端应避免发送不必要的“100-Continue”状态消息
要发送大量数据到服务器的客户端应用可能会先确定服务器是否可以接受该请求。在发送数据前,客户端应用程序可以提交一个HTTP请求,其中包含Expect:100-Continue标头、Content-Length标头(指示数据的大小),但消息正文为空。如果服务器可以处理该请求,则应指定HTTP状态100(继续)进行响应。然后,客户端应用程序可以继续操作并发送包含消息体数据的完整请求。


如果使用IIS来托管服务,则HTTP.sys驱动程序会自动检测并处理Expect:100-Continue标头,然后再将请求传递到Web应用程序。这意味着你很可能在应用程序代码中看不到这些标头,可以假设IIS已筛掉任何它认为不适合或太大的消息。如果使用.NETFramework生成客户端应用程序,则默认情况下所有POST和PUT消息都将先发送包含Expect:100-Continue标头的消息。与服务器端一样,NETFramework透明地处理了该过程。但是,每个POST和PUT请求会导致对服务器进行2次往返,即使是小请求也是如此。如果应用程序不发送包含大量数据的请求,则可在客户端应用程序中使用ServicePointManager类创建ServicePoint对象来禁用此功能。ServicePoint对象将基于标识服务器上资源的URI协议和主机的URI片段来完成客户端与服务器的连接。然后,可以将ServicePoint对象的Expect100Continue属性设置为false。客户端与ServicePoint对象的方案和主机片段匹配的URI发出所有后续POST和PUT请求在发送时将不包含Expect:100-Continue标头。下面的代码演示了如何配置ServicePoint对象,以便将所有请求都发送到协议为http且主机为www.contoso.com的URI。


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


还可以设置ServicePointManager类的静态Expect100Continue属性,以便为所有后续创建的ServicePoint对象指定该属性的默认值。有关详细信息,请参阅ServicePoint类。(https://msdn.microsoft.com/library/system.net.servicepoint.aspx)


为集合对象请求提供分页
如果集合包含了大量资源,则向相应URI发出GET请求可能会导致在托管WebAPI的服务器上进行大量处理而影响性能,并产生大量网络流量从而导致延迟增加。要应对这些情况,WebAPI应支持这样的查询字符串:使客户端应用程序可以优化请求或能够更细粒度的提取数据。下面的代码显示OrdersController中的GetAllOrders方法。检索订单的详细信息。如果此方法不受约束,它可能会返回大量数据。limit和offset参数旨在将数据量设为定值,在本示例中默认情况下返回前10个订单:


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

客户端应用程序可以使用http://www.adventure-works.com/api/orders?limit=30&offset=50 发送请求来检索偏移量为从50开始的30个订单。


提示
应避免在客户端应用程序传递超过2000个字符的查询字符串。许多Web客户端和服务器无法处理这么长的URI。


保持响应能力、可伸缩性和可用性
同一WebAPI可能为世界各地的客户端提供服务。务必确保WebAPI在高负载下的高响应能力、可扩展并支持高度变化的工作负荷,并保证关键业务操作的高可用性。要确定如何满足这些要求时,需要考虑以下几点:


对长时间运行的请求进行异步处理
需要长时间处理的请求应确保在执行时不会阻塞提交请求的客户端。WebAPI可以执行一些检查来验证请求,启动独立的任务来执行工作,并返回HTTP代码202(已接受)的响应消息。任务可作为WebAPI处理的一部分进行异步运行,或作为后台任务运行。


WebAPI还应提供一种向客户端返回处理结果的机制。可通过以下两种方案来达到目的:为客户端应用程序提供轮询机制定期查询处理是否已完成并返回结果,或使WebAPI在操作完成时发送通知。


可使用以下方法,通过提供虚拟资源的轮询URI来实现简单的轮询机制:
客户端应用程序将初始请求发送到WebAPI。
WebAPI将关于请求的信息存储在表或微软Azure缓存中,并以GUID作为唯一键。
WebAPI启动独立运行的任务。WebAPI在表中将该任务状态更新为“正在运行”。
WebAPI返回HTTP状态代码202(已接受)的响应消息,消息正文中包含该记录的GUID。
完成任务后,WebAPI将结果存储在表中,并将任务状态设置为“已完成”。注意,如果该任务失败,WebAPI还可能存储失败信息并将状态设置为“失败”。
任务运行时,客户端可以继续执行自己的处理逻辑。可以定期将请求发送到URI/polling/{guid},其中{guid}是WebAPI在202响应消息中返回的GUID。
/polling/{guid}URI中的WebAPI在表中查询相应的任务状态并返回HTTP状态代码“200(正常)”的响应消息,其中包含以下状态:“正在运行”、“完成”或“失败”。 如果任务已完成或失败,则响应消息还可包括处理结果或失败原因。


实现通知的方案包括:
使用Azure通知中心将响应异步推送到客户端。有关详细信息,请参阅Azure消息中心。(https://docs.microsoft.com/en-us/azure/notification-hubs/notification-hubs-aspnet-backend-windows-dotnet-wns-notification)
使用Comet模型来保持客户端与WebAPI服务之间的永久连接,并使用此连接将消息从服务器推送回客户端。MSDN杂志文章(在微软.NET框架中构建简单的Comet应用程序)介绍了一个示例解决方案。(https://msdn.microsoft.com/magazine/jj891053.aspx)使用SignalR通过永久网络连接将Web服务器中的实时数据推送到客户端。SignalR可作为NuGet程序包用于ASP.NET应用程序。可以在 ASP.NET SignalR网站上找到详细信息。


确保每个请求都是无状态的
每个请求应被看作原子操作。客户端发出的请求应与同一客户端提交的任何后续请求没有任何依赖关系。这样可帮助实现伸缩性;可以在多个服务器上部署Web服务实例。客户端请求可被定向到其中任一实例,并且结果应保持相同。由于类似的原因,它还提高了可用性;如果某个Web服务器失败,可以将请求路由到其他实例(通过使用Azure流量管理器),同时重新启动服务器,对客户端没有任何不良影响。


跟踪客户端请求并实施限制以减少被DOS攻击的可能性
如果特定客户端在给定的时间段内发出了大量请求,则它可能会独占服务并影响其他客户端请求的性能。若要缓解此问题,WebAPI可通过跟踪所有请求的IP或记录每次经过身份验证的访问来监视客户端的调用。可以使用这些信息来限制资源访问。如果客户端超出阈值,WebAPI可以返回状态503(服务不可用)的响应消息并包括Retry-After标头指定客户端可以发送下一个请求不会被拒绝的最大时间。该策略可帮助降低系统被一组客户端发起拒绝服务(DOS)攻击的可能性。


谨慎管理永久HTTP连接
HTTP协议支持永久HTTP连接。HTTP1.0规范已添加Connection:Keep-Alive标头以允许客户端应用程序向服务器表明它可能使用同一连接发送后续请求而不是打开新连接。如果客户端在主机定义的时间段内未重用连接,则该连接会被自动关闭。该行为在Azure服务使用的HTTP1.1中是默认设置,因此无需在消息中包括Keep-Alive标头。


让连接保持打开状态可减少延迟和网络阻塞,从而提高响应能力,但不必要的连接打开时间长于所需要的可能会不利于可扩展性,从而限制了其他并发客户端进行连接的能力。如果客户端应用程序运行在移动设备上,还会影响电池的使用寿命;如果应用程序只是偶尔向服务器发出请求,则保持连接打开状态可能会导致电池更快耗尽。若要确保不使用HTTP1.1建立永久连接,客户端可在消息中包括Connection:Close标头以覆盖默认行为。同样,如果服务器正在处理大量客户端请求,它可以在响应消息中包括Connection:Close标头,这样会关闭连接以节省服务器资源。


备注
永久HTTP连接纯粹是可选的,通常用于减少与反复建立通信的网络开销。WebAPI和客户端都不应依赖于永久HTTP连接的可用性。不要使用永久HTTP连接来实现Comet样式的通知系统,而应使用TCP层的套接字(或websocket,如果可能)。最后:如果客户端应用程序使用代理与服务器通信,则Keep-Alive标头的作用是有限的;只有与客户端和代理的连接将是持久的。


发布和管理Web API
要使WebAPI可供客户端使用,WebAPI必须部署到主机环境中。运行环境通常是Web服务器,尽管它可能是某种其他类型的主机进程。发布WebAPI时,应考虑以下几点:
所有请求都必须经过身份验证和授权,必须强制实施相应的访问控制。
商业WebAPI可能会受到与响应时间有关的质量约束。
如果负载随着时间的推移会发生显著变化,务必确保主机环境是可缩放的。
出于盈利目的,可能有必要对请求进行计量。
可能需要使用WebAPI对通信流量进行调控,并对已用完其配额的客户端实施限制。
可能会要求对所有请求和响应记录进行审核。
为了确保可用性,可能有必要监视托管WebAPI的服务器运行状况并在必要时重新启动。
如果能够将这些问题从有关WebAPI实现的技术问题中分离出来,会很有用。 因此可以考虑使用外观模式(https://en.wikipedia.org/wiki/Facade_pattern),作为独立的进程运行,并将请求路由到WebAPI。外观模式可用于操作管理,并将验证过的请求转发到WebAPI。 


外观模式还有其他功能用途,包括:
作为多个WebAPI的集成端。
传输消息并转换由不同技术生成的客户端协议。
对请求和响应进行缓存以减少托管WebAPI服务器上的负载。


测试WebAPI
WebAPI应和软件的任何其他部分一样,需要进行全面测试。应考虑创建单元测试来验证每个功能的正确性。WebAPI的性质使它在验证操作是否正确方面有其额外的要求。应特别注意以下几个方面:
测试所有路由以验证它们是否调用了正确的操作。特别要注意意外返回的HTTP状态代码405(不支持的方法),因为这可能表明路由与可分派给该路由的HTTP方法(GET、POST、PUT、DELETE)不匹配。
将HTTP请求发送到不支持这些请求的路由,例如,将POST请求提交到了特定资源(POST请求只应发送到资源集合)。在这些情况下,唯一有效的响应状态代码为“405 (不支持)”。
验证所有路由是否都得到了正确保护并受到相应身份验证和授权检查的制约。


备注
安全性的某些方面(如用户身份验证)最有可能是主机环境(而不是WebAPI)的职责,但仍有必要在部署过程中进行安全测试。
测试每个操作执行的异常处理,并验证是否将相应的有意义的HTTP响应返回到了客户端应用。
验证请求和响应消息的格式是否正确。例如,如果POST请求包含x-www-form-urlencoded格式的数据,需要确认相应的操作能够正确分析数据、创建该资源,并返回包含新资源的响应的详细信息,包括正确的Location标头。
验证响应消息中的所有链接和URI。例如,POST消息应返回新创建资源的URI。所有HATEOAS链接都应有效。
确保每个操作对不同输入组合返回了正确的状态码。例如:
如果查询成功,则应返回状态代码200(正常)
如果未找到资源,则操作应返回HTTP状态代码404(未找到)。
如果客户端发送的请求成功删除资源,则状态代码应为204(无内容)。
如果客户端发送的请求创建了新资源,则状态代码应为201(已创建)
需要注意5xx范围内的异常响应状态码。这些消息通常由主机服务器报告,以指示无法完成请求。
测试客户端应用程序可以指定的不同请求标头组合并确保WebAPI在响应消息中返回了预期的信息。
测试查询字符串。如果操作接受可选参数(例如分页请求),则测试参数的不同组合和顺序。
验证异步操作是否成功完成。如果WebAPI支持大型二进制对象(如视频或音频)的请求流处理,确保在流传输时不会阻塞客户端请求。 如果WebAPI实现了长时间轮询的数据修改操作,要确保这些操作在执行时状态报告是正确的。
还应创建并执行性能测试以检查WebAPI在一定压力下能够流畅地运行。可使用VisualStudio旗舰版创建Web性能和负载测试项目。 有关详细信息,请参阅在发布前对应用程序运行性能测试。(https://msdn.microsoft.com/library/dn250793.aspx)


使用AzureAPI管理器
在Azure上,可以考虑使用AzueAPI管理器(https://docs.microsoft.com/en-us/azure/api-management/)来发布和管理WebAPI。使用这种机制,可以生成一个或多个WebAPI的外观服务。服务本身是可缩放的Web服务,可使用Azure管理器进行创建和配置。也可以使用该服务发布和管理WebAPI,如下所示:


将WebAPI部署到网站、Azure云服务或Azure虚拟机。
将API管理服务连接到WebAPI。发送到API管理器的URL请求将映射到WebAPI中的URI。同一API管理服务可将请求路由到多个WebAPI。这样便可将多个WebAPI聚合为单一管理服务。同样,如果需要限制或分离不同应用程序的功能,则可从多个API管理服务引用同一WebAPI。
备注
作为GET请求响应的一部分生成的HATEOAS链接中的URI应引用API管理服务(而不是WebAPI的服务器)URL。


对于每个WebAPI,应指定该API公开的HTTP操作以及可选参数。还可配置API管理服务是否缓存从WebAPI接收的响应以优化对相同数据的重复请求。记录每个操作生成HTTP响应的详细信息。该信息用于为开发人员生成文档,因此它应是准确且完整的。可使用Azure管理门户网站向导提示的手动定义操作,也可以从WADL或Swagger文件中导入操作。
为API管理服务与托管WebAPI的Web服务器之间的通信进行安全设置。API管理服务目前支持证书和OAuth2.0用户授权来完成基本身份验证。


创建产品。每个产品是发布单元;可将之前连接到管理服务的WebAPI添加到产品。发布产品后,WebAPI便可供开发人员使用了。
备注
在发布产品之前,还可以定义可访问该产品的用户组,并将用户添加到这些组。来控制能使用该WebAPI的开发人员和应用程序。如果使用WebAPI需要批准,则在能够访问它之前,开发人员必须向产品管理员发送请求。管理员可以授予或拒绝开发人员的访问权限。在有些情况下,现有的开发人员可能也会被拒绝访问。


为每个WebAPI配置策略。策略可以控制以下方面:是否应允许跨域调用、如何对客户端进行身份验证、是否要在XML和JSON数据格式之间进行透明地转换、是否要限制给定IP范围的请求、使用配额,以及是否要限制调用率等。策略可以应用于整个产品、产品中的单个WebAPI,或者WebAPI中的某个操作。有关详细信息,请参阅API管理文档。(https://docs.microsoft.com/en-us/azure/api-management/)
提示
Azure提供了Azure流量管理器,它可以用于故障转移和负载均衡,并减少在不同地理位置托管的多个网站实例之间的延迟。可将Azure流量管理器与API管理服务结合使用;API管理服务可通过Azure流量管理器将请求路由到网站实例。有关详细信息,请参阅流量管理器路由方法(https://docs.microsoft.com/en-us/azure/traffic-manager/traffic-manager-routing-methods/)。在这样的结构中,如果要对网站自定义DNS名称,则应将每个网站相应的CNAME记录配置为指向Azure流量管理器网站的DNS名称。


为客户端开发人员提供支持
客户端应用程序的开发人员通常需要了解如何访问WebAPI和参数、数据类型、返回类型和返回代码(描述Web服务和客户端应用程序之间的不同请求响应)的相关信息。


记录WebAPI的REST操作
AzureAPI管理服务包括一个开发人员的门户网站,其中描述了由WebAPI所公开的REST操作。产品发布后,便会显示在门户网站上。开发人员可以注册后进行使用;然后管理员可以批准或拒绝该请求。 如果开发人员获得批准,则会向其分配一个订阅密钥,用于对客户端应用程序发出的请求进行身份验证。密钥必须在每次WebAPI调用一起提供,否则会被拒绝。


此门户网站还提供:
产品文档,列出它公开的操作、所需参数和可返回的不同响应。注意,这些信息可通过使用微软的AzureAPI管理服务所发布WebAPI列表的步骤3生成。
如何通过多种语言(包括 JavaScript、C#、Java、Ruby、Python 和 PHP)调用操作的代码示例。
通过使用开发人员控制台,能够发送HTTP请求来测试产品中的每个操作并查看结果。
一个面向开发人员的问题和错误报告页面。
在Azure管理门户网站中,开发人员可以进行自定义,根据自己公司的要求来更改样式和布局。


客户端SDK
创建使用REST请求来访问WebAPI的客户端需要编写很多代码来对每个请求设置格式,并将请求发送到托管Web服务的服务器,解析服务器响应以确定请求是否成功并提取返回数据。要使客户端应用程序跳过这些步骤,可以提供这样一个SDK:封装REST接口并将这些功能抽象到一组方法中。客户端应用程序使用这些方法,透明地将调用转换为REST请求,然后再将响应转换为方法的返回值。这是许多服务(包括AzureSDK)实现的一种常用技术。创建客户端SDK是一项要求相当高的任务,因为它必须一致地实现,并经过严格测试。但是,这个过程的大部分操作可以机械地进行,并且许多供应商已经提供了可自动执行上述任务的工具。


监视WebAPI
根据发布和部署WebAPI的方式,可以直接监视WebAPI,也可以通过API管理服务的流量来收集使用情况和运行状况信息。


直接监视WebAPI
如果使用ASP.NET——WebAPI模板(不管作为WebAPI项目还是作为Azure云服务中的Web角色)和VisualStudio2013实现WebAPI,则可使用ASP.NET-ApplicationInsights收集可用性、性能和使用情况数据。ApplicationInsights是在WebAPI部署到云后透明地跟踪并记录请求和响应信息的程序包;安装并配置该包后,无需修改WebAPI中的任何代码即可使用。将WebAPI部署到Azure网站时,所有通信和以下统计信息会被收集:
服务器响应时间。
服务器请求数和每个请求的详细信息。
就平均响应时间而言,速度最慢的前几个请求。
任何失败请求的详细信息。
由不同浏览器和用户代理启动的会话数。
最经常查看的网页(主要适用于Web应用程序而不是WebAPI)。
访问WebAPI不同用户角色。
可以从Azure管理门户实时监控这些数据。另外,还可以创建用于监视WebAPI运行状况的webtest。Webtest将定期发送请求到WebAPI中指定的URI,并解析其响应。可以设定成功响应的定义(如HTTP状态码200),如果请求未获得响应,可以安排向管理员发送警报。如有必要,管理员可以重新启动托管WebAPI服务器(如果它出现故障)。有关详细信息,请参阅ApplicationInsights-ASP.NET入门(https://docs.microsoft.com/en-us/azure/application-insights/app-insights-asp-net)。


使用API管理服务来监视WebAPI
如果已经使用API管理服务发布WebAPI,Azure管理门户上的API管理页会包含一个用于查看该服务整体性能的仪表板。使用“分析”页,可查询使用方式的详细信息。还包含以下选项:
用法。提供随时间推移已进行的API调用数和用于处理这些调用的带宽信息。可以按产品、API和操作来筛选使用情况详细信息。
运行状况。还可查看API请求的结果(返回的HTTP状态码)、缓存策略的有效性、API响应时间和服务响应时间。同样,可以按产品、API和操作来筛选运行状况的数据。
活动状态。此选项提供了以下信息:成功的调用数、失败的调用数、被阻止的调用数、平均响应时间以及每个产品、WebAPI和操作的响应时间。还列出了每个开发人员进行的调用数。
快速浏览。此选项显示性能数据的概要信息,包括负责进行大多数API调用的开发人员,以及接收这些调用的产品、WebAPI和操作。可以使用这些信息来确定是否是特定WebAPI或操作导致了瓶颈问题,如有必要,可横向扩展添加更多服务器。还可以确定一个或多个应用程序是否正在使用不相称的资源量,从而应用适当的策略以配额并限制调用率。
备注
可以更改已发布产品的详细信息,所做的更改将被立即应用。例如,可在WebAPI中添加或删除操作,而无需重新发布包含该WebAPI的的产品。


更多详细信息
ASP.NET Web API OData(http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api)包含了有关如何使用 ASP.NET 实现 OData Web API 的示例和详细信息。
Web API 和 Web API OData 中的批处理支持简介中(http://blogs.msdn.com/b/webdev/archive/2013/11/01/introducing-batch-support-in-web-api-and-web-api-odata.aspx)介绍了如何使用 OData 在 Web API 中实现批处理操作。
Jonathan Oliver 博客上的 Idempotency Patterns(幂等模式http://blog.jonathanoliver.com/idempotency-patterns/)描述了幂等性以及它如何与数据管理操作相关。
W3C 网站上的 Status Code Definitions(状态代码定义https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)包含了HTTP状态代码及其说明的完整列表。
使用WebJobs来运行后台任务(https://docs.microsoft.com/en-us/azure/app-service-web/web-sites-create-web-jobs/)提供了有关如何使用WebJobs执行后台操作的信息和示例。
Azure消息通知中心(https://docs.microsoft.com/en-us/azure/notification-hubs/notification-hubs-aspnet-backend-windows-dotnet-wns-notification/)介绍了如何使用Azure通知中心将异步响应推送到客户端应用程序。
API管理(https://azure.microsoft.com/en-us/services/api-management/)介绍了如何发布可对WebAPI进行受控安全访问的产品。
Azure API 管理 REST API 参考(https://msdn.microsoft.com/library/azure/dn776326.aspx)介绍了如何使用 API 管理 REST API 生成自定义管理应用程序。
流量管理器路由方法(https://docs.microsoft.com/en-us/azure/traffic-manager/traffic-manager-routing-methods)概述了如何使用 Azure 流量管理器对托管WebAPI的多个网站实例上上的请求进行负载均衡操作。
Application Insights -ASP.NET入门(https://docs.microsoft.com/en-us/azure/application-insights/app-insights-asp-net)——详细介绍了如何在 ASP.NET Web API 项目中安装和配置 Application Insights。

猜你喜欢

转载自blog.csdn.net/csharp25/article/details/80503215