Netty源码解读(二)Netty中的buffer

感谢网友【黄亿华】投递本稿。

上一篇文章我们概要介绍了Netty的原理及结构,下面几篇文章我们开始对Netty的各个模块进行比较详细的分析。Netty的结构最底层是buffer模块,这部分也相对独立,我们就先从buffer讲起。

What: buffer二三事

buffer中文名又叫缓冲区,按照维基百科的解释,是”在数据传输时,在内存里开辟的一块临时保存数据的区域”。它其实是一种化同步为异步的机制,可以解决数据传输的速率不对等以及不稳定的问题。

根据这个定义,我们可以知道涉及I/O(特别是I/O写)的地方,基本会有buffer的存在。就Java来说,我们非常熟悉的Old I/O–InputStream&OutputStream系列API,基本都是在内部使用到了buffer。Java课程老师就教过,outputStream.write()只将内容写入了buffer,必须调用outputStream.flush(),才能保证数据写入生效!

而NIO中则直接将buffer这个概念封装成了对象,其中最常用的大概是ByteBuffer了。于是使用方式变为了:将数据写入Buffer,flip()一下,然后将数据读出来。于是,buffer的概念更加深入人心了!

Netty中的buffer也不例外。不同的是,Netty的buffer专为网络通讯而生,所以它又叫ChannelBuffer(好吧其实没有什么因果关系…)。我们下面就来讲讲Netty中的buffer。当然,关于Netty,我们必须讲讲它的所谓”Zero-Copy-Capable”机制。

When & Where: TCP/IP协议与buffer

TCP/IP协议是目前的主流网络协议。它是一个多层协议,最下层是物理层,最上层是应用层(HTTP协议等),而在Java开发中,一般只接触TCP以上,即传输层和应用层的内容。这就是Netty的主要应用场景。

TCP报文有个比较大的特点,就是它传输的时候,会先把应用层的数据项拆开成字节,然后按照自己的传输需要,选择合适数量的字节进行传输。什么叫”自己的传输需要”?首先TCP包有最大长度限制,那么太大的数据项肯定是要拆开的。其次因为TCP以及下层协议会附加一些协议头信息,如果数据项太小,那么可能报文大部分都是没有价值的头信息,这样传输是很不划算的。因此有了收集一定数量的小数据,并打包传输的Nagle算法(这个东东在HTTP协议里会很讨厌,Netty里可以用setOption(“tcpNoDelay”, true)关掉它)。

这么说可能太抽象了一点,我们举个例子吧:

发送时,我们这样分3次写入(‘|’表示两个buffer的分隔):

   +-----+-----+-----+
   | ABC | DEF | GHI |
   +-----+-----+-----+

接收时,可能变成了这样:

   +----+-------+---+---+
   | AB | CDEFG | H | I |
   +----+-------+---+---+

很好懂吧?可是,说了这么多,跟buffer有个什么关系呢?别急,我们来看下面一部分。

Why: buffer中的分层思想

我们先回到之前的`messageReceived`方法:

1 public void messageReceived(
2         ChannelHandlerContext ctx, MessageEvent e) {
3     // Send back the received message to the remote peer.
4     transferredBytes.addAndGet(((ChannelBuffer) e.getMessage()).readableBytes());
5     e.getChannel().write(e.getMessage());
6 }

这里MessageEvent.getMessage()默认的返回值是一个ChannelBuffer。我们知道,业务中需要的”Message”,其实是一条应用层级别的完整消息,而一般的buffer工作在传输层,与”Message”是不能对应上的。那么这个ChannelBuffer是什么呢?

来一个官方给的图,我想这个答案就很明显了:

这里可以看到,TCP层HTTP报文被分成了两个ChannelBuffer,这两个Buffer对我们上层的逻辑(HTTP处理)是没有意义的。但是两个ChannelBuffer被组合起来,就成为了一个有意义的HTTP报文,这个报文对应的ChannelBuffer,才是能称之为”Message”的东西。这里用到了一个词”Virtual Buffer”,也就是所谓的”Zero-Copy-Capable Byte Buffer”了。是不是顿时觉得豁然开朗了?

我这里总结一下,如果要说NIO的Buffer和Netty的ChannelBuffer最大的区别的话,就是前者仅仅是传输上的Buffer,而后者其实是传输Buffer和抽象后的逻辑Buffer的结合。延伸开来说,NIO仅仅是一个网络传输框架,而Netty是一个网络应用框架,包括网络以及应用的分层结构。

