配合服務端,UDP 聊天室客戶端採用收發分離雙線程模型:主線程處理用戶輸入發送,獨立線程持續監聽服務端消息。本文詳細解析客戶端核心模塊:連接管理、用戶登錄、命令解析、消息收發、異常處理,附完整代碼。
-help/-getList/-to 私聊/-quit 退出public class Consumer implements Runnable {
private InetAddress serverAddress;
private int port;
private DatagramSocket client;
private DatagramPacket sendPacket;
private boolean testFlag = true; // 連接/暱稱測試標誌
// 核心方法...
}
public void connectServer(String ip, int port) {
fixServerLink(ip, port); // 解析地址
try { client = new DatagramSocket(); } // 綁定隨機本地端口
catch (SocketException e) { System.out.println("Connection error!"); }
}
private void fixServerLink(String ip, int port) {
this.port = (port == 0) ? 9999 : port;
try {
this.serverAddress = "0".equalsIgnoreCase(ip) ?
InetAddress.getLocalHost() : InetAddress.getByName(ip);
} catch (UnknownHostException e) {
System.out.println("Invalid IP address!");
}
}
public boolean testConnect(String ip, int port) {
connectServer(ip, port);
sendMes("-test"); // 發送測試包
if (!testFlag) return false; // 發送失敗
String response = getMesOnce(); // 接收響應
if (response != null && response.startsWith("-test")) {
testFlag = true;
} else testFlag = false;
if (client != null) client.close();
return testFlag;
}
private boolean checkNickName(String nick) {
// 1. 客戶端格式驗證:僅字母數字
for (char c : nick.toCharArray()) {
if (!Character.isLetterOrDigit(c)) { testFlag = false; break; }
}
// 2. 服務端重名檢查
if (testFlag) {
sendMes("-nick " + nick);
String response = getMesOnce();
System.out.println(response); // 顯示服務端回應
if (response != null && response.startsWith("-nick")) testFlag = false;
}
return testFlag;
}
// 主線程:用戶輸入循環 → 發送消息
// 接收線程:run() 方法 → 持續阻塞接收
public void run() { getMes(); } // 接收線程入口
public void getMes() {
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
try {
while (true) {
client.receive(packet); // 阻塞等待
String msg = new String(buf, 0, packet.getLength());
if (msg.startsWith("-getList")) parseUserList(msg); // 列表特殊處理
else System.out.println(msg); // 直接輸出聊天消息
}
} catch (IOException e) { System.out.println("Receive failed!"); }
}
private void parseUserList(String msg) {
String[] users = msg.split(",");
System.out.println("Online Users:\n-------------");
for (int i = 1; i < users.length; i++) System.out.println(" * " + users[i]);
System.out.println("-------------");
}
為什麼需要雙線程? UDP receive() 是阻塞調用,若在主線程調用會凍結用戶輸入。接收線程隔離阻塞,保證發送/界面響應流暢。
public void sendMes(String msg) {
byte[] buf = msg.getBytes();
sendPacket = new DatagramPacket(buf, buf.length, serverAddress, port);
testFlag = true;
try { client.send(sendPacket); }
catch (NullPointerException e) { testFlag = false; System.out.println("Address null!"); }
catch (IOException e) { testFlag = false; System.out.println("Send failed!"); }
}
// 主程序入口
public static void main(String[] args) {
Consumer client = new Consumer();
Scanner input = new Scanner(System.in);
System.out.println("Welcome to Sunspot Chat!");
// 1. 連接服務器(含測試重試)
String ip = null; int port = 0; boolean connecting = true;
while (connecting) {
System.out.print("Server IP (0=default): "); ip = input.next();
System.out.print("Server Port (0=default): ");
try { port = input.nextInt(); }
catch (Exception e) { System.out.println("Invalid port!"); continue; }
if (client.testConnect(ip, port)) { System.out.println("Connected!"); connecting = false; }
else System.out.println("Connection failed!");
}
// 2. 正式連接 + 暱稱註冊
client.connectServer(ip, port);
boolean naming = true;
while (naming) {
System.out.print("Nickname (letters/digits only): ");
String nick = input.next();
if (client.checkNickName(nick)) naming = false;
else System.out.println("Invalid or duplicate nickname!");
}
// 3. 啟動接收線程
new Thread(client).start();
System.out.println("!-You entered the chat room!");
// 4. 主發送循環
String msg;
while (!(msg = input.nextLine()).equalsIgnoreCase("-quit")) {
if (msg.trim().equalsIgnoreCase("-test")) client.testConnect(ip, port);
else if (msg.trim().equalsIgnoreCase("-getList")) client.sendMes(msg);
else if (msg.startsWith("-nick ")) System.out.println("Already named!");
else if (msg.equalsIgnoreCase("-help")) client.helpList();
else if (msg.isEmpty()) continue;
else client.sendMes(msg);
}
// 5. 清理退出
input.close();
client.disconnectServer(); // 發送 -quit 通知服務端
}
public void disconnectServer() {
sendMes("-quit"); // 通知服務端移除用戶
if (client != null) client.close();
}
| 異常類型 | 捕獲位置 | 處理策略 |
|---|---|---|
| SocketException | Socket 創建/發送 | 標記測試失敗、輸出錯誤信息 |
| UnknownHostException | IP 解析 | 提示 IP 無效 |
| IOException | 收發數據 | 標記失敗、輸出「發送/接收失敗」 |
| InputMismatchException | 端口輸入 | 提示格式錯誤、繼續循環 | 消息丟失/亂序 | 應用層序列號 + ACK 重傳(當前未實現,生產環境必須) |