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

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

     編輯:關於C語言

前言

Socket 程式設計是網路通訊的基礎。本文完整實作一個多執行緒 TCP 聊天室,包含伺服器端(支援廣播、私訊、上下線通知)與客戶端(WinForms 介面),展示 `TcpListener`/`Socket` 非同步通訊、執行緒同步、訊息協定設計等核心技術。

通訊協定設計

採用簡單的 管線分隔文字協定,格式:指令|參數1|參數2|...

指令方向格式說明
CONNC→SCONN|暱稱客戶端連線註冊
LISTS→CLIST|名單1|名單2|...伺服器回傳線上名單
JOINS→CJOIN|暱稱廣播新用戶上線
CHATC→S/S→CCHAT|發送者: 訊息公開頻道訊息
PRIVC→S/S→CPRIV|發送者|接收者|訊息私訊
GONEC→S/S→CGONE|暱稱用戶離線通知
QUITS→CQUIT伺服器關閉通知

伺服器端完整實作

using System;
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 EndPoint EndPoint { get; set; }
        public Thread Thread { get; set; }
    }

    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 _clientsLock = new object();

        public ServerForm()
        {
            InitializeComponent();
            this.FormClosing += ServerForm_FormClosing;
        }

        private void btnStart_Click(object sender, EventArgs e)
        {
            _listener = new TcpListener(IPAddress.Any, Port);
            _listener.Start();
            
            _listenThread = new Thread(ListenLoop) { IsBackground = true };
            _listenThread.Start();
            
            AppendLog($"伺服器已啟動,監聽埠 {Port}...");
            btnStart.Enabled = false;
            btnStop.Enabled = true;
        }

        private void btnStop_Click(object sender, EventArgs e)
        {
            StopServer();
        }

        private void ListenLoop()
        {
            try
            {
                while (true)
                {
                    Socket clientSocket = _listener.AcceptSocket();
                    var clientThread = new Thread(() => HandleClient(clientSocket)) { IsBackground = true };
                    clientThread.Start();
                }
            }
            catch (SocketException)
            {
                // 伺服器停止時會觸發
            }
        }

        private void HandleClient(Socket socket)
        {
            ClientInfo clientInfo = null;
            byte[] buffer = new byte[4096];
            
            try
            {
                while (true)
                {
                    int received = socket.Receive(buffer);
                    if (received == 0) break; // 客戶端正常關閉
                    
                    string message = Encoding.UTF8.GetString(buffer, 0, received);
                    string[] tokens = message.Split('|');
                    
                    if (tokens.Length == 0) continue;

                    switch (tokens[0])
                    {
                        case "CONN":
                            string name = tokens[1];
                            clientInfo = new ClientInfo { Name = name, Socket = socket, EndPoint = socket.RemoteEndPoint };
                            
                            lock (_clientsLock)
                            {
                                // 廣播新用戶加入
                                Broadcast($"JOIN|{name}", null);
                                _clients.Add(clientInfo);
                            }
                            
                            // 回傳目前線上名單
                            string list = "LIST|" + string.Join("|", _clients.ConvertAll(c => c.Name));
                            SendToClient(clientInfo, list);
                            AppendLog($"{name} ({socket.RemoteEndPoint}) 已連線");
                            break;

                        case "CHAT":
                            Broadcast(message, clientInfo);
                            break;

                        case "PRIV":
                            if (tokens.Length >= 4)
                            {
                                string targetName = tokens[2];
                                SendPrivateMessage(tokens[1], targetName, message);
                            }
                            break;

                        case "GONE":
                            RemoveClient(clientInfo);
                            return; // 結束處理迴圈
                    }
                }
            }
            catch (SocketException ex)
            {
                AppendLog($"Socket 錯誤: {ex.Message}");
            }
            catch (Exception ex)
            {
                AppendLog($"處理錯誤: {ex.Message}");
            }
            finally
            {
                RemoveClient(clientInfo);
            }
        }

        private void Broadcast(string message, ClientInfo exclude)
        {
            lock (_clientsLock)
            {
                foreach (var client in _clients)
                {
                    if (client != exclude)
                        SendToClient(client, message);
                }
            }
        }

        private void SendPrivateMessage(string from, string to, string fullMessage)
        {
            lock (_clientsLock)
            {
                var target = _clients.Find(c => c.Name == to);
                var sender = _clients.Find(c => c.Name == from);
                
                if (target != null) SendToClient(target, fullMessage);
                if (sender != null) SendToClient(sender, fullMessage); // 回送給發送者確認
            }
        }

        private void SendToClient(ClientInfo client, string message)
        {
            try
            {
                if (client?.Socket?.Connected == true)
                {
                    byte[] data = Encoding.UTF8.GetBytes(message);
                    client.Socket.Send(data);
                }
            }
            catch { /* 忽略發送失敗 */ }
        }

        private void RemoveClient(ClientInfo client)
        {
            if (client == null) return;
            
            lock (_clientsLock)
            {
                if (_clients.Remove(client))
                {
                    Broadcast($"GONE|{client.Name}", null);
                    AppendLog($"{client.Name} 已離線");
                }
            }
            
            try { client.Socket?.Close(); } catch { }
        }

        private void AppendLog(string text)
        {
            if (txtLog.InvokeRequired)
            {
                txtLog.Invoke(new Action(AppendLog), text);
            }
            else
            {
                txtLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {text}\r\n");
            }
        }

        private void StopServer()
        {
            _listener?.Stop();
            
            lock (_clientsLock)
            {
                foreach (var client in _clients)
                {
                    SendToClient(client, "QUIT");
                    client.Socket?.Close();
                }
                _clients.Clear();
            }
            
            _listenThread?.Join(1000);
            AppendLog("伺服器已停止");
            btnStart.Enabled = true;
            btnStop.Enabled = false;
        }

        private void ServerForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            StopServer();
        }
    }
}

客戶端完整實作

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

namespace ChatClient
{
    public partial class ClientForm : Form
    {
        private TcpClient _client;
        private NetworkStream _stream;
        private Thread _receiveThread;
        private string _userName;
        private bool _connected = false;

        public ClientForm()
        {
            InitializeComponent();
            this.FormClosing += ClientForm_FormClosing;
        }

        private void btnConnect_Click(object sender, EventArgs e)
        {
            _userName = txtName.Text.Trim();
            if (string.IsNullOrEmpty(_userName))
            {
                MessageBox.Show("請輸入暱稱");
                return;
            }

            try
            {
                _client = new TcpClient("127.0.0.1", 6666);
                _stream = _client.GetStream();
                _connected = true;

                // 發送連線註冊
                SendMessage($"CONN|{_userName}");

                // 啟動接收執行緒
                _receiveThread = new Thread(ReceiveLoop) { IsBackground = true };
                _receiveThread.Start();

                btnConnect.Enabled = false;
                btnDisconnect.Enabled = true;
                txtName.ReadOnly = true;
                this.Text = $"{_userName} - 已連線";
                AppendChat("系統", "連線成功!");
            }
            catch (Exception ex)
            {
                MessageBox.Show($"連線失敗: {ex.Message}");
            }
        }

        private void btnDisconnect_Click(object sender, EventArgs e)
        {
            Disconnect();
        }

        private void btnSend_Click(object sender, EventArgs e)
        {
            string msg = txtMessage.Text.Trim();
            if (string.IsNullOrEmpty(msg)) return;

            // 簡單判斷:@目標 暱稱 視為私訊
            if (msg.StartsWith("@"))
            {
                int spaceIndex = msg.IndexOf(' ');
                if (spaceIndex > 1)
                {
                    string target = msg.Substring(1, spaceIndex - 1);
                    string content = msg.Substring(spaceIndex + 1);
                    SendMessage($"PRIV|{_userName}|{target}|{content}");
                }
                else
                {
                    AppendChat("系統", "私訊格式: @暱稱 訊息內容");
                }
            }
            else
            {
                SendMessage($"CHAT|{_userName}: {msg}");
            }
            
            txtMessage.Clear();
        }

        private void SendMessage(string message)
        {
            try
            {
                if (_connected && _stream?.CanWrite == true)
                {
                    byte[] data = Encoding.UTF8.GetBytes(message);
                    _stream.Write(data, 0, data.Length);
                }
            }
            catch (Exception ex)
            {
                AppendChat("系統", $"發送失敗: {ex.Message}");
            }
        }

        private void ReceiveLoop()
        {
            byte[] buffer = new byte[4096];
            
            try
            {
                while (_connected)
                {
                    int read = _stream.Read(buffer, 0, buffer.Length);
                    if (read == 0) break; // 伺服器關閉
                    
                    string message = Encoding.UTF8.GetString(buffer, 0, read);
                    ProcessServerMessage(message);
                }
            }
            catch (IOException)
            {
                // 連線中斷
            }
            catch (Exception ex)
            {
                AppendChat("系統", $"接收錯誤: {ex.Message}");
            }
            finally
            {
                Disconnect();
            }
        }

