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

C# P2P 局域網聊天程式:TCP 直連、系統盤圖示、極簡 UI

     編輯:關於C語言

前言

本文實作一個無伺服器的 P2P 局域網聊天工具:每個執行個體同時是 Client 與 Server (雙向連線),支援系統盤最小化、閃動提醒、IP 自動偵測。適合離線內網、無中心節點場景。

架構設計

  • 傳輸層:TCP (`TcpListener` + `TcpClient`),訊息以 `\n` 結尾。
  • 發現機制:手動輸入對方 IP (可擴充 UDP Broadcast 自動發現)。
  • 執行緒模型:UI 執行緒 + 監聽執行緒 + 每連線一個接收執行緒。
  • 狀態機:Disconnected → Connecting → Connected。

核心程式碼


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());
        }
    }
}

功能亮點

  1. 零配置啟動:雙擊即用,自動監聽 5566 埠。
  2. 雙重角色:同時接受連線、主動連線,真正 P2P。
  3. 系統盤常駐:最小化隱藏、雙擊還原、右鍵選單退出。
  4. 新訊息閃動:托盤圖示交替閃爍提醒。
  5. 暱稱可改:連線前修改暱稱,握手時傳遞。

擴充方向

  • UDP 廣播自動發現:啟動時發送 `DISCOVER` 封包,收集區網線上使用者自動填入列表。
  • 訊息加密:AES-GCM + Diffie-Hellman 金鑰交換,防竊聽。
  • 檔案傳輸:協議擴充 `FILE|name|size|base64`,支援斷點續傳。
  • 群組聊天:引入 Mesh 拓撲或指定一節點為 Relay。
  • 跨平台 UI:Avalonia / MAUI 重寫 UI,邏輯層不變。

常見問題

現象原因解決
無法連線防火牆阻擋 5566 埠新增輸入規則允許 TCP 5566,或關閉公用網路防火牆
對方收不到訊息雙方皆需啟動監聽確認雙方「停止監聽」按鈕未按下
中文亂碼編碼不一致全程指定 `Encoding.UTF8`
最小化後無法還原Form 關閉而非隱藏Resize 事件處理 `Hide()`,`FormClosing` 僅 `Application.Exit()`

總結

不到 300 行程式碼即可構建一個功能完整的 P2P 局域網聊天工具。核心在於 `TcpListener`/`TcpClient` 非阻塞迴圈、執行緒安全的 UI 更新、系統盤整合。若需生產級穩定性,建議加入心跳、重連、加密、訊息確認 (ACK) 機制,並考慮使用 SignalR / gRPC 等成熟框架替換底層 Socket。

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