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

C# Socket 多執行緒聊天室完整實作(伺服器/客戶端)

     編輯:關於C語言

前言

本文提供一個以 TcpListener/TcpClient + Thread 實作的簡易多人聊天室,支援公開廣播、私訊、上線/下線通知、使用者列表同步。程式碼採用 .NET Framework 4.x 風格,核心邏輯可直接移植至 .NET Core / 5+(建議改用 async/await + Pipe/Channels)。

架構概覽

  • Server:TcpListener 監聽埠 → 每連線啟動一條 Thread 處理該客戶端全生命週期。
  • Protocol:以 | 分隔的純文字指令:CONN|name、CHAT|msg、PRIV|from|to|msg、GONE|name、LIST|name1|name2|...。
  • Client:WinForms UI,雙執行緒(UI + 接收迴圈)。

伺服器核心程式碼

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace ChatServer
{
    public class ClientInfo
    {
        public string Name { get; set; }
        public Socket Socket { get; set; }
        public Thread Thread { get; set; }
        public EndPoint EndPoint { get; set; }
        public override string ToString() => $"{EndPoint} : {Name}";
    }

    public partial class ServerForm : Form
    {
        private const int Port = 6666;
        private TcpListener _listener;
        private Thread _listenThread;
        private readonly List _clients = new List();
        private readonly object _lock = new object();
        private bool _running;

        // UI 元件
        private ListBox lstClients;
        private Button btnStart;
        private Label lblStatus;

        public ServerForm()
        {
            InitializeComponent();
            Text = "聊天室伺服器";
            FormClosing += (s, e) => StopServer();
        }

        private void InitializeComponent()
        {
            lstClients = new ListBox { Dock = DockStyle.Fill };
            btnStart = new Button { Text = "啟動伺服器", Dock = DockStyle.Bottom, Height = 40 };
            lblStatus = new Label { Text = "已停止", Dock = DockStyle.Top, Height = 30, TextAlign = ContentAlignment.MiddleCenter };
            btnStart.Click += (s, e) => { if (!_running) StartServer(); else StopServer(); };
            Controls.Add(lstClients);
            Controls.Add(btnStart);
            Controls.Add(lblStatus);
        }

        private void StartServer()
        {
            _listener = new TcpListener(IPAddress.Any, Port);
            _listener.Start();
            _running = true;
            _listenThread = new Thread(ListenLoop) { IsBackground = true };
            _listenThread.Start();
            btnStart.Text = "停止伺服器";
            lblStatus.Text = $"監聽中... 端口 {Port}";
        }

        private void StopServer()
        {
            _running = false;
            _listener?.Stop();
            lock (_lock)
            {
                foreach (var c in _clients)
                    try { c.Socket.Close(); } catch { }
                _clients.Clear();
            }
            lstClients.Items.Clear();
            btnStart.Text = "啟動伺服器";
            lblStatus.Text = "已停止";
        }

        private void ListenLoop()
        {
            while (_running)
            {
                try
                {
                    var socket = _listener.AcceptSocket();
                    var thread = new Thread(() => HandleClient(socket)) { IsBackground = true };
                    thread.Start();
                }
                catch (SocketException) { if (_running) Thread.Sleep(100); }
                catch (ObjectDisposedException) { break; }
            }
        }

        private void HandleClient(Socket clientSocket)
        {
            ClientInfo me = null;
            bool alive = true;
            var buffer = new byte[4096];

            try
            {
                while (alive && _running)
                {
                    int len = clientSocket.Receive(buffer, SocketFlags.None);
                    if (len == 0) break; // 客戶端正常關閉

                    string raw = Encoding.UTF8.GetString(buffer, 0, len);
                    foreach (var line in raw.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries))
                        ProcessCommand(clientSocket, line.Trim(), ref me, ref alive);
                }
            }
            catch (SocketException) { }
            finally
            {
                if (me != null) RemoveClient(me);
                clientSocket.Close();
            }
        }

        private void ProcessCommand(Socket socket, string cmd, ref ClientInfo me, ref bool alive)
        {
            var parts = cmd.Split('|');
            if (parts.Length == 0) return;

            switch (parts[0])
            {
                case "CONN":
                    string name = parts.Length > 1 ? parts[1] : "Anonymous";
                    lock (_lock)
                    {
                        // 廣播新用戶加入
                        foreach (var c in _clients)
                            Send(c.Socket, $"JOIN|{name}");
                        me = new ClientInfo { Name = name, Socket = socket, EndPoint = socket.RemoteEndPoint, Thread = Thread.CurrentThread };
                        _clients.Add(me);
                        // 回傳現有用戶列表
                        Send(socket, "LIST|" + string.Join("|", _clients.ConvertAll(c => c.Name)));
                    }
                    InvokeUI(() => lstClients.Items.Add(me));
                    break;

                case "CHAT":
                    lock (_lock)
                        foreach (var c in _clients)
                            Send(c.Socket, cmd); // 原樣轉發
                    break;

                case "PRIV":
                    // PRIV|from|to|msg
                    if (parts.Length >= 4)
                    {
                        string to = parts[2];
                        lock (_lock)
                        {
                            foreach (var c in _clients)
                                if (c.Name == to || c.Name == parts[1])
                                    Send(c.Socket, cmd);
                        }
                    }
                    break;

                case "GONE":
                    alive = false;
                    break;
            }
        }

        private void RemoveClient(ClientInfo client)
        {
            lock (_lock)
            {
                _clients.Remove(client);
                foreach (var c in _clients)
                    Send(c.Socket, $"GONE|{client.Name}");
            }
            InvokeUI(() => lstClients.Items.Remove(client));
        }

        private void Send(Socket s, string msg)
        {
            try
            {
                if (s.Connected)
                    s.Send(Encoding.UTF8.GetBytes(msg + "\n"));
            }
            catch { }
        }

        private void InvokeUI(Action act)
        {
            if (InvokeRequired) BeginInvoke(act);
            else act();
        }

        [STAThread]
        static void Main() => Application.Run(new ServerForm());
    }
}

