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

.NET Socket 非同步通訊完整實作:Server/Client 雙向傳輸教學

     編輯:ASP.NET 基礎

架構概覽

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

Client 端實作 (WinForms / Console)

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

WinForms UI 整合範例 (Server 端)

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.Shared.Rent/Return`、避免頻繁 GC。
編碼一致性 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、事件驅動架構

部署與測試清單

  • 防火牆:開放 TCP Port (範例 7878) Inbound/Outbound。
  • IP 綁定:`IPAddress.Any` (0.0.0.0) 監聽所有介面,或指定內網 IP。
  • 壓力測試:使用 `nc`、`telnet`、自製 Client 模擬 1000+ 連線,觀察記憶體/CPU/ThreadPool。
  • 日誌:記錄連線數、流量、錯誤率,接入 Serilog/ELK。
  • 優雅關閉:處理 `Ctrl+C` / `SIGTERM`,停止接受新連線、等待現有連線處理完、關閉 Socket。

總結

.NET Socket 非同步通訊核心在於 **`BeginAccept/Receive/Send` + 狀態物件傳遞 + 訊息邊界處理 + 執行緒安全 UI 更新**。本文提供可直接執行的 Server/Client 完整代碼、WinForms 整合、關鍵技術點深度解析與現代化替代方案對比。學習階段請先跑通 APM 版本理解流程,生產專案請評估 **SignalR / gRPC / Pipelines** 等高層框架,避免重複造輪子並獲得更好維護性與擴展性。

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