使用 Java NIO (`Selector`、`SelectionKey`) 實現雙向通訊。伺服器端在 `key.isWritable()` 中處理寫入,但業務資料產生時間隨機,無法像回應請求那樣即時寫入。
NIO `Selector` 機制是**就緒通知**:`isWritable()` 表示「通道緩衝區有空間可寫」,**不代表「有資料要發送」**。若在無資料時註冊 `OP_WRITE`,會導致 `Selector` 不斷觸發 `isWritable()`,CPU 佔用 100%。
// 連結附件類
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**,已完美封裝上述模式(`ChannelOutboundBuffer`、`writeAndFlush`、`EventLoop`),避免重造輪子踩坑。
NIO 伺服器主動推送核心在於:**發送佇列解耦業務與 I/O**、**動態註冊/取消 OP_WRITE**、**selector.wakeup() 跨執行緒通知**。切勿在無資料時持續註冊 OP_WRITE。