Netty开发笔记

前言

Netty 是一款用管道结构、责任链模式来开发网络协议应用的框架。
要用好 netty 有不少的地方需要了解,这里记录了一些 netty 开发的基本概念和使用注意事项。

Netty 和 TCP 协议的关系

Netty 收到的是 第4层TCP 层的数据,Netty 应用做的是7层应用层的工作。所以 Netty 看到的是 TCP 层以字节流方式提供的可靠数据,但不一定是完整的应用层协议单元。

Netty 核心概念

ByteBuf

这个指的是 Netty 自己的 ByteBuf 而不是 Java NIO 的那个。
三个核心类

  • ByteBuf
  • ByteBufHolder
  • ByteBufAllocator

ByteBuf 是 Netty 和 应用程序的数据交互界面,也就是 TCP层(ISO网络协议第4层)与应用层的数据交互界面。
然而这种交互方式是基于字节流的,所以在处理协议单元不完整的时候,需要充分利用 ByteBuf 类。

ByteBuf 使用引用计数来安全地释放 Buf 和资源,这样比 Java 的 ByteBuffer 更高效。并且计数是自动的,不需要使用方关心。

ByteBuf 的优点是:

  • 可以自定义 Buf 类型
  • 零拷贝
  • 容量可以按需扩展
  • 不需要调用 flip() 函数来转换读写模式
  • 独立的 read / write index
  • 方法可以链式调用
  • 应用计数
  • 池化 Pooling

ByteBuf 首先是可以作为一个字节容器来使用。ByteBuf 使用两个 index 来记录 read 和 write 的位置,当你读取一些数据后,只需要修改 read index,那么下次可以从上次离开的位置读起了,非常方便,这对处理应用协议单元数据不够特别有用。

ByteBuf 的 read / write 开头的函数会自动增加 readIndex 和 writeIndex;而 get / set 方法就不会。
如果 read 时 readIndex 等于 writeIndex 时,ByteBuf 进入不可读 unreadable 状态,继续读就会抛出 IndexOutOfBoundsException 异常。

ByteBuf 可以设置一个最大容量,默认是 Integer.MAX_VALUE,如果 writeIndex 超过它就会抛异常。

ByteBuf 有多种实现类型,常见的有三种

  • Heap Buffers,heap space of the JVM,用 java 的 array 实现,分配速度快,但是会被回收,可以返回后面的 array,和 java 交互容易。
ByteBuf heapBuf = ...; 
if (heapBuf.hasArray()) { #1 
 byte[] array = heapBuf.array(); #2 
 int offset = heapBuf.arrayOffset() + heapBuf.position(); #3 
 int length = heapBuf.readableBytes(); #4 
 YourImpl.method(array, offset, length); #5 
} 
#1 Check if ByteBuf is backed by array 
#2 Get reference to array 
#3 Calculate offset of first byte in it 
#4 Get amount of readable bytes 
#5 Call method using array, offset, length as parameter
  • Direct Buffer 在堆外直接分配的内存。它不会显示在 heap memory size 上,需要额外考虑。它送给socket的效率最高,因为 java 会将非 direct 的 buffer 拷贝一份成为 direct buffer 后再给 socket 去传输。但 direct buffer 的分配和销毁耗时。用 pooling 可以解决这个问题。另一个问题是 java 代码需要拷贝数据后才能操作这些数据,代码如下。
ByteBuf directBuf = ...; 
if (!directBuf.hasArray()) { #1 
 int length = directBuf.readableBytes(); #2 
 byte[] array = new byte[length]; #3 
 directBuf.getBytes(array); #4 
 YourImpl.method(array, 0, array.length); #5
}
#1 Check if ByteBuf not backed by array which will be false for direct buffer 
#2 Get number of readable bytes 
#3 Allocate new array with length of readable bytes 
#4 Read bytes into array 
#5 Call method that takes array, offset, length as parameter
  • Composite Buffers 组合Buffer,组合多个 ByteBuf 就像 List 一样。是多个ByteBuf的视图,而不是拷贝他们。而 Java 的 ByteBuffer 类需要用拷贝来组合多个buffer。
CompositeByteBuf compBuf = ...; 
ByteBuf heapBuf = ...; 
ByteBuf directBuf = ...; 
compBuf.addComponent(heapBuf, directBuf); #1 
..... 
compBuf.removeComponent(0); #2 
for (ByteBuf buf: compBuf) { #3 
 System.out.println(buf.toString()); 
} 
#1 Append ByteBuf instances to the composite 
#2 Remove ByteBuf on index 0 (heapBuf here) 
#3 Loop over all the composed ByteBuf

读取 Composite Buffer 和普通的ByteBuf 一样操作即可。

扫描二维码关注公众号,回复: 10714708 查看本文章

下面是 ByteBuf 程序员必看,ByteBuf 的各种操作

ByteBuf 的三个段

0                          readIndex                      writeIndex                   capacity
|     discardable  <=         |      readable     <=          |      writable              |

discardable : 可丢弃段
readable : 可读段
writable : 可写段
capacity : ByteBuf 的当前容量,注意不是 max capacity,max capacity 是不可变的,但是 capacity 只是当前可用的数字,如果writeIndex超过 capacity 后,会尝试扩展,直到 max capacity。

随机访问的索引方法 random access indexing

ByteBuf buffer = ...; 
 for (int i = 0; i < buffer.capacity(); i ++) { 
 byte b = buffer.getByte(i);    
 System.out.println((char) b); 
}

注意:这里 getByte() 不会修改 readIndex;如果需要顺序处理字节那么更高效的方法是用 int forEachByte(ByteProcessor processor)

顺序访问的索引方法 Sequential access indexing

上面说过,顺序访问时要用更高效的 forEachByte 方法,具体方法是先定义一个 ByteProcessor 类,实现 process() 方法,该方法返回 true 告诉 ByteBuf 继续循环,false则停止循环,返回最后访问的 Index。代码如下:

@Override
public boolean process(byte value) throws Exception {
    char nextByte = (char) (value & 0xFF);
    if (nextByte == HttpConstants.CR) {
        return true;
    }
    if (nextByte == HttpConstants.LF) {
        return false;
    }

    if (++ size > maxLength) {
        // TODO: Respond with Bad Request and discard the traffic
        //    or close the connection.
        //       No need to notify the upstream handlers - just log.
        //       If decoding a response, just throw an exception.
        throw newException(maxLength);
    }

    seq.append(nextByte);
    return true;
}

然后通知 buffer 使用它,forEachByte() 有一个隐含的约定,如果 ByteProcessor.process() 返回 false 了则 forEachByte() 返回最后访问的下标;但是如果 process 一直返回 true 导致超过了 buff 的 readable 范围,那么就会返回 -1。用这种约定,我们就能处理“应用协议单元数据不足”的情况了,即不足时,不修改 readIndex,并且返回一些标识如null,让调用方不要生成 out 对象,放入 out list,这样后续的 ChannelHandler 就不会收到 读取数据 的消息。当更多数据到达后,本 channel handler 重新执行一次应用协议解析,得到一个完整的单元,那么就可以生成 out 对象,放入 out list 了。

public AppendableCharSequence parse(ByteBuf buffer) {
    final int oldSize = size;
    seq.reset();
    int i = buffer.forEachByte(this);
    if (i == -1) {
        size = oldSize;
        return null;
    }
    buffer.readerIndex(i + 1);
    return seq;
}

可丢弃的字节

ByteBuf 容器中,小于 readIndex 的数据是可以丢弃的。调用 discardReadBytes() 可以释放这些空间,这能让“可写段”变得更大。注意,这个函数的代价较大,不要频繁调用,的确需要空间时才用。

可读数据

Listing 5.8 Read data 
 // Iterates the readable bytes of a buffer. 
ByteBuf buffer = ...; 
 while (buffer.readable()) { 
 System.out.println(buffer.readByte()); 
 }

查看还有多少可读字节用:int ByteBuf.readableBytes()

可写数据

可写段是需要被填充的“未知数据段”。
writeXXX 开头的函数会自动将 writeIndex 加一。
如果写操作的参数是另外一个 ByteBuf 且未指定源下标,那么会从参数 ByteBuf 的 readIndex 开始读取数据写入,并且自动将参数 ByteBuf 的 readIndex 增加
没有足够的可写空间,抛出 IndexOutOfBoundException 异常
新分配的 ByteBuf 写下标是 0

// Fills the writable bytes of a buffer with random integers. 
ByteBuf buffer = ...; 
 while (buffer.writableBytes() >= 4) { 
   buffer.writeInt(random.nextInt());
 }

清除 buffer 下标

clear() 函数会将 readIndex 和 writeIndex 设为 0,但不会清除数据,它非常快速,因为只是移动了下标,比 discardReadBytes() 廉价多了。

搜索操作

indexOf( from, to, byte ) 函数可以从来查找某些字节。另外还有 bytesBefore(byte) 函数可以用来处理类似 null-end-string 的情况。更复杂的情况可以用 ByteBufProcessor 或者自己读出字节来判断。

另外,Netty 内部使用了一个 AppendableCharSequence 来作为 char 的容器。【为什么不用 StringBuffer 呢?如果是我们也许就直接用 StringBuffer 了吧,猜测是性能更好。】

Mark and Reset index

我们可以给当前的 read/write Index 做一个标记,用函数 markReaderIndex() / markWriterIndex(),之后,用 resetReaderInde() 来让 readIndex 等于之前标记的读下标,但是无效的 mark 会触发异常。markWriterIndex 也类似地工作。

派生的 buffers

TODO

read / write 操作

TODO

ByteBufHolder

应用程序通过继承 DefaultByteBufHolder 来实现。

为什么要用 ByteBufHolder 来包裹 ByteBuf 呢?我想是因为需要一个不同于 ByteBuf 的类型,这样在 netty 的pipeline处理流程中才能区分哪个 channel handler 可以处理这个 message 对象。

