JDK 原生 NIO 程序的问题
1)NIO 的类库和 API 繁杂,使用麻烦:你需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
2)需要具备其他的额外技能做铺垫:例如熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的 NIO 程序。
3)可靠性能力补齐,开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大。
4)JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。官方声称在 JDK 1.6 版本的 update 18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 Bug 发生概率降低了一些而已,它并没有被根本解决。
事件驱动模型
![](https://i-blog.csdnimg.cn/blog_migrate/e9180ce2a6ea28d1ce3f16f0abb7f45e.jpeg)
- 主要包括 4 个基本组件:
- 1)事件队列(event queue):接收事件的入口,存储待处理事件;
- 2)分发器(event mediator):将不同的事件分发到不同的业务逻辑单元;
- 3)事件通道(event channel):分发器与处理器之间的联系渠道;
- 4)事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作。
Reactor 线程模型
![](https://i-blog.csdnimg.cn/blog_migrate/3bdbec3637431193606f57d531a53920.jpeg)
- Reactor 模型中有 2 个关键组成:
- 1)Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。
- 2)Handlers:处理程序执行 I/O 事件要完成的实际操作,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。
Netty高性能的原理
netty的高性能体现在I/O 模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据。
-
Netty I/O 模型
基于NIO,Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端连接。 -
Netty线程模型
Netty 主要基于主从 Reactors 多线程模型。
1)MainReactor 负责客户端的连接请求,并将请求转交给 SubReactor;
2)SubReactor 负责相应通道的 IO 读写请求;
3)非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理。特 别 说 明 的 是 : 虽 然 N e t t y 的 线 程 模 型 基 于 主 从 R e a c t o r 多 线 程 , 借 用 了 M a i n R e a c t o r 和 S u b R e a c t o r 的 结 构 。 但 实 际 实 现 上 S u b R e a c t o r 和 W o r k e r 线 程 在 同 一 个 线 程 池 中 , 如 下 代 码 : \color{red}{特别说明的是:虽然 Netty 的线程模型基于主从 Reactor 多线程,借用了 MainReactor 和 SubReactor 的结构。但实际实现上 SubReactor 和 Worker 线程在同一个线程池中,如下代码:} 特别说明的是:虽然Netty的线程模型基于主从Reactor多线程,借用了MainReactor和SubReactor的结构。但实际实现上SubReactor和Worker线程在同一个线程池中,如下代码:
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)上面代码中的 bossGroup 和 workerGroup 是 Bootstrap 构造方法中传入的两个对象,这两个 group 均是线程池:
1)bossGroup 线程池则只是在 Bind 某个端口后,获得其中一个线程作为 MainReactor,专门处理端口的 Accept 事件,每个端口对应一个 Boss 线程;
2)workerGroup 线程池会被各个 SubReactor 和 Worker 线程充分利用。
netty中的异步处理
-
Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture。
-
当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作。
常见有如下操作:
1)通过 isDone 方法来判断当前操作是否完成;
2)通过 isSuccess 方法来判断已完成的当前操作是否成功;
3)通过 getCause 方法来获取已完成的当前操作失败的原因;
4)通过 isCancelled 方法来判断已完成的当前操作是否被取消;
5)通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器;如果 Future 对象已完成,则立即通知指定的监听器。 -
代码示例
Netty框架的工作原理
![](https://i-blog.csdnimg.cn/blog_migrate/915d9aea0a1cb9f127692c989f2907c7.jpeg)
-
NioEventLoopGroup 相当于 1 个事件循环组,这个组里包含多个事件循环 NioEventLoop,每个 NioEventLoop 包含 1 个 Selector 和 1 个事件循环线程。
每个 Boss NioEventLoop 循环执行的任务包含 3 步:1)轮询 Accept 事件;
2)处理 Accept I/O 事件,与 Client 建立连接,生成 NioSocketChannel,并将 NioSocketChannel 注册到某个 Worker NioEventLoop 的 Selector 上;
3)处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用 eventloop.execute 或 schedule 执行的任务,或者其他线程提交到该 eventloop 的任务。 -
每个 Worker NioEventLoop 循环执行的任务包含 3 步:
1)轮询 Read、Write 事件;
2)处理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可读、可写事件发生时进行处理;
3)处理任务队列中的任务,runAllTasks。-
其中任务队列中的 Task 有 3 种典型使用场景:
① 用户程序自定义的普通任务:
② 非当前 Reactor 线程调用 Channel 的各种方法:
例如在推送系统的业务线程里面,根据用户的标识,找到对应的 Channel 引用,然后调用 Write 类方法向该用户推送消息,就会进入到这种场景。最终的 Write 会提交到任务队列中后被异步消费。③ 用户自定义定时任务:
-
Netty的核心组件
NioEventLoopGroup
-
主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。
-
NioEventLoopGroup 相当于 1 个事件循环组,这个组里包含多个事件循环 NioEventLoop,每个 NioEventLoop 包含 1 个 Selector 和 1 个事件循环线程。
-
职责:
(1). 作为服务端 Acceptor 线程,负责处理客户端的请求接入。
(2). 作为客户端 Connector 线程,负责注册监听连接操作位,用于判断异步连接结果。
(3). 作为IO 线程,监听网络读操作位,负责从 SocketChannel 中读取报文。
(4). 作为 IO 线程,负责向 SocketChannel 写入报文发送给对方,如果发生写半包,会自动注册监听写事件,用于后续继续发送半包数据,直到数据全部发送完成。
(5). 作为定时任务线程,可以执行定时任务,例如链路空闲检测和发送心跳消息等。
(6). 作为线程执行器可以执行普通的任务线程(Runnable)。
NioEventLoop
-
NioEventLoop 中维护了一个线程和任务队列 taskQueue,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务
(1) I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。
(2) 非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。 -
每个NioEventLoop都绑定了一个Selector,所以在Netty的线程模型中,是由多个Selector在监听IO就绪事件。而Channel注册到Selector。
- 一个Channel绑定一个NioEventLoop,相当于一个连接绑定一个线程,这个连接所有的ChannelHandler都是在一个线程中执行的,避免了多线程干扰。更重要的是ChannelPipline链表必须严格按照顺序执行。单线程的设计能够保证ChannelHandler的顺序执行。
- 一个NioEventLoop的selector可以被多个Channel注册,也就是说多个Channel共享一个EventLoop。EventLoop的Selecctor对这些Channel进行监听。
-
注意, 因为 EventLoop 既需要执行 IO 操作, 又需要执行 task, 因此我们在调用 EventLoop.execute 方法提交任务时, 不要提交耗时任务, 更不能提交一些会造成阻塞的任务, 不然会导致我们的 IO 线程得不到调度, 影响整个程序的并发量。
解决方法: Netty本身提供了EventExecutorGroup作为业务操作线程池,使用示例如下:
问题又来了,前面说过Netty是采用串行化设计理念处理连接上的IO操作的,这样做可以避免线程竞争,线程上下文切换带来的额外性能损耗。那如果NIO线程与业务线程同时往channel里写数据,这个不是破坏了Netty的串行化理念。刚开始我也这么想,可后来看到Netty底层的write方法才发现并不存在这个问题。
这里会判断当前EventExecutor是否是NIO线程,如果是则执行Write操作,否则仅仅是创建一个异步Task,执行该异步Task,该异步Task会到该连接对应的NIO线程里去执行,最终保证还是一个NIO线程串行化地处理该连接上的IO操作。
当然如果不想用EventExecutorGroup,也可以像最下面的示例一样,将长时间的任务丢进自己的业务线程池里处理。
ChannelHandler
ChannelPipeline不是单独存在,它肯定会和Channel、ChannelHandler、ChannelHandlerContext关联在一起,所以有关概念这里一起讲。另外接下来讲述的几个概念可以让你在无形中搞清楚 C h a n n e l H a n d l e r 单 线 程 顺 序 执 行 的 原 理 \color{red}{ChannelHandler单线程顺序执行的原理} ChannelHandler单线程顺序执行的原理
-
概念
ChannelHandler下主要是两个子接口- ChannelInboundHandler(入站): 处理输入数据和Channel状态类型改变。
适配器: ChannelInboundHandlerAdapter(适配器设计模式)
常用的: SimpleChannelInboundHandler - ChannelOutboundHandler(出站): 处理输出数据
适配器: ChannelOutboundHandlerAdapter
每一个Handler都一定会处理出站或者入站(可能两者都处理数据),例如对于入站的Handler可能会继承SimpleChannelInboundHandler或者ChannelInboundHandlerAdapter,而SimpleChannelInboundHandler又是继承于ChannelInboundHandlerAdapter,最大的区别在于SimpleChannelInboundHandler会对没有外界引用的资源进行一定的清理,并且入站的消息可以通过泛型来规定。
- ChannelInboundHandler(入站): 处理输入数据和Channel状态类型改变。
-
Channel 生命周期(执行顺序也是从上倒下)
(1)channelRegistered: channel注册到一个EventLoop。
(2)channelActive: 变为活跃状态(连接到了远程主机),可以接受和发送数据
(3)channelInactive: channel处于非活跃状态,没有连接到远程主机
(4)channelUnregistered: channel已经创建,但是未注册到一个EventLoop里面,也就是没有和Selector绑定 -
ChannelHandler 生命周期
handlerAdded: 当 ChannelHandler 添加到 ChannelPipeline 中调用
handlerRemoved: 当 ChannelHandler 从 ChannelPipeline 移除时调用
exceptionCaught: 当 ChannelPipeline 执行抛出异常时调用
ChannelPipeline
-
概念
ChannelPipeline类是ChannelHandler实例对象的链表,用于处理或截获通道的接收和发送数据。它提供了一种高级的截取过滤模式(类似serverlet中的filter功能),让用户可以在ChannelPipeline中完全控制一个事件以及如何处理ChannelHandler与ChannelPipeline的交互。
对于每个新的通道Channel,都会创建一个新的ChannelPipeline,并将其pipeline附加到channel中。下图描述ChannelHandler与pipeline中的关系,一个io操作可以由一个ChannelInboundHandler或ChannelOutboundHandle进行处理,并通过调用ChannelInboundHandler处理入站io或通过ChannelOutboundHandler处理出站IO。
-
常用方法
addFirst(...) //添加ChannelHandler在ChannelPipeline的第一个位置 addBefore(...) //在ChannelPipeline中指定的ChannelHandler名称之前添加ChannelHandler addAfter(...) //在ChannelPipeline中指定的ChannelHandler名称之后添加ChannelHandler addLast(...) //在ChannelPipeline的末尾添加ChannelHandler remove(...) //删除ChannelPipeline中指定的ChannelHandler replace(...) //替换ChannelPipeline中指定的ChannelHandler
ChannelPipeline可以动态添加、删除、替换其中的ChannelHandler,这样的机制可以提高灵活性。示例:
1. ChannelPipeline pipeline = ch.pipeline(); 2. FirstHandler firstHandler = new FirstHandler(); 3. pipeline.addLast("handler1", firstHandler); 4. pipeline.addFirst("handler2", new SecondHandler()); 5. pipeline.addLast("handler3", new ThirdHandler()); 6. pipeline.remove("“handler3“"); 7. pipeline.remove(firstHandler); 8. pipeline.replace("handler2", "handler4", new FourthHandler());
-
入站出站Handler执行顺序
一般的项目中,inboundHandler和outboundHandler有多个,在Pipeline中的执行顺序?
重点记住: InboundHandler顺序执行,OutboundHandler逆序执行。问题: 下面的handel的执行顺序?
ch.pipeline().addLast(new InboundHandler1()); ch.pipeline().addLast(new OutboundHandler1()); ch.pipeline().addLast(new OutboundHandler2()); ch.pipeline().addLast(new InboundHandler2()); 或者: ch.pipeline().addLast(new OutboundHandler1()); ch.pipeline().addLast(new OutboundHandler2()); ch.pipeline().addLast(new InboundHandler1()); ch.pipeline().addLast(new InboundHandler2());
其实上面的执行顺序都是一样的:
InboundHandler1--> InboundHandler2 -->OutboundHandler2 -->OutboundHandler1
结论
1)InboundHandler顺序执行,OutboundHandler逆序执行
2)InboundHandler之间传递数据,通过ctx.fireChannelRead(msg)
3)InboundHandler通过ctx.write(msg),则会传递到outboundHandler
4) 使用ctx.write(msg)传递消息,Inbound需要放在结尾,在Outbound之后,不然outboundhandler会不执行;但是使用channel.write(msg)、pipline.write(msg)情况会不一致,都会执行,那是因为channel和pipline会贯穿整个流。
5) outBound和Inbound谁先执行,针对客户端和服务端而言,客户端是发起请求再接受数据,先outbound再inbound,服务端则相反。
ChannelHandlerContext
-
ChannelPipeline并不是直接管理ChannelHandler,而是通过ChannelHandlerContext来间接管理,这一点通过ChannelPipeline的默认实现DefaultChannelPipeline可以看出来。
-
DefaultChannelHandlerContext和DefaultChannelPipeline是ChannelHandlerContext和ChannelPipeline的默认实现,在DefaultPipeline内部DefaultChannelHandlerContext组成了一个双向链表。 我们看下DefaultChannelPipeline的构造函数:
/**
* 可以看到,DefaultChinnelPipeline 内部使用了两个特殊的Hander 来表示Handel链的头和尾。
*/
public DefaultChannelPipeline(AbstractChannel channel) {
if (channel == null) {
throw new NullPointerException("channel");
}
this.channel = channel;
TailHandler tailHandler = new TailHandler();
tail = new DefaultChannelHandlerContext(this, null, generateName(tailHandler), tailHandler);
HeadHandler headHandler = new HeadHandler(channel.unsafe());
head = new DefaultChannelHandlerContext(this, null, generateName(headHandler), headHandler);
head.next = tail;
tail.prev = head;
}
- 所以对于DefaultChinnelPipeline它的Handler头部和尾部的Handler是固定的,我们所添加的Handler是添加在这个头和尾之前的Handler。(下面这个图更加清晰)
pipeline中组件的关系
-
先大致说下什么是Channel
通常来说, 所有的 NIO 的 I/O 操作都是从 Channel 开始的. 一个 channel 类似于一个 stream。在Netty中,Channel是客户端和服务端建立的一个连接通道。
虽然java Stream 和 NIO Channel都是负责I/O操作,但他们还是有许多区别的:
1)我们可以在同一个 Channel 中执行读和写操作, 然而同一个 Stream 仅仅支持读或写。
2)Channel 可以异步地读写, 而 Stream 是阻塞的同步读写。
3)Channel 总是从 Buffer 中读取数据, 或将数据写入到 Buffer 中。几者的关系图如下:
-
总结
一个Channel包含一个ChannelPipeline,创建Channel时会自动创建一个ChannelPipeline,每个Channel都有一个管理它的pipeline,这关联是永久性的。
每一个ChannelPipeline中可以包含多个ChannelHandler。所有ChannelHandler都会顺序加入到ChannelPipeline中,ChannelHandler实例与ChannelPipeline之间的桥梁是ChannelHandlerContext实例。
pipeline的整个传播流程
为了搞清楚事件如何在Pipeline里传播,让我们从Channel的抽象子类AbstractChannel开始,下面是AbstractChannel#write()方法的实现:
public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
// ...
@Override
public Channel write(Object msg) {
return pipeline.write(msg);
}
// ...
}
AbstractChannel直接调用了Pipeline的write()方法:
再看DefaultChannelPipeline的write()方法实现:
final class DefaultChannelPipeline implements ChannelPipeline {
// ...
@Override
public ChannelFuture write(Object msg) {
return tail.write(msg);
}
// ...
}
因为write是个outbound事件,所以DefaultChannelPipeline直接找到tail部分的context,调用其write()方法:
接着看DefaultChannelHandlerContext的write()方法
final class DefaultChannelHandlerContext extends DefaultAttributeMap implements ChannelHandlerContext {
// ...
@Override
public ChannelFuture write(Object msg) {
return write(msg, newPromise());
}
@Override
public ChannelFuture write(final Object msg, final ChannelPromise promise) {
if (msg == null) {
throw new NullPointerException("msg");
}
validatePromise(promise, true);
write(msg, false, promise);
return promise;
}
private void write(Object msg, boolean flush, ChannelPromise promise) {
DefaultChannelHandlerContext next = findContextOutbound();
next.invokeWrite(msg, promise);
if (flush) {
next.invokeFlush();
}
}
private DefaultChannelHandlerContext findContextOutbound() {
DefaultChannelHandlerContext ctx = this;
do {
ctx = ctx.prev;
} while (!ctx.outbound);
return ctx;
}
private void invokeWrite(Object msg, ChannelPromise promise) {
try {
((ChannelOutboundHandler) handler).write(this, msg, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
}
// ...
}
context的write()方法沿着context链往前找,直至找到一个outbound类型的context为止,然后调用其invokeWrite()方法
invokeWrite()接着调用handler的write()方法
最后看看ChannelOutboundHandlerAdapter的write()方法实现:
public class ChannelOutboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelOutboundHandler {
// ...
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
// ...
}
默认的实现调用了context的write()方法而不做任何处理,这样write事件就沿着outbound链继续传播:
![](https://i-blog.csdnimg.cn/blog_migrate/dbed1dcbaa1a64387691de4976b77984.png)
可见,Pipeline的事件传播,是靠Pipeline、Context和Handler共同协作完成的。
Server端示例代码
public static void main(String[] args) {
// 创建mainReactor
NioEventLoopGroup boosGroup = newNioEventLoopGroup();
// 创建工作线程组
NioEventLoopGroup workerGroup = newNioEventLoopGroup();
finalServerBootstrap serverBootstrap = newServerBootstrap();
.group(boosGroup, workerGroup) // 组装NioEventLoopGroup
.channel(NioServerSocketChannel.class) // 设置channel类型为NIO类型
.option(ChannelOption.SO_BACKLOG, 1024) // 设置连接配置参数
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childHandler(newChannelInitializer<NioSocketChannel>() { // 配置入站、出站事件handler
@Override
protectedvoidinitChannel(NioSocketChannel ch) {
ch.pipeline().addLast(...); // 配置入站、出站事件channel
ch.pipeline().addLast(...);
}
});
int port = 8080; // 绑定端口
serverBootstrap.bind(port).addListener(future -> {
if(future.isSuccess()) {
System.out.println(newDate() + ": 端口["+ port + "]绑定成功!");
} else{
System.err.println("端口["+ port + "]绑定失败!");
}
});
}
TCP 粘包/拆包的原因及解决方法
-
TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。
-
TCP粘包/分包的原因:
- 应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象。而应用程序写入数据小于套接字缓冲区大小,网卡将应用程序多次写入的数据发送到网络上,这将会发生粘包现象;
- 进行MSS大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包
- 以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片。
-
解决方法
- 消息定长:FixedLengthFrameDecoder类
- 包尾增加特殊字符分割:行分隔符类:LineBasedFrameDecoder或自定义分隔符类 :DelimiterBasedFrameDecoder
- 将消息分为消息头和消息体:LengthFieldBasedFrameDecoder类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。