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

Java UDP 聊天室客戶端實現:多線程收發分離與用戶交互詳解

     編輯:Java 相關知識

配合服務端,UDP 聊天室客戶端採用收發分離雙線程模型:主線程處理用戶輸入發送,獨立線程持續監聽服務端消息。本文詳細解析客戶端核心模塊:連接管理、用戶登錄、命令解析、消息收發、異常處理,附完整代碼。

功能特性

  • 服務器連接測試(IP/端口默認值支持)
  • 用戶暱稱註冊(字母數字驗證、重名檢測)
  • 命令系統:-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; // 連接/暱稱測試標誌
    // 核心方法...
}

關鍵模塊實現

1. 服務器地址解析與連接建立


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!");
    }
}

2. 連接測試:雙向驗證


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;
}

3. 暱稱註冊:客戶端驗證 + 服務端權威檢查


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;
}

4. 雙線程模型:核心架構決策


// 主線程:用戶輸入循環 → 發送消息
// 接收線程: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() 是阻塞調用,若在主線程調用會凍結用戶輸入。接收線程隔離阻塞,保證發送/界面響應流暢。

5. 消息發送與主循環


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 通知服務端
}

6. 斷開連接:優雅關閉


public void disconnectServer() {
    sendMes("-quit"); // 通知服務端移除用戶
    if (client != null) client.close();
}

異常處理與健壯性

UDP 客戶端特有挑戰應對

  • 服務端重啟/換端口:客戶端感知不到,需定期 -test 或心跳機制
  • NAT 穿透:客戶端隨機本地端口,路由器映射超時會導致接收失效,需保活包

代碼結構優化建議

  1. 拆分 ConnectionManager(連接/重連/心跳)、MessageCodec(編解碼)、UserSession(狀態管理)
  2. 引入 ExecutorService 管理接收線程,支持優雅關閉
  3. 使用 ConcurrentLinkedQueue 解耦接收與顯示邏輯
  4. 配置文件管理默認 IP/端口/緩衝區大小
  5. 日誌框架替換 System.out
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
異常類型捕獲位置處理策略
SocketExceptionSocket 創建/發送標記測試失敗、輸出錯誤信息
UnknownHostExceptionIP 解析提示 IP 無效
IOException收發數據標記失敗、輸出「發送/接收失敗」
InputMismatchException端口輸入提示格式錯誤、繼續循環
消息丟失/亂序應用層序列號 + ACK 重傳(當前未實現,生產環境必須)