理解Netty中的零拷贝(Zero-Copy)机制【转】

理解零拷贝

零拷贝是Netty的重要特性之一,而究竟什么是零拷贝呢? 
WIKI中对其有如下定义:

“Zero-copy” describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.

从WIKI的定义中,我们看到“零拷贝”是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。

Non-Zero Copy方式: 

Zero Copy方式: 

从上图中可以清楚的看到,Zero Copy的模式中,避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都实现了零拷贝的功能,而在Netty中也通过在FileRegion中包装了NIO的FileChannel.transferTo()方法实现了零拷贝。

而在Netty中还有另一种形式的零拷贝,即Netty允许我们将多段数据合并为一整段虚拟数据供用户使用,而过程中不需要对数据进行拷贝操作,这也是我们今天要讲的重点。我们都知道在stream-based transport(如TCP/IP)的传输过程中,数据包有可能会被重新封装在不同的数据包中,例如当你发送如下数据时:

有可能实际收到的数据如下:

因此在实际应用中,很有可能一条完整的消息被分割为多个数据包进行网络传输,而单个的数据包对你而言是没有意义的,只有当这些数据包组成一条完整的消息时你才能做出正确的处理,而Netty可以通过零拷贝的方式将这些数据包组合成一条完整的消息供你来使用。而此时,零拷贝的作用范围仅在用户空间中。

Netty3中零拷贝的实现机制

以下以Netty 3.8.0.Final的源代码来进行说明

ChannelBuffer接口

Netty为需要传输的数据制定了统一的ChannelBuffer接口。该接口的主要设计思路如下:

  • 使用getByte(int index)方法来实现随机访问
  • 使用双指针的方式实现顺序访问
    • 每个Buffer都有一个读指针(readIndex)和写指针(writeIndex)
    • 在读取数据时读指针后移,在写入数据时写指针后移 

定义了统一的接口之后,就是来做各种实现了。Netty主要实现了HeapChannelBuffer,ByteBufferBackedChannelBuffer等等,下面我们就来讲讲与Zero Copy直接相关的CompositeChannelBuffer类。

CompositeChannelBuffer类

CompositeChannelBuffer类的作用是将多个ChannelBuffer组成一个虚拟的ChannelBuffer来进行操作。为什么说是虚拟的呢,因为CompositeChannelBuffer并没有将多个ChannelBuffer真正的组合起来,而只是保存了他们的引用,这样就避免了数据的拷贝,实现了Zero Copy。 
下面我们来看看具体的代码实现,首先是成员变量

private int readerIndex;
private int writerIndex;
private ChannelBuffer[] components;
private int[] indices;
private int lastAccessedComponentId;

以上这里列出了几个比较重要的成员变量。其中readerIndex既读指针和writerIndex既写指针是从AbstractChannelBuffer继承而来的;然后components是一个ChannelBuffer的数组,他保存了组成这个虚拟Buffer的所有子Buffer,indices是一个int类型的数组,它保存的是各个Buffer的索引值;最后的lastAccessedComponentId是一个int值,它记录了最后一次访问时的子Buffer ID。从这个数据结构,我们不难发现所谓的CompositeChannelBuffer实际上就是将一系列的Buffer通过数组保存起来,然后实现了ChannelBuffer 的接口,使得在上层看来,操作这些Buffer就像是操作一个单独的Buffer一样。

创建

接下来,我们再看一下CompositeChannelBuffer.setComponents方法,它会在初始化CompositeChannelBuffer时被调用。

/**
 * Setup this ChannelBuffer from the list
 */
private void setComponents(List<ChannelBuffer> newComponents) {
    assert !newComponents.isEmpty();

    // Clear the cache.
    lastAccessedComponentId = 0;

    // Build the component array.
    components = new ChannelBuffer[newComponents.size()];
    for (int i = 0; i < components.length; i ++) {
        ChannelBuffer c = newComponents.get(i);
        if (c.order() != order()) {
            throw new IllegalArgumentException(
                    "All buffers must have the same endianness.");
        }

        assert c.readerIndex() == 0;
        assert c.writerIndex() == c.capacity();

        components[i] = c;
    }

    // Build the component lookup table.
    indices = new int[components.length + 1];
    indices[0] = 0;
    for (int i = 1; i <= components.length; i ++) {
        indices[i] = indices[i - 1] + components[i - 1].capacity();
    }

    // Reset the indexes.
    setIndex(0, capacity());
}

