UDP (User Datagram Protocol) 是無連接、不可靠、面向數據報的傳輸層協議:
本例採用客戶端/服務端對等模型:雙方各綁定一個本地端口接收數據,發送時指定對方 IP+端口。區別於 TCP 的 `ServerSocket.accept()`,UDP 無「連接」概念,雙方均為 `DatagramSocket`。
+----------------+ DatagramPacket +----------------+
| Client A | <----------------------------> | Client B |
| (sendPort=8888)| 發送目標: B_IP:6666 | (sendPort=6666)|
| (recvPort=6666)| 接收綁定: 0.0.0.0:6666 | (recvPort=8888)|
+----------------+ +----------------+
public class UdpChatThread extends Thread {
private final DatagramSocket receiveSocket; // 接收專用 Socket
private final DatagramSocket sendSocket; // 發送專用 Socket
private final JTextArea messageArea; // UI 顯示區域
private final String targetIp;
private final int targetPort;
private static final int BUFFER_SIZE = 4096;
public UdpChatThread(int localPort, String targetIp, int targetPort, JTextArea area)
throws SocketException {
this.receiveSocket = new DatagramSocket(localPort); // 綁定本地端口接收
this.sendSocket = new DatagramSocket(); // 發送端口由系統自動分配
this.targetIp = targetIp;
this.targetPort = targetPort;
this.messageArea = area;
}
@Override
public void run() {
byte[] buffer = new byte[BUFFER_SIZE];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
while (!isInterrupted() && !receiveSocket.isClosed()) {
receiveSocket.receive(packet); // 阻塞等待數據
String msg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
SwingUtilities.invokeLater(() ->
messageArea.append("[" + packet.getAddress().getHostAddress() + "] " + msg + "\n")
);
packet.setLength(buffer.length); // 重置長度,關鍵!否則後續包會被截斷
}
} catch (SocketException e) {
// Socket 關閉導致的異常,正常退出
} catch (IOException e) {
SwingUtilities.invokeLater(() ->
messageArea.append("[系統] 接收錯誤: " + e.getMessage() + "\n")
);
}
}
public void send(String text) {
try {
byte[] data = text.getBytes(StandardCharsets.UTF_8);
InetAddress addr = InetAddress.getByName(targetIp);
DatagramPacket packet = new DatagramPacket(data, data.length, addr, targetPort);
sendSocket.send(packet);
SwingUtilities.invokeLater(() -> messageArea.append("[我] " + text + "\n"));
} catch (IOException e) {
SwingUtilities.invokeLater(() -> messageArea.append("[系統] 發送失敗: " + e.getMessage() + "\n"));
}
}
public void close() {
receiveSocket.close();
sendSocket.close();
interrupt();
}
}
接收與發送使用獨立 Socket,避免發送時阻塞接收線程,也便於分別綁定/配置端口。
packet.setLength(buffer.length) 是新手最易忽略的坑:`receive()` 會將 `packet.length` 修改為實際接收長度,若不重置,下次接收大包時會被舊長度截斷。
網絡線程不可直接操作 UI 組件,必須用 `SwingUtilities.invokeLater()` 拋回 EDT (Event Dispatch Thread)。
使用 `Properties` 文件管理 IP/端口,避免硬編碼:
# udp-config.properties
local.port=6666
target.ip=127.0.0.1
target.port=8888
public class UdpChatApp {
private JFrame frame;
private JTextArea area;
private JTextField input;
private UdpChatThread chatThread;
public UdpChatApp(String title, int localPort, String targetIp, int targetPort) {
// ... Swing UI 初始化代碼省略 ...
try {
chatThread = new UdpChatThread(localPort, targetIp, targetPort, area);
chatThread.start();
} catch (SocketException e) { /* 錯誤處理 */ }
// 發送按鈕事件
sendBtn.addActionListener(e -> chatThread.send(input.getText().trim()));
// 窗口關閉釋放資源
frame.addWindowListener(new WindowAdapter() {
@Override public void windowClosing(WindowEvent e) { chatThread.close(); }
});
}
public static void main(String[] args) {
// 啟動 A: 本地 6666 接收,發送給 8888
new UdpChatApp("用戶 A", 6666, "127.0.0.1", 8888);
// 啟動 B: 本地 8888 接收,發送給 6666
// new UdpChatApp("用戶 B", 8888, "127.0.0.1", 6666);
}
}
| 問題 | 原因 | 解決方案 |
|---|---|---|
| 局域網不通 | 防火牆攔截 UDP | 開放對應端口,或使用 NAT 穿透技術 |
| 消息亂序/丟失 | UDP 本身不保證 | 應用層添加序列號、ACK 重傳、滑動窗口(變相實現可靠 UDP) |
| 大包分片丟失 | MTU 限制導致 IP 分片 | 應用層分片重組,或限制包大小 < 1400 字節 |
| 中文亂碼 | 編碼不一致 | 統一指定 `StandardCharsets.UTF_8` |