netty 高併發物聯網交流羣 651219170
Netty 核心代碼分析
背景
都說是 netty 的 write() 在非 EventLoop 線程調用時線程安全的。但是沒看過源碼,總是覺得不安心。可能之前自己在寫 Reactor 的時候也總是在考慮怎麼解決 Reactor 線程 和普通線程 write() 怎麼解決競爭問題。所以抱着學習的態度學習了下 Netty 的 EventLoop 對各種事件的處理過程(包括了我一直糾結的 write()。
準備知識
java nio
這不是 java nio 的教程,所以我們只是描述下主要流程(不僅僅是 java 語言,其他的編程語言實現多路複用基本上步驟是一樣的)。
1.初始化一個多路複用器
selector = Selector.open()
2.註冊感興趣的 channel 事件。
channel.register(selector, SelectionKey.OP_READ| SelectionKey.OP_WRITE);
3.等待事件發生並處理
while(true){
if(selector.select(TIMEOUT) == 0){
System.out.println("==");
continue;
}
//遍歷處理所有的事件
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
//準備好接受新鏈接
if(key.isAcceptable()){
handleAccept(key);
}
//處理可讀事件的請求
if(key.isReadable()){
handleRead(key);
}
//處理可寫事件的請求
if(key.isWritable() && key.isValid()){
handleWrite(key);
}
iter.remove();
}
}
EventLoop 的類圖
先看下 NioEventLoop 的類圖。通過這個類圖你只需要記住一點就是 NioEventLoop 是單線程,有序處理任務的線程池就好了。
NioEventLoop 核心代碼
SingleThreadEventExecutor 是一個線程池,所以 excute() 會執行。
excute() 執行最終會調用子類 NioEventLoop 的 run() 方法。
NioEventLoop 核心部分 run()。
//ioRate 是指此線程執行 io 操作佔時百分比,默認是 50
protected void run() {
for (;;) {
try {
//沒有任務,繼續 select() 等待滿足喚醒條件的操作(wakeup,事件發生),有任務執行 selectNow() 返回準備好事件的key的個數
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
//繼續 select()
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
// fall through
default:
}
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
//ioRatio 爲 100 的時候就執行完 io 事件之後執行所有的 task
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
//處理有事件發生的 key
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
//計算出執行普通任務佔的百分比
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
// Always handle shutdown even if the loop processing threw an exception.
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
核心部分爲 processSelectedKeys(); 處理有事件的所有的 key 的processSelectedKeys(); -> processSelectedKeysPlain()
循環處理所有有事件發生的 key
Iterator<SelectionKey> i = selectedKeys.iterator();
for (;;) {
final SelectionKey k = i.next();
//
final Object a = k.attachment();
i.remove();
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
if (!i.hasNext()) {
break;
}
}
接着看 processSelectedKey() 處理一個 key 的事件。
下面分別有對應的 accept write read 等事件的處理
try {
int readyOps = k.readyOps();
// We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
// the NIO JDK channel implementation may throw a NotYetConnectedException.
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
// Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
// to a spin loop
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
以上就已經基本清楚了, netty 對 io 的操作 nio Selector 編程的幾個步驟都是在 EventLoop 這個單線程中完成的。
好了,至於 netty 爲什麼在其他線程write() 是線程安全的。是怎麼實現的,在下節在討論。