65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET Web API 中大型 JSON 数组流式传输

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (19投票s)

2017 年 5 月 5 日

CPOL

2分钟阅读

viewsIcon

99830

downloadIcon

959

一个关于在 ASP.NET Web API 中流式传输大型 JSON 数组以及 HTTP 分块传输编码的示例

概述

在流式传输大型对象数组(例如,10,000 个对象)时,我们通常需要处理两个主要的性能问题

  1. 对象的大量内存分配
  2. 服务器长时间的响应时间

为了解决这些问题,我们有两种方法可以提高服务器端性能

  1. C# 中的迭代模式
  2. HTTP 中的分块传输编码

在接下来的章节中,我们将了解这些方法,看看它们如何解决这两个问题。我们还将看到两个示例,从服务器端到客户端,实现数组流式传输。

C# 中的迭代模式

众所周知,我们可以通过在返回类型为 IEnumerable(T) 的方法或属性中使用 yield 关键字来启用迭代模式。该模式背后的思想是枚举每个项目,而不是返回整个集合。

public IEnumerable<ReturnModel> Get()
{
    // An example of returning large number of objects
    foreach (var i in Enumerable.Range(0, 10000))
       yield return new ReturnModel() { SequenceNumber = i, ID = Guid.NewGuid() };
}

由于枚举在 foreach 循环开始时立即开始,而无需等待所有对象准备就绪,因此我们可以预期效率和内存使用通常会更好。

HTTP 中的分块传输编码

分块传输编码是一种允许服务器“逐片”返回数据的机制。在该编码中,数据由每个十六进制数字分隔,后跟一个“\r\n”,它告诉客户端以下块的长度。以下是 Mozilla Developer Network 提供的服务器响应示例,包含三行,每行是一个块。

HTTP/1.1 200 OK 
Content-Type: text/plain 
Transfer-Encoding: chunked

7\r\n
Mozilla\r\n 
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n 
\r\n

由于数据旨在以一系列块的形式发送,而不是整个块,因此省略了正常的 Content-Length 标头。

服务器端示例

以下代码片段显示了一个传输大型 JSON 数组的 Web API 方法。Get 方法以迭代模式返回每个对象,我们之前已经了解过这种模式。一个自定义的 HTTP 消息处理程序将在流开始返回之前启用分块传输编码。

// Namespaces omitted

namespace WebApplication1.Controllers
{
    public class ValuesController : ApiController
    {
        [HttpGet]
        public IEnumerable<ReturnModel> Get()
        {
            // An example of returning large number of objects
            foreach (var i in Enumerable.Range(0, 10000))
                yield return new ReturnModel() { SequenceNumber = i, ID = Guid.NewGuid() };
        }
    }

    public class Handler : DelegatingHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, 
            CancellationToken cancellationToken)
        {
            var response = base.SendAsync(request, cancellationToken);
            
            response.Result.Headers.TransferEncodingChunked = true; // Here!
            
            return response;
        }
    }

    [DataContract]
    public class ReturnModel
    {
        [DataMember]
        public int SequenceNumber { get; set; }

        [DataMember]
        public Guid ID { get; set; }
    }
}

以下代码片段显示了如何将我们的自定义 HTTP 消息处理程序添加到集合中。

// Namespaces omitted 

namespace WebApplication1
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services
            // Web API routes
            config.MapHttpAttributeRoutes();
            config.MessageHandlers.Add(new Handler());

            // Others omitted.
        }
    }
}

客户端示例

下一个代码片段演示了一个控制台应用程序,该应用程序从服务器消耗 JSON 数组。使用 JsonTextReaderJsonSerializer,客户端应用程序可以在无需等待整个 JSON 数据传输的情况下开始枚举数组中的每个对象。

// Namespaces omitted

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Press any key to continue...");
            Console.ReadKey(true);

            foreach (var value in GetValues())
                Console.WriteLine("{0}\t{1}", value.SequenceNumber, value.ID);

            Console.ReadKey(true);
        }

        static IEnumerable<ReturnModel> GetValues()
        {
            var serializer = new JsonSerializer();
            var client = new HttpClient();
            var header = new MediaTypeWithQualityHeaderValue("application/json");

            client.DefaultRequestHeaders.Accept.Add(header);

            // Note: port number might vary.
            using (var stream = client.GetStreamAsync
            ("https://:63025/api/values").Result)
            using (var sr = new StreamReader(stream))
            using (var jr = new JsonTextReader(sr))
            {
                while (jr.Read())
                {
                    // Don't worry about commas.
                    // JSON reader will handle them for us.
                    if (jr.TokenType != JsonToken.StartArray && jr.TokenType != JsonToken.EndArray)
                        yield return serializer.Deserialize<ReturnModel>(jr);
                }
            }
        }
    }
}

您可能已经注意到,客户端应用程序无需为分块传输编码做任何事情。原因是 默认情况下,客户端期望接受分块传输编码。由于 HttpClient 已经在后台处理了它,因此我们无需对编码采取额外的操作。

参考文献

历史

  • 2017-05-05 初始发布
  • 2017-05-05 添加示例下载链接
© . All rights reserved.