簡介
netty為什麼快呢?這是因為netty底層使用了JAVA的NIO技術,並在其基礎上進行了效能的優化,雖然netty不是單純的JAVA nio,但是netty的底層還是基於的是nio技術。
nio是JDK1.4中引入的,用於區別於傳統的IO,所以nio也可以稱之為new io。
nio的三大核心是Selector,channel和Buffer,本文我們將會深入探究NIO和netty之間的關係。
NIO常用用法
在講解netty中的NIO實現之前,我們先來回顧一下JDK中NIO的selector,channel是怎麼工作的。對於NIO來說selector主要用來接受客戶端的連線,所以一般用在server端。我們以一個NIO的伺服器端和客戶端聊天室為例來講解NIO在JDK中是怎麼使用的。
因為是一個簡單的聊天室,我們選擇Socket協議為基礎的ServerSocketChannel,首先就是open這個Server channel:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost", 9527));
serverSocketChannel.configureBlocking(false);
然後向server channel中註冊selector:
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
雖然是NIO,但是對於Selector來說,它的select方法是阻塞方法,只有找到匹配的channel之後才會返回,為了多次進行select操作,我們需要在一個while迴圈裡面進行selector的select操作:
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey selectionKey = iter.next();
if (selectionKey.isAcceptable()) {
register(selector, serverSocketChannel);
}
if (selectionKey.isReadable()) {
serverResponse(byteBuffer, selectionKey);
}
iter.remove();
}
Thread.sleep(1000);
}
selector中會有一些SelectionKey,SelectionKey中有一些表示操作狀態的OP Status,根據這個OP Status的不同,selectionKey可以有四種狀態,分別是isReadable,isWritable,isConnectable和isAcceptable。
當SelectionKey處於isAcceptable狀態的時候,表示ServerSocketChannel可以接受連線了,我們需要呼叫register方法將serverSocketChannel accept生成的socketChannel註冊到selector中,以監聽它的OP READ狀態,後續可以從中讀取資料:
private static void register(Selector selector, ServerSocketChannel serverSocketChannel)
throws IOException {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
當selectionKey處於isReadable狀態的時候,表示可以從socketChannel中讀取資料然後進行處理:
private static void serverResponse(ByteBuffer byteBuffer, SelectionKey selectionKey)
throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
socketChannel.read(byteBuffer);
byteBuffer.flip();
byte[] bytes= new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
log.info(new String(bytes).trim());
if(new String(bytes).trim().equals(BYE_BYE)){
log.info("說再見不如不見!");
socketChannel.write(ByteBuffer.wrap("再見".getBytes()));
socketChannel.close();
}else {
socketChannel.write(ByteBuffer.wrap("你是個好人".getBytes()));
}
byteBuffer.clear();
}
上面的serverResponse方法中,從selectionKey中拿到對應的SocketChannel,然後呼叫SocketChannel的read方法,將channel中的資料讀取到byteBuffer中,要想回復訊息到channel中,還是使用同一個socketChannel,然後呼叫write方法回寫訊息給client端,到這裡一個簡單的回寫客戶端訊息的server端就完成了。
接下來就是對應的NIO客戶端,在NIO客戶端需要使用SocketChannel,首先建立和伺服器的連線:
socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9527));
然後就可以使用這個channel來傳送和接受訊息了:
public String sendMessage(String msg) throws IOException {
byteBuffer = ByteBuffer.wrap(msg.getBytes());
String response = null;
socketChannel.write(byteBuffer);
byteBuffer.clear();
socketChannel.read(byteBuffer);
byteBuffer.flip();
byte[] bytes= new byte[byteBuffer.limit()];
byteBuffer.get(bytes);
response =new String(bytes).trim();
byteBuffer.clear();
return response;
}
向channel中寫入訊息可以使用write方法,從channel中讀取訊息可以使用read方法。
這樣一個NIO的客戶端就完成了。
雖然以上是NIO的server和client的基本使用,但是基本上涵蓋了NIO的所有要點。接下來我們來詳細瞭解一下netty中NIO到底是怎麼使用的。
NIO和EventLoopGroup
以netty的ServerBootstrap為例,啟動的時候需要指定它的group,先來看一下ServerBootstrap的group方法:
public ServerBootstrap group(EventLoopGroup group) {
return group(group, group);
}
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
...
}
ServerBootstrap可以接受一個EventLoopGroup或者兩個EventLoopGroup,EventLoopGroup被用來處理所有的event和IO,對於ServerBootstrap來說,可以有兩個EventLoopGroup,對於Bootstrap來說只有一個EventLoopGroup。兩個EventLoopGroup表示acceptor group和worker group。
EventLoopGroup只是一個介面,我們常用的一個實現就是NioEventLoopGroup,如下所示是一個常用的netty伺服器端程式碼:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new FirstServerHandler());
}
})
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true);
// 繫結埠並開始接收連線
ChannelFuture f = b.bind(port).sync();
// 等待server socket關閉
f.channel().closeFuture().sync();
這裡和NIO相關的有兩個類,分別是NioEventLoopGroup和NioServerSocketChannel,事實上在他們的底層還有兩個類似的類分別叫做NioEventLoop和NioSocketChannel,接下來我們分別講解一些他們的底層實現和邏輯關係。
NioEventLoopGroup
NioEventLoopGroup和DefaultEventLoopGroup一樣都是繼承自MultithreadEventLoopGroup:
public class NioEventLoopGroup extends MultithreadEventLoopGroup
他們的不同之處在於newChild方法的不同,newChild用來構建Group中的實際物件,NioEventLoopGroup來說,newChild返回的是一個NioEventLoop物件,先來看下NioEventLoopGroup的newChild方法:
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
SelectorProvider selectorProvider = (SelectorProvider) args[0];
SelectStrategyFactory selectStrategyFactory = (SelectStrategyFactory) args[1];
RejectedExecutionHandler rejectedExecutionHandler = (RejectedExecutionHandler) args[2];
EventLoopTaskQueueFactory taskQueueFactory = null;
EventLoopTaskQueueFactory tailTaskQueueFactory = null;
int argsLength = args.length;
if (argsLength > 3) {
taskQueueFactory = (EventLoopTaskQueueFactory) args[3];
}
if (argsLength > 4) {
tailTaskQueueFactory = (EventLoopTaskQueueFactory) args[4];
}
return new NioEventLoop(this, executor, selectorProvider,
selectStrategyFactory.newSelectStrategy(),
rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory);
}
這個newChild方法除了固定的executor引數之外,還可以根據NioEventLoopGroup的建構函式傳入的引數來實現更多的功能。
這裡引數中傳入了SelectorProvider、SelectStrategyFactory、RejectedExecutionHandler、taskQueueFactory和tailTaskQueueFactory這幾個引數,其中後面的兩個EventLoopTaskQueueFactory並不是必須的。
最後所有的引數都會傳遞給NioEventLoop的建構函式用來構造出一個新的NioEventLoop。
在詳細講解NioEventLoop之前,我們來研讀一下傳入的這幾個引數型別的實際作用。
SelectorProvider
SelectorProvider是JDK中的類,它提供了一個靜態的provider()方法可以從Property或者ServiceLoader中載入對應的SelectorProvider類並例項化。
另外還提供了openDatagramChannel、openPipe、openSelector、openServerSocketChannel和openSocketChannel等實用的NIO操作方法。
SelectStrategyFactory
SelectStrategyFactory是一個介面,裡面只定義了一個方法,用來返回SelectStrategy:
public interface SelectStrategyFactory {
SelectStrategy newSelectStrategy();
}
什麼是SelectStrategy呢?
先看下SelectStrategy中定義了哪些Strategy:
int SELECT = -1;
int CONTINUE = -2;
int BUSY_WAIT = -3;
SelectStrategy中定義了3個strategy,分別是SELECT、CONTINUE和BUSY_WAIT。
我們知道一般情況下,在NIO中select操作本身是一個阻塞操作,也就是block操作,這個操作對應的strategy是SELECT,也就是select block狀態。
如果我們想跳過這個block,重新進入下一個event loop,那麼對應的strategy就是CONTINUE。
BUSY_WAIT是一個特殊的strategy,是指IO 迴圈輪詢新事件而不阻塞,這個strategy只有在epoll模式下才支援,NIO和Kqueue模式並不支援這個strategy。
RejectedExecutionHandler
RejectedExecutionHandler是netty自己的類,和 java.util.concurrent.RejectedExecutionHandler類似,但是是特別針對SingleThreadEventExecutor來說的。這個介面定義了一個rejected方法,用來表示因為SingleThreadEventExecutor容量限制導致的任務新增失敗而被拒絕的情況:
void rejected(Runnable task, SingleThreadEventExecutor executor);
EventLoopTaskQueueFactory
EventLoopTaskQueueFactory是一個介面,用來建立儲存提交給EventLoop的taskQueue:
Queue<Runnable> newTaskQueue(int maxCapacity);
這個Queue必須是執行緒安全的,並且繼承自java.util.concurrent.BlockingQueue.
講解完這幾個引數,接下來我們就可以詳細檢視NioEventLoop的具體NIO實現了。
NioEventLoop
首先NioEventLoop和DefaultEventLoop一樣,都是繼承自SingleThreadEventLoop:
public final class NioEventLoop extends SingleThreadEventLoop
表示的是使用單一執行緒來執行任務的EventLoop。
首先作為一個NIO的實現,必須要有selector,在NioEventLoop中定義了兩個selector,分別是selector和unwrappedSelector:
private Selector selector;
private Selector unwrappedSelector;
在NioEventLoop的建構函式中,他們是這樣定義的:
final SelectorTuple selectorTuple = openSelector();
this.selector = selectorTuple.selector;
this.unwrappedSelector = selectorTuple.unwrappedSelector;
首先呼叫openSelector方法,然後通過返回的SelectorTuple來獲取對應的selector和unwrappedSelector。
這兩個selector有什麼區別呢?
在openSelector方法中,首先通過呼叫provider的openSelector方法返回一個Selector,這個Selector就是unwrappedSelector:
final Selector unwrappedSelector;
unwrappedSelector = provider.openSelector();
然後檢查DISABLE_KEY_SET_OPTIMIZATION是否設定,如果沒有設定那麼unwrappedSelector和selector實際上是同一個Selector:
DISABLE_KEY_SET_OPTIMIZATION表示的是是否對select key set進行優化:
if (DISABLE_KEY_SET_OPTIMIZATION) {
return new SelectorTuple(unwrappedSelector);
}
SelectorTuple(Selector unwrappedSelector) {
this.unwrappedSelector = unwrappedSelector;
this.selector = unwrappedSelector;
}
如果DISABLE_KEY_SET_OPTIMIZATION被設定為false,那麼意味著我們需要對select key set進行優化,具體是怎麼進行優化的呢?
先來看下最後的返回:
return new SelectorTuple(unwrappedSelector,
new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
最後返回的SelectorTuple第二個引數就是selector,這裡的selector是一個SelectedSelectionKeySetSelector物件。
SelectedSelectionKeySetSelector繼承自selector,建構函式傳入的第一個引數是一個delegate,所有的Selector中定義的方法都是通過呼叫
delegate來實現的,不同的是對於select方法來說,會首先呼叫selectedKeySet的reset方法,下面是以isOpen和select方法為例觀察一下程式碼的實現:
public boolean isOpen() {
return delegate.isOpen();
}
public int select(long timeout) throws IOException {
selectionKeys.reset();
return delegate.select(timeout);
}
selectedKeySet是一個SelectedSelectionKeySet物件,是一個set集合,用來儲存SelectionKey,在openSelector()方法中,使用new來例項化這個物件:
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
netty實際是想用這個SelectedSelectionKeySet類來管理Selector中的selectedKeys,所以接下來netty用了一個高技巧性的物件替換操作。
首先判斷系統中有沒有sun.nio.ch.SelectorImpl的實現:
Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
return Class.forName(
"sun.nio.ch.SelectorImpl",
false,
PlatformDependent.getSystemClassLoader());
} catch (Throwable cause) {
return cause;
}
}
});
SelectorImpl中有兩個Set欄位:
private Set<SelectionKey> publicKeys;
private Set<SelectionKey> publicSelectedKeys;
這兩個欄位就是我們需要替換的物件。如果有SelectorImpl的話,首先使用Unsafe類,呼叫PlatformDependent中的objectFieldOffset方法拿到這兩個欄位相對於物件示例的偏移量,然後呼叫putObject將這兩個欄位替換成為前面初始化的selectedKeySet物件:
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
if (PlatformDependent.javaVersion() >= 9 && PlatformDependent.hasUnsafe()) {
// Let us try to use sun.misc.Unsafe to replace the SelectionKeySet.
// This allows us to also do this in Java9+ without any extra flags.
long selectedKeysFieldOffset = PlatformDependent.objectFieldOffset(selectedKeysField);
long publicSelectedKeysFieldOffset =
PlatformDependent.objectFieldOffset(publicSelectedKeysField);
if (selectedKeysFieldOffset != -1 && publicSelectedKeysFieldOffset != -1) {
PlatformDependent.putObject(
unwrappedSelector, selectedKeysFieldOffset, selectedKeySet);
PlatformDependent.putObject(
unwrappedSelector, publicSelectedKeysFieldOffset, selectedKeySet);
return null;
}
如果系統設定不支援Unsafe,那麼就用反射再做一次:
Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
if (cause != null) {
return cause;
}
cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
if (cause != null) {
return cause;
}
selectedKeysField.set(unwrappedSelector, selectedKeySet);
publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
在NioEventLoop中我們需要關注的一個非常重要的重寫方法就是run方法,在run方法中實現瞭如何執行task的邏輯。
還記得前面我們提到的selectStrategy嗎?run方法通過呼叫selectStrategy.calculateStrategy返回了select的strategy,然後通過判斷
strategy的值來進行對應的處理。
如果strategy是CONTINUE,這跳過這次迴圈,進入到下一個loop中。
BUSY_WAIT在NIO中是不支援的,如果是SELECT狀態,那麼會在curDeadlineNanos之後再次進行select操作:
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
// fall-through to SELECT since the busy-wait is not supported with NIO
case SelectStrategy.SELECT:
long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
if (curDeadlineNanos == -1L) {
curDeadlineNanos = NONE; // nothing on the calendar
}
nextWakeupNanos.set(curDeadlineNanos);
try {
if (!hasTasks()) {
strategy = select(curDeadlineNanos);
}
} finally {
// This update is just to help block unnecessary selector wakeups
// so use of lazySet is ok (no race condition)
nextWakeupNanos.lazySet(AWAKE);
}
// fall through
default:
如果strategy > 0,表示有拿到了SelectedKeys,那麼需要呼叫processSelectedKeys方法對SelectedKeys進行處理:
private void processSelectedKeys() {
if (selectedKeys != null) {
processSelectedKeysOptimized();
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}
上面提到了NioEventLoop中有兩個selector,還有一個selectedKeys屬性,這個selectedKeys儲存的就是Optimized SelectedKeys,如果這個值不為空,就呼叫processSelectedKeysOptimized方法,否則就呼叫processSelectedKeysPlain方法。
processSelectedKeysOptimized和processSelectedKeysPlain這兩個方法差別不大,只是傳入的要處理的selectedKeys不同。
處理的邏輯是首先拿到selectedKeys的key,然後呼叫它的attachment方法拿到attach的物件:
final SelectionKey k = selectedKeys.keys[i];
selectedKeys.keys[i] = null;
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
如果channel還沒有建立連線,那麼這個物件可能是一個NioTask,用來處理channelReady和channelUnregistered的事件。
如果channel已經建立好連線了,那麼這個物件可能是一個AbstractNioChannel。
針對兩種不同的物件,會去分別呼叫不同的processSelectedKey方法。
對第一種情況,會呼叫task的channelReady方法:
task.channelReady(k.channel(), k);
對第二種情況,會根據SelectionKey的readyOps()的各種狀態呼叫ch.unsafe()中的各種方法,去進行read或者close等操作。
總結
NioEventLoop雖然也是一個SingleThreadEventLoop,但是通過使用NIO技術,可以更好的利用現有資源實現更好的效率,這也就是為什麼我們在專案中使用NioEventLoopGroup而不是DefaultEventLoopGroup的原因。
本文已收錄於 http://www.flydean.com/05-2-netty-nioeventloop/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!