深入Java网络编程与NIO(二)

时间:2022-12-27 20:14:51

Java NIO 与 Netty NIO

NIO的特性/NIO与IO区别:

  • 1)IO是面向流的,NIO是面向缓冲区的;
  • 2)IO流是阻塞的,NIO流是不阻塞的;
  • 3)NIO有选择器,而IO没有。

读数据和写数据方式:

  • 从通道进行数据读取 :创建一个缓冲区,然后请求通道读取数据。
  • 从通道进行数据写入 :创建一个缓冲区,填充数据,并要求通道写入数据。

NIO三大核心组件:Channels 、Buffers 、Selectors
Netty对应也有几大组件:

Channel: 它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作;将会为每个channel分配一个EventLoop
EventLoop: 控制流、多线程处理、并发
EventLoopGroup
ChannelHandler: 为了响应特定事件而被执行的回调: 每一个handler都在一个HanderPipeline中
ChannelFuture: 异步通知

深入Java网络编程与NIO(二)

1.Buffers

深入Java网络编程与NIO(二)
其实核心是最后的 ByteBuffer,前面的一大串类只是包装了一下它而已,我们使用最多的通常也是 ByteBuffer。
MappedByteBuffer 用于实现直接内存映射mmp。

Buffer 和数组差不多,它有 position、limit、capacity 几个重要属性。put() 一下数据、flip() 切换到读模式、然后用 get() 获取数据、clear() 一下清空数据、重新回到 put() 写入数据。

ByteBuffer

NIO的数据传输是基于缓冲区的,ByteBuffer正是NIO数据传输中所使用的缓冲区抽象。ByteBuffer支持在 堆外分配内存DirectBuffer,并且尝试避免在执行I/O操作中的多余复制。通过JNI调用来在堆外分配内存(调用malloc()函数在JVM堆外分配内存),这主要是为了避免额外的缓冲区复制操作。
一般的I/O操作都需要进行系统调用,这样会先切换到内核态,内核态要先从文件读取数据到它的缓冲区,只有等数据准备完毕后,才会从内核态把数据写到用户态,所谓的阻塞IO其实就是说的在等待数据准备好的这段时间内进行阻塞。如果想要避免这个额外的内核操作,可以通过使用mmap(虚拟内存映射)的方式来让用户态直接操作文件。

Netty 中的 ByteBuf

网络传输的基本单位是字节,在Java NIO中提供了ByteBuffer作为字节缓冲区容器,但该类的API使用起来不太方便,所以Netty实现了ByteBuf作为其替代品,下面是使用ByteBuf的优点:

相比ByteBuffer使用起来更加简单。
通过内置的复合缓冲区类型实现了透明的zero-copy。
容量可以按需增长。
读和写使用了不同的索引指针。
支持链式调用。
支持引用计数与池化。
可以被用户自定义的缓冲区类型扩展。

2.Channels

深入Java网络编程与NIO(二)

FileChannel:文件通道,用于文件的读和写
DatagramChannel:用于 UDP 连接的接收和发送
SocketChannel:把它理解为 TCP 连接通道,简单理解就是 TCP 客户端
ServerSocketChannel:TCP 对应的服务端,用于监听某个端口进来的请求

SocketChannel。它可以看作是 socket 的一个完善类,除了提供 Socket 的相关功能外,还提供了许多其他特性,如后面要讲到的向选择器注册的功能。

Socket相关的类图:
深入Java网络编程与NIO(二)

它类似于文件描述符,简单地来说它代表了一个实体(如一个硬件设备、文件、Socket或者一个能够执行一个或多个不同的I/O操作的程序组件)。你可以从一个Channel中读取数据到缓冲区,也可以将一个缓冲区中的数据写入到Channel。

Netty中的 Channel

  • ChannelHandler
    ChannelHandler充当了处理入站和出站数据的应用程序逻辑的容器,该类是基于事件驱动的,它会响应相关的事件然后去调用其关联的回调函数,例如当一个新的连接被建立时,ChannelHandler的channelActive()方法将会被调用。

Netty中到处都充满了 异步与事件驱动,而回调函数正是用于响应事件之后的操作。由于异步会直接返回一个结果,所以Netty提供了ChannelFuture(实现了java.util.concurrent.Future)来作为异步调用返回的占位符,真正的结果会在未来的某个时刻完成,到时候就可以通过ChannelFuture对其进行访问,每个Netty的出站I/O操作都将会返回一个ChannelFuture。

3.Selectors

Selector 建立在非阻塞的基础之上,大家经常听到的多路复用世界中指的就是它,用于实现一个线程管理多个 Channel。

对于 Selector,我们还需要非常熟悉以下几个方法:

1. select()

调用此方法,会将上次 select 之后的准备好的 channel 对应的 SelectionKey 复制到 selected set 中。如果没有任何通道准备好,这个方法会阻塞,直到至少有一个通道准备好。

2. electNow()

功能和 select 一样,区别在于如果没有准备好的通道,那么此方法会立即返回 0。

3. select(long timeout)

看了前面两个,这个应该很好理解了,如果没有通道准备好,此方法会等待一会

4. wakeup()

这个方法是用来唤醒等待在 select() 和 select(timeout) 上的线程的。如果 wakeup() 先被调用,此时没有线程在 select 上阻塞,那么之后的一个 select() 或 select(timeout) 会立即返回,而不会阻塞,当然,它只会作用一次。