Vernlium / vernlium.github.io

1 stars 3 forks source link

Netty学习记录 #1

Open Vernlium opened 8 years ago

Vernlium commented 8 years ago

Netty支持的传输协议如下

Name Package Description
NIO io.netty.channel.socket.nio Uses the java.nio.channels package
as a foundation—a selector-based approach.
Epoll io.netty.channel.epoll Uses JNI for epoll() and non-blocking IO.This transport supports features available only on Linux, such as SO_REUSEPORT, and is faster than the NIO transport as well as fully non-blocking.
OIO io.netty.channel.socket.oio Uses the java.net package as a foundation—uses blocking streams.
Local io.netty.channel.local A local transport that can be used to communicate in the VM via pipes.
Embedded io.netty.channel.embedded An embedded transport, which allows using ChannelHandlers without a true network-based transport. This can be quite useful for testing your ChannelHandler implementations.

NIO

NIO提供了完整的所有异步IO操作的实现。基于NIO的selector API实现的。

selector注册到服务上,然后Channel状态发生变化时会被通知。可能的状态变化如下:

下面是java.nio.channels.SelectionKey 类定义的状态:

image

下图展示了状态变化的流程:

image

Epoll——Linux中nio的本地接口

Vernlium commented 8 years ago

Netty的NIO是基于Java实现的,为了保持在所有平台上的一致性,JDK做出了一些妥协。 而Linux系统中,提供了高可靠性的IO特性,就是epoll。Netty专门为Linux提供了一个NIO API,使用的epoll,而不是代价更高的中断机制。

使用epoll版的NIO,可以用EpollEventLoopGroup替代NioEventLoopGroup,用EpollServerSocket.class替代NioServerSocketChannel.class。

OIO- old blocking I/O

Netty 中,该 OIO 代表了一种妥协。它通过了 Netty 的通用 API 访问但不是异步,而是构建在 java.net 的阻塞实现上。但它确实有它有效的用途。

假设你需要的端口使用该做阻塞调用库(例如 JDBC)。它可能不适合非阻塞。相反,你可以在短期内使用 OIO 传输,后来移植到纯异步的传输上。让我们看看它是如何工作的。

在 java.net API,你通常有一个线程接受新的连接到达监听在ServerSocket,并创建一个新的线程来处理新的 Socket 。这是必需的,因为在一个特定的 socket的每个 I/O 操作可能会阻塞在任何时间。在一个线程处理多个 socket 易造成阻塞操作,一个 socket 占用了所有的其他线程。

OBIO的处理逻辑

image

Netty是如何用相同的API来支持NIO的异步传输呢?这里Netty利用了SO_TIMEOUT Socket标志,这个参数指定了下一次I/O操作完成之前的最大等待的毫秒数。如果操作在指定的时间内失败,会抛出SocketTimeoutException异常。 Netty中捕获该异常并继续处理循环。在接下来的EvenetLoop运行时,它再次尝试。这其实是唯一的办法让像Netty的异步架构来支持OIO。

JVM内的通信协议

Netty提供本地异步连接用于在同一JVM中的client和server之间的通信。此通信协议支持所有的 Netty 常见的传输实现的 API。

在此传输中,与服务器 Channel 关联的 SocketAddress 不是“绑定”到一个物理网络地址中,而是被存储在注册表中当服务器处于运行中时。当 Channel 关闭时它会注销。由于传输不接受“真正的”网络通信,它不能与其他传输实现互操作。因此,客户端是希望连接到使用当地的交通必须使用它,以及一个服务器。除此限制之外,它的使用是与其他的传输是相同的。

Embedded transport 嵌入式通信

内嵌 Transport

Netty中 还提供了可以嵌入 ChannelHandler 实例到其他的 ChannelHandler 的传输,使用它们就像辅助类,增加了灵活性的方法,使您可以与你的 ChannelHandler 互动。

该嵌入技术通常用于测试 ChannelHandler 的实现,但它也可用于将功能添加到现有的 ChannelHandler 而无需更改代码。嵌入传输的关键是Channel 的实现,称为“EmbeddedChannel”。

第10章描述了使用 EmbeddedChannel 来测试 ChannelHandlers。

Vernlium commented 8 years ago

第五章 Buffer

Buffer API

主要包括

ByteBuf ByteBufHolder

Netty 使用 reference-counting(引用计数)来判断何时可以释放 ByteBuf 或 ByteBufHolder 和其他相关资源,从而可以利用池和其他技巧来提高性能和降低内存的消耗。这一点上不需要开发人员做任何事情,但是在开发 Netty 应用程序时,尤其是使用 ByteBuf 和 ByteBufHolder 时,你应该尽可能早地释放池资源。 Netty 缓冲 API 提供了几个优势:

ByteBuf - 字节数据的容器

因为所有的网络通信最终都是基于底层的字节流传输,因此一个高效、方便、易用的数据接口是必要的,而 Netty 的 ByteBuf 满足这些需求。

ByteBuf 是一个很好的经过优化的数据容器,我们可以将字节数据有效的添加到 ByteBuf 中或从 ByteBuf 中获取数据。ByteBuf 有2部分:一个用于读,一个用于写。我们可以按顺序的读取数据,也可以通过调整读取数据的索引或者直接将读取位置索引作为参数传递给get方法来重复读取数据。

ByteBuf 如何在工作?

写入数据到 ByteBuf 后,writerIndex(写入索引)增加。开始读字节后,readerIndex(读取索引)增加。你可以读取字节,直到写入索引和读取索引处在相同的位置,ByteBuf 变为不可读。当访问数据超过数组的最后位,则会抛出 IndexOutOfBoundsException。

调用 ByteBuf 的 "read" 或 "write" 开头的任何方法都会提升 相应的索引。另一方面,"set" 、 "get"操作字节将不会移动索引位置;他们只会操作相关的通过参数传入方法的相对索引。

可以给ByteBuf指定一个最大容量值,这个值限制着ByteBuf的容量。任何尝试将写入索引超过这个值的行为都将导致抛出异常。ByteBuf 的默认最大容量限制是 Integer.MAX_VALUE。

ByteBuf 类似于一个字节数组,最大的区别是读和写的索引可以用来控制对缓冲区数据的访问。下图显示了一个容量为16的空的 ByteBuf 的布局和状态,writerIndex 和 readerIndex 都在索引位置 0 :

image

ByteBuf 使用模式

HEAP BUFFER(堆缓冲区)

最常用的模式是 ByteBuf 将数据存储在 JVM 的堆空间,这是通过将数据存储在数组的实现。堆缓冲区可以快速分配,当不使用时也可以快速释放。它还提供了直接访问数组的方法,通过 ByteBuf.array() 来获取 byte[]数据。 这种方法,正如清单5.1中所示的那样,是非常适合用来处理遗留数据的。

Listing 5.1 Backing array

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

1.检查 ByteBuf 是否有支持数组。 2.如果有的话,得到引用数组。 3.计算第一字节的偏移量。 4.获取可读的字节数。 5.使用数组,偏移量和长度作为调用方法的参数。

注意:

访问非堆缓冲区 ByteBuf 的数组会导致UnsupportedOperationException, 可以使用 ByteBuf.hasArray()来检查是否支持访问数组。 这个用法与 JDK 的 ByteBuffer 类似.

DIRECT BUFFER(直接缓冲区)

“直接缓冲区”是另一个 ByteBuf 模式。对象的所有内存分配发生在 堆,对不对?好吧,并非总是如此。在 JDK1.4 中被引入 NIO 的ByteBuffer 类允许 JVM 通过本地方法调用分配内存,其目的是

这就解释了为什么“直接缓冲区”对于那些通过 socket 实现数据传输的应用来说,是一种非常理想的方式。如果你的数据是存放在堆中分配的缓冲区,那么实际上,在通过 socket 发送数据之前,JVM 需要将先数据复制到直接缓冲区。

但是直接缓冲区的缺点是在内存空间的分配和释放上比堆缓冲区更复杂,另外一个缺点是如果要将数据传递给遗留代码处理,因为数据不是在堆上,你可能不得不作出一个副本,如下:

Listing 5.2 Direct buffer data access

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

1.检查 ByteBuf 是不是由数组支持。如果不是,这是一个直接缓冲区。 2.获取可读的字节数 3.分配一个新的数组来保存字节 4.字节复制到数组 5.将数组,偏移量和长度作为参数调用某些处理方法

显然,这比使用数组要多做一些工作。因此,如果你事前就知道容器里的数据将作为一个数组被访问,你可能更愿意使用堆内存。

COMPOSITE BUFFER(复合缓冲区)

最后一种模式是复合缓冲区,我们可以创建多个不同的 ByteBuf,然后提供一个这些 ByteBuf 组合的视图。复合缓冲区就像一个列表,我们可以动态的添加和删除其中的 ByteBuf,JDK 的 ByteBuffer 没有这样的功能。

Netty 提供了 ByteBuf 的子类 CompositeByteBuf 类来处理复合缓冲区,CompositeByteBuf 只是一个视图。

警告 CompositeByteBuf.hasArray() 总是返回 false,因为它可能既包含堆缓冲区,也包含直接缓冲区

例如,一条消息由 header 和 body 两部分组成,将 header 和 body 组装成一条消息发送出去,可能 body 相同,只是 header 不同,使用CompositeByteBuf 就不用每次都重新分配一个新的缓冲区。下图显示CompositeByteBuf 组成 header 和 body:

Figure 5.2 CompositeByteBuf holding a header and body

image

下面代码显示了使用 JDK 的 ByteBuffer 的一个实现。两个 ByteBuffer 的数组创建保存消息的组件,第三个创建用于保存所有数据的副本。

Listing 5.3 Composite buffer pattern using ByteBuffer

// 使用数组保存消息的各个部分
ByteBuffer[] message = { header, body };

// 使用副本来合并这两个部分
ByteBuffer message2 = ByteBuffer.allocate(
        header.remaining() + body.remaining());
message2.put(header);
message2.put(body);
message2.flip();

这种做法显然是低效的;分配和复制操作不是最优的方法,操纵数组使代码显得很笨拙。

下面看使用 CompositeByteBuf 的改进版本

Listing 5.4 Composite buffer pattern using CompositeByteBuf

CompositeByteBuf messageBuf = ...;
ByteBuf headerBuf = ...; // 可以支持或直接
ByteBuf bodyBuf = ...; // 可以支持或直接
messageBuf.addComponents(headerBuf, bodyBuf);
// ....
messageBuf.removeComponent(0); // 移除头    //2

for (int i = 0; i < messageBuf.numComponents(); i++) {                        //3
    System.out.println(messageBuf.component(i).toString());
}

1.追加 ByteBuf 实例的 CompositeByteBuf 2.删除 索引1的 ByteBuf 3.遍历所有 ByteBuf 实例。

清单5.4 所示,你可以简单地把 CompositeByteBuf 当作一个可迭代遍历的容器。 CompositeByteBuf 不允许访问其内部可能存在的支持数组,也不允许直接访问数据,这一点类似于直接缓冲区模式,如listing 5.5所示。

Listing 5.5 Access data

CompositeByteBuf compBuf = ...;
int length = compBuf.readableBytes();    //1
byte[] array = new byte[length];        //2
compBuf.getBytes(compBuf.readerIndex(), array);    //3
handleArray(array, 0, length);    //4

1.得到的可读的字节数。 2.分配一个新的数组,数组长度为可读字节长度。 3.读取字节到数组 4.使用数组,把偏移量和长度作为参数

Netty 尝试使用 CompositeByteBuf 优化 socket I/O 操作,消除 原生 JDK 中可能存在的的性能低和内存消耗问题。虽然这是在Netty 的核心代码中进行的优化,并且是不对外暴露的,但是作为开发者还是应该意识到其影响。

Vernlium commented 8 years ago

5.3 字节层面的操作

5.3.1 随机读取

通过ByteBuf的getByte(i)方法随机读取,i的取值范围为0-capacity()-1。

这中读取操作并不会改变readerIndex或者writerIndex的值。要想改变的话可以通过调用readerIndex(index)或者 writerIndex(index)。

Vernlium commented 8 years ago

5月3号到5.3.2节

Vernlium commented 8 years ago

5.3.2 顺序存取

ByteBuf有读和写两个指针reader和writer。而JDK的ByteBuffer只有一个,因此需要调用flip()方法来在读和写模式之间切换。下图展示了ByteBuf被两个指针分割成三个区域。

image

百度

5.3.3 Discardable bytes可废弃字节

上图中标注为discardable的区域表示已经被读取的字节。这些字节可以废弃,这块区域可以通过调用discardReadBytes()来回收利用。这块雨区初始化时存储在readerIndex中,是0,每次read操作执行时此值会增加(get*操作不会移动readerIndex值)。

下图显示了调用discardReadBytes()方法后的结果。可废弃字节变成的可写区域。值得注意的是调用discardReadBytes()方法后,无法保证可写雨区的内容。

image

此方法会移动可读字节到内存的起始位置,因此不用经常调用此方法,建议在只有内存区域不够的时候调用。

Vernlium commented 8 years ago

5月4日早晨5.3.4