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