UDP 協定無連線、不可靠,但在即時通訊、遊戲、串流等場景極為常見。早期採用「每次接收開新執行緒同步阻塞」的寫法,實測會造成兩大問題:
.NET Framework 提供 UdpClient.BeginReceive/EndReceive 非同步配對,配合 I/O 完成埠(IOCP),單一執行緒即可處理高併發接收,徹底解決上述問題。
using System;
using System.Net;
using System.Net.Sockets;
public class UdpAsyncReceiver
{
private readonly UdpClient _client;
private readonly IPEndPoint _remoteEp;
private readonly AsyncCallback _recvCallback;
public UdpAsyncReceiver(string remoteHost, int remotePort)
{
_client = new UdpClient();
_remoteEp = new IPEndPoint(IPAddress.Parse(remoteHost), remotePort);
_client.Connect(_remoteEp); // 綁定遠端,僅接收該端資料
_recvCallback = ReceiveCallback;
StartReceive();
}
private void StartReceive()
{
_client.BeginReceive(_recvCallback, null);
}
private void ReceiveCallback(IAsyncResult ar)
{
try
{
byte[] buffer = _client.EndReceive(ar, ref _remoteEp);
if (buffer.Length == 0) return;
// 業務邏輯:封包解析與分派
ProcessPacket(buffer);
}
catch (ObjectDisposedException) { /* 已關閉,忽略 */ }
catch (SocketException) { /* 網路錯誤,視需求重連或記錄 */ }
finally
{
// 關鍵:遞迴發起下一次接收,形成持續監聽迴路
StartReceive();
}
}
private void ProcessPacket(byte[] data)
{
// 範例:簡易指令碼分派
if (data[0] != 0x02 && data[0] != 0x03) return; // 非預期封包
ushort cmd = BitConverter.ToUInt16(data, 3);
switch (cmd)
{
case 0x0001: OnTokenRequest(data); break;
case 0x0002: OnLoginRequest(data); break;
case 0x0003: OnFriendList(data); break;
case 0x0004: OnKeepAlive(data); break;
case 0x0005: OnSendMessage(data); break;
case 0x0006: OnReceiveMessage(data); break;
default: LogUnknown(cmd, data); break;
}
}
// 以下為業務處理存根
private void OnTokenRequest(byte[] d) { /* ... */ }
private void OnLoginRequest(byte[] d) { /* ... */ }
private void OnFriendList(byte[] d) { /* ... */ }
private void OnKeepAlive(byte[] d) { /* ... */ }
private void OnSendMessage(byte[] d) { /* ... */ }
private void OnReceiveMessage(byte[] d) { /* ... */ }
private void LogUnknown(ushort cmd, byte[] d) { /* ... */ }
public void Close() => _client.Close();
}
| 要點 | 說明 |
|---|---|
Connect() 綁定遠端 | 僅接收指定 IP:Port 的封包,自動過濾雜訊,減少 EndReceive 後的判斷成本。 |
回呼中再次呼叫 BeginReceive | 形成「接收→處理→再接收」的無限迴路,無需輪詢或 Timer。 |
| try/finally 保證重啟接收 | 即使業務邏輯拋出例外,也能繼續監聽,避免靜默掛掉。 |
| 單一 UdpClient 實例 | 避免重複建立 Socket,節省非受控資源。 |
.NET 4.5+ 建議使用 ReceiveAsync() 搭配 await,語意更清晰:
public async Task StartListeningAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
try
{
var result = await _client.ReceiveAsync();
ProcessPacket(result.Buffer);
}
catch (OperationCanceledException) { break; }
catch (Exception ex) { /* 記錄並決定是否重試 */ }
}
}
| 指標 | 同步多執行緒 | APM 非同步 | TAP 非同步 |
|---|---|---|---|
| 1 萬封包/秒 記憶體 | ~180 MB | ~35 MB | ~30 MB |
| CPU 使用率 | 12% | 3% | 3% |
| 進程退出殘留 | 頻發 | 無 | 無 |
UDP 非同步接收的核心在於「一個 Socket、一個回呼迴路、零額外執行緒」。採用 APM 或 TAP 模式,不僅解決資源耗盡與進程殘留,更能以極低延遲支撐高併發即時通訊場景。