《Netty in action》 读书笔记

声明:这篇文章是记录读书过程中的知识点,并加以归纳总结,成文。文中图片、代码出自《Netty in action》。

1. 为什么用Netty?

每个框架的流行,都一定有它出众的地方。Netty就是为了网络编程使用,它封装了大量的通讯的底层内容,简化开发者的工作量,让开发者的精力全都放于业务上,而且它能高效的处理网络通讯的东西。很多大的公司都使用Netty作为通讯框架,因此,使这个框架更加完善。

2. OIO(阻塞I/O)与NIO(非阻塞I/O)与AIO(异步I/O)

2.1 阻塞I/O

为什么叫阻塞I/O呢?因为每个线程对应一个连接,连接的建立、数据读取、写入,都会进行阻塞,所以叫阻塞I/O。

性能瓶颈:一个线程对应一个连接,当连接数量巨大时,线程数量过多,每个线程都需要内存,内存会被迅速耗尽,以及线程间切换的开销也十分巨大。另外,阻塞也是一个问题。accept()、read()、write()都会进行阻塞。

OIO线程模型:

处理逻辑:

2.2 非阻塞I/O

NIO三大组件:Buffer、Channel(数据来源或数据写入的目的地)、Selector(多路复用器)

开启Selector方式:Selector selector = Selector.open();

注册的四种事件:SelectionKey.OP_READ(可读)、SelectionKey.OP_WRITE(可写)、SelectionKey.OP_CONNECT(建立起连接)、SelectionKey.OP_ACCEPT(接收连接)

为什么叫非阻塞I/O呢?它不阻塞了吗?不是的。是因为,使用Selector对多个并发的连接进行轮询(也就是用更少的线程监视更多的连接),通过事件驱动来查明哪些可以进行I/O,不必等待到操作的结束。

NIO线程模型:

 处理逻辑:

2.3 异步I/O

在NIO的基础上,AIO可以让效率有更多的提高。AIO是基于回调和事件驱动,当异步方法提交任务后可以立即返回,由线程池负责来执行任务,当任务完成后可以立即通知给任务的提交者。异步I/O的目的,主要为了控制线程数量,减少过多的线程带来的内存消耗和CPU在线程调度上的开销。

 异步I/O的使用方式有两种,一种是Future,另一种是回调函数。

2.3.1 Future

它代表异步计算的结果。

 1 interface ArchiveSearcher { String search(String target); }
 2  class App {
 3    ExecutorService executor = ...
 4    ArchiveSearcher searcher = ...
 5    void showSearch(final String target)
 6        throws InterruptedException {
 7      Future<String> future
 8        = executor.submit(new Callable<String>() {
 9          public String call() {
10              return searcher.search(target);
11          }});
12      displayOtherThings(); // do other things while searching
13      try {
14        displayText(future.get()); // use future  这里会进行阻塞,一直到计算的完成
15      } catch (ExecutionException ex) { cleanup(); return; }
16    }
17  }

在Future这里,还要提到FutreTask这个类,它可以包装Callable和Runnable的对象,此外,因为它继承Runnable接口,所以它可以被提交给Executor来执行。

 2.3.2 回调函数 CompletionHandler

CompletionHandler它是一个处理器,用于异步计算结果。有两个方法,completed(V result, A attachment)(执行成功时调用)和failed(Throwable exc, A attachment)(执行失败时调用)。在下列地方,都有使用CompletionHandler作为回调函数的方法:

AsynchronousFileChannel 类的 read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer,? super A> handler)

AsynchronousServerSocketChannel类的 accept(A attachment, CompletionHandler<AsynchronousSocketChannel,? super A> handler)

AsynchronousSocketChannel 类的 read(ByteBuffer dst, A attachment, CompletionHandler<Integer,? super A> handler)

3. ByteBuf

Java NIO中提供ByteBuffer作为byte的容器,在Netty中,使用ByteBuf来替代ByteBuffer。ByteBuf功能更强大,更加灵活。Netty对于数据的处理主要是通过两个组件,抽象类ByteBuf和接口ByteBufHolder。

3.1 ByteBuf使用方式

HEAP BUFFERS  存储JVM的堆数据

1 ByteBuf heapBuf = ...;
2 if (heapBuf.hasArray()) {
3 byte[] array = heapBuf.array();
4 int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
5 int length = heapBuf.readableBytes();
6 handleArray(array, offset, length);
7 }

DIRECT BUFFERS

这里的Direct该如何理解呢?它是ByteBuf的一种形式,在使用JNI的时候,它避免了将buffer的内容复制到中间媒介buffer上,可以直接使用。这就是DIrect的说明。

在JavaDoc中,关于ByteBuffer有明确的说明:direct buffers会处于正常的垃圾回收之外,因此,它非常适用于网络传输方面。如果你的数据保留在heap-buffer上面,那么socket传输前,要将buffer复制到direct buffer上。至于,它的不足之处,在于使用direct buffer的时候,必须先复制一份。代码如下:

1 ByteBuf directBuf = ...;
2 if (!directBuf.hasArray()) {
3 int length = directBuf.readableBytes();
4 byte[] array = new byte[length];
5 directBuf.getBytes(directBuf.readerIndex(), array);
6 handleArray(array, 0, length);
7 }

COMPOSITE BUFFERS  就是前面两种的结合

3.2 接口ByteBufHolder

 在保存实际数值的时候,常需要保存一些附加的值。比如:Http返回的例子,除了返回的实际内容外,还有status code,cookies等等。

Netty用ByteBufholder来提供一些共通的方法使用,它有几个常用的方法:content()、copy()、duplicate()

如果你要实现一个在ByteBuf中存储实际负载的对象的话,那么ByteBufHolder是个好的选择。

3.3 ByteBuf allocation

这里讨论的是ByteBuf实例的管理。

3.3.1 On-demand: interface ByteBufAllocator

ByteBufAllocator 常见方法(部分记录):buffer()、heapBuffer()、directBuffer()、compositeBuffer()、ioBuffer()

Netty用ByteBufAllocator实现了缓存池。我们可以通过Channel或是绑定在ChannelHandler上的ChannelHandlerContext,来获得ByteBufAllocator的引用。示例如下:

1 Channel channel = ...;
2 ByteBufAllocator allocator = channel.alloc();
3 ....
4 ChannelHandlerContext ctx = ...;
5 ByteBufAllocator allocator2 = ctx.alloc();

ByteBufAllocator有两种实现:PooledByteBufAllocator 和 UnpooledByteBufAllocator  前者会进行缓存;后者,不会缓存ByteBuf的实例,每次调用都会返回新的。

3.3.2 Unpooled buffers

Netty有一个Unpooled的类,来提供静态方法,用于创建未被缓存ByteBuf实例。

常用的方法:buffer()、directBuffer()、wrappedBuffer()、copiedBuffer()

3.4 Reference counting

Reference counting被用来优化内存的使用,通过释放不再使用的资源。Netty中的Reference counting 和 ByteBufHolder 都实现了ReferenceCounted接口。ReferenceCounted的实现的实例,它们的活跃的实例的计数都是从1进行的,只要大于0,对象就一定不会被释放。当数字减为0时,资源就会被释放。下面给出两个例子:

Reference counting:
1
Channel channel = ...; 2 ByteBufAllocator allocator = channel.alloc(); 3 .... 4 ByteBuf buffer = allocator.directBuffer(); 5 assert buffer.refCnt() == 1;

Release reference-counted object

1 ByteBuf buffer = ...;
2 boolean released = buffer.release();   // 活跃的引用,变成0,对象被释放,方法返回true

4. ChannelHandler 和 ChannelPipline

4.1 The ChannelHandler family

4.1.1 Channel 生命周期

ChannelRegistered:Channel被注册到EventLoop

ChannelActive: Channel活跃,可以发送、接收数据

ChannelInactive:Channel没有连接到remote peer

ChannelUnregistered:Channel被创建了,但没被注册到EventLoop

 4.1.2 ChannelHandler

先看看它的接口层次图:

ChannelHandler 生命周期的方法:handlerAdded(ChannelHandler被加入到ChannelPipeline时调用)、handlerRemoved(被移除时)、exceptionCaught(ChannelPipeline的处理过程中发生exception时调用)

ChannelInboundHandler

