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






4.95/5 (19投票s)
一个关于在 ASP.NET Web API 中流式传输大型 JSON 数组以及 HTTP 分块传输编码的示例
概述
在流式传输大型对象数组(例如,10,000 个对象)时,我们通常需要处理两个主要的性能问题
- 对象的大量内存分配
- 服务器长时间的响应时间
为了解决这些问题,我们有两种方法可以提高服务器端性能
- C# 中的迭代模式
- 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 数组。使用 JsonTextReader
和 JsonSerializer
,客户端应用程序可以在无需等待整个 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 添加示例下载链接