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

C# Socket 訊息傳遞系統實作 (二):多執行緒伺服器與事件驅動用戶端

     編輯:關於C語言

面向物件 vs 實用主義:Switch 大戰多型

純 OO 設計會建立抽象基類 MessageBase,每個訊息類別繼承並覆寫虛擬方法 HandleMessage()。但這帶來三大痛點:

  1. 反射建構效能差:Activator.CreateInstance 遠慢於直接 new。
  2. 處理邏輯耦合:訊息處理邏輯被迫放入共用程式庫,違反關注點分離。
  3. 過度設計:簡單專案引入複雜階層結構,維護成本高。

本系列採用 Switch 模式 + 介面序列化,犧牲極少 OO 純度,換取極大的開發效率與執行效能。

伺服器端多執行緒架構:SocketListener 設計

核心策略:主埠監聽 + 動態子埠服務

單一埠口無法同時接受多個用戶端長連線。解決方案:主埠 (9999) 僅負責「握手分配」,實際通訊轉移至動態分配的子埠。

SocketListener 完整實作

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

public class SocketListener
{
    private readonly int _basePort;
    private readonly Thread _listenThread;
    private readonly IPAddress _localAddr = IPAddress.Parse("127.0.0.1");

    public SocketListener(int port)
    {
        _basePort = port;
        _listenThread = new Thread(WaitForConnection) { IsBackground = true };
        _listenThread.Start();
    }

    private void WaitForConnection()
    {
        TcpListener listener = new TcpListener(_localAddr, _basePort);
        listener.Start();
        Console.WriteLine($"主監聽器已在埠 {_basePort} 啟動。");

        while (true)
        {
            Console.WriteLine("等待初始連線請求...");
            using (Socket handshakeSocket = listener.AcceptSocket())
            using (NetworkStream stream = new NetworkStream(handshakeSocket))
            using (BinaryReader reader = new BinaryReader(stream))
            using (BinaryWriter writer = new BinaryWriter(stream))
            {
                Console.WriteLine("收到握手請求。");

                // 尋找可用子埠
                int clientPort = _basePort + 1;
                TcpListener clientListener = null;
                while (true)
                {
                    try
                    {
                        clientListener = new TcpListener(_localAddr, clientPort);
                        clientListener.Start();
                        break; // 成功佔用埠口
                    }
                    catch (SocketException)
                    {
                        clientPort++; // 埠口被佔用,嘗試下一個
                    }
                }

                // 啟動專屬服務執行緒
                SocketServer server = new SocketServer(clientListener);
                
                // 回傳子埠號給用戶端
                writer.Write(clientPort);
                writer.Flush();
                Console.WriteLine($"已分配子埠 {clientPort} 給用戶端。");
            }
        }
    }
}

SocketServer:專屬用戶端服務執行緒

public class SocketServer
{
    private readonly TcpListener _listener;
    private readonly Thread _serverThread;

    public SocketServer(TcpListener listener)
    {
        _listener = listener;
        _serverThread = new Thread(ProcessClient) { IsBackground = true };
        _serverThread.Start();
    }

    private void ProcessClient()
    {
        Console.WriteLine($"子伺服器在埠 {((IPEndPoint)_listener.LocalEndpoint).Port} 等待用戶端連線...");
        
        using (TcpClient client = _listener.AcceptTcpClient())
        using (NetworkStream stream = client.GetStream())
        using (BinaryReader reader = new BinaryReader(stream))
        using (BinaryWriter writer = new BinaryWriter(stream))
        {
            Console.WriteLine("用戶端已連線至子埠,開始處理訊息迴圈。");
            
            try
            {
                while (true)
                {
                    MessageType msgType = (MessageType)reader.ReadInt32();
                    switch (msgType)
                    {
                        case MessageType.RequestEmployee:
                            // 模擬資料庫查詢
                            Employee emp = new Employee("Eric Gunnerson", "One Microsoft Way");
                            emp.Serialize(writer);
                            writer.Flush();
                            break;
                        default:
                            Console.WriteLine($"忽略未知訊息:{msgType}");
                            break;
                    }
                }
            }
            catch (IOException)
            {
                Console.WriteLine("用戶端斷開連線。");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"處理錯誤:{ex.Message}");
            }
            finally
            {
                _listener.Stop(); // 釋放子埠
            }
        }
    }
}

