Netty作为一个Java生态中的网络组件有着举足轻重的位置,各种开源中间件都使用Netty进行网络通信,比如Dubbo、RocketMQ。可以说Netty是对Java NIO的封装,比如ByteBuf、channel等的封装让网络编程更简单。
在介绍Netty服务器启动之前需要简单了解两件事:
reactor线程模型
linux中的IO多路复用
reactor线程模型关于reactor线程模型请参考这篇文章,通过不同的配置Netty可以实现对应的三种reactor线程模型
reactor单线程模型
reactor多线程模型
reactor主从多线程模型
// reactor单线程模型,accept、connect、read、write都在一个线程中执行 EventLoopGroup group = new NioEventLoopGroup(1); bootStrap.group(group); // reactor多线程,accept在bossGroup中的一个线程执行,IO操作在workerGroup中的线程执行 EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); bootStrap.group(bossGroup , workerGroup); // reactor主从多线程,用来accept连接的是在一个线程池中执行,这个时候需要bind多个port,因为Netty一个bind的port会启动一个线程来accept EventLoopGroup bossGroup = new NioEventLoopGroup(2); EventLoopGroup workerGroup = new NioEventLoopGroup(); bootStrap.group(bossGroup , workerGroup);注意:本文后面的介绍如无特别说明都是基于reactor多线程模型
linux中的IO多路复用linux中的网络编程模型也是在不断演变的,下面是依次演变的顺序(具体可参考《UNIX网络编程卷1:套接字联网API》第三版的第4、6章)
accept阻塞等待连接,接收到新的连接后新起线程来处理接收到的连接,然后在新的线程中阻塞等待新的数据到来
select根据入参的不同有三种情况
永远等下去,直到监听的描述符有任意的IO事件才返回
等待一段固定时间,如果时间到之前有IO事件则提前返回,否则等待超时后返回
不等待,检查描述符后立即返回,称为轮询
select会返回就绪的文件描述符的个数,需要轮询所有socket,判断每个socket的状态来确定是否有事件、是什么事件
poll相比较于selectpoll是阻塞等待的,只有有读写事件的时候才会返回,返回的是有读写事件的socket个数,并且将对应的socket的事件置位,自己从所有socket中找到具体的socket
epoll相比较于poll,epoll可以将只有确实有IO事件的描述符返回,大并发下只有少量活跃连接的情况下使用
较poll的优势
不用开发者重新准备文件描述符集合(较poll入参简单)
无需遍历所有监听的描述符,只要遍历哪些被内核IO事件异步唤醒而加入ready队列的描述符集合
Java NIO在linux的实现就是基于epoll的。epoll的编程模型:
创建socket,socket方法
绑定服务器ip,port,bind方法
监听绑定了ip:port的文件描述符,listen方法
创建epoll句柄(文件描述符),配置最大监听的文件描述符个数,epoll_create方法
配置epoll监听的文件描述符的事件:注册、修改、删除某个文件描述符对应的事件
监听所有已配置的描述符,epoll_wait
有新的事件的时候遍历返回的描述符,处理对应的事件
如果是来自客户端的连接,则将accept到的文件描述符注册到epoll中
如果是读写事件则分别处理
注意:Netty封装的Java NIO是跨平台的,后面还是以linux平台为例来介绍
接下来言归正传,来看看Netty的服务器启动过程做了什么事情。Netty作为一个网络框架,和普通网络编程做的事情基本上一样,对应于上面epoll的编程模型,Netty的启动过程为
初始化线程池,初始化selector
初始化NioServerSocketChannel
绑定服务器ip:port
将NioServerSocketChannel注册到selector中
配置NioServerSocketChannel监听的事件
使用selector.select等待新的IO事件
如果是来自客户端的连接则将NioSocketChannel注册到selector上(如果是新的线程则是新的selector)
如果是普通IO事件则在worker线程中处理
线程池初始化在介绍NioEventLoopGroup之前先看下NioEventLoop
可以看到NioEventLoop继承自SingleThreadEventExecutor,是一个单线程的executor,在线程中死循环监听IO事件。主要方法有
// 初始化selector io.netty.channel.nio.NioEventLoop#openSelector // 将channel注册到selector io.netty.channel.nio.NioEventLoop#register // 监听selector上的事件 io.netty.channel.nio.NioEventLoop#select一个NioEventLoop会初始化一个selector,处理selector上注册的channel。
NioEventLoopGroup从名字上就可以看出来是由多个NioEventLoop组成,类关系图如下