Young's blog Young's blog
首页
Spring
  • 前端文章1

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Young

首页
Spring
  • 前端文章1

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 【Netty】从 BIO、NIO 聊到 Netty
  • 【Netty】Netty组件介绍
  • 【Netty】传输(Transport)
    • 多路复用 I/O 模型详解, 为什么他能支持更高的并发
    • 万字详解!Netty经典32连问!
    • netty
    andanyang
    2023-04-03
    目录

    【Netty】传输(Transport)

    # 传输(Transport)

    在网络中传递的数据总是具有相同的类型:字节。 这些字节流动的细节取决于网络传输,它是一个帮我们抽象 底层数据传输机制的概念,我们不需要关心字节流动的细节,只需要确保字节被可靠的接收和发送。

    当我们使用 Java 网络编程时,可能会接触到多种不同的网络 IO 模型,如 NIO,BIO(OIO: Old IO),AIO 等,我们可能因为 使用这些不同的 API 而遇到问题。 Netty 则为这些不同的 IO 模型实现了一个通用的 API,我们使用这个通用的 API 比直接使用 JDK 提供的 API 要 简单的多,且避免了由于使用不同 API 而带来的问题,大大提高了代码的可读性。 在传输这一部分,我们将主要学习这个通用的 API,以及它与 JDK 之间的对比。

    # 传输 API

    传输 API 的核心是 Channel(io.netty.Channel,而非 java nio 的 Channel)接口,它被用于所有的 IO 操作。

    Channel 结构层次:

    Channel接口层次

    每个 Channel 都会被分配一个 ChannelPipeline 和 ChannelConfig, ChannelConfig 包含了该 Channel 的所有配置,并允许在运行期间更新它们。

    ChannelPipeline 在上面已经介绍过了,它存储了所有用于处理出站和入站数据的 ChannelHandler, 我们可以在运行时根据自己的需求添加或删除 ChannelPipeline 中的 ChannelHandler。

    此外,Channel 还有以下方法值得留意:

    方法名 描述
    eventLoop 返回当前 Channel 注册到的 EventLoop
    pipeline 返回分配给 Channel 的 ChannelPipeline
    isActive 判断当前 Channel 是活动的,如果是则返回 true。 此处活动的意义依赖于底层的传输,如果底层传输是 TCP Socket,那么客户端与服务端保持连接便是活动的;如果底层传输是 UDP Datagram,那么 Datagram 传输被打开就是活动的。
    localAddress 返回本地 SocketAddress
    remoteAddress 返回远程的 SocketAddress
    write 将数据写入远程主机,数据将会通过 ChannelPipeline 传输
    flush 将之前写入的数据刷新到底层传输
    writeFlush 等同于调用 write 写入数据后再调用 flush 刷新数据

    # Netty 内置的传输

    Netty 内置了一些开箱即用的传输,我们上面介绍了传输的核心 API 是 Channel,那么这些已经封装好的 传输也是基于 Channel 的。

    Netty 内置 Channel 接口层次:

    Netty内置Channel接口层次

    名称 包 描述
    NIO io.netty.channel.socket.nio NIO Channel 基于 java.nio.channels,其 io 模型为 IO 多路复用
    Epoll io.netty.channel.epoll Epoll Channel 基于操作系统的 epoll 函数,其 io 模型为 IO 多路复用,不过 Epoll 模型只支持在 Linux 上的多种特性,比 NIO 性能更好
    KQueue io.netty.channel.kqueue KQueue 与 Epoll 相似,它主要被用于 FreeBSD 系统上,如 Mac 等
    OIO(Old Io) io.netty.channel.socket.oio OIO Channel 基于 java.net 包,其 io 模型是阻塞的,且此传输被 Netty 标记为 deprecated,故不推荐使用,最好使用 NIO / EPOLL / KQUEUE 等传输
    Local io.netty.channel.local Local Channel 可以在 VM 虚拟机内部进行本地通信
    Embedded io.netty.channel.embedded Embedded Channel 允许在没有真正的网络传输中使用 ChannelHandler,可以非常有用的测试 ChannelHandler

    # 零拷贝

    理解零拷贝 零拷贝是 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 方式: Non-Zero Copy

    应用程序启动后,向内核发出 read 调用(用户态切换到内核态),操作系统收到调用请求后, 会检查文件是否已经缓存过了,如果缓存过了,就将数据从缓冲区(直接内存)拷贝到用户应用进程(内核态切换到用户态), 如果是第一次访问这个文件,则系统先将数据先拷贝到缓冲区(直接内存),然后 CPU 将数据从缓冲区拷贝到应用进程内(内核态切换到用户态), 应用进程收到内核的数据后发起 write 调用,将数据拷贝到目标文件相关的堆栈内存(用户态切换到内核态), 最后再从缓存拷贝到目标文件。

    普通IO拷贝

    根据上面普通拷贝的过程我们知道了其缺点主要有:

    1. 用户态与内核态之间的上下文切换次数较多(用户态发送系统调用与内核态将数据拷贝到用户空间)。
    2. 拷贝次数较多,每次 IO 都需要 DMA 和 CPU 拷贝。

    而零拷贝正是针对普通拷贝的缺点做了很大改进,使得其拷贝速度在处理大数据的时候很是出色。

    Zero Copy 方式: 在此输入图片描述

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

    零拷贝主要有两种实现技术:

    1. 内存映射(mmp)
    2. 文件传输(sendfile)

    可以参照我编写的 demo 进行接下来的学习:

    zerocopy (opens new window)

    # 内存映射(Memory Mapped)

    内存映射对应JAVA NIO的API为
    FileChannel.map。
    
    1
    2

    当用户程序发起 mmp 系统调用后,操作系统会将文件的数据直接映射到内核缓冲区中, 且缓冲区会与用户空间共享这一块内存,这样就无需将数据从内核拷贝到用户空间了,用户程序接着发起 write 调用,操作系统直接将内核缓冲区的数据拷贝到目标文件的缓冲区,最后再将数据从缓冲区拷贝到目标文件。

    其过程如下: mmp零拷贝

    内存映射由原来的四次拷贝减少到了三次,且拷贝过程都在内核空间,这在很大程度上提高了 IO 效率。

    但是 mmp 也有缺点: 当我们使用 mmp 映射一个文件到内存并将数据 write 到指定的目标文件时, 如果另一个进程同时对这个映射的文件做出写的操作,用户程序可能会因为访问非法地址而产生一个错误的信号从而终止。

    试想一种情况:我们的服务器接收一个客户端的下载请求,客户端请求的是一个超大的文件,服务端开启一个线程 使用 mmp 和 write 将文件拷贝到 Socket 进行响应,如果此时又有一个客户端请求对这个文件做出修改, 由于这个文件先前已经被第一个线程 mmp 了,可能第一个线程会因此出现异常,客户端也会请求失败。

    解决这个问题的最简单的一种方法就对这个文件加读写锁,当一个线程对这个文件进行读或写时,其他线程不能操作此文件, 不过这样处理并发的能力可能就大打折扣了。

    # 文件传输(SendFile)

    文件传输对应JAVA NIO的API为
    FileChannel.transferFrom/transferTo
    
    1
    2

    在了解 sendfile 之前,先来看一下它的函数原型(linux 系统的同学可以使用 man sendfile 查看):

    #include<sys/sendfile.h>
    
    ssize_t
    
    sendfile(int out_fd,
            int in_fd,
            off_t *offset,
            size_t count);
    
    1
    2
    3
    4
    5
    6
    7
    8

    sendfile 在代表输入文件的文件描述符 in_fd 和 输入文件的文件描述符 out_fd 之间传输文件内容, 这个传输过程完全是在内核之中进行的,程序只需要把输入文件的描述符和输出文件的描述符传递给 sendfile 调用,系统自然会完成拷贝。 当然,sendfile 和 mmp 一样都有相同的缺点,在传输过程中, 如果有其他进程截断了这个文件的话,用户程序仍然会被终止。

    sendfile 传输过程如下:

    sendfile零拷贝

    它的拷贝次数与 mmp 一样,但是无需像 mmp 一样与用户进程共享内存了。

    编辑 (opens new window)
    上次更新: 2024/04/19, 08:52:45
    【Netty】Netty组件介绍
    多路复用 I/O 模型详解, 为什么他能支持更高的并发

    ← 【Netty】Netty组件介绍 多路复用 I/O 模型详解, 为什么他能支持更高的并发→

    最近更新
    01
    idea 热部署插件 JRebel 安装及破解,不生效问题解决
    04-10
    02
    spark中代码的执行位置(Driver or Executer)
    12-12
    03
    大数据技术之 SparkStreaming
    12-12
    更多文章>
    Theme by Vdoing | Copyright © 2019-2024 Young | MIT License
    浙ICP备20002744号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式