通过代码可以看到该方法的功能就是将一个ChannelBuffer的List给组合起来。它首先将List中得元素放入到components数组中,然后创建indices用于数据的查找,最后使用setIndex来重置指针。这里需要注意的是setIndex(0, capacity())会将读指针设置为0,写指针设置为当前Buffer的长度,这也就是前面需要做assert c.readerIndex() == 0assert c.writerIndex() == c.capacity()这两个判断的原因,否则很容易会造成数据重复读写的问题,所以Netty推荐我们使用ChannelBuffers.wrappedBuffer方法来进行Buffer的合并,因为在该方法中Netty会通过slice()方法来确保构建CompositeChannelBuffer是传入的所有子Buffer都是符合要求的。

数据访问

CompositeChannelBuffer.getByte(int index)的实现如下:

public byte getByte(int index) {
    int componentId = componentId(index);
    return components[componentId].getByte(index - indices[componentId]);
}

从代码我们可以看到,在随机查找时会首先通过index获取这个字节所在的componentId既字节所在的子Buffer序列,然后通过index - indices[componentId]计算出它在这个子Buffer中的第几个字节,然后返回结果。

下面再来看一下componentId(int index)的实现:

private int componentId(int index) {
    int lastComponentId = lastAccessedComponentId;
    if (index >= indices[lastComponentId]) {
        if (index < indices[lastComponentId + 1]) {
            return lastComponentId;
        }

        // Search right
        for (int i = lastComponentId + 1; i < components.length; i ++) {
            if (index < indices[i + 1]) {
                lastAccessedComponentId = i;
                return i;
            }
        }
    } else {
        // Search left
        for (int i = lastComponentId - 1; i >= 0; i --) {
            if (index >= indices[i]) {
                lastAccessedComponentId = i;
                return i;
            }
        }
    }

    throw new IndexOutOfBoundsException("Invalid index: " + index + ", maximum: " + indices.length);
}

从代码中我们发现,Netty以lastComponentId既上次访问的子Buffer序号为中心,向左右两边进行搜索,这样做的目的是,当我们两次随机查找的字符序列相近时(大部分情况下都是这样),可以最快的搜索到目标索引的componentId

参考资料

  1. http://my.oschina.net/flashsword/blog/164237
  2. http://en.wikipedia.org/wiki/Zero-copy
  3. http://stackoverflow.com/questions/20727615/is-nettys-zero-copy-different-from-os-level-zero-copy
  4. http://www-old.itm.uni-luebeck.de/teaching/ws1112/vs/Uebung/GrossUebungNetty/VS-WS1112-xx-Zero-Copy_Event-Driven_Servers_with_Netty.pdf?lang=de
时间: 2024-08-27 22:28:51

理解Netty中的零拷贝(Zero-Copy)机制【转】的相关文章

Linux 中的零拷贝技术,第 1 部分

概述 本系列由两篇文章组成,介绍了当前用于 Linux 操作系统上的几种零拷贝技术,简单描述了各种零拷贝技术的实现,以及它们的特点和适用场景.本文是本系列文章的第一部分,主要是介绍一些零拷贝技术的相关背景知识,简要概述了 Linux 为什么需要零拷贝技术以及 Linux 中都有哪几种零拷贝技术. 1 评论: 黄 晓晨, 软件工程师, IBM 冯 瑞, 软件工程师, IBM 2011 年 1 月 27 日 内容 在 IBM Bluemix 云平台上开发并部署您的下一个应用. 现在就开始免费试用 引

Linux 中的零拷贝技术,第 2 部分

技术实现 本系列由两篇文章组成,介绍了当前用于 Linux 操作系统上的几种零拷贝技术,简单描述了各种零拷贝技术的实现,以及它们的特点和适用场景.第一部分主要介绍了一些零拷贝技术的相关背景知识,简要概述了 Linux 为什么需要零拷贝技术以及 Linux 中都有哪几种零拷贝技术.本文是本系列文章的第二部分,针对第一部分内容中提到的几种零拷贝技术分别进行更详细的介绍,并对这些零拷贝技术的优缺点进行分析. 0 评论: 黄 晓晨, 软件工程师, IBM 冯 瑞, 软件工程师, IBM 2011 年 1

