首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 非同步 Socket 程式設計深度解析:APM 模式 Client/Server 完整實作

     編輯:關於C語言

前言

非同步 Socket 程式設計是建立高擴展性網路伺服器的關鍵技術。早期 .NET 使用 Asynchronous Programming Model (APM) 模式 (Begin/End 方法配合 IAsyncResult),雖然現代 .Core 推薦使用 async/await 搭配 SocketAsyncEventArgs 或 ValueTask,但理解 APM 模式對維護舊專案、掌握非同步 I/O 底層運作機制仍有重要價值。本文完整解析 APM 模式的非同步 TCP Client/Server 實作。

APM 模式核心概念

元件 說明
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();
}

非同步 TCP Client 完整實作 (APM)

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();
        }
    }
}

非同步 TCP Server 完整實作 (APM)

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 模式關鍵設計模式解析

1. 非同步作業鏈結

APM 的核心是「發起作業 → 回呼 → 發起下一作業」的鏈結:

BeginAccept → AcceptCallback → BeginReceive → ReadCallback → (循環或 Send)
                                           ↓
                                    BeginSend → SendCallback → 關閉/繼續接收

2. 狀態物件傳遞機制

  • BeginXxx(..., callback, state) 的 state 參數會原封不動傳給回呼的 ar.AsyncState
  • 狀態物件必須包含:Socket、byte[] Buffer、StringBuilder (累積資料)
  • 每個連線獨立擁有狀態物件,避免執行緒安全問題

3. 緩衝區管理

  • Client 範例使用 256 bytes,Server 使用 1024 bytes
  • 緩衝區可重複使用 (同一狀態物件),避免頻繁配置記憶體
  • 大訊息需多次 BeginReceive 累積

APM vs 現代非同步模式對比

特性 APM (Begin/End) Task-based (async/await) SocketAsyncEventArgs
程式碼可讀性 回呼地獄,邏輯分散 線性直觀,類似同步寫法 事件驅動,較複雜
例外處理 需在每個回呼 try-catch 統一 try-catch 事件屬性檢查
效能 (高併發) 良好 (使用 IOCP) 良好 (底層同 IOCP) 最佳 (零配置物件池)
取消支援 困難 原生 CancellationToken 手動實作
適用場景 維護舊專案、.NET Framework 4.0 以下 現代 .NET Core/.NET 5+ 專案 極高效能需求、自訂協定

現代化改寫範例 (Task-based async/await)

// .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);
}

常見陷阱與最佳實務

  1. 避免在回呼中執行耗時作業:回呼在 Thread Pool 執行緒,長時間運算會阻塞其他非同步作業。耗時工作請用 Task.Run 卸載。
  2. 正確處理 ObjectDisposedException:Socket 關閉後若有殘留非同步作業完成,呼叫 EndXxx 會拋出此例外,需捕捉忽略。
  3. 不要在生產 Server 使用 ManualResetEvent 阻塞主執行緒:範例為教學方便,實際 Server 應讓主執行緒處理訊號/管理工作,或直接在 Console 應用用 Console.ReadLine() 保持存活。
  4. 設定 Socket 選項提升穩定性:
    socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
    socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); // 關閉 Nagle
  5. 緩衝區大小調整:高吞吐量場景可增大至 8KB-64KB,配合 SocketAsyncEventArgs 物件池效果最佳。

總結

APM 模式的非同步 Socket 程式設計雖然語法較繁瑣,但深刻展示了非同步 I/O 的核心機制:發起非阻塞作業、註冊完成回呼、在回呼中續發下一作業。理解此模式有助於掌握現代 async/await 底層運作,並能維護大量存量的 .NET Framework 專案。對於新專案,強烈建議採用 async/await 搭配 SocketAsyncEventArgs 或高階庫 (如 System.IO.Pipelines、Kestrel、DotNetty) 以獲得更好的開發體驗與效能。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved