首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 程式設計綜合問答

Java NIO 伺服器主動推送訊息給客戶端架構設計

     編輯:程式設計綜合問答

問題

使用 Java NIO (`Selector`、`SelectionKey`) 實現雙向通訊。伺服器端在 `key.isWritable()` 中處理寫入,但業務資料產生時間隨機,無法像回應請求那樣即時寫入。

核心誤區

NIO `Selector` 機制是**就緒通知**:`isWritable()` 表示「通道緩衝區有空間可寫」,**不代表「有資料要發送」**。若在無資料時註冊 `OP_WRITE`,會導致 `Selector` 不斷觸發 `isWritable()`,CPU 佔用 100%。

正確架構:生產者-消費者 + 佇列

  1. 每個連結維護一個發送佇列(`ConcurrentLinkedQueue` 或 `LinkedBlockingQueue`)。
  2. 業務執行緒(生產者):隨機產生資料 → 封裝為 `ByteBuffer` → `queue.offer(buffer)` → **喚醒 Selector 執行緒**(關鍵步驟)。
  3. Selector 執行緒(消費者):
    • 輪詢 `selector.select()`。
    • 處理 `OP_READ`:讀取請求、解碼、交給業務處理。
    • 檢查發送佇列:若非空 → 註冊 `OP_WRITE` (`key.interestOps(key.interestOps() | SelectionKey.OP_WRITE)`)。
    • 處理 `OP_WRITE`:從佇列 `poll()` 取資料 → `channel.write(buffer)` → 若佇列空 → **取消註冊 `OP_WRITE`** (`key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE)`)。
  4. 喚醒 Selector:生產者執行緒呼叫 `selector.wakeup()`,使 `select()` 立即返回,及時處理新資料。

關鍵程式碼片段

// 連結附件類
class ConnectionContext {
    SocketChannel channel;
    Queue sendQueue = new ConcurrentLinkedQueue<>();
    // ...
}

// 業務執行緒發送資料
public void sendData(ConnectionContext ctx, ByteBuffer data) {
    ctx.sendQueue.offer(data);
    selector.wakeup(); // 喚醒 Selector 執行緒
}

// Selector 執行緒主迴圈
while (!Thread.interrupted()) {
    selector.select(); // 阻塞直到就緒或 wakeup
    Set keys = selector.selectedKeys();
    for (SelectionKey key : keys) {
        ConnectionContext ctx = (ConnectionContext) key.attachment();
        if (key.isReadable()) { handleRead(key, ctx); }
        if (key.isWritable()) { handleWrite(key, ctx); }
    }
    keys.clear();
    
    // 也可在這裡檢查佇列決定是否註冊 OP_WRITE(避免遺漏 wakeup 競態)
    for (SelectionKey key : selector.keys()) {
        ConnectionContext ctx = (ConnectionContext) key.attachment();
        boolean hasData = !ctx.sendQueue.isEmpty();
        boolean writeRegistered = (key.interestOps() & SelectionKey.OP_WRITE) != 0;
        if (hasData && !writeRegistered) {
            key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
        } else if (!hasData && writeRegistered) {
            key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
        }
    }
}

void handleWrite(SelectionKey key, ConnectionContext ctx) {
    ByteBuffer buffer;
    while ((buffer = ctx.sendQueue.poll()) != null) {
        ctx.channel.write(buffer);
        if (buffer.hasRemaining()) {
            ctx.sendQueue.addFirst(buffer); // 未寫完放回隊頭
            break;
        }
    }
    if (ctx.sendQueue.isEmpty()) {
        key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
    }
}

替代方案:Netty

生產環境強烈建議使用 **Netty**,已完美封裝上述模式(`ChannelOutboundBuffer`、`writeAndFlush`、`EventLoop`),避免重造輪子踩坑。

結論

NIO 伺服器主動推送核心在於:**發送佇列解耦業務與 I/O**、**動態註冊/取消 OP_WRITE**、**selector.wakeup() 跨執行緒通知**。切勿在無資料時持續註冊 OP_WRITE。

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