客戶端核心程式碼

using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace ChatClient
{
    public partial class ClientForm : Form
    {
        private const string ServerIp = "127.0.0.1";
        private const int ServerPort = 6666;

        private TcpClient _client;
        private NetworkStream _stream;
        private Thread _recvThread;
        private bool _connected;
        private string _myName;

        // UI
        private TextBox txtName, txtInput, txtServer;
        private ListBox lstUsers;
        private RichTextBox rtbChat;
        private Button btnConnect, btnSend, btnDisconnect;

        public ClientForm()
        {
            InitializeComponent();
            Text = "聊天室客戶端";
            FormClosing += (s, e) => Disconnect();
        }

        private void InitializeComponent()
        {
            // 簡易佈局
            var top = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 40 };
            txtName = new TextBox { Width = 100, Text = "User" + new Random().Next(1000) };
            btnConnect = new Button { Text = "連線", Width = 60 };
            btnDisconnect = new Button { Text = "離線", Width = 60, Enabled = false };
            txtServer = new TextBox { Width = 150, Text = ServerIp + ":" + ServerPort };
            top.Controls.AddRange(new Control[] { new Label { Text = "暱稱:", AutoSize = true }, txtName, btnConnect, btnDisconnect, new Label { Text = "伺服器:", AutoSize = true }, txtServer });

            lstUsers = new ListBox { Dock = DockStyle.Left, Width = 120 };
            rtbChat = new RichTextBox { Dock = DockStyle.Fill, ReadOnly = true };
            var bottom = new FlowLayoutPanel { Dock = DockStyle.Bottom, Height = 40 };
            txtInput = new TextBox { Width = 300 };
            btnSend = new Button { Text = "發送", Width = 60, Enabled = false };
            bottom.Controls.AddRange(new Control[] { txtInput, btnSend });

            Controls.Add(rtbChat);
            Controls.Add(lstUsers);
            Controls.Add(bottom);
            Controls.Add(top);

            btnConnect.Click += (s, e) => Connect();
            btnDisconnect.Click += (s, e) => Disconnect();
            btnSend.Click += (s, e) => SendChat();
            txtInput.KeyDown += (s, e) => { if (e.KeyCode == Keys.Enter) SendChat(); };
        }

        private void Connect()
        {
            try
            {
                _client = new TcpClient(ServerIp, ServerPort);
                _stream = _client.GetStream();
                _connected = true;
                _myName = txtName.Text.Trim();
                if (string.IsNullOrEmpty(_myName)) _myName = "Anonymous";

                // 發送 CONN
                Send($"CONN|{_myName}");

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

                btnConnect.Enabled = false;
                btnDisconnect.Enabled = true;
                btnSend.Enabled = true;
                txtName.Enabled = false;
                Text = $"{_myName} - 已連線";
            }
            catch (Exception ex)
            {
                MessageBox.Show("連線失敗: " + ex.Message);
            }
        }

        private void Disconnect()
        {
            if (!_connected) return;
            _connected = false;
            try { Send($"GONE|{_myName}"); } catch { }
            _stream?.Close();
            _client?.Close();
            _recvThread?.Join(1000);

            btnConnect.Enabled = true;
            btnDisconnect.Enabled = false;
            btnSend.Enabled = false;
            txtName.Enabled = true;
            lstUsers.Items.Clear();
            Text = "聊天室客戶端 - 未連線";
        }

        private void ReceiveLoop()
        {
            var buffer = new byte[4096];
            try
            {
                while (_connected)
                {
                    int len = _stream.Read(buffer, 0, buffer.Length);
                    if (len == 0) break;
                    string raw = Encoding.UTF8.GetString(buffer, 0, len);
                    foreach (var line in raw.Split('\n'))
                        ProcessServerMsg(line.Trim());
                }
            }
            catch { }
            finally { if (_connected) InvokeUI(Disconnect); }
        }

        private void ProcessServerMsg(string msg)
        {
            var parts = msg.Split('|');
            if (parts.Length == 0) return;

            switch (parts[0])
            {
                case "LIST":
                    InvokeUI(() =>
                    {
                        lstUsers.Items.Clear();
                        for (int i = 1; i < parts.Length; i++)
                            if (!string.IsNullOrEmpty(parts[i])) lstUsers.Items.Add(parts[i]);
                    });
                    break;
                case "JOIN":
                    InvokeUI(() =>
                    {
                        rtbChat.AppendText($"[系統] {parts[1]} 加入聊天室\n");
                        lstUsers.Items.Add(parts[1]);
                    });
                    break;
                case "GONE":
                    InvokeUI(() =>
                    {
                        rtbChat.AppendText($"[系統] {parts[1]} 離開聊天室\n");
                        lstUsers.Items.Remove(parts[1]);
                    });
                    break;
                case "CHAT":
                    InvokeUI(() => rtbChat.AppendText(parts[1] + "\n"));
                    break;
                case "PRIV":
                    // PRIV|from|to|msg
                    InvokeUI(() => rtbChat.AppendText($"[私訊] {parts[1]} → {parts[2]}: {parts[3]}\n"));
                    break;
            }
        }

        private void SendChat()
        {
            string text = txtInput.Text.Trim();
            if (string.IsNullOrEmpty(text)) return;
            Send($"CHAT|{_myName}: {text}");
            txtInput.Clear();
        }

        private void Send(string msg)
        {
            if (!_connected) return;
            try
            {
                var data = Encoding.UTF8.GetBytes(msg + "\n");
                _stream.Write(data, 0, data.Length);
            }
            catch { Disconnect(); }
        }

        private void InvokeUI(Action act)
        {
            if (InvokeRequired) BeginInvoke(act);
            else act();
        }

        [STAThread]
        static void Main() => Application.Run(new ClientForm());
    }
}

關鍵設計重點

  1. 訊息框架:以換行符 \n 為封包邊界,簡單易除錯;生產環境建議改用 Length-Prefix 或 MessagePack/Protobuf。
  2. 執行緒安全:lock(_clients) 保護共用集合;UI 更新統一 BeginInvoke。
  3. 優雅斷線:GONE 指令雙向通知,伺服器主動清理列表並廣播。
  4. 編碼統一:全程 UTF-8,支援中文/Emoji。

進階擴充方向

  • 非同步 I/O:async/await + NetworkStream.ReadAsync 取代 Thread,支援萬級連線 (C10K)。
  • 心跳機制:定時發送 PING,偵測斷網/假死連線。
  • 身分驗證:加入 Token/JWT,防止暱稱冒用。
  • 訊息持久化:接入 Redis / SQLite 記錄歷史訊息,支援離線推送。
  • 加密傳輸:SslStream 包裝 Socket 實現 TLS 1.3。

總結

這個範例展示了 TCP Socket 多執行緒伺服器的經典模式:Accept → Thread → Loop(Recv → Parse → Dispatch)。雖然現代 .NET 推薦 Kestrel/SignalR/gRPC,但理解底層 Socket 模型對排查網路問題、設計高效能協議仍不可或缺。

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