ByteBufAllocator

TODO

Unpooled 类和 ByteBufUtil 工具类

Unpooled 可以用来在无法获取 ByteBufAllocator 的情况下创建 ByteBuf。

buffer() 
buffer(int) 
buffer(int, int)            Returns an unpooled ByteBuf of type heap. 
directBuffer() 
directBuffer(int) 
directBuffer(int, int)      Returns an unpooled ByteBuf of type direct. 
wrappedBuffer()             Returns a ByteBuf, which wraps the given data. 
copiedBuffer()              Returns a ByteBuf, which copies the given data and uses it .
ByteBufUtil.hexdump()       输出16进制数据字符串,多用于调试

Pipeline、ChannelHandler 以及 Codec

TODO

需要知道的是,Codec 比如 Encoder、Decoder 是派生自 ChannelHandler 的。

有用的类 SimpleChannelInboundHandler。

让下一个 channel handler 处理 message 是靠调用 ChannelInboundHandler.channelRead() 函数来实现的,这个逻辑在 SimpleChannelInboundHandler 或 ChannelInboundHandlerAdapter 的 channelRead() 函数中,但是 SimpleChannelInboundHandler 并不简单,反而功能更强大,支持基于message类型的检查,确定本 handler 是否应该处理该 message;减少对 message 的引用计数。

SimpleChannelInboundHandler 减少引用计数的操作正好和前面提到过的 ByteBufHolder 对应,正因为它支持引用计数才能在这里作为一个 message 正常工作。

触发 Channel 读数据是靠 ChannelOutboundInvoker.read() 函数来触发的,为什么这个函数放在了 OutboundInvoker 中而不是 InboundInvoke ? 读数据不应该是 Inbound 的事情吗?我们可以这样来理解,一般情况下是因为“写Outbound的操作结束”了,我们才需要从 Channel 读数据来继续处理,所以放在了 OutboundInvoker 中。

这个函数会发起让 channel 读数据的请求,具体是如何实现的呢?原来OutboundInvoker 接口的实现类会让 pipeline 中“下一个Outbound handler”执行 read() 函数,一直递归到最前面的 handler,最前面是 Channel 对象,它也实现了 ChannelOutboundInvoker 接口,并且 read() 函数是读取网络字节。【这个解释好像不对,还需要深入研究,可以看下这篇文章。或者设断点跟踪一下。】

对实现 Proxy 有帮助的类

io.netty.handler.proxy.HttpProxyHandler,它是派生自 io.netty.handler.proxy.ProxyHandler,是一个“全向Handler”,即同时可以作为 InBound 和 OutBound channel handler 来使用。

关于 TCP 协议

The data packet looks different at each layer, and at each layer it goes by a different name. The names for the data packages created at each layer are as follows:

  • The data package created at the Application layer is called a message.
  • The data package created at the Transport layer, which encapsulates the Application layer message, is called a segment if it comes from the Transport layer’s TCP protocol. If the data package comes from the Transport layer’s
  • User Datagram Protocol (UDP) protocol, it is called a datagram.
  • The data package at the Internet layer, which encapsulates the Transport layer segment, is called a datagram.
  • The data package at the Network Access layer, which encapsulates and may subdivide the datagram, is called a frame. This frame is then turned into a bitstream at the lowest sublayer of the Network Access layer.

TCP资料

笔记

  • TCP 分组segment中的ack 和 seq 的关系是,ack 由接收方设置,指示下一个segment的seq值。
  • 应用层的数据(PDU - protocol data unit)可能会被拆分为多个 TCP segment 进行发送,接收方会收到多个 segment,但 TCP 向应用层提供的是一个字节流,应用层是看不到所谓 segment 的。Netty ChannelHandler 在解析一个完整的应用层协议 message 时,要用 netty 的 ByteBuffer 来读取字节流,对于还不是一个完整的协议单元的数据只需跳过,不向Pipeline写入 out 对象即可,等待后续更多的字节数据到达后再进行处理,处理数据后,设置 ByteBuffer 的 readPosition 为正确的位置即可。
  • 读取 netty ByteBuffer 数据时,要用 ByteBuffer.forEachByte() 函数来处理,因为这样不会因为遍历而更新 ByteBuffer 的 readPosition,为不完整协议单元的再次处理创造了条件。
  • 要能正确使用 netty 编写一个网络协议应用,需要掌握下面的知识:
    • Netty 的基本核心概念,比如 Bootstrap,Pipeline,Channel,ChannelHandler,ByteBuffer (netty的)
    • TCP 协议基本概念
    • Java 中的 NIO 和 OIO
    • Java 中的多线程同步与通信
    • 协议本身,尤其是 HTTP 协议
    • 阅读一下 Netty 的 HTTP相关 Codec 的实现代码
    • 阅读一遍《Netty In Action》

参考

资料

发布了63 篇原创文章 · 获赞 25 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/yangbo_hr/article/details/104589245