处理inbound数据和Channel状态的改变

常用方法:channelRegistered、channelActive、channelReadComplete、channelRead 等等

看一段代码:

1 @Sharable       //这里的注解的意思是,这个handler可以被加到多个ChannelPipelines中
2 public class DiscardHandler extends ChannelInboundHandlerAdapter {
// 当继承ChannelInboundHandler,覆盖channelRead时,需要明确地释放缓存的实例
3 @Override
4 public void channelRead(ChannelHandlerContext ctx, Object msg) {
5 ReferenceCountUtil.release(msg);
6 }
7 }

看另一个例子:

1 @Sharable
2 public class SimpleDiscardHandler       // SimpleDiscardHandler 会自动释放资源
3 extends SimpleChannelInboundHandler<Object> {
4 @Override
5 public void channelRead0(ChannelHandlerContext ctx,
6 Object msg) {
7 // 不需要任何明确地释放资源的操作,这里只需做业务处理就可以
8 }
9 }

注意:如果一个message被使用或是丢弃,并且没有被传给管道中下一个ChannelOutboundHandler时,就一定要调用ReferenceCountUtil.release() 释放资源。如果消息到达实际传输层,那么,write或是Channel关闭时,会自动释放资源。

ChannelOutboundHandler:处理outbound数据,并允许所有操作的拦截

常见方法:bind、connect、disconnect、close 等

ChannelHandlerAdapter有一个isSharable()方法,判断是否有注解@Sharable

4.1.3 Resource management

无论是ChannelInboundHandler.channelRead()还是ChannelOutboundHandler.write(),都需要确保没有资源泄露。当你使用ByteBuf之后,需要调整引用的计数reference count。为了帮助诊断潜在的问题,Netty提供了一个ResourceLeakDetector,检测到泄漏时,会打印log。

使用方法:java -Dio.netty.leakDetectionLevel=ADVANCED

参数有四种:DISABLED、SIMPLE(default)、ADVANCED、PARANOID

4.2 ChannelPipeline接口

每个新Channel创建时,都会被指定一个新的ChannelPipeline,这种对应关系是永久的,不会改变。

ChannelPipeline 和 ChannelHandlers 关系图:

ChannelPipeline一些常用方法:

addLast、addFirst、remove、replace、get、context(获得绑定在ChannelHandler上的ChannelHandlerContext)、names(获得ChannelPipeline上的所有ChannelHandler名字)

4.3 ChannelHandlerContext接口

4.3.1 ChannelHandlerContext的使用

它的主要作用就是管理handlers之间的交互。

常见API:bind、channel、close、connect、deregister、fireChannelActive、fireChannelRead、handler、pipeline、read、write 等等

ChannelHandlerContext的使用,如下图:

每个ChannelHandler都是和一个ChannelHandlerContext相互关联,关联关系一直不会改变,因此,缓存一个它的引用很安全。

下面看2个代码片段:

 1 ChannelHandlerContext ctx = ..;
 2 Channel channel = ctx.channel();   // 从ChannelHandlerContext关联到Channel
 3 channel.write(Unpooled.copiedBuffer("Netty in Action",
 4 CharsetUtil.UTF_8));
 5 
 。。。
 9 ChannelHandlerContext ctx = ..;
10 ChannelPipeline pipeline = ctx.pipeline();  // 从ChannelHandlerContext 关联到 ChannelPipeline 
11 pipeline.write(Unpooled.copiedBuffer("Netty in Action", 12 CharsetUtil.UTF_8));

上面代码中,虽然Channel 或是 ChannelPipeline 调用的write()方法,代理了pipline中事件的传递,实际上,ChannelHandler之间的传递是ChannelHandlerContext来做的。

如果你想从某个handler开始(不是从第一个开始),那么就可以利用ChannelHandlerContext。

1 ChannelHandlerContext ctx = ..;       // 获取ChannelHandlerContext的引用
2 ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));   //write() sends the buffer to the next ChannelHandler

4.3.2 ChannelHandler 和ChannelHandlerContext 的高级用法

6.3.2

To be continued!!!

猜你喜欢

转载自www.cnblogs.com/lihao007/p/10247228.html
今日推荐