本文完整走過一個簡易 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(); }
}
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 { }
}
}
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