非同步 Socket 程式設計是建立高擴展性網路伺服器的關鍵技術。早期 .NET 使用 Asynchronous Programming Model (APM) 模式 (Begin/End 方法配合 IAsyncResult),雖然現代 .Core 推薦使用 async/await 搭配 SocketAsyncEventArgs 或 ValueTask,但理解 APM 模式對維護舊專案、掌握非同步 I/O 底層運作機制仍有重要價值。本文完整解析 APM 模式的非同步 TCP Client/Server 實作。
| 元件 | 說明 |
|---|---|
| BeginXxx / EndXxx | 成對的非同步方法,如 BeginConnect/EndConnect、BeginSend/EndSend、BeginReceive/EndReceive、BeginAccept/EndAccept |
| IAsyncResult | 代表非同步作業狀態,包含 AsyncState (使用者自訂狀態物件)、AsyncWaitHandle (等待控制代碼)、IsCompleted 等 |
| AsyncCallback | 作業完成時呼叫的委派,在 Thread Pool 執行緒上執行 |
| ManualResetEvent | 同步基元,用於主執行緒等待非同步作業完成 (範例教學用,生產環境應避免阻塞) |
非同步作業需在回呼之間傳遞上下文 (Socket、緩衝區、資料累積器等),標準做法是定義狀態類別:
// Client 端狀態物件
public class ClientState
{
public Socket WorkSocket { get; set; } = null!;
public const int BufferSize = 256;
public byte[] Buffer { get; } = new byte[BufferSize];
public StringBuilder Sb { get; } = new StringBuilder();
}
// Server 端狀態物件 (緩衝區較大)
public class ServerState
{
public Socket WorkSocket { get; set; } = null!;
public const int BufferSize = 1024;
public byte[] Buffer { get; } = new byte[BufferSize];
public StringBuilder Sb { get; } = new StringBuilder();
}
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace AsyncSocketClientAPM
{
public class AsynchronousClient
{
private const int Port = 11000;
private const string Host = "host.contoso.com"; // 實際部署請修改
// 同步事件僅供教學示範主流程,生產環境應用事件驅動/async-await
private static readonly ManualResetEvent ConnectDone = new(false);
private static readonly ManualResetEvent SendDone = new(false);
private static readonly ManualResetEvent ReceiveDone = new(false);
private static string Response = string.Empty;
public static void StartClient()
{
try
{
// 1. 解析遠端端點
IPHostEntry hostEntry = Dns.GetHostEntry(Host);
IPAddress ipAddress = hostEntry.AddressList[0];
IPEndPoint remoteEP = new IPEndPoint(ipAddress, Port);
// 2. 建立 Socket
Socket client = new Socket(
ipAddress.AddressFamily,
SocketType.Stream,
ProtocolType.Tcp);
// 3. 開始非同步連線
ClientState state = new ClientState { WorkSocket = client };
client.BeginConnect(remoteEP, ConnectCallback, state);
// 等待連線完成 (教學用)
ConnectDone.WaitOne();
// 4. 發送測試資料
Send(client, "這是非同步 APM 測試訊息");
SendDone.WaitOne();
// 5. 接收回應
Receive(client);
ReceiveDone.WaitOne();
Console.WriteLine($"收到回應:{Response}");
// 6. 關閉連線
client.Shutdown(SocketShutdown.Both);
client.Close();
}
catch (Exception ex)
{
Console.WriteLine($"錯誤: {ex.Message}");
}
}
private static void ConnectCallback(IAsyncResult ar)
{
try
{
ClientState state = (ClientState)ar.AsyncState!;
Socket client = state.WorkSocket;
// 完成連線
client.EndConnect(ar);
Console.WriteLine($"Socket 已連線至 {client.RemoteEndPoint}");
ConnectDone.Set(); // 通知主執行緒
}
catch (Exception ex)
{
Console.WriteLine($"連線錯誤: {ex.Message}");
ConnectDone.Set();
}
}
private static void Receive(Socket client)
{
try
{
ClientState state = new ClientState { WorkSocket = client };
client.BeginReceive(state.Buffer, 0, ClientState.BufferSize, 0,
ReceiveCallback, state);
}
catch (Exception ex)
{
Console.WriteLine($"接收啟動錯誤: {ex.Message}");
ReceiveDone.Set();
}
}
private static void ReceiveCallback(IAsyncResult ar)
{
try
{
ClientState state = (ClientState)ar.AsyncState!;
Socket client = state.WorkSocket;
int bytesRead = client.EndReceive(ar);
if (bytesRead > 0)
{
// 累積資料
state.Sb.Append(Encoding.UTF8.GetString(state.Buffer, 0, bytesRead));
// 檢查結尾標記
string content = state.Sb.ToString();
if (content.Contains(""))
{
Response = content;
ReceiveDone.Set();
}
else
{
// 繼續接收剩餘資料
client.BeginReceive(state.Buffer, 0, ClientState.BufferSize, 0,
ReceiveCallback, state);
}
}
else
{
// 連線關閉
if (state.Sb.Length > 1)
Response = state.Sb.ToString();
ReceiveDone.Set();
}
}
catch (Exception ex)
{
Console.WriteLine($"接收回呼錯誤: {ex.Message}");
ReceiveDone.Set();
}
}
private static void Send(Socket client, string data)
{
byte[] byteData = Encoding.UTF8.GetBytes(data);
client.BeginSend(byteData, 0, byteData.Length, 0, SendCallback, client);
}
private static void SendCallback(IAsyncResult ar)
{
try
{
Socket client = (Socket)ar.AsyncState!;
int bytesSent = client.EndSend(ar);
Console.WriteLine($"已發送 {bytesSent} 位元組至伺服器");
SendDone.Set();
}
catch (Exception ex)
{
Console.WriteLine($"發送回呼錯誤: {ex.Message}");
SendDone.Set();
}
}
public static void Main()
{
StartClient();
Console.WriteLine("按任意鍵結束...");
Console.ReadKey();
}
}
}
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace AsyncSocketServerAPM
{
public class AsynchronousSocketListener
{
private const int Port = 11000;
private const int Backlog = 100;
// 用於同步主迴圈接受連線 (教學用)
public static readonly ManualResetEvent AllDone = new(false);
public static void StartListening()
{
// 1. 建立本機端點
IPHostEntry hostEntry = Dns.GetHostEntry(Dns.GetHostName());
IPAddress ipAddress = hostEntry.AddressList[0];
IPEndPoint localEP = new IPEndPoint(ipAddress, Port);
// 2. 建立監聽 Socket
Socket listener = new Socket(
AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.Tcp);
try
{
listener.Bind(localEP);
listener.Listen(Backlog);
Console.WriteLine($"非同步伺服器啟動,監聽 {localEP}");
while (true)
{
// 重置事件,準備接受下一個連線
AllDone.Reset();
Console.WriteLine("等待連線...");
// 3. 開始非同步接受連線
listener.BeginAccept(AcceptCallback, listener);
// 等待連線建立 (教學用,實際生產不應阻塞主執行緒)
AllDone.WaitOne();
}
}
catch (Exception ex)
{
Console.WriteLine($"伺服器錯誤: {ex.Message}");
}
}
private static void AcceptCallback(IAsyncResult ar)
{
// 通知主執行緒可以繼續接受下一個連線
AllDone.Set();
Socket listener = (Socket)ar.AsyncState!;
Socket handler = listener.EndAccept(ar);
Console.WriteLine($"客戶端已連線:{handler.RemoteEndPoint}");
// 4. 為新連線建立狀態物件並開始接收資料
ServerState state = new ServerState { WorkSocket = handler };
handler.BeginReceive(state.Buffer, 0, ServerState.BufferSize, 0,
ReadCallback, state);
}
private static void ReadCallback(IAsyncResult ar)
{
ServerState state = (ServerState)ar.AsyncState!;
Socket handler = state.WorkSocket;
try
{
int bytesRead = handler.EndReceive(ar);
if (bytesRead > 0)
{
state.Sb.Append(Encoding.UTF8.GetString(state.Buffer, 0, bytesRead));
string content = state.Sb.ToString();
if (content.Contains(""))
{
// 5. 完整訊息已接收,顯示並回傳
Console.WriteLine($"讀取 {content.Length} 位元組:{content}");
Send(handler, content);
}
else
{
// 繼續接收
handler.BeginReceive(state.Buffer, 0, ServerState.BufferSize, 0,
ReadCallback, state);
}
}
else
{
// 客戶端斷線
handler.Shutdown(SocketShutdown.Both);
handler.Close();
}
}
catch (Exception ex)
{
Console.WriteLine($"讀取回呼錯誤: {ex.Message}");
try { handler.Close(); } catch { }
}
}
private static void Send(Socket handler, string data)
{
byte[] byteData = Encoding.UTF8.GetBytes(data);
handler.BeginSend(byteData, 0, byteData.Length, 0, SendCallback, handler);
}
private static void SendCallback(IAsyncResult ar)
{
try
{
Socket handler = (Socket)ar.AsyncState!;
int bytesSent = handler.EndSend(ar);
Console.WriteLine($"已發送 {bytesSent} 位元組至客戶端");
// 完成傳送後關閉連線 (簡單 Echo Server 行為)
handler.Shutdown(SocketShutdown.Both);
handler.Close();
}
catch (Exception ex)
{
Console.WriteLine($"發送回呼錯誤: {ex.Message}");
}
}
public static void Main()
{
StartListening();
}
}
}
APM 的核心是「發起作業 → 回呼 → 發起下一作業」的鏈結:
BeginAccept → AcceptCallback → BeginReceive → ReadCallback → (循環或 Send)
↓
BeginSend → SendCallback → 關閉/繼續接收
BeginXxx(..., callback, state) 的 state 參數會原封不動傳給回呼的 ar.AsyncStateSocket、byte[] Buffer、StringBuilder (累積資料)BeginReceive 累積| 特性 | APM (Begin/End) | Task-based (async/await) | SocketAsyncEventArgs |
|---|---|---|---|
| 程式碼可讀性 | 回呼地獄,邏輯分散 | 線性直觀,類似同步寫法 | 事件驅動,較複雜 |
| 例外處理 | 需在每個回呼 try-catch | 統一 try-catch | 事件屬性檢查 |
| 效能 (高併發) | 良好 (使用 IOCP) | 良好 (底層同 IOCP) | 最佳 (零配置物件池) |
| 取消支援 | 困難 | 原生 CancellationToken | 手動實作 |
| 適用場景 | 維護舊專案、.NET Framework 4.0 以下 | 現代 .NET Core/.NET 5+ 專案 | 極高效能需求、自訂協定 |
// .NET Core 2.1+ / .NET 5+ 推薦寫法
public async Task StartClientAsync(string host, int port)
{
using Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await client.ConnectAsync(host, port);
string msg = "現代非同步測試";
await client.SendAsync(Encoding.UTF8.GetBytes(msg), SocketFlags.None);
byte[] buffer = new byte[1024];
StringBuilder sb = new StringBuilder();
while (true)
{
int received = await client.ReceiveAsync(buffer, SocketFlags.None);
if (received == 0) break;
sb.Append(Encoding.UTF8.GetString(buffer, 0, received));
if (sb.ToString().Contains("")) break;
}
Console.WriteLine($"回應: {sb}");
client.Shutdown(SocketShutdown.Both);
}
Task.Run 卸載。Console.ReadLine() 保持存活。socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); // 關閉 Nagle
SocketAsyncEventArgs 物件池效果最佳。APM 模式的非同步 Socket 程式設計雖然語法較繁瑣,但深刻展示了非同步 I/O 的核心機制:發起非阻塞作業、註冊完成回呼、在回呼中續發下一作業。理解此模式有助於掌握現代 async/await 底層運作,並能維護大量存量的 .NET Framework 專案。對於新專案,強烈建議採用 async/await 搭配 SocketAsyncEventArgs 或高階庫 (如 System.IO.Pipelines、Kestrel、DotNetty) 以獲得更好的開發體驗與效能。