BIO、NIO、AIO与Linnux下的IO模型 概览
Netty 是一个 异步、基于事件驱动 的网络应用框架,用作快速开发高性能、高可用性的网络 I/O 应用。它的出现,是为了解决源生 NIO 的问题,如:
Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题,方便了开发,且拥有活跃的社区更新等。
Netty 的高性能等特点,使得它成为众多开源框架的底层通讯架构,如 ElasticSearch、Dubbo、Hadoop 中的多个组件。
netty 4.1 API
netty GitHub
一个客户端连接请求,使用服务器一个线程
带来的问题:
Reactor 模式中的两个核心成员:
服务器端用一个线程,通过多路复用搞定所有的 IO 操作(包括连接,读、写等),编码简单、没有线程切换的性能消耗,但是如果客户端连接数量较多,或 Handler 执行耗时任务时,将出现卡顿情况。
使用场景:客户端的数量有限,业务处理非常快速,比如 Redis 在业务处理的时间复杂度为 O(1) 的情况
缺点:多线程数据共享和访问比较复杂, reactor 处理所有的事件的监听和响应,是单线程运行状态, 在高并发场景容易出现性能瓶颈。
Netty 抽象出两组线程池 BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写 ,程序中的线程数默认为实际 cpu 核数 * 2,也可在初始化 NioEventLoopGroup 时传入指定的线程数
NioEventLoopGroup 组中含有多个 NioEventLoop 事件循环 ,执行如图所示的3步工作,默认轮询的方式去处理多个事件
每个 NioEventLoop 包含有一个 Selector,一个 taskQueue ;每个 Selector 上可以注册监听多个 NioChannel
在 WorkerGroup 的每个 NIOEventLoop 处理业务时,会使用 pipeline(管道), pipeline 中包含了 channel(频道、通道) , 即通过 pipeline 可以获取到对应通道, 管道中维护了很多的 handler 处理器。
两者分别是 netty 客户端、服务端的启动引导类;其中常见的方法如下:
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup):用于服务器端设置主从 EventLoop
public B group(EventLoopGroup group) :用于客户端设置一个 EventLoop
public B channel(Class channelClass):用来设置通道实现,如服务器端的 NioServerSocketChannel ,客户端的 NioSocketChannel
public < T > B option(ChannelOption< T > option, T value):用来给 parentGroup 添加配置
public < T > ServerBootstrap childOption(ChannelOption< T > childOption, T value),用来给 childGroup 通道添加配置
public ServerBootstrap childHandler(ChannelHandler childHandler):用来设置业务处理类(自定义的 handler )
public ChannelFuture bind(int inetPort) :用于服务器端设置占用的端口号,注:当绑定的端口提供的是 Http 相关的服务,需要注意有 默认非安全端口 的限制,即服务正常,但部分浏览器会限制访问,更多请自行百度
public ChannelFuture connect(String inetHost, int inetPort) :用于客户端连接服务器端
Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件
当向一个 Selector 中注册 Channel 后,Selector 不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),然后驱动事件执行
ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。
ChannelPipeline 提供了 ChannelHandler 链的容器,何时是入站,何时是出站???
ChannelPipeline 是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和操作,相当于一个贯穿 Netty 的链。(ChannelPipeline 是 保存 ChannelHandler 的 List,用于处理或拦截Channel 的入站事件和出站操作)
ChannelPipeline 实现了拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互
每个 Channel 有且仅有一个 ChannelPipeline 与之对应,两者可以通过一个对象获取到另外一个对象;链表中 header 和 tail 节点为 DefaultChannelPipeline(是接口 ChannelHandlerContext 的子类) :
ChannelPipeline addFirst(ChannelHandler… handlers),把一个业务处理类(handler)添加到链中的第一个位置
ChannelPipeline addLast(ChannelHandler… handlers),把一个业务处理类(handler)添加到链中的最后一个位置
Netty 中所有的 IO 操作都是异步的
通过 isDone 方法来判断当前操作是否完成;
通过 isSuccess 方法来判断已完成的当前操作是否成功;
通过 getCause 方法来获取已完成的当前操作失败的原因;
通过 isCancelled 方法来判断已完成的当前操作是否被取消;
通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器;如果
Future 对象已完成,则通知指定的监听器
Channel channel(),返回当前正在进行 IO 操作的通道
ChannelFuture sync(),等待异步操作执行完毕
/**
* byte[] buff //buff即内部用于缓存的数组。
* position //当前读取的位置。
* mark //为某一读过的位置做标记,便于某些时候回退到该位置。
* capacity //初始化时候的容量。
* limit //当写数据到buffer中时,limit一般和capacity相等,当读数据时,limit代表buffer中有效数据的长度。
* 0 - mark - position - limit - capacity
*/
ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[10]);
/**
* 该对象包含一个数组 arr , 是一个 byte[10]。在 netty 的 ByteBuf 中,不需要使用 flip 进行反转
* 底层维护了 readerindex 和 writerIndex,通过 readerindex 和 writerIndex 和 capacity, 将 buffer 分成三个区域
* 0---readerindex 已经读取的区域 ;readerindex---writerIndex , 可读的区域 ; writerIndex -- capacity, 可写的区域
*/
ByteBuf byteBuf = Unpooled.buffer(10);
心跳机制是客户端和服务端在 TCP 三次握手进入 ESTABLISH 状态后,通过发送一个最简单的包来保持连接的存活,或监控另一边服务的可用性等。
基于运输层 TCP 的 keepalive 机制,由具体的 TCP 协议栈来实现长连接的维持。如在netty 中可以在创建 channel 的时候,指定 SO_KEEPALIVE 参数来实现:
存在的问题:
Netty 只能控制 SO_KEEPALIVE 是否开启,其他参数,则需要从系统中读取,其中比较关键的是 tcp_keepalive_time,发送心跳包检测的时间间隔,默认为每2小时检测一次。如果客户端在这2小时内断开了,那么服务端也要维护这个连接2小时,浪费服务端资源;另外就是对于需要实时传输数据的场景,客户端断开了,服务端也要2小时后才能发现。服务端发送心跳检测,具体可能出现的情况如下:
连接正常:客户端仍然存在,网络连接状况良好。此时客户端会返回一个 ACK 。 服务端接收到ACK后重置计时器,在2小时后再发送探测。如果2小时内连接上有数据传输,那么在该时间基础上向后推延2个小时;
连接断开:客户端异常关闭,或是网络断开。在这两种情况下,客户端都不会响应服务器,服务器在一定时间(默认为 1000 ms )后重复发送 keep-alive packet ,并且重复发送一定次数。
客户端曾经崩溃,但已经重启:这种情况下,服务器将会收到对其存活探测的响应,但该响应是一个复位,从而引起服务器对连接的终止。
IdleStateHandler 是 netty 提供的处理空闲状态的处理器
public IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime,TimeUnit unit)
long readerIdleTime : 表示在 tcp 连接建立后 ,多长时间没有读操作, 就会发送一个心跳检测包到客户端检测连接是否存活。0表示不检测
long writerIdleTime : 表示多长时间没有写, 就会发送一个心跳检测包检测是否连接
long allIdleTime : 表示多长时间没有读写, 就会发送一个心跳检测包检测是否连接
当 IdleStateEvent 触发后 , 就会传递给 管道 的下一个 handler 去处理,通过调用(触发)下一个 handler 的 userEventTiggered 方法, 去完成对应的 IdleStateEvent(读空闲,写空闲,读写空闲)操作
遗留问题:
当 Netty 发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码,从字节转换为另一种
格式(比如 java 对象);出站消息被编码成字节。
Netty 提供的编解码器,他们都实现了 ChannelInboundHadnler 或 ChannelOutboundHandler 接口,并将已经编解码的对象转发给 ChannelPipeline 中的下一个 Handler。
public class ByteBufDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception {
if(in.readableBytes()>4){
out.add(in.readInt());
}
}
}
tcp 协议下,数据被分段传输过来,有可能出现粘包拆包的问题,ByteToMessageDecoder 类会对入站数据进行缓冲,直到有足够的数据被处理。
p82,4‘00
p95 接受请求。。。