本文提供一個以 TcpListener/TcpClient + Thread 實作的簡易多人聊天室,支援公開廣播、私訊、上線/下線通知、使用者列表同步。程式碼採用 .NET Framework 4.x 風格,核心邏輯可直接移植至 .NET Core / 5+(建議改用 async/await + Pipe/Channels)。
TcpListener 監聽埠 → 每連線啟動一條 Thread 處理該客戶端全生命週期。| 分隔的純文字指令:CONN|name、CHAT|msg、PRIV|from|to|msg、GONE|name、LIST|name1|name2|...。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());
}
}
lock(_clients) 保護共用集合;UI 更新統一 BeginInvoke。GONE 指令雙向通知,伺服器主動清理列表並廣播。async/await + NetworkStream.ReadAsync 取代 Thread,支援萬級連線 (C10K)。PING,偵測斷網/假死連線。SslStream 包裝 Socket 實現 TLS 1.3。這個範例展示了 TCP Socket 多執行緒伺服器的經典模式:Accept → Thread → Loop(Recv → Parse → Dispatch)。雖然現代 .NET 推薦 Kestrel/SignalR/gRPC,但理解底層 Socket 模型對排查網路問題、設計高效能協議仍不可或缺。