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

從零打造 C# Socket 聊天程式:需求分析、介面設計與完整實作

     編輯:關於C#

本文完整走過一個簡易 P2P 聊天程式的生命週期:需求釐清 → 用例撰寫 → 介面定義 → Socket 實作 → WinForms 整合,展示面向介面程式設計與事件驅動架構的實戰應用。

第一階段:需求釐清與用例建模

核心功能清單

  • 登入(僅需使用者名稱,無密碼)
  • 雙方連線(雙工通訊,雙方皆可主動發起)
  • 發送/接收訊息(含發送者、時間、內容)
  • 登出/關閉(皆需通知對方)
  • 單一對單一,無伺服器中繼

關鍵用例片段

用例本地端遠端
連線輸入 IP/Port → 連線成功/失敗提示自動偵聽 → 顯示「遠端已連線」
發送輸入文字 → 送出 → 本地顯示自動接收 → 顯示訊息
登出點擊登出 → 斷線 → 狀態復原偵測斷線 → 顯示「對方已離線」

第二階段:介面契約設計

訊息資料結構


public struct Message
{
    public readonly string UserName;
    public readonly string Content;
    public readonly DateTime PostDate;

    public Message(string userName, string content)
    { UserName = userName; Content = content; PostDate = DateTime.Now; }

    public override string ToString() => $"{UserName}[{PostDate:HH:mm:ss}]:\r\n{Content}\r\n";
}

發送端介面 IMessageSender


public interface IMessageSender
{
    bool Connect(IPAddress ip, int port);
    bool SendMessage(Message msg);
    void SignOut();
}

接收端介面 IMessageReceiver — 事件驅動


public delegate void MessageReceivedHandler(string msg);
public delegate void ClientConnectedHandler(IPEndPoint endPoint);
public delegate void ConnectionLostHandler(string info);

public interface IMessageReceiver
{
    event MessageReceivedHandler MessageReceived;
    event ClientConnectedHandler ClientConnected;
    event ConnectionLostHandler ClientLost;

    void StartListen();
    void StopListen();
}

外觀類別 Talker — 整合雙介面


public class Talker
{
    private readonly IMessageReceiver _receiver;
    private readonly IMessageSender _sender;

    public Talker(IMessageReceiver receiver, IMessageSender sender)
    { _receiver = receiver; _sender = sender; }

    public Talker() : this(new MessageReceiver(), new MessageSender()) { }

    // 事件轉發
    public event MessageReceivedHandler MessageReceived
    { add => _receiver.MessageReceived += value; remove => _receiver.MessageReceived -= value; }
    public event ClientConnectedHandler ClientConnected { add => _receiver.ClientConnected += value; remove => _receiver.ClientConnected -= value; }
    public event ConnectionLostHandler ClientLost { add => _receiver.ClientLost += value; remove => _receiver.ClientLost -= value; }

    public bool ConnectByHost(string host, int port) => _sender.Connect(Dns.GetHostAddresses(host)[0], port);
    public bool ConnectByIp(string ip, int port) => _sender.Connect(IPAddress.Parse(ip), port);
    public bool SendMessage(Message msg) => _sender.SendMessage(msg);
    public void SignOut() => _sender.SignOut();
    public void Dispose() { _sender.SignOut(); _receiver.StopListen(); }
}

第三階段:Socket 具體實作

發送實作 MessageSender — TcpClient


public class MessageSender : IMessageSender
{
    private TcpClient _client;
    private Stream _stream;

    public bool Connect(IPAddress ip, int port)
    {
        try { _client = new TcpClient(); _client.Connect(ip, port); _stream = _client.GetStream(); return true; }
        catch { return false; }
    }

    public bool SendMessage(Message msg)
    {
        try
        {
            lock (_stream)
            {
                byte[] buf = Encoding.Unicode.GetBytes(msg.ToString());
                _stream.Write(buf, 0, buf.Length);
            }
            return true;
        }
        catch { return false; }
    }

    public void SignOut()
    {
        _stream?.Dispose();
        _client?.Close();
    }
}

接收實作 MessageReceiver — TcpListener + 背景執行緒


