在區域網路環境下,若想快速建立一個輕量級的即時通訊工具,採用 UDP 廣播 與 Peer-to-Peer (P2P) 架構是極佳的選擇。本文將深入剖析一個以 C# 實作的「區域網 QQ」原始碼,展示如何在不需專屬伺服器的情況下,實現線上用戶偵測、文字訊息收發與新訊息提醒等核心功能。
2525,廣播位址為 255.255.255.255。& 字元分隔欄位,定義指令碼:0(上線/重整)、1(發言)、2(傳檔預留)、9(已讀確認)。public class Form1 : System.Windows.Forms.Form
{
// 網路核心物件
public UdpClient listen = new UdpClient(2525); // 監聽用 UdpClient
public IPEndPoint End; // 廣播端點
public IPAddress groupAddress = IPAddress.Parse("255.255.255.255");
public int groupPort = 2525;
// 本機身分資訊
public string MachineName; // 主機名稱
public string MachineIP; // 本機 IP
// 當前會話對象資訊
public string romeName; // 對方暱稱
public string romeIP; // 對方 IP
public string romeCon; // 對方訊息內容
// UI 控制項宣告...
private System.Windows.Forms.ListBox box; // 線上用戶列表
private System.Windows.Forms.TextBox t_rec; // 接收訊息區
private System.Windows.Forms.TextBox t_send; // 發送訊息區
private System.Windows.Forms.Button button1; // 發送鈕
private System.Windows.Forms.Button button3; // 重整鈕
// ... 其他控制項
}
表單載入時 (Form1_Load) 執行三項關鍵動作:
Dns.GetHostByName 取得主機名稱與首個 IP 位址。ListenPort(),持續接收 UDP 封包。0&IP&Name&IP 的封包,通知區網其他節點。private void Form1_Load(object sender, System.EventArgs e)
{
End = new IPEndPoint(groupAddress, groupPort);
// 取得本機資訊
IPHostEntry myentry = Dns.GetHostByName(Dns.GetHostName());
IPAddress myaddress = new IPAddress(myentry.AddressList[0].Address);
MachineName = Dns.GetHostName();
MachineIP = myaddress.ToString();
// 初始化列表標題
box.Items.Add("IP 主機名");
// 啟動監聽執行緒
runing = true;
Thread myThread = new Thread(new ThreadStart(this.ListenPort));
myThread.Start();
// 廣播上線
string sends = "0&" + MachineIP + "&" + MachineName + "&" + MachineIP;
SendPack(sends);
}
ListenPort() 在迴圈中呼叫 UdpClient.Receive 封鎖等待資料,收到後交由 checkMessage() 依指令碼分流處理:
public void ListenPort()
{
IPEndPoint tempEnd = new IPEndPoint(IPAddress.Any, 2525);
while (runing)
{
Application.DoEvents(); // 讓 UI 保持回應
try
{
byte[] recb = listen.Receive(ref tempEnd);
checkMessage(recb);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
break;
}
}
listen.Close();
box.Items.Add("執行緒已經退出!");
runing = false;
}
public void checkMessage(byte[] recbb)
{
string recStr = Encoding.Default.GetString(recbb);
string[] rec = recStr.Split('&');
switch (rec[0])
{
case "0": // 重整 / 上線通知
// 若是自己發的,忽略;否則回覆自身資訊並加入列表
if (rec[3] == MachineIP)
{
if (box.FindString(rec[1] + " " + rec[2]) <= 0)
box.Items.Add(rec[1] + " " + rec[2]);
}
else if (rec[1] == rec[3]) // 來自他人的重整請求
{
string sendstr = "0&" + MachineIP + "&" + MachineName + "&" + rec[3];
SendPack(sendstr);
if (box.FindString(rec[1] + " " + rec[2]) <= 0)
box.Items.Add(rec[1] + " " + rec[2]);
}
break;
case "1": // 一般聊天訊息
if (rec[3] == MachineIP) // 確認是發給自己的
{
romeName = rec[2];
romeIP = rec[1];
romeCon = rec[4];
showNe(); // 顯示新訊息提鈕
}
break;
case "2": // 傳檔 (預留)
break;
case "9": // 已讀確認
if (rec[3] == MachineIP)
MessageBox.Show("訊息來自:" + rec[2] + "(" + rec[1] + ")\r\n" + rec[4], "消息");
break;
}
}
統一透過 SendPack 將字串轉為位元組陣列並以 UDP 廣播發送:
public void SendPack(string sendStr)
{
byte[] sendb = Encoding.Default.GetBytes(sendStr);
try
{
listen.Send(sendb, sendb.Length, End);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
點擊「重整」鈕會清空列表並重新廣播上線封包:
private void button3_Click(object sender, System.EventArgs e)
{
box.Items.Clear();
box.Items.Add("IP 主機名");
string temp2 = "0&" + MachineIP + "&" + MachineName + "&" + MachineIP;
SendPack(temp2);
}
需先在列表選取目標使用者,封包格式為 1&MyIP&MyName&TargetIP&Content:
private void button1_Click(object sender, System.EventArgs e)
{
if (string.IsNullOrEmpty(t_send.Text))
{
MessageBox.Show("不能發送空訊息!");
return;
}
if (box.SelectedItem == null || box.SelectedIndex == 0)
{
MessageBox.Show("請先選擇一個用戶!");
return;
}
// 解析列表項目 "IP Name" 取得 IP
string targetIP = box.SelectedItem.ToString().Split(' ')[0];
string send = "1&" + MachineIP + "&" + MachineName + "&" + targetIP + "&" + t_send.Text;
SendPack(send);
}
收到訊息時呼叫 showNe() 將 newmsg 按鈕置頂顯示「有新消息了!」,使用者點擊後將訊息寫入接收區並發送 9 號確認封包:
private void newmsg_Click(object sender, System.EventArgs e)
{
t_rec.Text = "消息來自 " + romeName + "(" + romeIP + ")" + DateTime.Now.ToShortDateString();
t_rec.Text += "\r\n> " + romeCon;
string back = "9&" + MachineIP + "&" + MachineName + "&" + romeIP + "&信息被打開!";
SendPack(back);
newmsg.SendToBack();
}
public void showNe()
{
if (this.WindowState != FormWindowState.Normal)
{
this.Visible = true;
this.WindowState = FormWindowState.Normal;
}
newmsg.BringToFront();
}
表單關閉時將 runing 設為 false 並發送一個空封包喚醒 Receive 讓執行緒順利結束:
private void Form1_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
runing = false;
UdpClient mm = new UdpClient();
IPEndPoint tempIPEnd = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 2525);
string temps = "0&0&0&0&0";
byte[] sendb = Encoding.Default.GetBytes(temps);
mm.Send(sendb, sendb.Length, tempIPEnd);
}
Encoding.Default,跨語系環境建議改用 UTF-8。ListenPort 中直接操作 UI 控制項 (box.Items.Add),應改用 Invoke 或 SynchronizationContext。2 已預留,可擴充為分塊傳輸協定。此專案展示了利用 C# System.Net.Sockets 命名空間快速構建區域網 P2P 通訊原型的完整流程。雖然功能尚屬基礎,但架構清晰、程式碼精簡,極適合作為學習網路程式設計或二次開發的起點。若您有興趣深入擴充,歡迎 Fork 原始碼並提交 Pull Request!