.NET TcpClient.Connect / BeginConnect **內建無連線逾時參數**。預設作業系統 TCP 連線逾時長達 20~30 秒,伺服器離線或網路故障時,客戶端會長時間卡死,嚴重影響使用者體驗。SocketOptionName.SendTimeout 僅控制**資料發送**逾時,**不適用於連線建立階段**。
核心邏輯:
BeginConnect 發起非同步連線。WaitHandle.WaitOne(timeout) 等待訊號。EndConnect 完成連線,設定成功旗標並 Set() 釋放等待。TimeoutException。using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace SocketTimeoutDemo
{
///
/// 支援連線逾時的 TcpClient 包裝器
///
public static class TcpClientEx
{
///
/// 嘗試非同步連線,支援毫秒級逾時控制
///
/// 遠端端點
/// 逾時毫秒數,-1 表示無限等待
/// 已連線的 TcpClient
/// 逾時
/// 連線失敗
public static TcpClient ConnectWithTimeout(IPEndPoint remoteEndPoint, int timeoutMs)
{
if (remoteEndPoint == null) throw new ArgumentNullException(nameof(remoteEndPoint));
if (timeoutMs < -1) throw new ArgumentOutOfRangeException(nameof(timeoutMs));
var client = new TcpClient();
var asyncResult = client.BeginConnect(remoteEndPoint.Address, remoteEndPoint.Port, null, null);
// 等待連線完成或逾時
bool completed = asyncResult.AsyncWaitHandle.WaitOne(timeoutMs, exitContext: false);
if (!completed)
{
// 逾時:關閉 Socket,釋放資源
client.Close();
throw new TimeoutException($"連線逾時({timeoutMs} ms):{remoteEndPoint}");
}
try
{
// 完成連線,若失敗會拋出 SocketException
client.EndConnect(asyncResult);
return client;
}
catch
{
client.Close();
throw;
}
}
///
/// 簡易同步連線包裝(內部同樣使用非同步+逾時)
///
public static TcpClient Connect(string host, int port, int timeoutMs = 5000)
{
var addresses = Dns.GetHostAddresses(host);
if (addresses.Length == 0) throw new SocketException((int)SocketError.HostNotFound);
foreach (var ip in addresses)
{
try
{
return ConnectWithTimeout(new IPEndPoint(ip, port), timeoutMs);
}
catch (SocketException)
{
// 嘗試下一個 IP
}
}
throw new SocketException((int)SocketError.ConnectionRefused);
}
}
}
try
{
// 連線 192.168.1.100:8080,最多等待 3 秒
using (var client = TcpClientEx.Connect("192.168.1.100", 8080, 3000))
using (var stream = client.GetStream())
{
Console.WriteLine("連線成功!");
// 收發資料...
}
}
catch (TimeoutException)
{
Console.WriteLine("連線逾時,伺服器無回應或網路不通。");
}
catch (SocketException ex)
{
Console.WriteLine($"Socket 錯誤:{ex.SocketErrorCode} - {ex.Message}");
}
原文提供的教學版使用 static 欄位儲存狀態,**非執行緒安全**,多執行緒並發呼叫會互相覆蓋旗標與例外。生產版採用 **APM 模式原生 IAsyncResult.AsyncWaitHandle**,無共享狀態、天然執行緒安全、程式碼更簡潔。
| 維度 | 教學版 (原文) | 生產版 (建議) |
|---|---|---|
| 執行緒安全 | ❌ static 欄位共享 | ✅ 無共享狀態 |
| 程式碼複雜度 | 手動管理 ManualResetEvent、旗標、例外 | 利用內建 AsyncWaitHandle,極簡 |
| 資源釋放 | 需手動 Close | using 區塊自動釋放 |
| 多 IP 嘗試 | 不支援 | 支援 DNS 多 IP 輪嘗 |
// .NET Core 2.1+ 推薦寫法
public static async ValueTask ConnectAsync(string host, int port, TimeSpan timeout, CancellationToken ct = default)
{
var client = new TcpClient();
try
{
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(ct))
{
cts.CancelAfter(timeout);
await client.ConnectAsync(host, port).WaitAsync(cts.Token);
}
return client;
}
catch
{
client.Dispose();
throw;
}
}
WaitAsync(.NET 6+) 直接支援逾時取消,無需手造Task.WhenAny。
Socket 連線逾時控制是網路程式健壯性的基石。**棄用同步 Connect、拒絕 static 共享狀態**,採用 BeginConnect + AsyncWaitHandle(.NET Framework)或 ConnectAsync + WaitAsync/CancellationToken(.NET Core/5+),即可實現精準、安全、可維護的逾時機制。