原生NIO存在的问题
- NIO的类库和API繁杂,使用恶心;
- 需要周边能力:要熟悉JAVA多线程编程,因为NIO编辑涉及到Reactor模式;
- 工作复杂:包含客户端面临的断连、网络闪断、半包、黏包、自定义协议等;
- JDK NIO 的BUG:Epoll 空轮等。
Netty的优点:
- 使用 JDK 自带的NIO需要了解太多的概念,编程复杂,一不小心 bug 横飞
- Netty 底层 IO 模型随意切换,而这一切只需要做微小的改动,改改参数,Netty可以直接从 NIO 模型变身为 IO 模型
- Netty 自带的拆包解包,异常检测等机制让你从NIO的繁重细节中脱离出来,让你只需要关心业务逻辑
- Netty 解决了 JDK 的很多包括空轮询在内的 Bug
- Netty 底层对线程,selector 做了很多细小的优化,精心设计的 reactor 线程模型做到非常高效的并发处理
- 自带各种协议栈让你处理任何一种通用协议都几乎不用亲自动手
- Netty 社区活跃,遇到问题随时邮件列表或者 issue
- Netty 已经历各大 RPC 框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大
一) Netty的线程模型
1)单Reactor多线程
执行过程:
- Reactor 对象通过Select监控客户端请求时间,通过dispatch进行分发;
- 如果建立连接请求,则左Acceptor通过accept处理连接请求,然后创建一个Handler对象处理完成连接后的各种事情。
- 如果不是连接请求,则由reactor分发调用连接对应的handler。
- hander只负责响应事件,不去做具体的业务处理,通过read读取数据后,会委派worker线程池进行某个业务处理。
- worker线程池会分配独立线程完成真正的任务,并将返回结果告知handler。
- handler收到线程池worker后,通过send告知返回结果给client。
优点:利用多核cpu能力
缺点:多线程共享和访问比较复杂,reactor承担所有的事件的监听和响应,在单线程运行,容易形成瓶颈。
2)主(MainReactor)从(SubReactor)Reactor多线程
执行过程:
- Reactor 主线程MainReactor对象通过select监听连接事件,收到事件后,通过Acceptor处理连接事件;
- 当Acceptor处理连接事件后,MainReactor将连接分配SubReactor;
- SubReactor将连接加入到连接队列进行监听,并创建hander进行各种事件处理(非OP);
- 当有新的事件发生时,SubReactor就会调用对应的handler进行处理;
- handler通过read读取数据,分发个后面的worker线程处理;
- worker线程池会分配独立的worker线程进行业务处理,并返回结果;
- handler收到响应的结果后,在通过send将结果返回给Client;
优点:父线程与子线程的数据交互简单职责明确,父线程只需接收新连接,子线程完成后续的操作;
优点:Reactor主线程只需要把新连接传给子线程,子线程无需再返回给主线程。
缺点:编程复杂度高
应用实例:Nginx主从Reactor多线程模型、Memcached主从多线程模型,同时Netty也对模型支持。
3)Netty线程模型
-1:概览
- Boss线程维护Selector,并只关注Accept事件;
- 当接收到Acceptor事件后,获取到对应的socketChannel,并进一步封装成NIOSocketChannel注册到Worker线程(事件循环),并进行维护;
- 当Worker线程监听到Selector中的通道发生关注的时间后,将任务派发至对应的handler
-2:综述
Netty主要基于主从Reactor多线程模型做一定更改,其中主从Reactor多线程模型有多个Reactor;
- Netty是抽象出两组线程池的,一组为BossGroup用于接收客户端的连接,WorkerGroup专门负责网络的读写。
- BossGroup和WorkerGroup类型都是NioEventLoopGroup。
- NioEventLoopGroup相当于事件循环组,这个组中含有很多事件循环,每一个事件循环是NioEventLoop
- NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个Selector,用于监听绑定在其上的socket的网络通讯。
- NioEventLoopGroup可以有多个线程,既可以含有多个NioEventLoop
- 每个Boss NioEventLoop 执行的步骤有3步骤:
1:轮询Acceot事件
2:处理Accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某个worker
NIOEventLoop上的Selector
3:处理任务队列的任务,既runAllTasks
- 每个Worker NIOEventLoop循环执行的步骤:
1:轮询read、write事件
2:处理i/o事件,既read、write事件,在NioSocketChannel进行处理
3:处理任务队列的任务,既runAllTasks
TaskQueue
- 用户定义的普通任务
- 用户定义的定时任务
- 非当前Reactor线程调用Channel的各种方法
例如:在推送系统的业务线程里面,根据用户的标识,找到对应的Channel引用,然后调用Write类方法向该用户推送消息,就会进入到这种场景。最终的Write会提交到任务队列中后被异步消费。
//如果异步发送,则channelRead不会等待该消息,因为任务加在了scheduleTask中。并且会触发channelReadComplete ,eventLoop().schedule()同样
ctx.channel().eventLoop().execute(() -> {
try {
Thread.sleep(1000 * 10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ctx.writeAndFlush(Unpooled.copiedBuffer("hello , 客户端\r\n", CharsetUtil.UTF_8));
System.out.println("go on");
});