当然,使用ChannelBuffer表示”Message”,不失为一个比较实用的方法,但是使用一个对象来表示解码后的Message可能更符合习惯一点。在Netty里,MessageEvent.getMessage()是可以存放一个POJO的,这样子抽象程度又高了一些,这个我们在以后讲到ChannelPipeline的时候会说到。

How: Netty中的ChannelBuffer及实现

好了,终于来到了代码实现部分。之所以啰嗦了这么多,因为我觉得,关于”Zero-Copy-Capable Rich Byte Buffer”,理解为什么需要它,比理解它是怎么实现的,可能要更重要一点。

关于代码阅读,我想可能很多朋友跟我一样,喜欢”顺藤摸瓜”式读代码–找到一个入口,然后顺着查看它的调用,直到理解清楚。很幸运,ChannelBuffers(注意有s!)就是这样一根”藤”,它是所有ChannelBuffer实现类的入口,它提供了很多静态的工具方法来创建不同的Buffer,靠“顺藤摸瓜”式读代码方式,大致能把各种ChannelBuffer的实现类摸个遍。先列一下ChannelBuffer相关类图。

此外还有WrappedChannelBuffer系列也是继承自AbstractChannelBuffer,图放到了后面。

ChannelBuffer中的readerIndex和writerIndex

Netty中的buffer是完全重新实现的,与NIO ByteBuffer与ByteBuffer不同的是,它内部保存了一个读指针readerIndex和一个写指针writerIndex,可以同时进行读和写,而不需要使用flip()进行读写切换。AbstactChannelBuffer类里面包含了主要的读写逻辑,贴一段代码,让大家能看的更明白一点:

01 public void writeByte(int value) {
02 setByte(writerIndex ++, value);
03 }
04  
05 public byte readByte() {
06 if (readerIndex == writerIndex) {
07 throw new IndexOutOfBoundsException("Readable byte limit exceeded: "
08 + readerIndex);
09 }
10 return getByte(readerIndex ++);
11 }
12  
13 public int writableBytes() {
14 return capacity() - writerIndex;
15 }
16  
17 public int readableBytes() {
18 return writerIndex - readerIndex;
19 }

这里readerIndex总是小于writerIndex。我觉得这样的方式非常自然,比单指针与flip()要更加好理解一些。AbstactChannelBuffer还有两个相应的mark指针markedReaderIndexmarkedWriterIndex,跟NIO的原理一样,作标记用,这里不再赘述了。

字节序Endianness与HeapChannelBuffer

HeapChannelBuffer是最常用的Buffer,跟NIO HeapByteBuffer作用相当,其底层也是一个byte[]。

HeapChannelBuffer有两个子类:BigEndianHeapChannelBufferLittleEndianHeapChannelBuffer。这里有个很基础的概念:字节序(ByteOrder/Endianness)。字节序规定了多于一个字节的数字(int啊long什么的),如何在内存中表示。BIG_ENDIAN(大端序)表示高位在前,按照大端序,整型数12会被存储为0 0 0 12这样四个字节,而LITTLE_ENDIAN则正好相反。可能搞C/C++的程序员对这个会比较熟悉,而Javaer则比较陌生一点,因为Java已经把内存给管理好了。但是在网络编程方面,根据协议的不同,不同的字节序也可能会被用到。目前大部分协议还是采用大端序,可参考RFC1700

了解了这些知识,我们也很容易就知道为什么会有BigEndianHeapChannelBufferLittleEndianHeapChannelBuffer了。

DynamicChannelBuffer

DynamicChannelBuffer是一个很方便的Buffer,之所以叫Dynamic是因为它的长度会根据内容的长度来扩充,你可以像使用ArrayList一样,无须关心其容量。DynamicChannelBuffer实现自动扩容的核心在于ensureWritableBytes方法,算法很简单:在写入前做容量检查,容量不够时,新建一个容量x2的buffer,跟ArrayList的扩容是相同的。贴一段代码吧(为了代码易懂,这里我删掉了一些边界检查,只保留主逻辑):

01 public void writeByte(int value) {
02     ensureWritableBytes(1);
03     super.writeByte(value);
04 }
05  
06 public void ensureWritableBytes(int minWritableBytes) {
07     if (minWritableBytes <= writableBytes()) {
08         return;
09     }
10  
11     int newCapacity = capacity();
12     int minNewCapacity = writerIndex() + minWritableBytes;
13     while (newCapacity < minNewCapacity) {
14         newCapacity <<= 1;
15     }
16  
17     ChannelBuffer newBuffer = factory().getBuffer(order(), newCapacity);
18     newBuffer.writeBytes(buffer, 0, writerIndex());
19     buffer = newBuffer;
20 }

CompositeChannelBuffer

CompositeChannelBuffer是由多个ChannelBuffer组合而成的,可以看做一个整体进行读写。这里有一个技巧:CompositeChannelBuffer并不会开辟新的内存并直接复制所有ChannelBuffer内容,而是直接保存了所有ChannelBuffer的引用,并在子ChannelBuffer里进行读写,从而实现了”Zero-Copy-Capable”。来段简略版的代码,应该更能说明其原理:

01 public class CompositeChannelBuffer{
02  
03     //components保存所有内部ChannelBuffer
04     private ChannelBuffer[] components;
05     //indices记录在整个CompositeChannelBuffer中,每个components的起始位置
06     private int[] indices;
07     //缓存上一次读写的componentId
08     private int lastAccessedComponentId;
09  
10     public byte getByte(int index) {
11         //通过indices中记录的位置索引到对应第几个子Buffer
12         int componentId = componentId(index);
13         return components[componentId].getByte(index - indices[componentId]);
14     }
15  
16     public void setByte(int index, int value) {
17         int componentId = componentId(index);
18         components[componentId].setByte(index - indices[componentId], value);
19     }
20  
21 }

查找componentId的算法再次不作介绍了,大家自己实现起来也不会太难。值得一提的是,基于ChannelBuffer连续读写的特性,使用了顺序查找(而不是二分查找),并且用lastAccessedComponentId来进行缓存。

ByteBufferBackedChannelBuffer

前面说ChannelBuffer是自己的实现的,其实只说对了一半。ByteBufferBackedChannelBuffer就是封装了NIO ByteBuffer的类,用于实现堆外内存的Buffer(使用NIO的DirectByteBuffer)。当然,其实它也可以放其他的ByteBuffer的实现类。代码实现就不说了,也没啥可说的。

WrappedChannelBuffer

WrappedChannelBuffer都是几个对已有ChannelBuffer进行包装,完成特定功能的类。代码不贴了,实现都比较简单,列一下功能吧。

类名 入口 功能
SlicedChannelBuffer ChannelBuffer.slice()
ChannelBuffer.slice(int,int)
某个ChannelBuffer的一部分
TruncatedChannelBuffer ChannelBuffer.slice()
ChannelBuffer.slice(int,int)
某个ChannelBuffer的一部分, 可以理解为其实位置为0的SlicedChannelBuffer
DuplicatedChannelBuffer ChannelBuffer.duplicate() 与某个ChannelBuffer使用同样的存储, 区别是有自己的index
ReadOnlyChannelBuffer ChannelBuffers
.unmodifiableBuffer(ChannelBuffer)
不可变的buffer

至此Netty 3.7的buffer部分我们基本了解了,相关内容还是比较简单的,也没有太多费脑细胞的地方。

Netty 4.0之后就不同了,ChannelBuffer改名ByteBuf,成为了单独项目buffer,并且为了性能优化,加入了BufferPool之类的机制,已经变得比较复杂了(本质倒没怎么变)。性能优化是个比较复杂的事情,研究源码时,建议先避开这些东西,了解其整体结构,等到需要深入时再对算法进行细致研究。举个例子,Netty4.0里为了优化,将Map换成了Java 8里6000行的ConcurrentHashMapV8,你们感受一下…

下篇文章我们开始讲Channel。

时间: 2024-11-16 01:45:06

Netty源码解读(二)Netty中的buffer的相关文章

Netty源码解读(一)概述

感谢网友[黄亿华]投递本稿. Netty和Mina是Java世界非常知名的通讯框架.它们都出自同一个作者,Mina诞生略早,属于Apache基金会,而Netty开始在Jboss名下,后来出来自立门户netty.io.关于Mina已有@FrankHui的Mina系列文章,我正好最近也要做一些网络方面的开发,就研究一下Netty的源码,顺便分享出来了. Netty目前有两个分支:4.x和3.x.4.0分支重写了很多东西,并对项目进行了分包,规模比较庞大,入手会困难一些,而3.x版本则已经被广泛使用.

SEDA源码解读(二)

接着上一篇的话题,本篇继续探讨SEDA的实践项目--sandstorm. 首先,看看package里面的类文件: ResponseTimeControllerIF:该接口代表一个响应时间的控制器,通常情况下被stage的线程管理器执行,以执行事件准入控制策略来达到特定响应时间的目标. StageStatsIF:该接口允许各种各样的系统组件在执行时记录以及收集关于stage的统计. StageWrapperIF:它是一个应用程序stage的内部表示,一个applicationstage包含一个ev

Netty源码解读(四)Netty与Reactor模式

一:Netty.NIO.多线程? 时隔很久终于又更新了!之前一直迟迟未动也是因为积累不够,后面比较难下手.过年期间@李林锋hw发布了一个Netty5.0架构剖析和源码解读 ,看完也是收获不少.前面的文章我们分析了Netty的结构,这次咱们来分析最错综复杂的一部分-Netty中的多线程以及NIO的应用. 理清NIO与Netty的关系之前,我们必须先要来看看Reactor模式.Netty是一个典型的多线程的Reactor模式的使用,理解了这部分,在宏观上理解Netty的NIO及多线程部分就不会有什么

Netty源码解读(三)Channel与Pipeline

Channel是理解和使用Netty的核心.Channel的涉及内容较多,这里我使用由浅入深的介绍方法.在这篇文章中,我们主要介绍Channel部分中Pipeline实现机制.为了避免枯燥,借用一下<盗梦空间>的"梦境"概念,希望大家喜欢. 一层梦境:Channel实现概览 在Netty里,Channel是通讯的载体,而ChannelHandler负责Channel中的逻辑处理. 那么ChannelPipeline是什么呢?我觉得可以理解为ChannelHandler的容器

jQuery 1.5 源码解读 面向中高阶JSER_jquery

几乎很难从jQuery分离其中的一部分功能.所以在这里我分享下应该读 jQuery 源码的一些成果,以及读源码的方法.啃代码是必须的. 1. 代码折叠是必须的. 因此必须在支持语法折叠的编辑器里打开源码. 根据折叠层次,我们可以很快知道: 所有 jQuery 的代码都在一个函数中: (function( window, undefined ) {// jQuery 代码 })(window); 这样可以避免内部对象污染全局.传入的参数1是 window, 参数2是 undefined , 加快j

jQuery选择器源码解读(二):select方法_jquery

/* * select方法是Sizzle选择器包的核心方法之一,其主要完成下列任务: * 1.调用tokenize方法完成对选择器的解析 * 2.对于没有初始集合(即seed没有赋值)且是单一块选择器(即选择器字符串中没有逗号), * 完成下列事项: * 1) 对于首选择器是ID类型且context是document的,则直接获取对象替代传入的context对象 * 2) 若选择器是单一选择器,且是id.class.tag类型的,则直接获取并返回匹配的DOM元素 * 3) 获取最后一个id.cl

Ajax::prototype 源码解读_javascript技巧

AJAX之旅(1):由prototype_1.3.1进入javascript殿堂-类的初探  还是决定冠上ajax的头衔,毕竟很多人会用这个关键词搜索.虽然我认为这只是个炒作的概念,不过不得不承认ajax叫起来要方便多了.所以ajax的意思我就不详细解释了. 写这个教程的起因很简单:经过一段时间的ajax学习,有一些体会,并且越发认识到ajax技术的强大,所以决定记录下来,顺便也是对自己思路的整理.有关这个教程的后续,请关注http://www.x2design.net 前几年,javascri

CI框架源码解读之利用Hook.php文件完成功能扩展的方法_php实例

本文实例讲述了CI框架源码解读之利用Hook.php文件完成功能扩展的方法.分享给大家供大家参考,具体如下: 看了hook.php的源码,就知道CI使用hook来进行扩展的原理了. hook的基本知识 http://codeigniter.org.cn/user_guide/general/hooks.html CI中hook的使用经历了一个:开启hook,定义hook,调用hook,执行hook的过程. 手册中已经告知了开启.定义.调用的方法.那么hook的实现原理是啥呢. <?php if

一个引用java接口但任何没有实现的源码解读

问题描述 一个引用java接口但任何没有实现的源码解读 有个类一直无法理解,情况是这样的,该类有个内部接口,确定没有任何实现方法,怀疑代码不全需要自己补充,请高手支招确定下,第一次发帖,望大家捧捧场,谢谢! PresenceEventDispatcher继承的一个引用自己内部接口的抽象类,有"<"刚被csdn隐藏了 public class PresenceEventDispatcher extends EventDispatcher<PresenceEventDispat