public class MessageReceiver : IMessageReceiver
{
    public event MessageReceivedHandler MessageReceived;
    public event ClientConnectedHandler ClientConnected;
    public event ConnectionLostHandler ClientLost;
    public event Action PortNumberReady; // 動態端口通知 UI

    private TcpListener _listener;
    private Thread _worker;

    public MessageReceiver() => StartListen();

    void IMessageReceiver.StartListen()
    {
        _worker = new Thread(ListenLoop) { IsBackground = true };
        _worker.Start();
    }

    private void ListenLoop()
    {
        _listener = new TcpListener(IPAddress.Loopback, 0); // 0 = OS 指派可用端口
        _listener.Start();
        PortNumberReady?.Invoke(((IPEndPoint)_listener.LocalEndpoint).Port);

        while (true)
        {
            TcpClient client;
            try { client = _listener.AcceptTcpClient(); }
            catch { break; } // StopListen 呼叫時會拋出例外跳出

            ClientConnected?.Invoke((IPEndPoint)client.Client.RemoteEndPoint);
            var stream = client.GetStream();
            var buffer = new byte[8192];

            while (true)
            {
                try
                {
                    int read = stream.Read(buffer, 0, buffer.Length);
                    if (read == 0) throw new Exception("對方斷線");
                    string msg = Encoding.Unicode.GetString(buffer, 0, read);
                    MessageReceived?.Invoke(msg);
                }
                catch (Exception ex)
                {
                    ClientLost?.Invoke(ex.Message);
                    break;
                }
            }
        }
    }

    public void StopListen()
    {
        try { _listener?.Stop(); _worker?.Abort(); } catch { }
    }
}

第四階段:WinForms 整合與跨執行緒 UI 更新


public partial class ChatForm : Form
{
    private readonly Talker _talker;
    private readonly string _userName;

    public ChatForm(string name)
    {
        InitializeComponent();
        _userName = name;
        Text = $"{name} Talking...";
        _talker = new Talker();

        _talker.PortNumberReady += p => Invoke(() => lblPort.Text = p.ToString());
        _talker.ClientConnected += ep => Invoke(() => AppendSys($"遠端 {ep} 已連線"));
        _talker.ClientLost += msg => Invoke(() => AppendSys($"連線中斷:{msg}"));
        _talker.MessageReceived += msg => Invoke(() => txtHistory.AppendText(msg));
    }

    private void AppendSys(string msg) => txtHistory.AppendText($"System[{DateTime.Now:HH:mm:ss}]:\r\n{msg}\r\n");

    private void btnConnect_Click(object sender, EventArgs e)
    {
        if (!int.TryParse(txtPort.Text, out int port)) return;
        if (_talker.ConnectByHost(txtHost.Text, port) || _talker.ConnectByIp(txtHost.Text, port))
        {
            AppendSys("連線成功");
            SetConnectedUI(true);
        }
        else MessageBox.Show("連線失敗");
    }

    private void btnSend_Click(object sender, EventArgs e)
    {
        if (string.IsNullOrWhiteSpace(txtInput.Text)) return;
        var msg = new Message(_userName, txtInput.Text);
        if (_talker.SendMessage(msg)) { txtHistory.AppendText(msg.ToString()); txtInput.Clear(); }
        else { AppendSys("發送失敗,對方已離線"); SetConnectedUI(false); }
    }

    private void btnSignOut_Click(object sender, EventArgs e)
    {
        _talker.SignOut();
        SetConnectedUI(false);
        AppendSys("已登出");
    }

    private void SetConnectedUI(bool connected)
    {
        btnConnect.Enabled = !connected;
        btnSignOut.Enabled = connected;
        btnSend.Enabled = connected;
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        _talker.Dispose();
        base.OnFormClosing(e);
    }
}

架構優缺點與擴充方向

優點缺點/限制擴充建議
介面隔離,測試容易無伺服器,需知 IP/Port,無法穿透 NAT引入中繼伺服器或 STUN/TURN
事件驅動,UI 解耦同步封鎖,單連線,無法多用戶改用 async/await + SocketAsyncEventArgs
代碼結構清晰無加密、無認證加入 TLS (SslStream) + 簡單 Token 認證

完整原始碼下載:ChatDemo.zip

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