.NET 2.0 的 Begin/End 非同步模式 (APM) 每次操作都需分配 IAsyncResult 物件,高併發下造成大量 Gen 0 回收壓力。.NET 3.5 引入 SocketAsyncEventArgs (SAEA) 模式,採用物件重用 + 事件回調 機制,大幅降低分配開銷,是高效能 Socket 服務端的新標準。
| 特性 | APM (2.0) | SAEA (3.5+) |
|---|---|---|
| 上下文物件 | IAsyncResult (每次新建) | SocketAsyncEventArgs (重用) |
| 完成通知 | 回調委派 | 事件 Completed |
| 參數傳遞 | 方法參數 | SAEA 屬性 (Buffer, UserToken) |
| 完成狀態 | IAsyncResult 屬性 | SAEA 屬性 (BytesTransferred, SocketError) |
| 記憶體壓力 | 高 (頻繁 GC) | 低 (物件池模式) |
public class UserObject {
public byte[] ReceiveBuffer { get; } = new byte[1024];
public byte[] SendBuffer { get; } = new byte[1024];
public Socket Socket { get; }
public SocketAsyncEventArgs ReceiveEventArgs { get; }
public SocketAsyncEventArgs SendEventArgs { get; }
public UserObject(Socket socket) {
Socket = socket;
ReceiveEventArgs = new SocketAsyncEventArgs { UserToken = this };
SendEventArgs = new SocketAsyncEventArgs { UserToken = this };
ReceiveEventArgs.SetBuffer(ReceiveBuffer, 0, ReceiveBuffer.Length);
SendEventArgs.SetBuffer(SendBuffer, 0, SendBuffer.Length);
}
}
Socket listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
listenSocket.Bind(localEP);
listenSocket.Listen(100);
var acceptArgs = new SocketAsyncEventArgs();
acceptArgs.Completed += AcceptCompleted;
listenSocket.AcceptAsync(acceptArgs); // 投遞接受操作
void AcceptCompleted(object sender, SocketAsyncEventArgs e) {
var client = new UserObject(e.AcceptSocket);
client.ReceiveEventArgs.Completed += ReceiveCompleted;
client.SendEventArgs.Completed += SendCompleted;
client.Socket.ReceiveAsync(client.ReceiveEventArgs); // 投遞接收
// 重要:立即再次投遞下一個接受操作
listenSocket.AcceptAsync(acceptArgs);
}
void ReceiveCompleted(object sender, SocketAsyncEventArgs e) {
var client = (UserObject)e.UserToken;
if (e.BytesTransferred == 0 || e.SocketError != SocketError.Success) {
CloseClient(client); // 連線斷開
return;
}
// 處理收到的資料
string msg = Encoding.Unicode.GetString(e.Buffer, 0, e.BytesTransferred);
Console.WriteLine($"Socket:{client.Socket.Handle} recv: {msg}");
// 準備回送
string reply = $"{e.BytesTransferred} bytes received";
int len = Encoding.Unicode.GetBytes(reply, 0, reply.Length, client.SendBuffer, 0);
client.SendEventArgs.SetBuffer(0, len); // 設定有效資料範圍
// 並行投遞:先發送、再接收 (順序不影響)
client.Socket.SendAsync(client.SendEventArgs);
client.Socket.ReceiveAsync(client.ReceiveEventArgs);
}
void SendCompleted(object sender, SocketAsyncEventArgs e) {
var client = (UserObject)e.UserToken;
if (e.BytesTransferred == 0 || e.SocketError != SocketError.Success) {
CloseClient(client);
} else {
Console.WriteLine($"Sent {e.BytesTransferred} bytes to {client.Socket.Handle}");
}
}
void CloseClient(UserObject client) {
Console.WriteLine($"Socket:{client.Socket.Handle} closed");
client.Socket.Close();
}
SocketAsyncEventArgs 池,AcceptAsync/ReceiveAsync 從池取用,完成後歸還,極致減少 GC。SetBuffer(offset, count) 動態調整有效資料範圍,避免重新分配陣列。UserObject 掛載於 UserToken,回調中強轉取用,無需字典查找。e.SocketError 與 BytesTransferred == 0 (優雅斷開)。SendEventArgs 循環使用,只需 SetBuffer 更新長度。AcceptCompleted 末尾必須再次呼叫 AcceptAsync,維持監聽管線。.NET Core / .NET 5+ 推薦使用 System.Net.Sockets.Socket 搭配 ValueTask / Memory<byte> 的新式 API (ReceiveAsync(Memory<byte>)),但 SAEA 仍是高效能場景的核心基石,SocketAsyncEventArgs 內部已優化支援 Memory<byte> 零複製操作。
掌握 SAEA 模式,是從「能跑通」邁向「經得起生產環境壓力」的關鍵門檻。