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

基於 Socket 的 Java 網絡編程精粹:從單線程到多線程並發服務端演進

     編輯:Java 相關知識

Socket 通信的本質是「雙向字節流」。本文通過三個版本演進,展示如何從「單線程順序處理」→「多線程並發處理」構建健壯 TCP 服務端,並對比兩種流包裝方式 (BufferedReader/PrintWriter vs DataInputStream/DataOutputStream) 的適用場景。

版本 1:單線程順序處理

public class MyServerV1 {
    public static void main(String[] args) throws IOException {
        try (ServerSocket server = new ServerSocket(5678)) {
            while (true) {
                try (Socket client = server.accept();
                     BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
                     PrintWriter out = new PrintWriter(client.getOutputStream())) {
                    
                    String line;
                    while ((line = in.readLine()) != null) {
                        System.out.println("收到: " + line);
                        out.println("has receive....");
                        out.flush(); // 關鍵:手動 flush
                        if ("end".equals(line)) break;
                    }
                }
            }
        }
    }
}

問題:處理完一個客戶端才能接受下一個,併發能力為零。

版本 2:多線程並發 (繼承 Thread)

public class MultiUserServer extends Thread {
    private final Socket client;

    public MultiUserServer(Socket client) { this.client = client; }

    @Override public void run() {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
             PrintWriter out = new PrintWriter(client.getOutputStream())) {
            
            String line;
            while ((line = in.readLine()) != null) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + line);
                out.println("has receive....");
                out.flush();
                if ("end".equals(line)) break;
            }
        } catch (IOException e) {
            // 記錄日誌
        } finally {
            try { client.close(); } catch (IOException ignored) {}
        }
    }

    public static void main(String[] args) throws IOException {
        try (ServerSocket server = new ServerSocket(5678)) {
            System.out.println("多線程服務端啟動,端口 5678");
            while (true) {
                new MultiUserServer(server.accept()).start();
            }
        }
    }
}

優點:每個客戶端獨立線程,真正並發。缺點:無限創建線程,高併發下 OOM。

版本 3:線程池優化 (生產就緒)

public class ThreadPoolServer {
    private static final ExecutorService POOL = new ThreadPoolExecutor(
        50, 200, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000),
        new ThreadFactoryBuilder().setNameFormat("Worker-%d").build(),
        new ThreadPoolExecutor.AbortPolicy() // 隊列滿拒絕新連接
    );

    public static void main(String[] args) throws IOException {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> POOL.shutdown()));
        
        try (ServerSocket server = new ServerSocket(5678)) {
            server.setSoTimeout(1000);
            System.out.println("線程池服務端啟動,端口 5678");
            while (true) {
                try {
                    Socket client = server.accept();
                    POOL.submit(() -> handleClient(client));
                } catch (SocketTimeoutException ignored) {}
            }
        }
    }

    private static void handleClient(Socket client) {
        try (BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream(), StandardCharsets.UTF_8));
             PrintWriter out = new PrintWriter(new OutputStreamWriter(client.getOutputStream(), StandardCharsets.UTF_8), true)) {
            
            String line;
            while ((line = in.readLine()) != null) {
                System.out.println("[" + Thread.currentThread().getName() + "] " + line);
                out.println("Server echo: " + line);
                if ("end".equalsIgnoreCase(line)) break;
            }
        } catch (IOException e) {
            // log
        } finally {
            try { client.close(); } catch (IOException ignored) {}
        }
    }
}

流包裝方式對比:字符流 vs 數據流

維度BufferedReader + PrintWriterDataInputStream + DataOutputStream
數據類型文本/字符串 (行制)基本類型 + 字節/字符串 (二進制)
編碼處理InputStreamReader/OutputStreamWriter 指定編碼UTF-8 修飾字符串 (readUTF/writeUTF),其餘為二進制
適用協議文本協議 (HTTP, SMTP, Redis RESP, 自定義行制)二進制協議、混合類型、需精確控制字節序
字符串讀取readLine() (換行符分隔)readUTF() (長度前綴 + UTF-8 字節)
性能字符轉換開銷稍大二進制直接讀寫,效能略優
推薦場景文本協議首選自定義二進制協議、與 C/C++ 交互、高性能內部 RPC

DataInputStream/DataOutputStream 範例

// 發送端
try (DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
    out.writeInt(12345);           // 4字節 int
    out.writeLong(9876543210L);    // 8字節 long
    out.writeUTF("Hello 中文");    // 2字節長度 + UTF-8字節
    out.writeBoolean(true);        // 1字節
    out.flush();
}

// 接收端
try (DataInputStream in = new DataInputStream(socket.getInputStream())) {
    int i = in.readInt();
    long l = in.readLong();
    String s = in.readUTF(); // 必須雙端協議一致
    boolean b = in.readBoolean();
}

關鍵設計原則總結

  1. 協議先行:先定義消息格式 (文本行制 / 二進制 TLV / Protobuf),再選流包裝
  2. 線程池隔離:固定核心 + 有界隊列 + 拒絕策略,保護系統穩定
  3. 編碼顯式:StandardCharsets.UTF_8,杜絕亂碼
  4. 資源 try-with-resources:Socket/流自動關閉,零洩漏
  5. 優雅關閉:ShutdownHook + accept 超時檢查 + 線程池 shutdown
  6. 異常隔離:單連接異常不影響主循環/其他連接

總結

Socket 編程核心不在 API,而在 併發模型、協議設計、資源管理、異常隔離。從單線程 → 多線程 → 線程池的演進,體現了「先正確、再健壯、後高性能」的工程哲學。文本協議用 BufferedReader/PrintWriter,二進制協議用 DataInputStream/DataOutputStream 或專業序列化框架 (Protobuf, MessagePack)。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved