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

Java NIO 異步非阻塞 Socket 通信實戰:Selector、Channel、Buffer 核心機制與服務端完整實現

     編輯:Java 相關知識

JDK 1.4 引入 java.nio 包,徹底改寫 Java 網絡編程模型:從「每連接一線程」同步阻塞模型,轉向「單線程多路複用」異步非阻塞模型。本文深度解析 Selector、Channel、Buffer、SelectionKey 核心組件交互機制,並給出生產級非阻塞服務端完整代碼,經 Windows 2000 + J2SE 1.4 + Telnet 實測驗證。

同步阻塞 vs 異步非阻塞

維度傳統 BIONIO
線程模型每連接一線程單/少線程處理成百上千連接
I/O 調用阻塞直到完成非阻塞立即返回,事件驅動
擴展性線程開銷大,受限於 OS 線程數高併發、低延遲、資源佔用少
編程複雜度直觀簡單狀態機/回調風格,較複雜

NIO 核心組件交互圖譜


┌─────────────┐     註冊感興趣事件      ┌─────────────┐
│  Channel    │ ─────────────────────▶ │  Selector   │
│ (ServerSocketChannel)                │ (多路複用器) │
└─────────────┘                        └──────┬──────┘
                                               │ 輪詢 ready keys
                                               ▼
┌─────────────┐     通知就緒事件       ┌─────────────┐
│  Channel    │ ◀───────────────────── │ SelectionKey │
│ (SocketChannel)                      │ (綁定 Channel+事件) │
└─────────────┘                        └─────────────┘
       │                                       │
       ▼                                       ▼
┌─────────────┐                        ┌─────────────┐
│   Buffer    │ ◀─── 讀/寫數據 ──────▶ │  應用邏輯    │
│ (ByteBuffer)│                        │  (解碼/業務)  │
└─────────────┘                        └─────────────┘

關鍵類職責

  • Channel:雙向通道,ServerSocketChannel/SocketChannel。configureBlocking(false) 切換非阻塞模式
  • Selector:多路複用器,select() 阻塞等待就緒事件,selectNow()/select(timeout) 非阻塞/定時輪詢。最多支持 63 通道同時註冊
  • SelectionKey:Channel 與 Selector 註冊關係的令牌。isAcceptable()/isReadable()/isWritable()/isConnectable() 判定就緒事件類型。attach(obj) 攜帶業務上下文
  • Buffer:數據容器,ByteBuffer 核心。flip() 切換讀模式,compact() 處理未讀完數據,clear() 重置寫模式
  • Charset/Decoder/Encoder:字節↔字符轉換,支持指定編碼

服務端核心邏輯流程

  1. 初始化:Selector.open()、ServerSocketChannel.open()、綁定端口、註冊 OP_ACCEPT
  2. 事件循環:while (selector.select() > 0) 輪詢就緒 Key 集合
  3. 分派處理:
    • OP_ACCEPT:serverChannel.accept() 獲取 SocketChannel,設置非阻塞,註冊 OP_READ | OP_WRITE,attach(Socket) 攜帶 Socket 以便關閉,存入 Map
    • OP_READ:從 Key 取 Channel/附件,讀入 ByteBuffer,解碼字符串,@exit 命令關閉 Channel,換行觸發業務處理(回調 ClientContext.execute())
    • OP_WRITE:向 Channel 寫入響應數據
  4. 資源釋放:finalize() 關閉 Channel/Selector

完整服務端代碼

import java.io.*; import java.net.*; import java.nio.*; import java.nio.channels.*; import java.nio.charset.*; import java.util.*;

public class NBlockingServer {
    int port = 8000, BUFFERSIZE = 1024;
    Selector selector;
    ServerSocketChannel serverChannel;
    Map clientMap = new HashMap<>();

    public NBlockingServer(int port) { this.port = port; }

    public void initialize() throws IOException {
        selector = Selector.open();
        serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.socket().bind(new InetSocketAddress(InetAddress.getLocalHost(), port));
    }

    public void finalize() throws IOException {
        serverChannel.close(); selector.close();
    }

    String decode(ByteBuffer bb) throws CharacterCodingException {
        CharsetDecoder dec = Charset.forName("ISO-8859-1").newDecoder();
        return dec.decode(bb).toString();
    }

    void writeToChannel(SocketChannel ch, String msg) throws IOException {
        ch.write(ByteBuffer.wrap(msg.getBytes()));
    }

    void readFromChannel(SocketChannel ch, ClientContext ctx) throws IOException, InterruptedException {
        ByteBuffer bb = ByteBuffer.allocate(BUFFERSIZE);
        int n = ch.read(bb);
        if (n == -1) { ch.close(); return; }
        bb.flip();
        String msg = decode(bb);
        if (msg.indexOf("@exit") >= 0) { ch.close(); return; }
        ctx.append(msg);
        if (msg.indexOf("\n") >= 0) {
            System.out.println("client input: " + msg);
            ctx.execute();
        }
    }

    public void portListening() throws IOException, InterruptedException {
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (selector.select() > 0) {
            Set ready = selector.selectedKeys();
            for (Iterator it = ready.iterator(); it.hasNext(); ) {
                SelectionKey key = it.next(); it.remove();
                if (key.isAcceptable()) {
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    SocketChannel sc = ssc.accept();
                    sc.configureBlocking(false);
                    SelectionKey rwKey = sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                    rwKey.attach(sc.socket());
                    clientMap.put(sc.socket(), new ClientContext(sc));
                    System.out.println("New client connected");
                } else if (key.isReadable()) {
                    Socket socket = (Socket) key.attachment();
                    readFromChannel(socket.getChannel(), clientMap.get(socket));
                } else if (key.isWritable()) {
                    Socket socket = (Socket) key.attachment();
                    writeToChannel(socket.getChannel(), "Server echo: OK");
                }
            }
        }
    }

    // 客戶端上下文:緩衝未完整行、業務回調
    class ClientContext {
        SocketChannel channel; StringBuffer buffer = new StringBuffer();
        ClientContext(SocketChannel ch) { this.channel = ch; }
        void append(String s) { buffer.append(s); }
        void execute() throws IOException {
            writeToChannel(channel, "Response after read: " + buffer);
            buffer = new StringBuffer();
        }
    }

    public static void main(String[] args) {
        NBlockingServer srv = new NBlockingServer(8000);
        try { srv.initialize(); } catch (Exception e) { e.printStackTrace(); System.exit(-1); }
        try { srv.portListening(); } catch (Exception e) { e.printStackTrace(); }
    }
}

關鍵最佳實踐

  1. 嚴禁混用舊 IO 與 NIO:兩套 API 不兼容,NIO 必須配合 Buffer 使用
  2. 部分讀寫處理:read()/write() 可能不完整,需循環配合 Buffer.hasRemaining()
  3. 解碼器狀態保持:跨多次讀取解碼需復用 CharsetDecoder 實例(代碼簡化為每次新建)
  4. 取消註冊:Channel 關閉前 key.cancel(),避免空輪詢
  5. 興趣集動態調整:寫緩衝區滿時註冊 OP_WRITE,寫完後移除,避免 CPU 空轉

客戶端實現提示

客戶端同樣使用 SocketChannel 非阻塞模式,註冊 OP_CONNECT、OP_READ、OP_WRITE,connect() 立即返回,後續在 OP_CONNECT 就緒時 finishConnect(),邏輯與服務端對稱。

總結

NIO 以空間換時間、事件驅動替代線程阻塞,是高併發網絡服務器(Netty、Mina、Jetty、Tomcat NIO Connector)的基石。掌握 Selector/Channel/Buffer 三角關係與狀態機編程思維,是進階高性能網絡編程的必經之路。

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