到目前為止,我們一直在使用字符串創建請求體,並讀取響應的內容。但是我們可以通過使用流提高性能和優化內存。因此,在本文中,我們將學習如何在請求和響應中使用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/