不要用 using 包住 HttpClient ── C# 商用應用程式的 HTTP 通訊實務(生成模式・逾時・重試)
· Go Komura · CSharp, .NET, HttpClient, IHttpClientFactory, 網路, async/await, Windows 開發, 技術諮詢
「一到下午,對外部 API 的連線就開始出現 SocketException 失敗」「明明切換了連線目標,應用程式卻還是持續連往舊伺服器」──C# 的 HttpClient,如果只是呼叫 GetAsync,用起來很簡單,但如果弄錯了要如何生成、持有實例,就會埋下這種「當下能動,但上線運行一段時間後才壞掉」的問題。
本文假設 Windows 商用應用程式呼叫外部 Web API 或內部服務的情境,依照實務上容易感到迷惘的順序,整理 HttpClient 的正確生成模式、逾時設計、重試、錯誤處理,以及 Windows 環境特有的陷阱。
1. 先講結論(判斷表)
HttpClient 該怎麼持有,答案會依應用程式的架構而異。先整理成判斷表。
| 應用程式的形態 | 建議模式 | 理由 |
|---|---|---|
| 執行時間僅數秒到數分鐘的主控台工具 (.NET) | 使用 1 個 static/單例的 HttpClient | 若是短命的行程,DNS 變更問題實質上可以忽略 |
| 長時間常駐的應用程式・Windows 服務 (.NET,不使用 DI) | static 的 HttpClient +設定 SocketsHttpHandler.PooledConnectionLifetime | 可同時解決 Socket 枯竭與 DNS 變更問題 |
| 使用 Generic Host / DI 的應用程式 (.NET) | IHttpClientFactory (AddHttpClient) | 可以把 Handler 的集區與汰換交給工廠處理。可用具名/型別化用戶端,依連線目標分離設定 |
| .NET Framework 的應用程式 | 透過 Microsoft.Extensions.Http 套件導入 IHttpClientFactory | 在 .NET Framework 中自行生成容易發生連接埠枯竭,官方也建議使用工廠1 |
| 依連線目標而異的 Proxy・Cookie・憑證設定 | 依設定分別建立 HttpClient(不共用) | Handler 的連線設定在送出第一次請求後就無法變更2 |
在此基礎上,先寫出結論。
- 不能每次請求都
new HttpClient(),然後用using釋放。HttpClient內部持有連線集區,其設計前提是被重複使用。若每次都產生、釋放,高負載時會把可用的 Socket 用盡,發生SocketException2。 - 但也不是把它設成
static就結束了。HttpClient只會在建立連線時解析 DNS,因此即使連線目標的 IP 位址改變,也會繼續使用舊的連線。官方建議的解法是用SocketsHttpHandler.PooledConnectionLifetime為連線設定壽命上限1。 - 如果使用 Generic Host 或 DI,就交給
IHttpClientFactory處理。 工廠會將 Handler 集區化,並在預設 2 分鐘後汰換,藉此同時應對 Socket 枯竭與 DNS 變更兩個問題3。 - 逾時的預設值是 100 秒。 對商用應用程式的使用體感來說,這跟「無限期卡住」沒什麼兩樣,請依連線目標決定需求並明確設定。
- 重試不要自己寫,先從
Microsoft.Extensions.Http.Resilience的標準處理器開始。 它能以一組經過驗證的預設值,導入重試、斷路器(circuit breaker)、逾時等機制,並在設計階段就能避免自製重試迴圈常見的「無條件重送失敗的 POST,導致重複登錄」之類的事故4。
2. 為什麼「每次都用 using 生成」不行 ── Socket 枯竭
因為 HttpClient 實作了 IDisposable,所以下面這樣的程式碼乍看之下似乎沒問題。
// 反模式:每次請求都產生並釋放
public async Task<string> GetDataAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
問題在於,即使呼叫 Dispose,在作業系統層級 Socket 也不會立刻被釋放。依 TCP 規格,關閉一方的 Socket 會以 TIME_WAIT 狀態殘留一段時間。呼叫頻率低時不會有任何問題,但負載一旦升高,未釋放的 Socket 就會不斷累積,某個時間點會突然因 SocketException 而無法連線2。
這類問題麻煩的地方在於,開發、測試階段幾乎不會重現。它通常會以「只有在正式環境的尖峰時段才會掛掉」、「只有在月底批次處理時才會掛掉」這種形式出現。若要調查,可以在問題發生時用下面的指令確認 TIME_WAIT 狀態的 Socket 數量,藉此推測原因所在。
# 依連線目標統計 TIME_WAIT 狀態的 Socket 數量
netstat -ano | Select-String "TIME_WAIT" | Measure-Object
另外,透過 IHttpClientFactory 取得的 HttpClient 不在此討論範圍內。工廠製造的用戶端即使呼叫 Dispose,Handler(連線集區的實體)也不會被釋放,因此用 using 包起來是安全的3。
3. 設成 static 並非萬事 OK ── DNS 變更問題
為了因應 Socket 枯竭而將 HttpClient 設成 static,方向是對的,但這樣做仍會留下另一個問題。HttpClient 只會在建立連線時解析 DNS,也不會參照 DNS 記錄的 TTL2。只要連線還存活在集區中,即使連線目標的 IP 位址改變,也會持續連往舊的 IP。
「明明因為容錯移轉(failover)切換了 DNS,應用程式卻一直連往舊伺服器,直到重新啟動為止」這類故障,原因就出在這個機制。官方指南建議的解法,是用 SocketsHttpHandler.PooledConnectionLifetime 為連線設定壽命上限1。
// .NET (Core) / .NET 5+ 的建議模式:
// 讓連線每隔一段時間重建,藉此跟上 DNS 變更
private static readonly HttpClient SharedClient = new(new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2)
});
壽命到期的連線會在下一次請求時重建,並在那個時間點重新解析 DNS。這個數值要依「希望多快跟上 DNS 變更」來決定。官方文件的範例使用 2 分鐘,但如果連線目標鮮少變動(例如公司內部系統),設定得更長也沒問題1。
另外,SocketsHttpHandler 是 .NET Core 2.1 之後才有的實作,無法在 .NET Framework 中使用。若是 .NET Framework,請使用下一章介紹的 IHttpClientFactory1。
4. 若使用 DI,就用 IHttpClientFactory
在使用 Generic Host 或 DI 容器的應用程式中,IHttpClientFactory(AddHttpClient)是首選。關於 Generic Host 本身的說明,請參考「.NET 的 Generic Host 是什麼 - 先整理 DI、設定、日誌、BackgroundService」;在桌面應用程式中導入的方式,請參考「把 Generic Host / BackgroundService 帶進桌面應用程式的理由 - 啟動・壽命・graceful shutdown 的整理會輕鬆很多」。
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// 具名用戶端:依連線目標分離設定
builder.Services.AddHttpClient("OrderApi", client =>
{
client.BaseAddress = new Uri("https://order.example.co.jp/");
client.Timeout = TimeSpan.FromSeconds(10);
});
關於工廠的行為,有 3 點需要掌握。
- Handler 會被集區化,預設每 2 分鐘汰換一次。 每次呼叫
CreateClient都會回傳新的HttpClient,但背後的 Handler(連線集區)是共用的,因此不會發生 Socket 枯竭,而定期汰換也能讓其跟上 DNS 變更3。 - 工廠製造的
HttpClient的設計前提是短命使用。 若把取得的實例保存在單例(singleton)欄位中,就無法參與 Handler 的汰換,也就無法跟上 DNS 變更。基於相同理由,也請避免把型別化用戶端注入到單例服務中的架構3。 - 依賴 Cookie 的應用程式需要特別注意。 由於 Handler 會被集區化,
CookieContainer會在非預期的情況下被共用。若要使用 Cookie,官方的建議是避免使用工廠,或是停用 Cookie 處理、改為自行附加標頭1。
若要呼叫需要驗證的 API(例如受 Microsoft Entra ID 保護的 API),關於權杖(token)取得的部分,已在「在 WinForms/WPF 應用程式中導入 Entra ID 驗證 ── MSAL.NET 與 WAM 代理的實務架構」中說明,請參考該文。
5. 逾時設計 ── 預設的 100 秒對商用應用程式來說太長了
HttpClient.Timeout 的預設值是 100 秒5。對於延續畫面操作而呼叫 API 的商用應用程式來說,讓使用者等待 100 秒跟「當機了」沒兩樣,因此請依連線目標明確設定。
// 用戶端整體的預設逾時
client.Timeout = TimeSpan.FromSeconds(10);
// 若只想讓特定請求的逾時變短/變長,可使用 CancellationTokenSource
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
HttpResponseMessage response = await client.GetAsync(url, cts.Token);
設計上的注意事項如下。
- 逾時發生時的例外是
TaskCanceledException。 .NET 5 以降,因HttpClient.Timeout造成的逾時,其內部例外(inner exception)會是TimeoutException5。不過,像上面那樣使用自訂CancellationTokenSource造成的逾時,並不會附帶內部例外。要區分「逾時」與「使用者取消」,與其看內部例外,不如判斷「呼叫端傳入的 token 是否已經被取消」會更可靠(見第 7 章的程式碼範例)。請注意,如果 catch 區塊只捕捉HttpRequestException,會漏接逾時的情況。 Timeout是「整個請求」的限制。 若只想限制建立連線的時間,可以併用SocketsHttpHandler.ConnectTimeout。像「伺服器掛掉時想在 3 秒內放棄,但正常情況下較大的回應想等待 60 秒」這樣的需求,就可以用這兩者的組合來表達2。- 下載大型檔案時要避免預設的緩衝行為。
HttpClient預設會把整個回應讀進記憶體,因此在下載數十 MB 以上的檔案時,應指定HttpCompletionOption.ResponseHeadersRead,改以串流方式處理2。
若想把逾時值抽出到 appsettings.json,依環境切換不同數值,「不只是 appsettings.json ── Windows 商用應用程式的組態管理實務(環境別設定・機密資訊・寫入位置)」中的判斷表可以直接套用。
另外,從 WinForms/WPF 呼叫 HTTP 通訊時,如果用 .Result 或 .Wait() 進行阻塞式呼叫,會導致 UI 執行緒發生死結(deadlock)。這個經典陷阱在「C# async/await 的最佳實踐 - Task.Run 與 ConfigureAwait 的判斷表」中有詳細說明,建議在撰寫通訊處理程式碼之前先讀一遍。
6. 重試 ── 不要自製迴圈,改用標準的 resilience 處理器
網路本來就會發生暫時性的失敗,因此呼叫外部 API 的處理需要重試機制。不過,用 for 迴圈搭配 Task.Delay 自製重試,需要把下面這些點全部自行正確實作,實在不划算。
- 區分「可以重試的失敗」(逾時、HTTP 408/429/5xx)與「重試也沒用的失敗」(HTTP 400/401/404)
- 排除重新執行會造成事故的 HTTP 方法(例如 POST 造成的重複登錄)
- 指數退避與抖動(jitter,防止所有用戶端同時重送,再度把伺服器打垮的隨機擾動)
- 在故障持續發生時,直接停止呼叫本身的斷路器(circuit breaker)
Microsoft.Extensions.Http.Resilience 套件的標準處理器,會以一組經過驗證的預設值提供這整套機制4。
builder.Services.AddHttpClient("OrderApi", client =>
{
client.BaseAddress = new Uri("https://order.example.co.jp/");
})
.AddStandardResilienceHandler(); // 重試+斷路器+逾時的標準組合
標準處理器的預設值為:整個請求逾時 30 秒、最多 3 次的指數退避重試(首次延遲 2 秒,附帶抖動)、每次嘗試逾時 10 秒,並將 HTTP 408/429/5xx 與 HttpRequestException 視為暫時性錯誤處理4。
有一點在預設值上需要特別注意:標準處理器預設會對所有 HTTP 方法進行重試。 若是登錄類的 POST 一旦重複執行就會造成困擾的 API,請停用不安全方法的重試4。
httpClientBuilder.AddStandardResilienceHandler(options =>
{
// 停用 POST/PUT/DELETE 等方法的重新執行
options.Retry.DisableForUnsafeHttpMethods();
});
另外,重試能解決的終究只是暫時性的失敗。像「連線已經建立,但資料傳不過來」「回應極端緩慢」這類症狀,很多時候其實是 TCP 層的問題,這種情況的排查方式可以參考「TCP 重送讓工業相機通訊卡幾秒時 - RFC1323 timestamp 與重送等待的切分」中介紹的手法。
7. 錯誤處理 ── 該如何處理狀態碼
HttpClient 對於像 HTTP 404 或 500 這類「就 HTTP 而言確實有回應,但屬於失敗」的情況,並不會拋出例外。會拋出例外的,是連線失敗、逾時、取消等「連回應本身都沒能取得」的情況。撰寫程式碼時要意識到這兩種系統並分開處理。
try
{
using HttpResponseMessage response = await client.GetAsync(url, ct);
if (!response.IsSuccessStatusCode)
{
// 有回應但屬於失敗:可依狀態碼分支處理
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null; // 將「不存在」視為正常情況處理的範例
}
response.EnsureSuccessStatusCode(); // 其餘情況導向 HttpRequestException
}
return await response.Content.ReadFromJsonAsync<Order>(ct);
}
catch (HttpRequestException ex)
{
// 連線失敗,或是因 EnsureSuccessStatusCode 而產生的失敗狀態。
// .NET 5 以降可透過 ex.StatusCode 取得失敗時的狀態碼
logger.LogError(ex, "注文APIの呼び出しに失敗しました。StatusCode={StatusCode}", ex.StatusCode);
throw;
}
catch (TaskCanceledException) when (ct.IsCancellationRequested)
{
// 由呼叫端傳入的 token 所觸發的取消(例如關閉畫面等)。
// 這不算錯誤,因此不弄髒日誌,直接原樣往上傳播
throw;
}
catch (TaskCanceledException ex)
{
// HttpClient.Timeout 或自訂逾時用 CTS 到期
logger.LogError(ex, "注文APIがタイムアウトしました");
throw;
}
像「要把 404 當成例外處理,還是當成 null 處理」這類判斷,取決於連線目標 API 的語意。如果什麼都用 EnsureSuccessStatusCode 一律拋成例外,呼叫端的 catch 區塊就會變得肥大。哪些該做成例外、哪些該用回傳值表達,這條界線可以直接套用「應該在哪裡 catch 例外並輸出日誌、進行錯誤處理 - 以實務向整理呼叫階層的邊界與職責」中的思路。
JSON 的傳送與接收,只要使用 System.Net.Http.Json 的 GetFromJsonAsync / PostAsJsonAsync / ReadFromJsonAsync,就不必自己撰寫透過字串的序列化程式碼。
8. Windows 商用應用程式特有的陷阱
最後,整理在 Windows 環境的實務中經常踩到的陷阱。
- Proxy 自動偵測會拖慢第一次請求。 在 Windows 的預設設定下,
HttpClient會使用作業系統的 Proxy 設定(包含自動偵測)。若確定不需要 Proxy,將HttpClientHandler.UseProxy設為false停用即可省去偵測等待時間2。反之,在必須使用公司內部 Proxy 的環境中,透過WebProxy明確指定,較能避免「開發機能動,伺服器卻不能動」的情況。 - Proxy 設定要在第一次請求之前就完成。 Handler 的連線相關設定,一旦送出過請求後再變更也不會生效2。
- 同時連線數的預設值,在 .NET 與 .NET Framework 上恰好相反。 在 .NET(
SocketsHttpHandler)中,HTTP/1.1 的同時連線數預設是無限制,因此在大量平行請求下連線數會持續增加,有時會碰到防火牆或伺服器端的限制。在高並行度的處理中,請用MaxConnectionsPerServer設定上限2。反之,在 .NET Framework 中,ServicePointManager.DefaultConnectionLimit的預設值很小(非 ASP.NET 環境下為 2),會發生平行請求在內部被排隊等待、進而逾時的問題。若要在 .NET Framework 中提高並行度,請明確調高這個上限6。 - 從 Windows 服務呼叫時,Proxy 與 TLS 的執行內容和使用者不同。 服務的執行帳戶並沒有使用者的 Proxy 設定或憑證,這是「互動式使用者能動,但服務不能動」這類通訊問題的經典成因。關於服務特有的執行內容,請參考「Windows 服務的建立與維運 ── 從工作排程器的取捨到 BackgroundService 服務化」。
- 不要把通訊目標的 URL 或 API 金鑰寫死在程式碼裡。 連線目標的切換請交給設定檔(「不只是 appsettings.json ── Windows 商用應用程式的組態管理實務(環境別設定・機密資訊・寫入位置)」),機密資訊的保存則請採用「Windows 應用程式不要把機密資訊以明文存進設定檔的最佳實踐」中的方法。
結語
HttpClient 的實務品質,與其說取決於「怎麼呼叫」,不如說取決於「怎麼持有」。每次請求都生成會招致 Socket 枯竭,天真地設成 static 又會導致無法跟上 DNS 變更,而這兩者在開發階段都看不出來。若是 .NET,答案是附帶 PooledConnectionLifetime 的共用實例或 IHttpClientFactory;若是 .NET Framework,答案就是導入 IHttpClientFactory。在此基礎之上,再依連線目標明確設定逾時,並將重試交給標準的 resilience 處理器──做到這個程度,才算是一個把「網路偶爾會失敗」當作前提的商用應用程式。
對既有應用程式通訊相關的檢討(只在尖峰時段掛掉的通訊、逾時設計的整理、外部 API 串接的新開發),往往需要一邊查看實際程式碼與運行環境一邊做判斷,如果拿不定主意,歡迎與我們聯絡諮詢。
相關文章
- C# async/await 的最佳實踐 - Task.Run 與 ConfigureAwait 的判斷表
- .NET 的 Generic Host 是什麼 - 先整理 DI、設定、日誌、BackgroundService
- 把 Generic Host / BackgroundService 帶進桌面應用程式的理由 - 啟動・壽命・graceful shutdown 的整理會輕鬆很多
- 不只是 appsettings.json ── Windows 商用應用程式的組態管理實務(環境別設定・機密資訊・寫入位置)
- 在 WinForms/WPF 應用程式中導入 Entra ID 驗證 ── MSAL.NET 與 WAM 代理的實務架構
- TCP 重送讓工業相機通訊卡幾秒時 - RFC1323 timestamp 與重送等待的切分
- TCP 中「Send 單位=Receive 單位」的誤解 ── 把 TCP 視為位元組串流來設計接收邏輯
- 應該在哪裡
catch例外並輸出日誌、進行錯誤處理 - 以實務向整理呼叫階層的邊界與職責 - Windows 應用程式的機密資訊保存 - 用 DPAPI 避免明文設定
相關諮詢領域
合同會社小村軟體處理包含外部 API 串接在內的 Windows 商用應用程式開發,以及既有應用程式通訊問題(Socket 枯竭・逾時・間歇性連線失敗)的調查與改善方針的技術諮詢。
參考資料
-
Microsoft Learn, Guidelines for using HttpClient。關於 .NET Core/.NET 5+ 應使用設定了
PooledConnectionLifetime的長壽命用戶端,或是IHttpClientFactory製造的短壽命用戶端;.NET Framework 建議使用IHttpClientFactory;以及使用 Cookie 的應用程式應避免IHttpClientFactory(因CookieContainer會被共用)等內容。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Microsoft Learn, HttpClient Class。關於每次請求都生成會導致 Socket 枯竭與
SocketException;DNS 只在建立連線時解析、也不參照 TTL;Handler 的連線設定在第一次請求後就無法變更;HTTP/1.1 的同時連線數預設無限制;大型下載建議使用串流;以及 Proxy 的預設行為與透過UseProxy停用等內容。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
Microsoft Learn, IHttpClientFactory with .NET。關於 Handler 的預設壽命為 2 分鐘;工廠製造的
HttpClient應以短命方式使用;工廠製造的用戶端呼叫Dispose不會釋放 Handler;以及把型別化用戶端注入單例會導致無法跟上 DNS 變更等內容。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, Build resilient HTTP apps: Key development patterns。關於
AddStandardResilienceHandler所組成的 5 段式策略(速率限制器/整體逾時 30 秒/最多 3 次的指數退避重試/斷路器/每次嘗試逾時 10 秒)、適用的狀態碼(408/429/5xx)與例外、以及DisableForUnsafeHttpMethods停用 POST 等方法的重試等內容。 ↩ ↩2 ↩3 ↩4 -
Microsoft Learn, HttpClient.Timeout Property。關於預設值為 100 秒;以及 .NET 5 以降在逾時發生時,會拋出內部例外為
TimeoutException的TaskCanceledException等內容。 ↩ ↩2 -
Microsoft Learn, ServicePointManager.DefaultConnectionLimit Property。關於預設同時連線數在 ASP.NET 代管的應用程式中為 10,其他情況(桌面應用程式等)為 2 等內容。 ↩
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
WinForms/WPF 應用程式的多語言化 ── resx、附屬組件與文化特性切換的實務
本文從實務角度整理 Windows 桌面應用程式的多語言化,包括 CurrentCulture 與 CurrentUICulture 的差異、resx 與附屬組件(Satellite Assembly)所構成的資源機制、WinForms 的 Localizable 屬性、W...
CSV不是「純文字」而已 ── C# 業務應用程式的 CSV 實務(字元編碼・Excel 相容性・注入攻擊防範)
整理業務應用程式在 CSV 讀寫時容易出事故的典型模式──用 Split(',') 自行解析、無 BOM 的 UTF-8 在 Excel 中亂碼、開頭零消失、CSV 注入攻擊──從 RFC 4180 的規則、.NET 中 Shift_JIS 的處理方式(CodePagesE...
用 PerfView 與 dotnet-trace 找出「變慢」的原因 ── .NET 效能調查實務入門
當商用應用程式「變慢」「CPU 卡住不動」「偶爾當掉」時,該用哪個工具看什麼?本文整理 PerfView 與 dotnet-trace 的角色分工、CPU 取樣的讀法(inclusive/exclusive)、用 ThreadTime 調查阻塞時間,以及與 EventSou...
不只是 appsettings.json ── Windows 商用應用程式的組態管理實務(環境別設定・機密資訊・寫入位置)
本文從實務角度整理 Windows 商用應用程式的組態管理,涵蓋 appsettings.json 的分層、IConfiguration 與 IOptions/IOptionsSnapshot/IOptionsMonitor 的選用方式、環境別設定、可寫入設定的存放位置、機...
Windows 業務應用程式的列印與 PDF 輸出 ── System.Drawing.Printing / WPF / 報表函式庫的取捨
本文以要件別判斷表整理 PrintDocument 的 WinForms 列印、WPF 的 FlowDocument/FixedDocument 列印,以及 PDF 輸出的各種選項。並從實務角度說明分頁控制、DPI 落差等實作陷阱,以及從 Windows 服務列印、依賴預設...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
UI 執行緒 & 計時器
整理 WPF / WinForms UI 執行緒、非同步流程、Dispatcher 使用、計時器判斷的主題頁面。
故障調查 & 長期運行故障
整理間歇性故障、通訊診斷、長期運行當機、失敗路徑測試基礎的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
因為外部 API 串接與 HTTP 通訊相關的設計與實作,屬於 Windows 應用程式開發的實務諮詢範圍。
技術諮詢 & 設計審查
因為既有應用程式通訊問題(Socket 枯竭・逾時設計)的檢討,屬於伴隨設計審查的技術諮詢範疇。
常見問題
整理諮詢這個主題時常見的問題。
- HttpClient 明明實作了 IDisposable,為什麼不能用 using 包起來?
- 問題出在「每次請求都產生並釋放」這種用法。HttpClient 內部持有連線集區(connection pool),其設計前提是在應用程式的生命週期內重複使用。若每次都產生、釋放,即使釋放後 Socket 仍會以 TIME_WAIT 狀態殘留一段時間,高負載時就會把 Socket 用盡,進而引發 SocketException。請改成只在應用程式結束時釋放一次,或是透過 IHttpClientFactory 取得。透過工廠(factory)取得的 HttpClient,即使呼叫 Dispose,其 Handler 也不會被釋放,因此用 using 包起來也沒問題。
- 應該從 .NET Framework 的 WebClient 或 HttpWebRequest 遷移過來嗎?
- 建議新寫的程式碼統一改用 HttpClient。WebClient 與 HttpWebRequest 是為了相容性而保留下來的舊 API,Microsoft 也建議新開發一律使用 HttpClient。並不需要一次性替換所有既有程式碼,但只要在調整通訊相關程式碼時逐步遷移到 HttpClient,在逾時控制、async 支援、易測試性方面都會讓維護變得輕鬆許多。
- 重試應該做幾次、間隔多久比較好?
- 與其自行決定,不如以 Microsoft.Extensions.Http.Resilience 標準處理器的預設值(最多 3 次、指數退避+抖動(jitter)、首次延遲 2 秒)作為起點,這樣比較安全。比起次數,更重要的是判斷「這個請求是否可以重試」──像 POST 這種重新執行可能造成重複登錄的操作,應該預設停用重試,或是在伺服器端確保冪等性(同一個請求送兩次結果也不會改變的特性)之後,才啟用重試。
- 為什麼在公司內部 Proxy 環境下,只有第一次請求會特別慢?
- 在 Windows 的預設設定下,HttpClient 會嘗試自動偵測 Proxy,因此第一次建立連線時,偵測處理可能會花費一些時間。若確定環境不需要 Proxy(例如伺服器內部通訊),可以將 HttpClientHandler 的 UseProxy 設為 false 停用自動偵測即可改善。反之,在必須使用 Proxy 的公司內部環境中,與其依賴自動偵測,不如透過 WebProxy 明確指定,行為會更穩定。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。