到目前为止,我们一直在使用字符串创建请求体,并读取响应的内容。但是我们可以通过使用流提高性能和优化内存。因此,在本文中,我们将学习如何在请求和响应中使用HttpClient流。
什么是流
流是以文件、输入/输出设备或网络流量的形式表示一个字节序列的抽象。C#中的Stream类是一个抽象类,它可以从源文件读取或写入字节。这使我们可以跳过可能增加内存使用量或降低性能的中间变量。
这里需要知道的重要一点是,在客户端处理流与API级别无关。这是一个完全独立的过程。
我们的API可能适用于流,也可能不适用,但这不会影响客户端。这无疑是一个优势,因为我们可以在客户端应用程序中使用流来提高性能和减少内存使用,同时仍然使用API。
使用HttpClient流来获取数据
在本系列的第一篇文章中,我们已经了解了在从API获取数据时,我们必须:
- 向API的URI发送请求
- 等待响应到达
- 使用ReadAsStringAsync方法从响应体中读取内容
- 并使用System.Text.Json反序列化内容
如前所述,对于流,我们可以删除中间的操作,即使用ReadAsStringAsync方法从响应体读取字符串内容。我们来看看怎么做。
首先,我们要在客户端应用程序中创建一个新的HttpClientStreamService:
public class HttpClientStreamService : IHttpClientServiceImplementation { private static readonly HttpClient _httpClient = new HttpClient(); private readonly JsonSerializerOptions _options; public HttpClientStreamService() { _httpClient.BaseAddress = new Uri("https://localhost:5001/api/"); _httpClient.Timeout = new TimeSpan(0, 0, 30); _httpClient.DefaultRequestHeaders.Clear(); _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; } public async Task Execute() { throw new NotImplementedException(); } }
这是我们在本系列中已经见过几次的标准配置。接下来,我们可以创建一个使用流发送GET请求的方法:
private async Task GetCompaniesWithStream() { using (var response = await _httpClient.GetAsync("companies")) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } }
在这个方法中,我们使用GetAsync方法从API中获取数据。但正如我们在本系列的第一篇文章中解释的那样,你可以使用HttpRequestMessage类来对请求进行更高级别的控制。另外,注意这次我们将响应包装在using指令中,因为我们现在使用的是流。
在确保接收状态码成功之后,我们使用ReadAsStreamAsync方法序列化HTTP内容并将其作为流返回。有了这些,我们就不再需要字符串序列化和创建字符串变量了。
一旦我们有了流,我们就调用JsonSerializer.DeserializeAsync 方法从流中读取并将结果反序列化到company对象列表中。
在启动我们的应用程序之前,必须在Execute方法中调用这个方法:
public async Task Execute() { await GetCompaniesWithStream(); }
同时,在Program类中注册这个新服务:
private static void ConfigureServices(IServiceCollection services) { //services.AddScoped<IHttpClientServiceImplementation, HttpClientCrudService>(); //services.AddScoped<IHttpClientServiceImplementation, HttpClientPatchService>(); services.AddScoped<IHttpClientServiceImplementation, HttpClientStreamService>(); }
就是这样。我们可以同时启动两个应用程序并检查结果:
可以看到,我们从流中读取了结果。
额外改进
在前面的示例中,当我们从响应中读取内容时,我们删除了一个字符串创建操作。
因此,我们取得了进步。但是,我们可以通过使用HttpCompletionMode来进一步改进这个解决方案。它是一个有两个值的枚举,控制HttpClient的操作在什么点上被认为已完成。
默认值是HttpCompletionMode.ResponseContentRead。这意味着只有当整个响应和内容一起读取时,HTTP操作才完成。
第二个值是HttpCompletionMode.ResponseHeadersRead。当我们在HTTP请求中选择此选项时,我们声明当响应头被完全读取时操作就完成了。此时,响应体根本不必被完全处理。这显然意味着我们将使用更少的内存,因为我们不必将整个内容保存在内存中。此外,这也会影响性能,因为我们可以更快地处理数据。
为了实现这一改进,我们需要做的就是修改GetCompaniesWithStream方法中的GetAsync方法:
private async Task GetCompaniesWithStream() { using (var response = await _httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } }
如果运行我们的应用程序,将看到与前面示例相同的结果。但这一次,做了更多的改进。
现在,让我们看看如何在POST请求中使用流。
使用HttpClient的流发送POST请求
在本系列的第二篇文章中,学习了如何使用HttpClient发送POST请求。在这个示例中,在发送请求之前将负载序列化为JSON字符串。当然,对于流,我们可以跳过这一部分。
首先,让我们创建一个新方法:
private async Task CreateCompanyWithStream() { var companyForCreation = new CompanyForCreationDto { Name = "Eagle IT Ltd.", Country = "USA", Address = "Eagle IT Street 289" }; var ms = new MemoryStream(); await JsonSerializer.SerializeAsync(ms, companyForCreation); ms.Seek(0, SeekOrigin.Begin); var request = new HttpRequestMessage(HttpMethod.Post, "companies"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); using (var requestContent = new StreamContent(ms)) { request.Content = requestContent; requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStreamAsync(); var createdCompany = await JsonSerializer.DeserializeAsync<CompanyDto>(content, _options); } } }
在这个方法中,我们首先创建一个具有所有必需属性companyForCreation对象。然后,我们需要一个内存流对象。调用JsonSerializer.SerializeAsync时,我们将companyForCreation对象序列化到创建的内存流中。同样,使用Seek方法在流的开头设置一个位置。然后,用所需的参数初始化HttpReqestMessage对象的新实例,并将accept头设置为application/json。
在此之后,我们使用前面的内存流创建一个名为requestContent的新流。StreamContent对象将是请求的内容,因此,我们在代码中声明这一点,并设置请求的ContentType。
最后,我们使用SendAsync方法发送请求,确保响应是成功的,并将内容作为流读取。读取内容后,我们将其反序列化到createdCompany对象中。
所以,正如你所看到的,通过整个方法,我们使用流,避免了使用大字符串时不必要的内存使用。
我们现在要做的就是在Execute方法中调用这个方法:
public async Task Execute() { //await GetCompaniesWithStream(); await CreateCompanyWithStream(); }
结论
在HTTP请求中使用流可以帮助我们减少内存消耗并优化我们的应用程序的性能。在这篇文章中,我们看到了如何使用流从服务器获取数据,并在发送POST请求时为我们的请求体创建一个StreamContent。
原文链接:https://code-maze.com/using-streams-with-httpclient-to-improve-performance-and-memory-usage/