在ASP.NET Core中用HttpClient(四)——提高效能和優化記憶體

碼農譯站發表於2021-03-25

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

相關文章