在使用 HttpClient
發(fā)起 HTTP 請求時,可能會遇到請求頭丟失的問題,尤其是像 Accept-Language
這樣的請求頭丟失。這個問題可能會導(dǎo)致請求的內(nèi)容錯誤,甚至影響整個系統(tǒng)的穩(wěn)定性和功能。本文將深入分析這一問題的根源,并介紹如何通過 HttpRequestMessage
來解決這一問題。
1. 問題的背景:HttpClient的設(shè)計與共享機制
HttpClient
是 .NET 中用于發(fā)送 HTTP 請求的核心類,它是一個設(shè)計為可復(fù)用的類,其目的是為了提高性能,減少在高并發(fā)情況下頻繁創(chuàng)建和銷毀 HTTP 連接的開銷。HttpClient
的復(fù)用能夠利用操作系統(tǒng)底層的連接池機制,避免了每次請求都要建立新連接的性能損失。
但是,HttpClient
復(fù)用的機制也可能導(dǎo)致一些問題,尤其是在多線程并發(fā)請求時。例如,如果我們在共享的 HttpClient
實例上頻繁地修改請求頭,可能會導(dǎo)致這些修改在不同的請求之間意外地“傳遞”或丟失。
2. 常見問題:丟失請求頭
假設(shè)我們有如下的代碼,其中我們希望在每次請求時設(shè)置 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(); // 復(fù)用HttpClient實例
private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(100); // 限制并發(fā)請求數(shù)量為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 () =>
{
// 等待信號量,控制最大并發(fā)數(shù)
await semaphore.WaitAsync();
try
{
var postData = new
{
taskno = taskNoCounter++,
content = "等待翻譯的內(nèi)容"
};
var json = JsonConvert.SerializeObject(postData, serializerSettings);
var reqdata = new StringContent(json, Encoding.UTF8, "application/json");
// 設(shè)置請求頭語言
httpClient.DefaultRequestHeaders.Add("Accept-Language", "en-US");
// 發(fā)送請求
var result = await httpClient.PostAsync("http://localhost:5000/translate", reqdata);
// 讀取并反序列化 JSON 數(shù)據(jù)
var content = await result.Content.ReadAsStringAsync();
var jsonResponse = JsonConvert.DeserializeObject<Response>(content);
var response = jsonResponse.Data.Content;
// 反序列化后,直接輸出解碼后的文本
Console.WriteLine($"結(jié)果為:{response}");
}
catch (Exception ex)
{
Console.WriteLine($"請求失敗: {ex.Message}");
}
finally
{
// 釋放信號量
semaphore.Release();
}
}));
}
await Task.WhenAll(tasks);
}
}
// 定義與響應(yīng)結(jié)構(gòu)匹配的類
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 數(shù)據(jù)
data = request.get_json()
# 獲取請求的文本內(nèi)容
text = data.get('content')
taskno = data.get('taskno', 1)
# 獲取請求頭中的 Accept-Language 信息,默認為 'zh-CN'
accept_language = request.headers.get('Accept-Language', 'zh-CN')
# 調(diào)用 Google Translate API 進行翻譯
result = translator.translate(text, target_language=accept_language)
# 構(gòu)造響應(yīng)數(shù)據(jù)
response_data = {
"code": 200,
"msg": "OK",
"data": {
"taskno": taskno,
"content": result['translatedText'],
"lang": accept_language
}
}
# 返回 JSON 響應(yīng)
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)
來設(shè)置的。這是一個常見的做法,目的是為每個請求指定特定的語言。然而,在實際應(yīng)用中,尤其是當(dāng) HttpClient
被復(fù)用并發(fā)發(fā)送多個請求時,這種方法可能會引發(fā)請求頭丟失或錯誤的情況。
測試結(jié)果:每20個請求就會有一個接收拿不到語言,會使用默認的zh-CN,這條請求就不會翻譯。在上面的代碼中,
3. 為什么會丟失請求頭?
丟失請求頭的問題通常出現(xiàn)在以下兩種情況:
并發(fā)請求之間共享 HttpClient
實例:當(dāng)多個線程或任務(wù)共享同一個 HttpClient
實例時,它們可能會修改 DefaultRequestHeaders
,導(dǎo)致請求頭在不同請求之間互相干擾。例如,如果一個請求修改了 Accept-Language
,它會影響到后續(xù)所有的請求,而不是每個請求都獨立使用自己的請求頭。
頭部緩存問題:HttpClient
實例可能會緩存頭部信息。如果請求頭未正確設(shè)置,緩存可能會導(dǎo)致丟失之前設(shè)置的頭部。
在這種情況下,丟失請求頭或請求頭不一致的現(xiàn)象就會發(fā)生,從而影響請求的正確性和響應(yīng)的準(zhǔn)確性。
4. 解決方案:使用 HttpRequestMessage
為了解決這個問題,我們可以使用 HttpRequestMessage
來替代直接修改 HttpClient.DefaultRequestHeaders
。HttpRequestMessage
允許我們?yōu)槊總€請求獨立地設(shè)置請求頭,從而避免了多個請求之間共享頭部的風(fēng)險。
以下是改進后的代碼:
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(); // 復(fù)用HttpClient實例
private static readonly SemaphoreSlim semaphore = new SemaphoreSlim(100); // 限制并發(fā)請求數(shù)量為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 () =>
{
// 等待信號量,控制最大并發(fā)數(shù)
await semaphore.WaitAsync();
try
{
var postData = new
{
taskno = taskNoCounter++,
content = "等待翻譯的內(nèi)容"
};
var json = JsonConvert.SerializeObject(postData, serializerSettings);
var reqdata = new StringContent(json, Encoding.UTF8, "application/json");
// 使用HttpRequestMessage確保每個請求都可以單獨設(shè)置頭
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/translate")
{
Content = reqdata
};
// 設(shè)置請求頭
requestMessage.Headers.Add("Accept-Language", "en-US");
// 發(fā)起POST請求
var result = await httpClient.SendAsync(requestMessage);
// 讀取并反序列化 JSON 數(shù)據(jù)
var content = await result.Content.ReadAsStringAsync();
var jsonResponse = JsonConvert.DeserializeObject<Response>(content);
var response = jsonResponse.Data.Content;
// 反序列化后,直接輸出解碼后的文本
Console.WriteLine($"結(jié)果為:{response}");
}
catch (Exception ex)
{
Console.WriteLine($"請求失敗: {ex.Message}");
}
finally
{
// 釋放信號量
semaphore.Release();
}
}));
}
await Task.WhenAll(tasks);
}
}
// 定義與響應(yīng)結(jié)構(gòu)匹配的類
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
是一個每個請求都可以獨立設(shè)置頭部的類,它允許我們?yōu)槊總€ HTTP 請求單獨配置請求頭,而不會被其他請求所干擾。通過這種方式,我們可以確保每個請求都使用準(zhǔn)確的請求頭。
高并發(fā)控制:當(dāng) HttpClient
實例被多個請求共享時,HttpRequestMessage
確保每個請求都能夠獨立處理頭部。即使在高并發(fā)環(huán)境下,每個請求的頭部設(shè)置都是獨立的,不會相互影響。
請求靈活性:HttpRequestMessage
不僅可以設(shè)置請求頭,還可以設(shè)置請求方法、請求體、請求的 URI 等,這使得它比直接使用 DefaultRequestHeaders
更加靈活和可控。
6. 小結(jié):優(yōu)化 HttpClient
請求頭管理
總結(jié)來說,當(dāng)使用 HttpClient
時,若多個請求共用一個實例,直接修改 DefaultRequestHeaders
會導(dǎo)致請求頭丟失或不一致的問題。通過使用 HttpRequestMessage
來管理每個請求的頭部,可以避免這個問題,確保請求頭的獨立性和一致性。
轉(zhuǎn)自https://www.cnblogs.com/morec/p/18529308
該文章在 2024/11/7 8:47:54 編輯過