如何避免 HttpClient 丟失請求頭:透過 HttpRequestMessage 解決並最佳化

星仔007發表於2024-11-06

在使用 HttpClient 發起 HTTP 請求時,可能會遇到請求頭丟失的問題,尤其是像 Accept-Language 這樣的請求頭丟失。這個問題可能會導致請求的內容錯誤,甚至影響整個系統的穩定性和功能。本文將深入分析這一問題的根源,並介紹如何透過 HttpRequestMessage 來解決這一問題。

1. 問題的背景:HttpClient的設計與共享機制

HttpClient 是 .NET 中用於傳送 HTTP 請求的核心類,它是一個設計為可複用的類,其目的是為了提高效能,減少在高併發情況下頻繁建立和銷燬 HTTP 連線的開銷。HttpClient 的複用能夠利用作業系統底層的連線池機制,避免了每次請求都要建立新連線的效能損失。

但是,HttpClient 複用的機制也可能導致一些問題,尤其是在多執行緒併發請求時。例如,如果我們在共享的 HttpClient 例項上頻繁地修改請求頭,可能會導致這些修改在不同的請求之間意外地“傳遞”或丟失。

2. 常見問題:丟失請求頭

假設我們有如下的程式碼,其中我們希望在每次請求時設定 Accept-Language 頭:

using System.Net.Http;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace ConsoleApp9
{
    internal class Program
    {
        private static readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            NullValueHandling = NullValueHandling.Ignore
        };

        private static readonly HttpClient httpClient = new HttpClient(); // 複用HttpClient例項
        private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(100); // 限制併發請求數量為100

        static async Task Main(string[] args)
        {
            List<Task> tasks = new List<Task>();
            int taskNoCounter = 1; // 用於跟蹤 taskno
            // 只使用一個HttpClient物件(全域性共享)
            for (int i = 0; i < 50; i++)
            {
                tasks.Add(Task.Run(async () =>
                {
                    // 等待訊號量,控制最大併發數
                    await semaphore.WaitAsync();

                    try
                    {
                        var postData = new
                        {
                            taskno = taskNoCounter++,
                            content = "等待翻譯的內容"
                        };
                        var json = JsonConvert.SerializeObject(postData, serializerSettings);
                        var reqdata = new StringContent(json, Encoding.UTF8, "application/json");

                        // 設定請求頭語言
                        httpClient.DefaultRequestHeaders.Add("Accept-Language", "en-US");                      
                        // 傳送請求
                        var result = await httpClient.PostAsync("http://localhost:5000/translate", reqdata);

                        // 讀取並反序列化 JSON 資料
                        var content = await result.Content.ReadAsStringAsync();
                        var jsonResponse = JsonConvert.DeserializeObject<Response>(content);
                        var response = jsonResponse.Data.Content;
                       
                        // 反序列化後,直接輸出解碼後的文字
                        Console.WriteLine($"結果為:{response}");
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"請求失敗: {ex.Message}");
                    }
                    finally
                    {
                        // 釋放訊號量
                        semaphore.Release();
                    }
                }));
            }

            await Task.WhenAll(tasks);
        }
    }

    // 定義與響應結構匹配的類
    public class Response
    {
        public int Code { get; set; }
        public ResponseData Data { get; set; }
        public string Msg { get; set; }
    }

    public class ResponseData
    {
        public string Content { get; set; }
        public string Lang { get; set; }
        public int Taskno { get; set; }
    }
}

  

接收程式碼如下:

from flask import Flask, request, jsonify
from google.cloud import translate_v2 as translate

app = Flask(__name__)

# 初始化 Google Cloud Translate 客戶端
translator = translate.Client()

@app.route('/translate', methods=['POST'])
def translate_text():
    try:
        # 從請求中獲取 JSON 資料
        data = request.get_json()

        # 獲取請求的文字內容
        text = data.get('content')
        taskno = data.get('taskno', 1)

        # 獲取請求頭中的 Accept-Language 資訊,預設為 'zh-CN'
        accept_language = request.headers.get('Accept-Language', 'zh-CN')

        # 呼叫 Google Translate API 進行翻譯
        result = translator.translate(text, target_language=accept_language)

        # 構造響應資料
        response_data = {
            "code": 200,
            "msg": "OK",
            "data": {
                "taskno": taskno,
                "content": result['translatedText'],
                "lang": accept_language
            }
        }

        # 返回 JSON 響應
        return jsonify(response_data), 200

    except Exception as e:
        return jsonify({"code": 500, "msg": str(e)}), 500


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=5000)

 

Accept-Language 請求頭是透過 httpClient.DefaultRequestHeaders.Add("Accept-Language", language) 來設定的。這是一個常見的做法,目的是為每個請求指定特定的語言。然而,在實際應用中,尤其是當 HttpClient 被複用併發傳送多個請求時,這種方法可能會引發請求頭丟失或錯誤的情況。

測試結果:每20個請求就會有一個接收拿不到語言,會使用預設的zh-CN,這條請求就不會翻譯。在上面的程式碼中,

3. 為什麼會丟失請求頭?

丟失請求頭的問題通常出現在以下兩種情況:

  • 併發請求之間共享 HttpClient 例項:當多個執行緒或任務共享同一個 HttpClient 例項時,它們可能會修改 DefaultRequestHeaders,導致請求頭在不同請求之間互相干擾。例如,如果一個請求修改了 Accept-Language,它會影響到後續所有的請求,而不是每個請求都獨立使用自己的請求頭。
  • 頭部快取問題HttpClient 例項可能會快取頭部資訊。如果請求頭未正確設定,快取可能會導致丟失之前設定的頭部。

在這種情況下,丟失請求頭或請求頭不一致的現象就會發生,從而影響請求的正確性和響應的準確性。

4. 解決方案:使用 HttpRequestMessage

為了解決這個問題,我們可以使用 HttpRequestMessage 來替代直接修改 HttpClient.DefaultRequestHeadersHttpRequestMessage 允許我們為每個請求獨立地設定請求頭,從而避免了多個請求之間共享頭部的風險。

以下是改進後的程式碼:

using System.Net.Http;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace ConsoleApp9
{
    internal class Program
    {
        private static readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            NullValueHandling = NullValueHandling.Ignore
        };

        private static readonly HttpClient httpClient = new HttpClient(); // 複用HttpClient例項
        private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(100); // 限制併發請求數量為100

        static async Task Main(string[] args)
        {
            List<Task> tasks = new List<Task>();
            int taskNoCounter = 1; // 用於跟蹤 taskno
            // 只使用一個HttpClient物件(全域性共享)
            for (int i = 0; i < 50; i++)
            {
                tasks.Add(Task.Run(async () =>
                {
                    // 等待訊號量,控制最大併發數
                    await semaphore.WaitAsync();

                    try
                    {
                        var postData = new
                        {
                            taskno = taskNoCounter++,
                            content = "等待翻譯的內容"
                        };
                        var json = JsonConvert.SerializeObject(postData, serializerSettings);
                        var reqdata = new StringContent(json, Encoding.UTF8, "application/json");

                        // 使用HttpRequestMessage確保每個請求都可以單獨設定頭
                        var requestMessage = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/translate")
                        {
                            Content = reqdata
                        };

                        // 設定請求頭
                        requestMessage.Headers.Add("Accept-Language", "en-US");

                        // 發起POST請求
                        var result = await httpClient.SendAsync(requestMessage);

                        // 讀取並反序列化 JSON 資料
                        var content = await result.Content.ReadAsStringAsync();
                        var jsonResponse = JsonConvert.DeserializeObject<Response>(content);
                        var response = jsonResponse.Data.Content;
                     
                        // 反序列化後,直接輸出解碼後的文字
                        Console.WriteLine($"結果為:{response}");
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"請求失敗: {ex.Message}");
                    }
                    finally
                    {
                        // 釋放訊號量
                        semaphore.Release();
                    }
                }));
            }

            await Task.WhenAll(tasks);
        }
    }

    // 定義與響應結構匹配的類
    public class Response
    {
        public int Code { get; set; }
        public ResponseData Data { get; set; }
        public string Msg { get; set; }
    }

    public class ResponseData
    {
        public string Content { get; set; }
        public string Lang { get; set; }
        public int Taskno { get; set; }
    }
}

  

5. 解析解決方案:為何 HttpRequestMessage 更加可靠

  • 獨立請求頭HttpRequestMessage 是一個每個請求都可以獨立設定頭部的類,它允許我們為每個 HTTP 請求單獨配置請求頭,而不會被其他請求所干擾。透過這種方式,我們可以確保每個請求都使用準確的請求頭。
  • 高併發控制:當 HttpClient 例項被多個請求共享時,HttpRequestMessage 確保每個請求都能夠獨立處理頭部。即使在高併發環境下,每個請求的頭部設定都是獨立的,不會相互影響。
  • 請求靈活性HttpRequestMessage 不僅可以設定請求頭,還可以設定請求方法、請求體、請求的 URI 等,這使得它比直接使用 DefaultRequestHeaders 更加靈活和可控。

6. 小結:最佳化 HttpClient 請求頭管理

總結來說,當使用 HttpClient 時,若多個請求共用一個例項,直接修改 DefaultRequestHeaders 會導致請求頭丟失或不一致的問題。透過使用 HttpRequestMessage 來管理每個請求的頭部,可以避免這個問題,確保請求頭的獨立性和一致性。

  • 使用 HttpRequestMessage 來獨立設定請求頭,是確保請求頭正確性的最佳實踐。
  • 複用 HttpClient 例項是提升效能的好方法,但要注意併發請求時請求頭可能會丟失或錯誤,HttpRequestMessage 是解決這一問題的有效工具。

透過這種方式,我們不僅避免了請求頭丟失的問題,還提升了請求的可靠性和可控性,使得整個 HTTP 請求管理更加高效和精確。


總結

以上從 HttpClient 設計和併發請求的角度,詳細探討了請求頭丟失的問題,並透過例項程式碼展示瞭如何透過 HttpRequestMessage 來最佳化請求頭管理。透過這種方式,能夠確保在高併發或多執行緒環境中每個請求的請求頭都能夠獨立設定,從而避免了請求頭丟失或錯誤的問題。

相關文章