        private void ProcessServerMessage(string message)
        {
            string[] tokens = message.Split('|');
            if (tokens.Length == 0) return;

            switch (tokens[0])
            {
                case "LIST":
                    // tokens[1..] 為線上名單
                    UpdateUserList(tokens.Skip(1).ToArray());
                    break;
                    
                case "JOIN":
                    AppendChat("系統", $"{tokens[1]} 加入聊天室");
                    AddUserToList(tokens[1]);
                    break;
                    
                case "CHAT":
                    // 格式: CHAT|發送者: 訊息
                    string chatContent = string.Join("|", tokens.Skip(1));
                    int colonIdx = chatContent.IndexOf(':');
                    if (colonIdx > 0)
                    {
                        string from = chatContent.Substring(0, colonIdx);
                        string content = chatContent.Substring(colonIdx + 1).TrimStart();
                        AppendChat(from, content);
                    }
                    break;
                    
                case "PRIV":
                    // PRIV|發送者|接收者|訊息
                    if (tokens.Length >= 4)
                    {
                        AppendChat($"[私訊] {tokens[1]}", tokens[3]);
                    }
                    break;
                    
                case "GONE":
                    AppendChat("系統", $"{tokens[1]} 離開聊天室");
                    RemoveUserFromList(tokens[1]);
                    break;
                    
                case "QUIT":
                    AppendChat("系統", "伺服器已關閉");
                    Disconnect();
                    break;
            }
        }

        private void AppendChat(string from, string message)
        {
            if (rtbChat.InvokeRequired)
            {
                rtbChat.Invoke(new Action(AppendChat), from, message);
            }
            else
            {
                rtbChat.AppendText($"[{DateTime.Now:HH:mm:ss}] {from}: {message}\r\n");
                rtbChat.ScrollToCaret();
            }
        }

        private void UpdateUserList(string[] users)
        {
            if (lbUsers.InvokeRequired)
            {
                lbUsers.Invoke(new Action(UpdateUserList), users);
            }
            else
            {
                lbUsers.Items.Clear();
                lbUsers.Items.AddRange(users);
            }
        }

        private void AddUserToList(string name)
        {
            if (lbUsers.InvokeRequired)
            {
                lbUsers.Invoke(new Action(AddUserToList), name);
            }
            else if (!lbUsers.Items.Contains(name))
            {
                lbUsers.Items.Add(name);
            }
        }

        private void RemoveUserFromList(string name)
        {
            if (lbUsers.InvokeRequired)
            {
                lbUsers.Invoke(new Action(RemoveUserFromList), name);
            }
            else
            {
                lbUsers.Items.Remove(name);
            }
        }

        private void Disconnect()
        {
            if (!_connected) return;
            _connected = false;
            
            try
            {
                SendMessage($"GONE|{_userName}");
                _stream?.Close();
                _client?.Close();
            }
            catch { }
            
            if (this.InvokeRequired)
            {
                this.Invoke(new Action(Disconnect));
            }
            else
            {
                btnConnect.Enabled = true;
                btnDisconnect.Enabled = false;
                txtName.ReadOnly = false;
                lbUsers.Items.Clear();
                this.Text = "聊天客戶端 - 未連線";
                AppendChat("系統", "已斷線");
            }
        }

        private void ClientForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            Disconnect();
        }
    }
}

進階優化方向

1. 非同步 Socket (Begin/End 或 async/await)

// 現代寫法:使用 SocketAsyncEventArgs 或 Task-based
public async Task StartServerAsync()
{
    var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    listener.Bind(new IPEndPoint(IPAddress.Any, Port));
    listener.Listen(100);
    
    while (true)
    {
        Socket client = await listener.AcceptAsync();
        _ = HandleClientAsync(client); // 不等待,平行處理
    }
}

2. 訊息框架化(解決黏包/拆包)

// 長度前綴協定:[4 bytes 長度][JSON/Protobuf 內容]
public class MessageFrame
{
    public int Length { get; set; }
    public byte[] Payload { get; set; }
    
    public static byte[] Encode(string json) => 
        BitConverter.GetBytes(json.Length).Concat(Encoding.UTF8.GetBytes(json)).ToArray();
}

3. 心跳機制偵測斷線

// 伺服器定期發送 PING,客戶端回 PONG
// 若 N 次無回應視為斷線,主動清理資源

常見問題排查

現象可能原因解決方向
客戶端收到亂碼編碼不一致統一使用 UTF-8,避免 ASCII/Default
訊息不完整/合併TCP 黏包/拆包實作訊息框架(長度前綴/分隔符)
跨執行緒 UI 例外非 UI 執行緒存取控制項使用 Control.Invoke/BeginInvoke
伺服器記憶體遞增斷線客戶端未清理在 finally 區塊確實 Close/Dispose
防火牆阻擋埠未開放防火牆新增輸入規則允許 TCP 6666

總結

本範例展示了經典的「每連線一執行緒」模型,適合中小規模連線數(<500)。大規模高併發場景建議改用 IOCP (IO Completion Port) 或 async/await 配合 SocketAsyncEventArgs。核心關鍵點:協定設計、執行緒同步、資源釋放、異常處理、UI 執行緒安全。掌握這些基礎,才能進階建構高效能的網路應用系統。

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