南京建设主管部门网站,国企500强公司有哪些,自学室内装修设计教程,兼职给企业做网站优质博文#xff1a;IT-BLOG-CN 一、BIO、NIO 和 AIO
【1】阻塞 IO(Blocking I/O)#xff1a; 同步阻塞I/O模式#xff0c;当一条线程执行 read() 或者 write() 方法时#xff0c;这条线程会一直阻塞直到读取一些数据或者写出去的数据已经全部写出#xff0c;在这期间这条… 优质博文IT-BLOG-CN 一、BIO、NIO 和 AIO
【1】阻塞 IO(Blocking I/O) 同步阻塞I/O模式当一条线程执行 read() 或者 write() 方法时这条线程会一直阻塞直到读取一些数据或者写出去的数据已经全部写出在这期间这条线程不能做任何其他的事情。在活动连接数不是特别高小于单机1000的情况下这种模型是比较不错的可以让每一个连接专注于自己的 I/O 并且编程模型简单也不用过多考虑系统的过载、限流等问题。但是当面对十万甚至百万级连接的时候传统的 BIO模型是无能为力的。因此我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
【2】非阻塞 NIO(New I/O) NIO是一种同步非阻塞的 I/O模型NIO 与原有的 IO 有同样的作用和目的但是使用的方式完全不同NIO 支持面向缓冲区、基于通道的操作。NIO 将以更加高效的方式进行文件读写操作。JAVA NIO的核心在于通道Channel和缓冲区Buffer。通道表示打开 IO 设备例如文件、套接字的连接。若需要使用 NIO系统需要获取用于连接 IO设备的通道以及用于容纳数据的缓冲区。对数据进行处理。阻塞IO 会一直等待所以非阻塞IO 是用来解决 IO线程与 Socket 之间的解耦问题【通过引入事件机制】如果 Socket 发送缓冲区可写的话会通知 IO线程进行 write如果 Socket 的接收缓冲区可读的话会通知 IO线程进行 read。NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel。对于高负载、高并发的网络应用应使用 NIO 的非阻塞模式来开发。
【3】I/O 复用模型 非阻塞 IO中引入的机制就是 IO多路复用模型Linux 提供了 epoll系统调用epoll使用基于事件驱动方式代替顺序扫描select/poll因此性能更高。当有fd 就绪时立即回调函数 rollback。IO多路复用技术底层就是通过 epoll 函数实现的。
【4】AIO (Asynchronous I/O) AIO 也就是 NIO2。在 Java 7 中引入了 NIO 的改进版 NIO2它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的也就是应用操作之后会直接返回不会堵塞在那里当后台处理完成操作系统会通知相应的线程进行后续的操作。AIO 是异步IO 的缩写虽然 NIO 在网络操作中提供了非阻塞的方法但是 NIO 的 IO 行为还是同步的。对于 NIO 来说我们的业务线程是在 IO 操作准备好时得到通知接着就由这个线程自行进行 IO 操作IO操作本身是同步的。目前来说 AIO 的应用还不是很广泛Netty 之前也尝试使用过 AIO不过又放弃了。
二、Netty 的各大组件
【1】Channel Channel 是 Netty 网络操作抽象类它除了包括基本的 I/O 操作如 bind、connect、read、write 之外还包括了 Netty 框架相关的一些功能如获取该 Channel 的 EventLoop。在传统的网络编程中作为核心类的 Socket 它对程序员来说并不是那么友好直接使用其成本还是稍微高了点。而 Netty 的 Channel 则提供的一系列的 API 它大大降低了直接与 Socket 进行操作的复杂性。而相对于原生 NIO 的 ChannelNetty 的 Channel 具有如下优势 ①、在 Channel 接口层采用 Facade 模式进行统一封装将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来统一对外提供。②、Channel 接口的定义尽量大而全为 SocketChannel 和 ServerSocketChannel 提供统一的视图由不同子类实现不同的功能公共功能在抽象父类中实现最大程度地实现功能和接口的重用。③、具体实现采用聚合而非包含的方式将相关的功能类聚合在 Channel 中有 Channel 统一负责和调度功能实现更加灵活。
【2】EventLoop Netty 基于事件驱动模型使用不同的事件来通知我们状态的改变或者操作状态的改变。它定义了在整个连接的生命周期里当有事件发生的时候处理的核心抽象。Channel 为 Netty 网络操作抽象类EventLoop 主要是为 Channel 处理 I/O 操作两者配合参与 I/O 操作。Channel、EventLoop、Thread、EventLoopGroup之间的关系 ①、一个 EventLoopGroup 包含一个或多个 EventLoop。 ②、一个 EventLoop 在它的生命周期内只能与一个 Thread绑定。 ③、所有 EnventLoop 处理的 I/O事件都将在它专有的 Thread 上被处理。 ④、一个 Channel 在它的生命周期内只能注册于一个 EventLoop。 ⑤、一个 EventLoop 可被分配至一个或多个 Channel 。 当一个连接到达时Netty 就会注册一个 Channel然后从 EventLoopGroup 中分配一个 EventLoop 绑定到这个Channel上在该 Channel的整个生命周期中都是有这个绑定的 EventLoop 来服务的。
【3】ChannelFutureNetty 为异步非阻塞 即所有的 I/O 操作都为异步的因此我们不能立刻得知消息是否已经被处理了。Netty 提供了 ChannelFuture 接口通过该接口的 addListener() 方法注册一个 ChannelFutureListener当操作执行成功或者失败时监听就会自动触发返回结果。
【4】ChannelHandlerChannelHandler 为 Netty 中最核心的组件它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 主要用来处理各种事件这里的事件很广泛比如可以是连接、数据接收、异常、数据转换等。ChannelHandler 有两个核心子类 ChannelInboundHandler 和 ChannelOutboundHandler其中 ChannelInboundHandler 用于接收、处理入站数据和事件而 ChannelOutboundHandler 则相反。
【5】ChannelPipelineChannelPipeline 为 ChannelHandler 链提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API。一个数据或者事件可能会被多个 Handler 处理在这个过程中数据或者事件经流 ChannelPipeline由 ChannelHandler 处理。在这个处理过程中一个 ChannelHandler 接收数据后处理完成后交给下一个 ChannelHandler或者什么都不做直接交给下一个 ChannelHandler。当一个数据流进入 ChannlePipeline 时它会从 ChannelPipeline 头部开始传给第一个 ChannelInboundHandler 当第一个处理完后再传给下一个一直传递到管道的尾部。与之相对应的是当数据被写出时它会从管道的尾部开始先经过管道尾部的 “最后” 一个ChannelOutboundHandler当它处理完成后会传递给前一个 ChannelOutboundHandler 。当 ChannelHandler 被添加到 ChannelPipeline 时它将会被分配一个 ChannelHandlerContext它代表了 ChannelHandler 和 ChannelPipeline 之间的绑定。其中 ChannelHandler 添加到 ChannelPipeline 过程如下 ①、一个 ChannelInitializer 的实现被注册到了 ServerBootStrap 中 ②、当 ChannelInitializer.initChannel() 方法被调用时ChannelInitializer 将在 ChannelPipeline 中安装一组自定义的ChannelHandler ③、ChannelInitializer 将它自己从 ChannelPipeline 中移除
三、Netty 的线程模型
【参考答案】链接
四、TCP 粘包/拆包的原因及解决方法
【参考答案】链接
五、了解哪几种序列化协议包括使用场景和如何去选择
【序列化的缺点】链接 【序列化框架 Protobuf】链接 【序列化框架 Thrift】链接
六、Netty 的零拷贝实现
【参考答案】链接
七、Netty 的高性能分析
【1】传输 IO 模型在很大程度上决定了框架的性能相比于BIONetty建议采用异步通信模式因为 NIO一个线程可以并发处理N个客户端连接和读写操作这从根本上解决了传统同步阻塞IO “一连接一线程模型”架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。正如代码中所示使用的是 NioEventLoopGroup 和 NioSocketChannel 来提升传输效率。 【2】协议 采用什么样的通信协议对系统的性能极其重要Netty 默认提供了对 Google Protobuf 的支持也可以通过扩展Netty 的编解码接口用户可以实现其它的高性能序列化框架。 【3】线程 Netty 使用了Reactor线程模型但 Reactor 模型不同对性能的影响也非常大上面线程模型中就介绍常用的 Reactor 模型的三种情况。
八、基于BIO实现的Server端当建立了100个连接时会有多少个线程如果基于NIO又会是多少个线程 为什么
BIO由于不是 NIO那样的事件机制在连接的 IO读取上无论是否真的有读/写发生都需要阻塞住当前的线程对于基于 BIO实现的 Server端通常的实现方法都是用一个线程去 accept连接当连接建立后将这个连接的 IO读写放到一个专门的处理线程所以当建立100个连接时通常会产生1个 Accept线程 100个处理线程。
NIO通过事件来触发这样就可以实现在有需要读/写的时候才处理不用阻塞当前线程NIO在处理 IO的读写时当从网卡缓冲区读或写入缓冲区时这个过程是串行的所以用太多线程处理 IO事件其实也没什么意义连接事件由于通常处理比较快用1个线程去处理就可以IO事件呢通常会采用 cpu core数1或 cpu core数 * 2这个的原因是 IO线程通常除了从缓冲区读写外还会做些比较轻量的例如解析协议头等这些是可以并发的为什么不只用1个线程处理是因为当并发的 IO事件非常多时1个线程的效率不足以发挥出多核CPU 的能力从而导致这个地方成为瓶颈这种在分布式场景中比较明显按照这个也就更容易理解为什么在基于 Netty等写程序时不要在 IO线程里直接做过多动作而应该把这些动作转移到另外的线程池里去处理就是为了能保持好 IO事件能被高效处理。从上面可以看出对于大多数需要建立大量连接但并发读写并不会同时产生的场景而言NIO的优势是非常明显的。这种关于BIO、NIO的问法的变化空间是非常大的还可以进一步拓展问问AIO和BIO、NIO的根本不同。
九、Netty 的心跳检测及重连机制怎么实现的 十、ChannelInboundHandlerAdapter 与 SimpleChannelInboundHandler 的区别
如下就是两个类的声明SimpleChannelInboundHandler是继承 ChannelInboundHandlerAdapter的。也就是说SimpleChannelInboundHandler 也拥有 ChannelInboundHandlerAdapter的方法。
//ChannelInboundHandlerAdapter 类
ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler
//SimpleChannelInboundHandler 类
SimpleChannelInboundHandlerI extends ChannelInboundHandlerAdapter继承关系图 SimpleChannelInboundHandler的 channelRead相比 ChannelInboundHandlerAdapter而言主要做了类型匹配以及用完之后释放指向保存该消息的 ByteBuf的内存引用。这里提供了一个模板作用是把处理逻辑不变的内容写好在channelRead(ctx,msg) 中并且在里面调用 channelRead0 这样变化的内容通过抽象方法实现传递到子类中去了(在Netty5中channelRead0已被重命名为 messageReceived)。
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {boolean release true;try {if (this.acceptInboundMessage(msg)) {this.channelRead0(ctx, msg);} else {release false;ctx.fireChannelRead(msg);}} finally {if (this.autoRelease release) {ReferenceCountUtil.release(msg);}}
}protected abstract void channelRead0(ChannelHandlerContext var1, I var2) throws Exception;相比之下ChannelInboundHandlerAdapter好像一无是处毕竟他要自己处理资源的释放例如如下的调用buf.release();如果说 channelRead都是同步操作的话SimpleChannelInboundHandler是不错的选择如果操作是异步的话那他的逻辑就有点麻烦了例如你把数据交给另外的线程处理了还没处理就会释放了 。这里必须说明一个问题他的回收和 jvm的垃圾回收还不完全是一回事。netty是自己做了引用计数的操作。 buf.refCnt(); 通过上面的 api就可以获取到计数的个数。具体的引用计数的部分不知道也不影响 netty的学习这个点后面具体再说。ChannelInboundHandlerAdapter 处理自由的优点也就提现出来了可以更好的处理更多的特定场景。
那该方法是什么时候释放资源的呢 当 writeAndFlush() 方法被调用时才被释放我们点进去源码验证一下
public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {if (msg null) {// msg不能为空throw new NullPointerException(msg);}if (isNotValidPromise(promise, true)) {ReferenceCountUtil.release(msg);// 释放资源(保存消息的ByteBuf)// cancelledreturn promise;}write(msg, true, promise);// 异步写操作return promise;
}上面源码中最后资源是通过 ReferenceCountUtil来释放的。也就是说当我们需要释放 ByteBuf相关内存的时候也可以使用 ReferenceCountUtil.release()。ReferenceCountUtil 底层实现是 ReferenceCounted 当新的对象初始化的时候计数为1retain() 方法被调用时引用计数加1release()方法被调用时引用计数减1当计数减少到0的时候会被显示清除再次访问被清除的对象会出现访问冲突(这里想起了JVM判断对象是否存活的引用计数算法)。ReferenceCountUtil.release源码如下
public static boolean release(Object msg) {if (msg instanceof ReferenceCounted) {return ((ReferenceCounted) msg).release();// Decreases the reference count by 1}return false;
}SimpleChannelInboundHandler 和 ChannelInboundHandlerAdapter区别: 在客户端的业务 Handler继承的是 SimpleChannelInboundHandler而在服务器端继承的是 ChannelInboundHandlerAdapter。最主要的区别就是 SimpleChannelInboundHandler在接收到数据后会自动 release掉数据占用的 Bytebuffer资源(自动调用Bytebuffer.release())。而为何服务器端不能用呢因为我们想让服务器把客户端请求的数据发送回去而 write() 操作是异步的而服务器端有可能在channelRead方法返回后还没有写完数据因此不能让它自动release。SimpleChannelInboundHandler 是有泛型参数的。可以指定一个具体的类型参数通过 decoder配合使用非常方便。ChannelInboundHandlerAdapter 则是直接操作 byte数组的。
SimpleChannelInboundHandler 的好处是可以处理不同的类型对象并且可以做释放。ChannelInboundHandlerAdapter的好处则是更自由在异步的场景下更适合。
ChannelInboundHandlerAdapter 注意事项 : 用户自定义了一个 Handler类代码如下:
Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {this.ctx ctx;
}Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {super.channelRead(ctx, msg);log.debug(thread.name{}, Thread.currentThread().getName());ByteBuf in (ByteBuf) msg;String readStr in.toString(CharsetUtil.UTF_8);log.debug(Server received: {}, readStr);log.debug(release msg);ReferenceCountUtil.release(msg);}Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {ctx.flush();
}Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {cause.printStackTrace();ctx.close();
}行时出现了如下堆栈异常
io.netty.util.IllegalReferenceCountException: refCnt: 0
at io.netty.buffer.AbstractByteBuf.ensureAccessible(AbstractByteBuf.java:1173)
at io.netty.buffer.AbstractByteBuf.checkIndex(AbstractByteBuf.java:1119)
at io.netty.buffer.UnpooledUnsafeDirectByteBuf.internalNioBuffer(UnpooledUnsafeDirectByteBuf.java:385)
at io.netty.buffer.ByteBufUtil.decodeString(ByteBufUtil.java:568)
at io.netty.buffer.AbstractByteBuf.toString(AbstractByteBuf.java:979)
at io.netty.buffer.AbstractByteBuf.toString(AbstractByteBuf.java:974)
at com.spy.apollo.netty.demo.demo02_biz_logic.ServerBizHandler.channelRead(ServerBizHandler.java:50)异常说是引用计数 refCnf 0也就是没有被引用因此报错常规的 channelRead中消息读取完毕是要立即释放当前消息的引用计数即减一操作如下代码操作。但是其他 Handler也去释放时发现没有引用就会报错。
ReferenceCountUtil.release(msg);分析 通过调试代码发现根源就在 super.channelRead(ctx, msg);这个函数。其实 ChannelInboundHandlerAdapter 是提供了一种默认实现子类如果要继承需要覆盖父类中的方法并且不需要调用 super.xxxxMethod()。源码如下意思是此实现只是将操作转发到下一个 channelhandler。子类可以重写方法实现来改变这一点。
/*** p* This implementation just forward the operation to the next {link ChannelHandler} in the* {link ChannelPipeline}. Sub-classes may override a method implementation to change this.* /p*/
public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {