使用 SocketAsyncEventArgs (SAEA) 或傳統 **APM (Begin/End)** 模式實作高併發 TCP Server/Client。本教學採用 **APM (BeginAccept/BeginReceive/BeginSend)** 兼容 .NET Framework 2.0+,邏輯清晰易懂;生產環境高併發建議改用 **SAEA** 或 **System.IO.Pipelines / Kestrel / gRPC**。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Collections.Concurrent;
namespace SocketDemo
{
/// <summary>封裝 Socket 連線狀態與緩衝區</summary>
public class ClientState
{
public Socket Socket { get; set; }
public byte[] Buffer { get; } = new byte[4096];
public StringBuilder MessageBuilder { get; } = new StringBuilder();
public DateTime LastActive { get; set; } = DateTime.UtcNow;
public string ClientId { get; } = Guid.NewGuid().ToString("N");
}
/// <summary>非同步 TCP Server</summary>
public class AsyncTcpServer : IDisposable
{
private readonly Socket _listener;
private readonly ConcurrentDictionary _clients = new ConcurrentDictionary();
private readonly int _port;
private bool _disposed = false;
// 事件:供 UI/上層訂閱
public event Action MessageReceived; // clientId, message
public event Action ClientConnected;
public event Action ClientDisconnected;
public event Action ErrorOccurred;
public AsyncTcpServer(int port = 7878)
{
_port = port;
_listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}
public void Start()
{
var localEndPoint = new IPEndPoint(IPAddress.Any, _port);
_listener.Bind(localEndPoint);
_listener.Listen(100); // backlog
_listener.BeginAccept(AcceptCallback, null);
Console.WriteLine($"[Server] Listening on 0.0.0.0:{_port}");
}
private void AcceptCallback(IAsyncResult ar)
{
if (_disposed) return;
try
{
Socket clientSocket = _listener.EndAccept(ar);
var state = new ClientState { Socket = clientSocket };
_clients.TryAdd(state.ClientId, state);
ClientConnected?.Invoke(state.ClientId);
Console.WriteLine($"[Server] Client connected: {state.ClientId} ({clientSocket.RemoteEndPoint})");
// 繼續接收下一個連線
_listener.BeginAccept(AcceptCallback, null);
// 開始非同步接收資料
clientSocket.BeginReceive(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ReceiveCallback, state);
}
catch (ObjectDisposedException) { /* Server stopping */ }
catch (Exception ex)
{
ErrorOccurred?.Invoke("Accept", ex);
// 繼續監聽
if (!_disposed) _listener.BeginAccept(AcceptCallback, null);
}
}
private void ReceiveCallback(IAsyncResult ar)
{
var state = (ClientState)ar.AsyncState;
if (state == null || !state.Socket.Connected) return;
try
{
int bytesRead = state.Socket.EndReceive(ar);
if (bytesRead > 0)
{
state.LastActive = DateTime.UtcNow;
string chunk = Encoding.UTF8.GetString(state.Buffer, 0, bytesRead);
state.MessageBuilder.Append(chunk);
// 簡易訊息邊界:換行符 \n 為一則訊息結束
while (state.MessageBuilder.Length > 0)
{
int newlineIndex = state.MessageBuilder.ToString().IndexOf('\n');
if (newlineIndex < 0) break; // 訊息不完整,等待下次接收
string message = state.MessageBuilder.ToString(0, newlineIndex).TrimEnd('\r');
state.MessageBuilder.Remove(0, newlineIndex + 1);
// 觸發事件
MessageReceived?.Invoke(state.ClientId, message);
Console.WriteLine($"[Server] Received from {state.ClientId}: {message}");
// 回應 Echo (示範雙向)
Send(state.ClientId, $"ECHO: {message}\n");
}
// 繼續接收
state.Socket.BeginReceive(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ReceiveCallback, state);
}
else
{
// bytesRead == 0 表示客戶端正常關閉
DisconnectClient(state.ClientId, "Client closed");
}
}
catch (SocketException sex) when (sex.SocketErrorCode == SocketError.ConnectionReset)
{
DisconnectClient(state.ClientId, "Connection reset");
}
catch (ObjectDisposedException) { /* Socket closed */ }
catch (Exception ex)
{
ErrorOccurred?.Invoke(state.ClientId, ex);
DisconnectClient(state.ClientId, "Error");
}
}
public void Send(string clientId, string message)
{
if (_clients.TryGetValue(clientId, out var state) && state.Socket.Connected)
{
byte[] data = Encoding.UTF8.GetBytes(message);
try
{
state.Socket.BeginSend(data, 0, data.Length, SocketFlags.None, SendCallback, state);
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(clientId, ex);
}
}
}
public void Broadcast(string message)
{
foreach (var kvp in _clients)
{
Send(kvp.Key, message);
}
}
private void SendCallback(IAsyncResult ar)
{
var state = (ClientState)ar.AsyncState;
try
{
int bytesSent = state.Socket.EndSend(ar);
// 可選:記錄發送位元組數
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(state.ClientId, ex);
}
}
private void DisconnectClient(string clientId, string reason)
{
if (_clients.TryRemove(clientId, out var state))
{
try { state.Socket.Shutdown(SocketShutdown.Both); } catch { }
state.Socket.Close();
ClientDisconnected?.Invoke(clientId);
Console.WriteLine($"[Server] Client disconnected: {clientId} ({reason})");
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
foreach (var kvp in _clients) DisconnectClient(kvp.Key, "Server shutdown");
_listener.Close();
}
}
}
public class AsyncTcpClient : IDisposable
{
private readonly Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
private readonly byte[] _buffer = new byte[4096];
private readonly StringBuilder _msgBuilder = new StringBuilder();
private bool _disposed = false;
public event Action MessageReceived;
public event Action Connected;
public event Action ErrorOccurred;
public event Action Disconnected;
public void Connect(string ip, int port)
{
var endpoint = new IPEndPoint(IPAddress.Parse(ip), port);
_socket.BeginConnect(endpoint, ConnectCallback, null);
}
private void ConnectCallback(IAsyncResult ar)
{
try
{
_socket.EndConnect(ar);
Connected?.Invoke();
Console.WriteLine("[Client] Connected to server");
_socket.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, ReceiveCallback, null);
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(ex);
}
}
private void ReceiveCallback(IAsyncResult ar)
{
try
{
int bytesRead = _socket.EndReceive(ar);
if (bytesRead > 0)
{
string chunk = Encoding.UTF8.GetString(_buffer, 0, bytesRead);
_msgBuilder.Append(chunk);
while (true)
{
int idx = _msgBuilder.ToString().IndexOf('\n');
if (idx < 0) break;
string msg = _msgBuilder.ToString(0, idx).TrimEnd('\r');
_msgBuilder.Remove(0, idx + 1);
MessageReceived?.Invoke(msg);
}
_socket.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, ReceiveCallback, null);
}
else
{
Disconnected?.Invoke();
}
}
catch (ObjectDisposedException) { }
catch (SocketException sex) when (sex.SocketErrorCode == SocketError.ConnectionReset)
{
Disconnected?.Invoke();
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(ex);
}
}
public void Send(string message)
{
if (_socket.Connected)
{
byte[] data = Encoding.UTF8.GetBytes(message + "\n");
_socket.BeginSend(data, 0, data.Length, SocketFlags.None, SendCallback, null);
}
}
private void SendCallback(IAsyncResult ar)
{
try { _socket.EndSend(ar); } catch { }
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
try { _socket.Shutdown(SocketShutdown.Both); } catch { }
_socket.Close();
}
}
public partial class ServerForm : Form
{
private AsyncTcpServer _server;
public ServerForm()
{
InitializeComponent();
_server = new AsyncTcpServer(7878);
_server.MessageReceived += (id, msg) => InvokeLog($"[Client {id.Substring(0,8)}] {msg}");
_server.ClientConnected += id => InvokeLog($"Client connected: {id}");
_server.ClientDisconnected += id => InvokeLog($"Client disconnected: {id}");
_server.ErrorOccurred += (ctx, ex) => InvokeLog($"Error [{ctx}]: {ex.Message}");
}
private void btnStart_Click(object sender, EventArgs e)
{
_server.Start();
btnStart.Enabled = false;
txtLog.AppendText("Server started on port 7878\r\n");
}
private void btnBroadcast_Click(object sender, EventArgs e)
{
_server.Broadcast($"[Server Broadcast] {txtBroadcast.Text}\n");
txtBroadcast.Clear();
}
private void InvokeLog(string msg)
{
if (InvokeRequired)
{
BeginInvoke(new Action(InvokeLog), msg);
return;
}
txtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {msg}\r\n");
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
_server?.Dispose();
base.OnFormClosing(e);
}
}
| 主題 | 說明 | 最佳實務 |
|---|---|---|
| 訊息邊界 (Framing) | TCP 是串流,無訊息邊界。範例用 `\n` 分隔。 | 生產環境用 **Length Prefix (4 bytes 長度 + Body)** 或 **Protobuf/MessagePack** 序列化框架自動處理。 |
| 非同步模式選擇 | APM (Begin/End) 舊但相容;TAP (async/await) 需配合 `Task.Factory.FromAsync` 或 `SocketAsyncEventArgs`。 | .NET Core 2.1+ 建議用 `SocketAsyncEventArgs` 或 `System.IO.Pipelines` 達成零分配、高併發。 |
| 緩衝區管理 | 每連線一個 `byte[4096]`,接收後轉字串。 | 高效能用 `ArrayPool |
| 編碼一致性 | Server/Client 必須同編碼 (UTF-8 推薦)。 | 明確指定 `Encoding.UTF8`,勿用 `Encoding.Default` (隨作業系統)。 |
| 心跳/保活 | 長連線需偵測斷線 (防火牆/NAT 閒置斷開)。 | 應用層每 30 秒發送 `PING`,超時 3 次無回應則斷開;或啟用 `Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true)`。 |
| 執行緒安全 | APM 回呼在 ThreadPool 執行緒,UI 更新需 `Invoke/BeginInvoke`。 | 所有共享集合用 `ConcurrentDictionary`、事件觸發前檢查 `InvokeRequired`。 |
| 例外處理 | `ObjectDisposedException` (關閉中)、`SocketException` (ConnectionReset/Timeout)。 | 分類處理:正常斷線不記錯、異常記錄並嘗試重連 (Client 端)。 |
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| SignalR (ASP.NET Core) | 自動協商 WebSockets/Server-Sent Events/Long Polling、自動重連、群組、Hub 模型、支援 JS/.NET/Java 客戶端 | 需 ASP.NET Core 環境、非純 TCP | 即時 Web 應用 (聊天、通知、儀表板) |
| gRPC (HTTP/2 + Protobuf) | 強型別契約、雙向串流、效能極高、代碼生成、跨語言 | 瀏覽器需 gRPC-Web Proxy、較複雜 | 微服務內部通訊、高效能 API |
| System.IO.Pipelines + Kestrel | 零複製、高吞吐、非同步管線、ASP.NET Core 內建 | 學習曲線陡 | 自訂高效能 TCP 協定、代理伺服器 |
| MQTT / RabbitMQ / Kafka | 訊息佇列、持久化、發布訂閱、解耦 | 引入基礎設施複雜度 | 分散式系統、IoT、事件驅動架構 |
.NET Socket 非同步通訊核心在於 **`BeginAccept/Receive/Send` + 狀態物件傳遞 + 訊息邊界處理 + 執行緒安全 UI 更新**。本文提供可直接執行的 Server/Client 完整代碼、WinForms 整合、關鍵技術點深度解析與現代化替代方案對比。學習階段請先跑通 APM 版本理解流程,生產專案請評估 **SignalR / gRPC / Pipelines** 等高層框架,避免重複造輪子並獲得更好維護性與擴展性。