当前位置 博文首页 > 努力充实,远方可期:【网络通信】Java NIO

    努力充实,远方可期:【网络通信】Java NIO

    作者:[db:作者] 时间:2021-08-22 18:08

    二、NIO

    事件驱动+多路复用

    写事件代表底层缓冲区是否有空间,有则响应true

    2.1 NIO 简介

    NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

    NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。

    对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

    • BIO模型中,一个连接来了,会创建一个线程,对应一个while死循环,死循环的目的就是不断监测这条连接上是否有数据可以读,大多数情况下,1w个连接里面同一时刻只有少量的连接有数据可读,因此,很多个while死循环都白白浪费掉了,因为读不出啥数据。
    • 而在NIO模型中,他把这么多while死循环变成一个死循环,这个死循环由一个线程控制
      • NIO中,新来一个连接不再创建一个新的线程,而是可以把这条连接直接绑定到某个固定的线程,然后这条连接的所有读写都由这个线程来负责

    那么NIO如何做到一个线程,一个while死循环就能监测1w个连接是否有数据可读的呢?这就是NIO模型中selector的作用,一条连接来了之后,现在不创建一个while死循环去监听是否有数据可读了,而是直接把这条连接注册到selector上,然后,通过检查这个selector,就可以批量监测出有数据可读的连接,进而读取数据

    NIO和BIO之间第一个最大的区别是,

    • BIO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
    • NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

    IO的各种流是阻塞的。这意味着,当一个线程调用read()write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。

    NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞所以直至数据变的可以读取之前,该线程可以继续做其他的事情。

    非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

    • Channel中数据的读取是通过Buffer , 一种非阻塞的读取方式。
      • 你既可以读取也可以写入到Channel,流只能读取或者写入,inputStream和outputStream。
      • Channel可以异步地读和写。
      • channel永远都是从一个buffer中读或者写入到一个buffer中去。
    • Selector 多路复用器 单线程模型,线程的资源开销相对比较小。一个selector同时检查一组信道的I/O状态。Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

    2.2 NIO的组成

    NIO是一种同步非阻塞IO,主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(多路复用器)。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(多路复用器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。

    • 通道(铁路)负责传输,通道表示打开到IO设备的连接
    • 缓存区(火车)负责存储

    1)Channel (通道)

    NIO 通过Channel(通道) 进行读写

    java.nio 包中实现的以下几个 Channel:

    8

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

    Channel 经常翻译为通道,类似 IO 中的流,用于读取和写入。它与前面介绍的 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。

    • Channel表示IO源与目标打开的连接。Channel类似于传统的”流“,只不过Channel本身不能直接访问数据,Channel只能与Buffer进行交互
    • 通道是双向的,可读也可写,而流的读写是单向的
    • 无论读写,通道只能和Buffer交互。通过 Buffer,通道可以异步地读写。buffer与socket交互
    • Channel是一个独立的处理器,专门用于IO操作,附属于CPU。
    • 在提出IO请求的时候,CPU不需要进行干预,也就提高了效率。

    内存向CPU申请权限,得到DMA权限后就可由内存进行IO,无需占用CPU资源,CPU就可以进行别的计算了。

    但DMA总线过多就造成总线冲突的问题,也会影响性能。所以就提出了通道的方式。通道是一个完全独立的处理器,专门进行IO操作。

    通道用于源节点与目标节点的连接。在Java NIO中负责缓冲区中数据的传输。Channel本身并不存储数据,因此需要配合Buffer一起使用

    获取通道

    方式1:通过流获取该流对应的通道。

    // 流对象.getChannel() 方法
    FileChannel fc = new FileOutputStream("data.txt").getChannel();
    

    方式2:在JDK1.7中的NIO.2 针对各个通道提供了静态方法 FileChannel.open();

    /*
    java.nio.channels.Channel通道接口:
    
    主要实现类
    用于本地数据传输:
     |-- FileChannel
     |-- RandomAccessFile
    
    用于网络数据传输:
     |-- SocketChannel
     |-- ServerSocketChannel
     |-- DatagramChannel
    */
    例:
    

    方式3:在JDK1.7中的NIO2 的Files工具类的 newByteChannel();

    FileChannel

    我想文件操作对于大家来说应该是最熟悉的,不过我们在说 NIO 的时候,其实 FileChannel 并不是关注的重点。而且后面我们说非阻塞的时候会看到,FileChannel 是不支持非阻塞的。

    这里算是简单介绍下常用的操作吧,感兴趣的读者瞄一眼就是了。

    初始化:

    FileInputStream inputStream = new FileInputStream(new File("/data.txt"));
    FileChannel fileChannel = inputStream.getChannel();
    

    当然了,我们也可以从 RandomAccessFile#getChannel 来得到 FileChannel。

    读取文件内容:

    ByteBuffer buffer = ByteBuffer.allocate(1024);
    
    int num = fileChannel.read(buffer);
    

    前面我们也说了,所有的 Channel 都是和 Buffer 打交道的。

    写入文件内容:

    ByteBuffer buffer = ByteBuffer.allocate(1024);
    buffer.put("随机写入一些内容到 Buffer 中".getBytes());
    // Buffer 切换为读模式
    buffer.flip();
    while(buffer.hasRemaining()) {
        // 将 Buffer 中的内容写入文件
        fileChannel.write(buffer);
    }
    
    SocketChannel

    我们前面说了,我们可以将 SocketChannel 理解成一个 TCP 客户端。虽然这么理解有点狭隘,因为我们在介绍 ServerSocketChannel 的时候会看到另一种使用方式。

    打开一个 TCP 连接:

    SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("https://www.javadoop.com", 80));
    

    当然了,上面的这行代码等价于下面的两行:

    // 打开一个通道
    SocketChannel socketChannel = SocketChannel.open();
    // 发起连接
    socketChannel.connect(new InetSocketAddress("https://www.javadoop.com", 80));
    

    SocketChannel 的读写和 FileChannel 没什么区别,就是操作缓冲区。

    // 读取数据
    socketChannel.read(buffer);
    
    // 写入数据到网络连接中
    while(buffer.hasRemaining()) {
        socketChannel.write(buffer);   
    }
    

    不要在这里停留太久,先继续往下走。

    ServerSocketChannel

    之前说 SocketChannel 是 TCP 客户端,这里说的 ServerSocketChannel 就是对应的服务端。

    ServerSocketChannel 用于监听机器端口,管理从这个端口进来的 TCP 连接。

    // 实例化
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 监听 8080 端口
    serverSocketChannel.socket().bind(new InetSocketAddress(8080));
    
    while (true) {
        // 一旦有一个 TCP 连接进来,就对应创建一个 SocketChannel 进行处理
        SocketChannel socketChannel = serverSocketChannel.accept();
    }
    

    这里我们可以看到 SocketChannel 的第二个实例化方式

    到这里,我们应该能理解 SocketChannel 了,它不仅仅是 TCP 客户端,它代表的是一个网络通道,可读可写。

    ServerSocketChannel 不和 Buffer 打交道了,因为它并不实际处理数据,它一旦接收到请求后,实例化 SocketChannel,之后在这个连接通道上的数据传递它就不管了,因为它需要继续监听端口,等待下一个连接。

    DatagramChannel

    UDP 和 TCP 不一样,DatagramChannel 一个类处理了服务端和客户端。

    科普一下,UDP 是面向无连接的,不需要和对方握手,不需要通知对方,就可以直接将数据包投出去,至于能不能送达,它是不知道的

    监听端口:

    DatagramChannel channel = DatagramChannel.open();
    channel.socket().bind(new InetSocketAddress(9090));
    
    ByteBuffer buf = ByteBuffer.allocate(48);
    
    channel.receive(buf);
    

    发送数据:

    String newData = "New String to write to file..."
                        + System.currentTimeMillis();
        
    ByteBuffer buf = ByteBuffer.allocate(48);
    buf.put(newData.getBytes());
    buf.flip();
    
    int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));
    

    ServerSocketChannel.java

    ServerSocketChannel 在服务器端监听新的客户端 Socket 连接。

    NIO 中的 ServerSocketChannel 功能类似 ServerSocket, SocketChannel 功能类似 Socket

    public abstract class ServerSocketChannel
        extends AbstractSelectableChannel
        implements NetworkChannel{
        protected ServerSocketChannel(SelectorProvider provider) {
            super(provider);
        }
        
        // 得到一个ServerSocketChannel通道
        public static ServerSocketChannel open() throws IOException {
            return SelectorProvider.provider().openServerSocketChannel();
        }
    
        // 给ServerSocketChannel绑定端口号
        public final ServerSocketChannel bind(SocketAddress local)
            throws IOException{
            return bind(local, 0);
        }
        
       
        public abstract ServerSocket socket();
        
         // 接收一个连接,返回代表这个连接的通道对象
        public abstract SocketChannel accept() throws IOException;
        
        // 设置阻塞或非阻塞,取值 false 表示采用非阻塞模式 // AbstractSelectableChannel.java
        public final SelectableChannel configureBlocking(boolean block) {
            synchronized (regLock) {
                if (!isOpen())
                    throw new ClosedChannelException();
                if (blocking == block)
                    return this;
                if (block && haveValidKeys())
                    throw new IllegalBlockingModeException();
                implConfigureBlocking(block);
                blocking = block;
            }
            return this;
        }
        
        //注册一个选择器并设置监听事件
        public final SelectionKey register(Selector sel, int ops)
    SocketChannel.java

    SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。

    public abstract class SocketChannel
        extends AbstractSelectableChannel
        implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
    {//多了 ByteChannel, ScatteringByteChannel, GatheringByteChannel 分散聚集功能
        
        protected SocketChannel(SelectorProvider provider) {
            super(provider);
        }
        
        //得到一个 SocketChannel 通道
        public static SocketChannel open() throws IOException {
            return SelectorProvider.provider().openSocketChannel();
        }
        
        //设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
        public final SelectableChannel configureBlocking(boolean block);
        
        public abstract Socket socket();
        
        //从通道里读数据
        public final long read(ByteBuffer[] dsts) throws IOException {
            return read(dsts, 0,
    
    下一篇:没有了