本文實作一個無伺服器的 P2P 局域網聊天工具:每個執行個體同時是 Client 與 Server (雙向連線),支援系統盤最小化、閃動提醒、IP 自動偵測。適合離線內網、無中心節點場景。
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace P2PChat
{
public class ChatForm : Form
{
private const int Port = 5566;
private readonly List _clients = new();
private TcpListener _listener;
private Thread _listenThread;
private NotifyIcon _trayIcon;
private bool _running;
// UI
private TextBox _txtLog, _txtInput, _txtTargetIp, _txtName;
private Button _btnSend, _btnConnect, _btnListen;
private ListBox _lstPeers;
public ChatForm()
{
InitializeComponent();
StartListener(); // 啟動時自動監聽
Load += (s, e) => _txtName.Text = "User_" + new Random().Next(1000, 9999);
}
private void InitializeComponent()
{
Text = "P2P 局域網聊天";
Size = new Size(500, 500);
MinimumSize = new Size(400, 400);
// 托盤圖示
_trayIcon = new NotifyIcon
{
Icon = SystemIcons.Application,
Text = "P2P Chat",
Visible = true,
ContextMenuStrip = new ContextMenuStrip()
};
_trayIcon.ContextMenuStrip.Items.Add("顯示", null, (s, e) => { Show(); WindowState = FormWindowState.Normal; });
_trayIcon.ContextMenuStrip.Items.Add("退出", null, (s, e) => { _trayIcon.Visible = false; Application.Exit(); });
_trayIcon.DoubleClick += (s, e) => { Show(); WindowState = FormWindowState.Normal; };
Resize += (s, e) => { if (WindowState == FormWindowState.Minimized) Hide(); };
// 版面
var top = new Panel { Dock = DockStyle.Top, Height = 80, Padding = new Padding(5) };
_txtName = new TextBox { Left = 60, Top = 10, Width = 100, PlaceholderText = "暱稱" };
_txtTargetIp = new TextBox { Left = 60, Top = 40, Width = 180, PlaceholderText = "目標 IP (空白=廣播)" };
_btnConnect = new Button { Text = "連線", Left = 250, Top = 38, Width = 60 };
_btnListen = new Button { Text = "停止監聽", Left = 320, Top = 38, Width = 80 };
top.Controls.AddRange(new Control[] {
new Label { Text = "暱稱:", Left = 10, Top = 12, AutoSize = true },
new Label { Text = "目標:", Left = 10, Top = 42, AutoSize = true },
_txtName, _txtTargetIp, _btnConnect, _btnListen
});
_lstPeers = new ListBox { Dock = DockStyle.Right, Width = 120 };
_txtLog = new TextBox { Dock = DockStyle.Fill, Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Vertical, BackColor = Color.White };
var bottom = new Panel { Dock = DockStyle.Bottom, Height = 40, Padding = new Padding(5) };
_txtInput = new TextBox { Dock = DockStyle.Fill, PlaceholderText = "輸入訊息..." };
_btnSend = new Button { Text = "發送", Dock = DockStyle.Right, Width = 60, Enabled = false };
bottom.Controls.Add(_txtInput);
bottom.Controls.Add(_btnSend);
Controls.Add(_txtLog);
Controls.Add(_lstPeers);
Controls.Add(bottom);
Controls.Add(top);
// 事件
_btnConnect.Click += (s, e) => ConnectToPeer();
_btnListen.Click += (s, e) => ToggleListener();
_btnSend.Click += (s, e) => SendMessage();
_txtInput.KeyDown += (s, e) => { if (e.KeyCode == Keys.Enter) SendMessage(); };
FormClosing += (s, e) => StopAll();
}
#region 監聽端
private void StartListener()
{
_listener = new TcpListener(IPAddress.Any, Port);
_listener.Start();
_running = true;
_listenThread = new Thread(ListenLoop) { IsBackground = true };
_listenThread.Start();
AppendLog($"[系統] 監聽啟動於 {GetLocalIP()}:{Port}");
_btnListen.Text = "停止監聽";
}
private void StopListener()
{
_running = false;
_listener?.Stop();
_listenThread?.Join(1000);
_btnListen.Text = "啟動監聽";
AppendLog("[系統] 監聽已停止");
}
private void ToggleListener()
{
if (_running) StopListener(); else StartListener();
}
private void ListenLoop()
{
while (_running)
{
try
{
var client = _listener.AcceptTcpClient();
var thread = new Thread(() => HandleClient(client)) { IsBackground = true };
thread.Start();
}
catch (SocketException) { if (_running) Thread.Sleep(100); }
catch (ObjectDisposedException) { break; }
}
}
private void HandleClient(TcpClient client)
{
string peerName = null;
try
{
var stream = client.GetStream();
var reader = new System.IO.StreamReader(stream, Encoding.UTF8);
var writer = new System.IO.StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
// 簡單握手:第一行傳送暱稱
peerName = reader.ReadLine() ?? "Unknown";
lock (_clients) _clients.Add(client);
InvokeUI(() => { _lstPeers.Items.Add(peerName); AppendLog($"[系統] {peerName} 已連線"); });
string line;
while ((line = reader.ReadLine()) != null)
{
InvokeUI(() => AppendLog($"{peerName}: {line}"));
// 廣播給其他連線 (P2P 這裡僅雙向,不轉發)
}
}
catch { }
finally
{
lock (_clients) _clients.Remove(client);
client.Close();
InvokeUI(() => { _lstPeers.Items.Remove(peerName); AppendLog($"[系統] {peerName} 已斷線"); });
}
}
#endregion
#region 連線端
private void ConnectToPeer()
{
string ip = _txtTargetIp.Text.Trim();
if (string.IsNullOrEmpty(ip))
{
MessageBox.Show("請輸入目標 IP");
return;
}
try
{
var client = new TcpClient();
client.Connect(ip, Port);
var stream = client.GetStream();
var writer = new System.IO.StreamWriter(stream, Encoding.UTF8) { AutoFlush = true };
writer.WriteLine(_txtName.Text); // 發送暱稱
lock (_clients) _clients.Add(client);
var thread = new Thread(() => HandleOutgoingClient(client)) { IsBackground = true };
thread.Start();
_btnSend.Enabled = true;
AppendLog($"[系統] 已連線至 {ip}:{Port}");
}
catch (Exception ex)
{
MessageBox.Show("連線失敗: " + ex.Message);
}
}
private void HandleOutgoingClient(TcpClient client)
{
try
{
var stream = client.GetStream();
var reader = new System.IO.StreamReader(stream, Encoding.UTF8);
string line;
while ((line = reader.ReadLine()) != null)
InvokeUI(() => AppendLog($"[對方] {line}"));
}
catch { }
finally
{
lock (_clients) _clients.Remove(client);
client.Close();
InvokeUI(() => { _btnSend.Enabled = false; AppendLog("[系統] 連線已斷開"); });
}
}
#endregion
private void SendMessage()
{
string msg = _txtInput.Text.Trim();
if (string.IsNullOrEmpty(msg)) return;
var data = Encoding.UTF8.GetBytes(_txtName.Text + ": " + msg + "\n");
lock (_clients)
{
foreach (var c in _clients)
{
try { c.GetStream().Write(data, 0, data.Length); } catch { }
}
}
AppendLog("我: " + msg);
_txtInput.Clear();
}
private void AppendLog(string text)
{
_txtLog.AppendText(DateTime.Now.ToString("HH:mm:ss ") + text + Environment.NewLine);
_txtLog.ScrollToCaret();
// 托盤閃動提醒
if (WindowState == FormWindowState.Minimized || !Visible)
FlashTray();
}
private void FlashTray(int count = 3)
{
var original = _trayIcon.Icon;
for (int i = 0; i < count; i++)
{
_trayIcon.Icon = SystemIcons.Exclamation;
Thread.Sleep(200);
_trayIcon.Icon = original;
Thread.Sleep(200);
}
}
private void StopAll()
{
_running = false;
_listener?.Stop();
lock (_clients)
{
foreach (var c in _clients) c.Close();
_clients.Clear();
}
_trayIcon.Visible = false;
}
private void InvokeUI(Action act)
{
if (InvokeRequired) BeginInvoke(act);
else act();
}
private string GetLocalIP()
{
var host = Dns.GetHostEntry(Dns.GetHostName());
foreach (var ip in host.AddressList)
if (ip.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(ip))
return ip.ToString();
return "127.0.0.1";
}
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new ChatForm());
}
}
}
| 現象 | 原因 | 解決 |
|---|---|---|
| 無法連線 | 防火牆阻擋 5566 埠 | 新增輸入規則允許 TCP 5566,或關閉公用網路防火牆 | 對方收不到訊息 | 雙方皆需啟動監聽 | 確認雙方「停止監聽」按鈕未按下 |
| 中文亂碼 | 編碼不一致 | 全程指定 `Encoding.UTF8` |
| 最小化後無法還原 | Form 關閉而非隱藏 | Resize 事件處理 `Hide()`,`FormClosing` 僅 `Application.Exit()` |
不到 300 行程式碼即可構建一個功能完整的 P2P 局域網聊天工具。核心在於 `TcpListener`/`TcpClient` 非阻塞迴圈、執行緒安全的 UI 更新、系統盤整合。若需生產級穩定性,建議加入心跳、重連、加密、訊息確認 (ACK) 機制,並考慮使用 SignalR / gRPC 等成熟框架替換底層 Socket。