在使用 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.DefaultRequestHeaders
。HttpRequestMessage
允許我們為每個請求獨立地設定請求頭,從而避免了多個請求之間共享頭部的風險。
以下是改進後的程式碼: