Netty

几乎所有我们叫得上名字的框架,底层用的都是Netty,包括RocketMQ、Elasticsearch、gRPC、Apache Dubbo、Spring5、HSF、 Zookeeper、Spark、Hadoop,这足以说明Netty有用且好用。

学习Netty之前需要先了解下一些基本概念。

阻塞、非阻塞、异步、同步

IO 中阻塞、非阻塞、异步、同步这几个术语的含义和关系:

  • 阻塞:如果线程调用 read/write 过程,但 read/write 过程没有就绪或没有完成,则调用 read/write 过程的线程会一直等待,这个过程叫做阻塞式读写。
  • 非阻塞:如果线程调用 read/write 过程,但 read/write 过程没有就绪或没有完成,调用 read/write 过程的线程并不会一直等待,而是去处理其他工作,等到 read/write 过程就绪或完成后再回来处理,这个过程叫做非阻塞式读写。
  • 异步:read/write 过程托管给操作系统来完成,完成后操作系统会通知(通过回调或者事件)应用网络 IO 程序(其中的线程)来进行后续的处理。
  • 同步:read/write 过程由网络 IO 程序(其中的线程)来完成。

可以看出:异步 IO 一定是非阻塞 IO;同步 IO 既可以是阻塞 IO、也可以是非阻塞 IO。

BIO、NIO、AIO

Java 中的网络 IO 模型有三种:BIO、NIO、AIO。

1)BIO

同步的、阻塞式 IO。在这种模型中,服务器上一个线程处理一次连接,即客户端每发起一个请求,服务端都要开启一个线程专门处理该请求。这种模型对线程量的耗费极大,且线程利用率低,难以承受请求的高并发。BIO 虽然可以使用线程池+等待队列进行优化,避免使用过多的线程,但是依然无法解决线程利用率低的问题。

使用 BIO 构建 C/S 系统的 Java 编程组件是 ServerSocket 和 Socket。服务端示例代码为:

public static void main(String[] args) throws IOException {
    ExecutorService threadPool = Executors.newCachedThreadPool();
    ServerSocket serverSocket = new ServerSocket(8080);
    while (true) {
        Socket socket = serverSocket.accept();
        threadPool.execute(() -> {
            handler(socket);
        });
    }
}

// 处理客户端请求
private static void handler(Socket socket) throws IOException {
    byte[] bytes = new byte[1024];
    InputStream inputStream = socket.getInputStream();
    socket.close();
    while (true) {
        int read = inputStream.read(bytes);
        if (read != -1) {
            System.out.println("msg from client: " + new String(bytes, 0, read));
        } else {
            break;
        }
    }
}

上面获取流和读取流的过程如果很慢的话,那么服务端这个线程就不能处理别的客户端请求,即阻塞住了。 

2)NIO

同步的、非阻塞式 IO。在这种模型中,服务器上一个线程处理多个连接,即多个客户端请求都会被注册到多路复用器(后文要讲的 Selector)上,多路复用器会轮训这些连接,轮训到连接上有 IO 活动就进行处理。NIO 降低了线程的需求量,提高了线程的利用率。Netty 就是基于 NIO 的(这里有一个问题:前文大力宣扬 Netty 是一个异步高性能网络应用框架,为何这里又说 Netty 是基于同步的 NIO 的?请读者跟着文章的描述找寻答案)。

NIO让线程和客户端的关系由BIO的一对一,变成了一对多

Java NIO 的非阻塞模式,使得一个线程从某个通道读取数据的时候,若当前有可用数据,则该线程进行处理,若当前无可用数据,则该线程不会保持阻塞等待状态,而是可以去处理其他工作(比如处理其他通道的读写);同样,一个线程向某个通道写入数据的时候,一旦开始写入,该线程无需等待写完即可去处理其他工作(比如处理其他通道的读写)。这种特性使得一个线程能够处理多个客户端请求,而不是像 BIO 那样,一个线程只能处理一个请求。 

使用 NIO 构建 C/S 系统的 Java 编程组件是 Channel、Buffer、Selector。服务端示例代码为:

public static void main(String[] args) throws IOException {
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    Selector selector = Selector.open();
    // 绑定端口
    serverSocketChannel.socket().bind(new InetSocketAddress(8080));
    // 设置 serverSocketChannel 为非阻塞模式
    serverSocketChannel.configureBlocking(false);
    // 注册 serverSocketChannel 到 selector,关注 OP_ACCEPT 事件
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
        // 没有事件发生
        if (selector.select(1000) == 0) {
            continue;
        }
        // 有事件发生,找到发生事件的 Channel 对应的 SelectionKey 的集合
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        while (iterator.hasNext()) {
            SelectionKey selectionKey = iterator.next();
            // 发生 OP_ACCEPT 事件,处理连接请求
            if (selectionKey.isAcceptable()) {
                SocketChannel socketChannel = serverSocketChannel.accept();
                // 将 socketChannel 也注册到 selector,关注 OP_READ
                // 事件,并给 socketChannel 关联 Buffer
                socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
            }
            // 发生 OP_READ 事件,读客户端数据
            if (selectionKey.isReadable()) {
                SocketChannel channel = (SocketChannel) selectionKey.channel();
                ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                channel.read(buffer);
                System.out.println("msg form client: " + new String(buffer.array()));
            }
            // 手动从集合中移除当前的 selectionKey,防止重复处理事件
            iterator.remove();
        }
    }
}

上述服务端的工作流程为:

  • 1)当客户端发起连接时,会通过 ServerSocketChannel 创建对应的 SocketChannel。
  • 2)调用 SocketChannel 的注册方法将 SocketChannel 注册到 Selector 上,注册方法返回一个 SelectionKey,该 SelectionKey 会被放入 Selector 内部的 SelectionKey 集合中。
  • 3)  该 SelectionKey 和 Selector 关联(即通过 SelectionKey 可以找到对应的 Selector),也和 SocketChannel 关联(即通过 SelectionKey 可以找到对应的 SocketChannel)。
  • 4)Selector 会调用 select()/select(timeout)/selectNow()方法对内部的 SelectionKey 集合关联的 SocketChannel 集合进行监听,找到有事件发生的 SocketChannel 对应的 SelectionKey。
  • 5)通过 SelectionKey 找到有事件发生的 SocketChannel,完成数据处理。 

3)AIO

异步非阻塞式 IO。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。

在NIO中获取到可读或可写的事件之后,后续还需要用户线程自己主动去处理的。

在AIO中获取到可读或可写的事件之后,是内核去主动处理,用户线程直接拿结果就行了。

AIO这种模型中,由操作系统完成与客户端之间的 read/write,之后再由操作系统主动通知服务器线程去处理后面的工作,在这个过程中服务器线程不必同步等待 read/write 完成。

使用 Java NIO 构建的 IO 程序,它的工作模式是:主动轮训 IO 事件,IO 事件发生后程序的线程主动处理 IO 工作,这种模式也叫做 Reactor 模式。它将 IO 事件的处理托管给操作系统,操作系统完成 IO 工作之后会通知程序的线程去处理后面的工作,这种模式也叫做 Proactor 模式。

由于不同的操作系统对 AIO 的支持程度不同,AIO 目前未得到广泛应用。

小结:

BIO 以流的方式处理数据,而 NIO 以缓冲区(也被叫做块)的方式处理数据,块 IO 效率比流 IO 效率高很多。BIO 基于字符流或者字节流进行操作,而 NIO 基于 Channel 和 Buffer 进行操作,数据总是从通道读取到缓冲区或者从缓冲区写入到通道。Selector 用于监听多个通道上的事件(比如收到连接请求、数据达到等等),因此使用单个线程就可以监听多个客户端通道。

NIO中的 Buffer、Channel、Selector

NIO 基于 Channel 和 Buffer 进行操作,数据总是从通道读取到缓冲区或者从缓冲区写入到通道。

Selector 用于监听多个通道上的事件(比如收到连接请求、数据达到等等),因此使用单个线程就可以监听多个客户端通道。

关于上图,再进行几点说明:

  • 一个 Selector 对应一个处理线程
  • 一个 Selector 上可以注册多个 Channel
  • 每个 Channel 都会对应一个 Buffer(有时候一个 Channel 可以使用多个 Buffer,这时候程序要进行多个 Buffer 的分散和聚集操作),Buffer 的本质是一个内存块,底层是一个数组
  • Selector 会根据不同的事件在各个 Channel 上切换
  • Buffer 是双向的,既可以读也可以写,切换读写方向要调用 Buffer 的 flip()方法
  • 同样,Channel 也是双向的,数据既可以流入也可以流出

1. 缓冲区(Buffer)

缓冲区(Buffer)本质上是一个可读可写的内存块,可以理解成一个容器对象,Channel 读写文件或者网络都要经由 Buffer。在 Java NIO 中,Buffer 是一个顶层抽象类,它的常用子类有:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • DoubleBuffer
  • FloatBuffer

涵盖了 Java 中除 boolean 之外的所有的基本数据类型。其中 ByteBuffer 支持类型化的数据存取,即可以往 ByteBuffer 中放 byte 类型数据、也可以放 char、int、long、double 等类型的数据,但读取的时候要做好类型匹配处理,否则会抛出 BufferUnderflowException。

2. 通道(Channel)

通道(Channel)是双向的,可读可写。
在 Java NIO 中,Channel 是一个顶层接口,它的常用子类有:

  • FileChannel:用于文件读写
  • DatagramChannel:用于 UDP 数据包收发
  • ServerSocketChannel:用于服务端 TCP 数据包收发
  • SocketChannel:用于客户端 TCP 数据包收发

3. 选择器(Selector)

选择器(Selector)是实现 IO 多路复用的关键,多个 Channel 注册到某个 Selector 上,当 Channel 上有事件发生时,Selector 就会取得事件然后调用线程去处理事件。也就是说只有当连接上真正有读写等事件发生时,线程才会去进行读写等操作,这就不必为每个连接都创建一个线程,一个线程可以应对多个连接。这就是 IO 多路复用的要义。

在 Java NIO 中,Selector 是一个抽象类,它的常用方法有:

public abstract class Selector implements Closeable {
    // 得到一个选择器对象
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }
    
    // 返回所有发生事件的 Channel 对应的 SelectionKey 的集合,通过
    // SelectionKey 可以找到对应的 Channel
    public abstract Set<SelectionKey> selectedKeys();
    
    // 返回所有 Channel 对应的 SelectionKey 的集合,通过 SelectionKey
    // 可以找到对应的 Channel
    public abstract Set<SelectionKey> keys();
    
    // 监控所有注册的 Channel,当其中的 Channel 有 IO 操作可以进行时,
    // 将这些 Channel 对应的 SelectionKey 找到。参数用于设置超时时间
    public abstract int select(long timeout) throws IOException;
    
    // 无超时时间的 select 过程,一直等待,直到发现有 Channel 可以进行
    // IO 操作
    public abstract int select() throws IOException;
    
    // 立即返回的 select 过程
    public abstract int selectNow() throws IOException;
   
    // 唤醒 Selector,对无超时时间的 select 过程起作用,终止其等待
    public abstract Selector wakeup();
}

Netty 的 IO 线程 NioEventLoop 聚合了 Selector,可以同时并发处理成百上千的客户端连接。

 操作系统中的中断

系统中断是指 CPU 对系统发生的某个事件做出的一种反应:CPU 暂停正在执行的程序,保留现场后转去执行相应的处理程序,处理完该事件后再返回断点继续执行被打断的程序。

中断可以避免 CPU 轮询等待某条件成立,减小系统开销。中断是多程序并发执行的前提条件,因为没有中断cpu会一直处理当前的任务,不会停下转而处理别的任务。

当中断发生时,由于操作系统的管理工作需要特权指令,CPU 会立刻进入内核态,使得操作系统获得计算机的控制权。用户态到核心态的转换就是通过中断机制实现的,并且是唯一途径。

中断可以分为硬件中断和软件中断:‌

  • 硬件中断‌:由外部硬件设备生成,例如键盘输入、鼠标移动等。当硬件设备需要操作系统的注意时,会发送一个中断信号给CPU,CPU会暂停当前任务,保存状态,然后执行中断处理程序(ISR),完成后恢复原任务。
  • 软件中断‌:由运行的程序产生,主要用于实现系统调用。例如,当一个程序需要访问受保护的内存时,会发送一个软件中断请求,操作系统会响应这个请求,完成相应的操作。

中断的处理流程

当中断发生时,CPU会执行以下步骤:

  1. 保存当前状态‌:CPU会保存当前执行的任务的状态,包括寄存器的值。
  2. 查找中断处理程序‌:根据中断向量,CPU从中断向量表中查找中断处理程序入口地址。
  3. 执行中断处理程序‌:执行找到的中断处理程序,处理中断请求。
  4. 恢复状态‌:处理完成后,CPU恢复之前保存的状态,继续执行原来的任务。

中断的优缺点

优点‌:

  • 异步处理‌:中断使得程序之间可以异步通信,不需要一直等待某个任务完成,提高了系统的并发处理能力。
  • 资源调度‌:中断机制使得CPU可以抢占资源进行任务调度,允许多个程序同时运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沙滩de流沙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值