Netty深入浅出(无处不在的IO)

为什么要有Netty

Netty是为了解决网络编程的复杂性和提供易于使用、高性能和可扩展的框架而开发的。它通过提供一组可重用的组件来处理网络通信的低级细节,例如套接字管理、线程和缓冲,简化了开发网络应用程序的过程。这使开发人员可以专注于应用程序逻辑而不是网络编程的复杂性。此外,Netty支持各种协议和传输机制,使其成为构建各种网络应用程序的多功能选择。

Java中的IO模型

Netty是一个Java编写的网络IO库,Netty在其底层仍然使用Java I/O库,如java.nio包。它使用了Java NIO(New I/O)的一些特性,例如非阻塞通道(Channel)、选择器(Selector)等,以实现高性能的网络通信。

三种通信模型

  • BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。

  • NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架。NIO提供了与传统BIO模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。

  • AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。

NIO

示例代码
  • 服务端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式

        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("Server is listening on port 8080...");

        while (true) {
            int readyChannels = selector.select();
            if (readyChannels == 0) {
                continue;
            }

            Set selectedKeys = selector.selectedKeys();
            Iterator keyIterator = selectedKeys.iterator();

            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();

                if (key.isAcceptable()) {
                    SocketChannel clientChannel = serverSocketChannel.accept();
                    clientChannel.configureBlocking(false);
                    clientChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("Accepted connection from " + clientChannel.getRemoteAddress());
                } else if (key.isReadable()) {
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int bytesRead = clientChannel.read(buffer);

                    if (bytesRead == -1) {
                        clientChannel.close();
                        System.out.println("Client disconnected.");
                    } else if (bytesRead > 0) {
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            clientChannel.write(buffer); // 回显客户端发送的数据
                        }
                        buffer.clear();
                    }
                }

                keyIterator.remove();
            }
        }
    }
}
  • 客户端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 8080));

        String message = "Hello, NIO Server!";
        ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());

        socketChannel.write(buffer); // 发送消息给服务器

        ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
        int bytesRead = socketChannel.read(responseBuffer); // 读取服务器的响应

        if (bytesRead != -1) {
            responseBuffer.flip();
            byte[] bytes = new byte[responseBuffer.remaining()];
            responseBuffer.get(bytes);
            String response = new String(bytes);
            System.out.println("Received from server: " + response);
        }

        socketChannel.close(); // 关闭客户端连接
    }
}

AIO

示例代码
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.Executors;

public class AIOTimeServer {
    public static void main(String[] args) throws IOException {
        int port = 8080;
        AsynchronousChannelGroup group = AsynchronousChannelGroup.withThreadPool(Executors.newFixedThreadPool(10));

        final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open(group);
        serverChannel.bind(new InetSocketAddress(port));
        System.out.println("Server is listening on port " + port);

        serverChannel.accept(null, new CompletionHandler() {
            @Override
            public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
                serverChannel.accept(null, this); // 接受下一个连接

                String response = "Current time: " + System.currentTimeMillis();
                ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
                clientChannel.write(buffer, null, new CompletionHandler() {
                    @Override
                    public void completed(Integer result, Void attachment) {
                        try {
                            clientChannel.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                    @Override
                    public void failed(Throwable exc, Void attachment) {
                        exc.printStackTrace();
                        try {
                            clientChannel.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }

            @Override
            public void failed(Throwable exc, Void attachment) {
                exc.printStackTrace();
            }
        });

        try {
            group.awaitTermination(Long.MAX_VALUE, java.util.concurrent.TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

为什么Netty依旧使用NIO的API?

Netty 不看重 Windows 上的使用,在 Linux 系统上,AIO 的底层实现仍使用 EPOLL(后续会讲),没有很好实现 AIO,因此在性能上没有明显的优势,而且被 JDK 封装了一层不容易深度优化。

Linux的IO模型

Java的I/O模型是在Java编程语言层面的抽象,而Linux的I/O模型是操作系统内核层面的实现。因此,虽然它们有一些相似之处,但并不是完全相同的概念。

LINUX五种IO模型

同步和异步:同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO 操作并等待或者轮询的去查看IO 操作是否就绪,而异步是指用户进程触发IO 操作以后便开始做自己的事情,而当IO 操作已经完成的时候会得到IO 完成的通知。

阻塞和非阻塞:阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作方法的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入方法会立即返回一个状态值。

  1. 阻塞式IONetty深入浅出(无处不在的IO)_第1张图片

  2. 非阻塞式IONetty深入浅出(无处不在的IO)_第2张图片

  3. IO多路复用Netty深入浅出(无处不在的IO)_第3张图片

  4. 信号驱动Netty深入浅出(无处不在的IO)_第4张图片

  5. 异步IONetty深入浅出(无处不在的IO)_第5张图片

前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。

从图中可以看出用户态与内核态的切换,那么什么是用户/内核态呢?这也是下面的内容。

用户态与内核态

I/O(Input/Output,输入/输出)和用户态与内核态之间存在密切的关系,特别是在操作系统中。用户态和内核态是操作系统中的两个不同特权级别,它们用于管理和保护计算机系统的资源。以下是关于I/O、用户态和内核态之间的关系的重要信息:

  1. I/O操作涉及用户态和内核态

    • I/O操作包括从应用程序到外部设备(如磁盘、网络、键盘、显示器等)的数据传输。这些操作通常涉及到用户态和内核态之间的切换。
    • 当应用程序需要执行I/O操作时,它会调用操作系统提供的I/O系统调用(例如,读取文件或发送数据包)。这些系统调用是在用户态执行的。
    • 操作系统内核负责管理系统资源和I/O设备,因此在执行I/O操作时,控制必须从用户态切换到内核态,以便内核可以直接访问硬件资源。
  2. 用户态和内核态的切换

    • 用户态和内核态是不同的CPU执行模式。在用户态下,应用程序只能访问有限的资源,而在内核态下,操作系统内核可以访问系统的全部资源。
    • 当应用程序需要执行需要特权访问的操作,例如执行系统调用或访问设备驱动程序,它必须通过软中断或异常将控制权切换到内核态。这个切换是由操作系统的内核来处理的。
    • 用户态到内核态的切换会涉及一些开销,因为需要保存和恢复CPU寄存器、切换堆栈等操作。因此,频繁的切换会影响性能。
  3. 内核态的I/O处理

    • 一旦控制切换到内核态,操作系统内核就可以执行I/O操作。它会管理设备驱动程序、缓冲区、中断处理等细节,以确保数据的正确传输。
    • 内核还会维护I/O请求队列,以有效地处理多个I/O请求。
  4. 异步I/O和用户态I/O

    • 异步I/O是一种I/O模型,允许应用程序继续执行其他任务,而不必等待I/O操作完成。在这种情况下,通常使用异步I/O库来管理I/O操作,而不需要频繁地切换用户态和内核态。
    • 异步I/O通常通过回调函数或事件通知机制来处理I/O完成事件。

I/O操作涉及用户态和内核态之间的切换,因为操作系统内核必须管理和控制I/O设备。这个切换是操作系统的核心功能之一,用于确保计算机系统的稳定性、安全性和性能。不同的I/O模型可以影响用户态和内核态之间的切换方式和频率。说到内核态切换,下面不得不介绍的就是零拷贝。

Zero Copy(零拷贝)

Netty与零拷贝(Zero-Copy)之间有密切的关系,因为Netty是一个网络应用框架,专门设计用于高性能的网络通信,而零拷贝是一项技术,可以用于提高数据传输的效率,特别是在网络通信中。

以下是Netty与零拷贝的关系和如何在Netty中利用零拷贝技术的一些重要信息:

Netty的高性能特性:Netty被设计为高性能的网络应用框架,它旨在处理大量并发连接和高吞吐量的网络通信。为了实现这一目标,Netty采用了多种性能优化技术,其中之一就是零拷贝。

零拷贝是一种优化技术,旨在减少数据在内存之间的复制次数。传统的数据传输通常涉及将数据从一个缓冲区复制到另一个缓冲区,这会引入额外的CPU和内存开销。零拷贝技术通过操作系统或硬件支持,允许数据在不复制的情况下从一个地方传输到另一个地方,从而提高了数据传输的效率。
   
`ByteBuf`是Netty的自定义缓冲区类型,它支持零拷贝和引用计数等特性。在Netty中,`ByteBuf`可以在数据传输时直接暴露底层数据,而不需要进行数据复制,从而减少了CPU和内存开销。

零拷贝的实现

传统IO

read:将数据从磁盘通过DMA读取到内核缓存区中,在拷贝到用户缓冲区

write: 先将数据写入到socket缓冲区中,经过DMA写入网卡设备

Netty深入浅出(无处不在的IO)_第6张图片

4次切换,4次拷贝

虚拟内存 mmap

1.虚拟内存空间可以远远大于物理内存空间
2.多个虚拟内存可以指向同一个物理地址


正是多个虚拟内存可以指向同一个物理地址,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少IO的数据拷贝次数。用户态可以直接访问内核态的数据。

4次切换,3次拷贝

sendFile

sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作。

Netty深入浅出(无处不在的IO)_第7张图片

2次切换,3次拷贝

sendfile + DMA scatter/gather实现的零拷贝

linux2.4版本后,对sendfile做了优化升级,引入SG-DMA技术,其实就是对DMA拷贝加入了scatter/gather操作,它可以直接从内核空间缓冲区中将数据读取到网卡,这样的话还可以省去CPU拷贝。

Netty深入浅出(无处不在的IO)_第8张图片

2次切换,2次拷贝,CPU全程不参与数据搬运

直接内存 Direct ByteBuf

Netty通常使用直接内存(Direct Memory)来提高性能。直接内存是一种特殊的内存分配方式,不同于Java堆内存。

  1. ByteBuf与直接内存:Netty中的ByteBuf是一个用于处理字节数据的缓冲区抽象。ByteBuf可以使用直接内存分配,这称为"Direct ByteBuf"。直接内存分配意味着ByteBuf中的数据存储在堆外内存,而不是在Java堆中。

  2. 减少内存复制:在进行网络数据传输时,数据通常需要从应用程序的缓冲区复制到操作系统内核缓冲区,然后再从内核缓冲区复制到网络适配器。使用直接内存,可以在这些步骤中减少或消除数据复制,提高了性能。

  3. 零拷贝:直接内存可以与零拷贝相结合,使数据可以在应用程序和操作系统之间进行高效的传输。

  4. 缓冲区池:Netty通常使用池化的ByteBuf来管理直接内存的分配和释放。这种方式可以避免频繁地分配和释放直接内存,提高了内存管理的效率。

  5. 内存管理控制:Netty提供了一些工具和机制,帮助开发者有效地管理直接内存,包括手动释放、自动回收等。

示例代码
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

public class NettyDirectMemoryExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建两个EventLoopGroup,一个用于接受客户端连接,一个用于处理客户端请求
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 创建ServerBootstrap
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        ch.pipeline().addLast(new EchoServerHandler());
                    }
                });

            // 绑定端口并启动服务器
            serverBootstrap.bind(8080).sync().channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    // 自定义ChannelHandler处理客户端消息
    static class EchoServerHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            ByteBuf in = (ByteBuf) msg;
            ByteBuf out = Unpooled.directBuffer(); // 创建Direct ByteBuf

            try {
                out.writeBytes(in); // 将接收到的数据写入Direct ByteBuf
                ctx.write(out); // 写入回应数据到客户端
                ctx.flush();
            } finally {
                in.release(); // 释放接收缓冲区
                out.release(); // 释放Direct ByteBuf
            }
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            ctx.close();
        }
    }
}

使用直接内存需要谨慎管理,以避免内存泄漏和其他问题。

你可能感兴趣的:(后端必知必会,后端,java)