深入剖析Linux IO原理和几种零拷贝机制的实现

深入剖析Linux IO原理和几种零拷贝机制的实现 来源 https://zhuanlan.zhihu.com/p/83398714 零壹技术栈      公众号[零壹技术栈] 前言 零拷贝(Zero-copy)技术指在计算机执行操作时,CPU 不需要先将数据从一个内存区域复制到另一个内存区域,从而可以减少上下文切换以及 CPU 的拷贝时间.它的作用是在数据报从网络设备到用户程序空间传递的过程中,减少数据拷贝次数,减少系统调用,实现 CPU 的零参与,彻底消除 CPU 在这方面的负载.实现零拷贝

彻底理解Netty,这一篇文章就够了

Netty到底是什么 从HTTP说起 有了Netty,你可以实现自己的HTTP服务器,FTP服务器,UDP服务器,RPC服务器,WebSocket服务器,Redis的Proxy服务器,MySQL的Proxy服务器等等. 我们回顾一下传统的HTTP服务器的原理 1.创建一个ServerSocket,监听并绑定一个端口 2.一系列客户端来请求这个端口 3.服务器使用Accept,获得一个来自客户端的Socket连接对象 4.启动一个新线程处理连接 4.1.读Socket,得到字节流 4.2.解码协议

Linux网络编程——sendfile和splice零拷贝

sendfile和splice零拷贝 sendfile: ? 原始:首先将文件读到内核态的文件描述符中,然后再拷贝给用户态buf,再重新拷贝给内核态中网络缓冲区发给客户端.拷贝2次 ? sendfile是一个接口能直接让文件从内核态的文件描述符送到网络缓冲区从而实现零拷贝 ? 函数原型:ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); ? out_fd为文件描述符,in_fd为网络缓冲区描述符,offset

Netty 零拷贝(三)Netty 对零拷贝的改进

Netty 零拷贝(三)Netty 对零拷贝的改进 Netty 系列目录 (https://www.cnblogs.com/binarylei/p/10117436.html) Netty 的"零拷贝"主要体现以下几个方面: Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝.如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer

Netty中的那些坑

Netty中的那些坑(上篇) 最近开发了一个纯异步的redis客户端,算是比较深入的使用了一把netty.在使用过程中一边优化,一边解决各种坑.儿这些坑大部分基本上是Netty4对Netty3的改进部分引起的. 注:这里说的坑不是说netty不好,只是如果这些地方不注意,或者不去看netty的代码,就有可能掉进去了. 坑1: Netty 4的线程模型转变 在Netty 3的时候,upstream是在IO线程里执行的,而downstream是在业务线程里执行的.比如netty从网络读取一个包传递给

【iOS面试系列-1】iOS中@property 后assign,copy,retain,weak,strong的意义与区别(必考-必须详细掌握)

来源:网络 assign: 普通(简单)赋值,一般常用于基本数据类型,常见委托设计模式,一次来防止循环引用.不更改索引计数(Reference Counting). 对基础数据类型 (NSInteger,CGFloat)和C数据类型(int, float, double, char, 等等) retain:保留计数,获得到了对象的所有权,引用计数在原有基础上+1.释放旧的对象,将旧对象的值赋予输入对象,再提高输入对象的索引计数为1 .我们给那块内存设一个引用计数,当内存被分配并且赋值给a时,引用

C语言中的位拷贝与值拷贝浅谈(转载)

注:C语言实现的PHP变量的赋值过程中,就涉及到了 深拷贝和浅拷贝 位拷贝拷贝的是地址(也叫浅拷贝),而值拷贝则拷贝的是内容(深拷贝).深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝. 位拷贝,及"bitwise assignment"是指将一个对象的内存映像按位原封不动的复制给另一个对象,所谓值拷贝就是指,将原对象的值复制一份给新对象. 在用"bitwise assig