用戶端事件驅動模型:整合 UI 執行緒

WinForms/WPF 應用需在 UI 執行緒更新介面,Socket 接收執行緒不能直接操作 UI 控制項。解決方案:委派事件 + Control.Invoke。

SocketClient 核心實作

using System;
using System.Net.Sockets;
using System.IO;
using System.Windows.Forms;

public class SocketClient
{
    private readonly TcpClient _client;
    private readonly BinaryReader _reader;
    private readonly BinaryWriter _writer;
    private readonly Thread _receiveThread;
    private readonly Form _uiForm;

    // 定義事件
    public event Action EmployeeReceived;
    public event Action CurrentCountReceived;

    public SocketClient(string host, int port, Form uiForm)
    {
        _uiForm = uiForm;
        _client = new TcpClient(host, port);
        NetworkStream stream = _client.GetStream();
        _reader = new BinaryReader(stream);
        _writer = new BinaryWriter(stream);

        // 先連主埠取得子埠
        int subPort = _reader.ReadInt32();
        _reader.Close(); _writer.Close(); stream.Close(); _client.Close();

        // 連接子埠
        _client = new TcpClient(host, subPort);
        stream = _client.GetStream();
        _reader = new BinaryReader(stream);
        _writer = new BinaryWriter(stream);

        _receiveThread = new Thread(ReceiveLoop) { IsBackground = true };
        _receiveThread.Start();
    }

    public void RequestEmployee(int id)
    {
        RequestEmployee req = new RequestEmployee(id);
        req.Serialize(_writer);
        _writer.Flush();
    }

    private void ReceiveLoop()
    {
        try
        {
            while (true)
            {
                MessageType msgType = (MessageType)_reader.ReadInt32();
                switch (msgType)
                {
                    case MessageType.Employee:
                        Employee emp = new Employee(_reader);
                        // 跨執行緒安全觸發事件
                        _uiForm.Invoke(new Action(() => EmployeeReceived?.Invoke(emp)));
                        break;
                    case MessageType.CurrentCount:
                        int count = _reader.ReadInt32();
                        _uiForm.Invoke(new Action(() => CurrentCountReceived?.Invoke(count)));
                        break;
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"接收執行緒異常:{ex.Message}");
        }
    }
}

WinForms 使用範例

public partial class MainForm : Form
{
    private SocketClient _client;

    public MainForm()
    {
        InitializeComponent();
        _client = new SocketClient("127.0.0.1", 9999, this);
        _client.EmployeeReceived += OnEmployeeReceived;
        _client.CurrentCountReceived += OnCountReceived;
    }

    private void btnRequest_Click(object sender, EventArgs e)
    {
        if (int.TryParse(txtId.Text, out int id))
            _client.RequestEmployee(id);
    }

    private void OnEmployeeReceived(Employee emp)
    {
        // 此處已在 UI 執行緒,可直接操控控制項
        lblName.Text = emp.Name;
        lblAddress.Text = emp.Address;
    }

    private void OnCountReceived(int count)
    {
        lblCount.Text = count.ToString();
    }
}

架構優勢總結

  • 輕量級:無 WCF/Remoting 繁重組態,僅依賴 System.Net.Sockets。
  • 跨平台:相同程式碼可在 .NET Framework、.NET Core、Mono (Pocket PC) 執行。
  • 非同步原生支援:伺服器主動推播 (CurrentCount 每秒廣播) 自然實現,無需輪詢。
  • 擴充性強:新增訊息類型只需擴充 MessageType、實作 ISocketMessage、補充 Switch case。

生產環境強化建議

  1. 採用 async/await + ReadAsync 取代專用接收執行緒,降低執行緒佔用。
  2. 實作長度前綴協議解決 TCP 黏包/拆包問題。
  3. 加入 TLS 加密 (SslStream) 保護資料機密性。
  4. 引入連線池與心跳機制管理長連線生命週期。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved