Netty的官方文档使用Discard Server Demo作为第一个入门的例子。我跟了一下这段代码后面的Netty框架代码,以了解Netty是怎么玩NIO的。 我看的代码版本是Netty 3.2.7
一些关键步骤如下,
1. Bootstrap.bind()时会最终调用到 Boss的构造方法,在这里会创建selector,并注册OP_ACCEPT
2. 然后进入Boss这个runnable的run方法,在这个方法里它会轮询连接。奇怪的是,即使select()没有找到key, 它也会用accept()方法试图获得连接;而且如果找到key了,它也会将所有key清空。也就是说,它对key根本不感兴趣。
3. 建立连接后,Boss. registerAcceptedChannel()的方法会调用woker.register()方法。在这个方法里,会新建一个worker级别的selector。但这时Netty并不会立即注册worker channel,而是生成一个RegisterTask,置入某个队列里
4. 而NioWorker这个Runnable的run方法已经跑起来了,它会把RegisterTask从队列里取出来,并同步地调用RegisterTask.run()方法(很怪)。在这个方法里,会设置configureBlocking(false), 并暂时把OP_READ作为interest将channel注入到这个worker自己的selector中
5. 同时NioWorker的run()方法里会自已通过selector.select(500)不停地轮询,等待read readiness.默认情况下的NioWorker个数是cpu个数的两倍(对我的机器来说,就是4个),加上Boss,共有5个线程在侦听
6. 客户发送一段数据后,NioWorker会走到processSelectedKeys()这个方法。在这里面的迭代逻辑是:
a) 取一个key,然后立即将其从selectKeys()里移除。
b) 调用read(key)方法,把数据读出来。整个过程是同步的。
i. 读出ByteBuffer
ii. 把buffer包装成Event对象通过fireMessageReceived()方法传给Handler
iii. Handler处理Event
7. 如果连接数超过4个,worker数并不会跟着增长,相反worker会被共用. 也就是说,一个worker负责超过一个连接,这个worker里的selector上登记了超过一个channel. 而总的工作线程数仍等于worker数。
总结
Netty会使用一个Boss线程通过轮询来侦听新连接,一旦有新连接,就把它丢到一个线程数固定的worker线程池里去。池里的worker线程会接收、登记新连接并轮询当前是否有read readiness,如果有,则同步地处理数据,除非你的handler里自己实现了异步处理。
Boss和worker的轮询都是通过selector.select()来实现的,并且都使用了超时参数,对worker来说,使用超时机制可以避免让自己阻塞过长时间,它可以阻塞一小段时间后做点别的事情,这个事情就是接收、登记新连接。
另外还可以看出,Worker既做select(),又亲自处理数据。