u07-非阻塞流

188 阅读5分钟

1. NIO概念入门

1.1 IO底层流程

概念: OS可以执行所有的CPU指令,包括很多危险操作,所以为了程序的健壮性,CPU指令需要被分类:

  • 内核态:位于内核内存空间,间由OS直接使用。
  • 用户态:位于用户内存空间,如果想访问内核态指令,必须向OS申请。

IO的数据读取真实流程:磁盘上的数据读取到内核空间,然后再读取到用户空间。

1.2 IO常见模型

概念:

  • IO模型:阻塞式IO模型:当用户发起一个read请求后,必须等到read出了结果,才可以去做别的事情。
  • NIO模型:非阻塞式IO模型:当用户发起一个read请求后,立刻得到结果,这个结果是你要读的硬盘数据是否已经加载到了内核空间:
    • 如果已经加载到了,开始从内核空间读取数据到用户空间(这个过程是阻塞的)。
    • 如果没有加载到,每隔一段时间就重问一遍(间隔轮询)。
    • 这种模式下,因为每次都会立刻得到结果而无需等待,所以不会阻塞,线程可以做其他的事情。
  • NIO复用模型:同时执行并监听多个NIO,哪个NIO的内核空间从硬盘上成功加载到了数据,执行哪个NIO。
  • AIO,异步IO模型:多个IO独立进行工作,互不干扰。

1.3 NIO

概念: NIO(Non-Block IO)是从jdk1.4版本开始引入的一个新的IO流API,可以替代标准的Java IO API,自带一个缓冲区,可以进行更加高效的文件读写操作。

  • 对比IO:
    • NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同。
    • IO面向流,NIO面向缓冲区。
    • IO是阻塞的,读或者写的时候都会阻塞当前线程,NIO是非阻塞的,可以同时读和写。
    • IO是单向的,要么读,要么写,NIO是双向的,同一个NIO实例,既可以读也可以写。
  • 在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。

2. NIO核心之Buffer缓冲

概念:

  • Buffer是NIO的核心之一,可以保存多个相同类型的数据,且支持双向操作。
  • Buffer根据数据类型不同(boolean除外) ,有以下Buffer的常用子类:
    • ByteBuffer/ShortBuffer/IntBuffer/LongBuffer
    • FloatBuffer/DoubleBuffer/CharBuffer

2.1 Buffer创建方式

概念: Buffer有两种类型:

  • 直接缓冲区:DirectBuffer
    • DirectBuffer位于计算机本地内存(native),与JVM无关,创建和销毁的开销比较大,也不容易被控制。
    • DirectBuffer的传输效率高,因为本地内存和内核空间因为都在同一OS中,可以进行直接进行通信。
    • DirectBuffer的内存回收工作需要自己想办法解决。
    • 创建方式:XXXBuffer.allocateDirect(1024):必须指定初始缓冲区容量字节大小。
  • 非直接缓冲区:HeapBuffer
    • HeapBuffer也叫Java堆缓冲区,位于JVM的堆内存中,创建和销毁的开销比较小,容易控制。
    • HeapBuffer的传输效率低,因为JVM属于另一块虚拟的OS,想要和本机OS的内核空间进行数据传输,需要先再开辟一块本地内存作为中间媒介,这块本地内存被称为中间缓冲区。
    • HeapBuffer的内存回收工作由JVM的GC管理,不用我们操心。
    • 创建方式:XXXBuffer.allocate(1024):必须指定初始缓冲区容量字节大小。
  • buffer.isDirect():判断一个缓冲区实例是否是直接缓冲区。

源码: /javase-advanced/

  • src: c.y.nio.BufferTest.build()
/**
 * @author yap
 */
public class BufferTest {

    @Test
    public void build() {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        System.out.println(byteBuffer.isDirect() ? "direct..." : "heap...");
    }
}

2.2 Buffer核心属性

概念: Buffer有四个核心属性:

  • capacity: 最大容量:只能装这么多个字节的数据,一旦声明不能改变。
    • 设置:必须在构造缓冲区的时候指定。
    • 获取:Buffer capacity()
  • limit: 操作上限:你只能操作这么多个字节的数据,默认等于最大容量。
    • 设置:void limit(int pos)
    • 获取:int limit()
  • position: 当前位置:你已经操作到了这个位置,默认为0。
    • 设置:Buffer position(int pos)
    • 获取:int position()
  • mark: 位置备份:你在这个位置做了标记,标记为-1时失效。
    • 设置:void mark():将position备份给mark。
    • 获取:void reset():将position移动到mark位置。

四个核心值的大小关系:0 <= mark <= position <= limit <= capacity

源码: /javase-advanced/

  • src: c.y.nio.BufferTest.coreAttributes()
/**
 * @author yap
 */
public class BufferTest {

    @Test
    public void coreAttributes() {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.position(0).limit(1024);
        System.out.println("capacity: " + byteBuffer.capacity());
        System.out.println("position: " + byteBuffer.position());
        System.out.println("limit: " + byteBuffer.limit());
    }
}

2.3 Buffer基础操作

概念:ByteBuffer 为例:

  • ByteBuffer put(byte[] src): 将字节数组src的全部内容传输到缓冲区。
  • byte[] array():返回缓冲区内容的字节数组形式,该操作不移动position。
  • Buffer flip():翻转为读:limit=position,position=0,mark=-1。
  • byte get():获取缓冲区中的数据,每获取一个字节,position+1。
  • byte get(index i):获取缓冲区中指i号置上的字节,该操作不移动position。
  • Buffer rewind():倒带重读:position=0,mark=-1。
  • Buffer clear():清除重置:position=0,limit=capacity,mark=-1。
  • boolean hasRemaining():缓冲区中是否还有未读数据。
  • int remaining():返回缓冲区中未读数据的字节数。

源码: /javase-advanced/

  • src: c.y.nio.BufferTest.bufferApi()
/**
 * @author yap
 */
public class BufferTest {

    @Test
    public void bufferApi() {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        byteBuffer.put("hello".getBytes(), 0, 5);
        byteBuffer.put("world".getBytes());
        System.out.println("data: " + new String(byteBuffer.array()));

        byteBuffer.flip();
        System.out.print("flip: ");
        System.out.print((char) byteBuffer.get(3));
        System.out.print((char) byteBuffer.get());
        System.out.println((char) byteBuffer.get());

        byteBuffer.rewind();
        System.out.print("rewind: ");
        System.out.print((char) byteBuffer.get());
        System.out.println((char) byteBuffer.get());

        byteBuffer.clear();
        System.out.print("clear: ");
        System.out.print((char) byteBuffer.get());
        System.out.println((char) byteBuffer.get());

        System.out.print("remaining: ");
        if (byteBuffer.hasRemaining()) {
            System.out.println(byteBuffer.remaining());
        }
    }
}

3. NIO核心之Channel通道

概念: 阻塞式IO的两端是App和硬盘数据,而非阻塞式NIO的两端是Buffer和硬盘数据(Buffer在App中),通过一个 java.nio.channels.FileChannel 接口来进行通信.

  • 构造:基础IO流实例.getChannel()
  • 方法:以ByteBuffer为例:
    • int read(ByteBuffer dst):将数据读到buffer中,读完返回-1。
    • int write(ByteBuffer src):将buffer中的内容写出去。

3.1 FileChannel

概念: FileChannel是Channel的文件IO实现类,负责连接硬盘文件数据和App中的Buffer。

源码: /javase-advanced/

  • src: c.y.nio.FileChannelTest
/**
 * @author yap
 */
public class FileChannelTest {

    /**
     * copy `nio-src.txt` to `nio-dest.txt`
     */
    @Test
    public void fileChannel() throws IOException {
        String srcPath = "D:" + File.separator + "java-io" + File.separator + "nio-src.txt";
        String destPath = "D:" + File.separator + "java-io" + File.separator + "nio-dest.txt";
        FileInputStream fis = new FileInputStream(srcPath);
        FileOutputStream fos = new FileOutputStream(destPath);
        FileChannel fisChannel = fis.getChannel();
        FileChannel fosChannel = fos.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        while (true) {
            byteBuffer.clear();
            int read = fisChannel.read(byteBuffer);
            if (read == -1) {
                break;
            }
            byteBuffer.flip();
            fosChannel.write(byteBuffer);
        }
        fosChannel.close();
        fisChannel.close();
        fis.close();
        fos.close();
        System.out.println("copy over...");
    }
}

3.2 SocketChannel

概念: SocketChannel是Channel的Socket实现类,是负责连接TCP客户端的通道,和它相对应的是ServerSocketChannel,是负责连接TCP服务端的通道。

  • ServerSocketChannel相关API方法:
    • static ServerSocketChannel open():打开一个服务端通道。
    • ServerSocketChannel bind(SocketAddress local):对服务端通道绑定一个Socket地址。
      • new InetSocketAddress(int port):绑定只需要指定端口号,IP可选。
    • SocketChannel accept():服务端通道等待接收数据,该方法会阻塞当前线程。
  • SocketChannel相关API方法:
    • static SocketChannel open(SocketAddress remote):打开一个客户端通道并连接到指定Socket地址。
      • new InetSocketAddress(String host, int port):必须指定IP和端口。

源码: /javase-advanced/

  • src: c.y.nio.BlockSocketServer
package com.yap.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

/**
 * @author yap
 */
public class BlockSocketServer {
    public static void main(String[] args) throws IOException {
        int port = 9999;
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        SocketAddress socketAddress = new InetSocketAddress(port);
        serverSocketChannel.bind(socketAddress);
        System.out.println("ready to accept data...");
        SocketChannel socketChannel = serverSocketChannel.accept();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        while (socketChannel.read(byteBuffer) != -1) {
            byteBuffer.flip();
            for (int i = 0, j = byteBuffer.limit(); i < j; i++) {
                System.out.print((char) byteBuffer.get());
            }
            System.out.println();
            byteBuffer.clear();
        }
        socketChannel.close();
        serverSocketChannel.close();
    }
}

  • src: c.y.nio.BlockSocketClient
package com.yap.nio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * @author yap
 */
public class BlockSocketClient {
    public static void main(String[] args) throws IOException {
        String ip = "127.0.0.1";
        int port = 9999;
        SocketAddress socketAddress = new InetSocketAddress(ip, port);
        SocketChannel socketChannel = SocketChannel.open(socketAddress);
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        System.out.println("please input your message...");
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String str;
        while ((str = br.readLine()) != null) {
            byteBuffer.put(("=> " + str).getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            byteBuffer.clear();
            if ("exit".equalsIgnoreCase(str)) {
                break;
            }
        }
        br.close();
        socketChannel.close();
    }
}

4. NIO核心之Selector选择

概念: Selector可以监控多个Channel状态并作出对应处理,是设计非阻塞IO模型的最佳选择。

  • Channel对应状态有四种
    • CONNECT:Channel已经准备好完成连接序列了(IP,端口等搭建完成)。
    • ACCEPT:Channel可以调用 accept() 了。
    • READ:Channel可以调用 `read(
    )` 了。
    • WRITE:Channel可以调用 write() 了。
  • 构造:static Selector open():开启一个Selector实例。
  • Channel相关API方法:
    • SelectableChannel configureBlocking(boolean block):配置Channel是否为可阻塞Channel。
    • SelectionKey register(Selector sel, int ops):将Channel注册给Selector:
      • param1: 将Channel注册给哪个Selector。
      • param2: 此Channel感兴趣的状态,只有在这个状态下开可以调用 channel() 获取我。
  • Selector常用API方法:
    • int select():返回选择器中已经处于就绪状态(准备好进行IO操作)的状态个数,这个状态被记作是一个SelectionKey,至少选择一个才会返回,否则阻塞。
    • Set<SelectionKey> selectedKeys():返回选择器中已经处于就绪状态(可以进行IO操作)的Set集合。
  • SelectionKey常用API方法:
    • boolean isAcceptable():判断是否是OP_ACCEPT状态。
    • boolean isReadable():判断是否是OP_READ状态。
    • SelectableChannel channel():获取当前状态对应的Channel。

源码: /javase-advanced/

  • src: c.y.nio.NonBlockSocketServer
package com.yap.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
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;

/**
 * @author yap
 */
public class NonBlockSocketServer {
    public static void main(String[] args) throws IOException {
        int port = 9002;
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        SocketAddress socketAddress = new InetSocketAddress(port);
        serverSocketChannel.bind(socketAddress);
        serverSocketChannel.configureBlocking(false);
        System.out.println("ready to accept data...");
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        SocketChannel socketChannel = null;
        while (selector.select() > 0) {
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
                SelectionKey selectionKey = it.next();
                if (selectionKey.isConnectable()) {
                    socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    System.out.println(socketChannel.getLocalAddress());
                }

                if (selectionKey.isAcceptable()) {
                    socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    socketChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    while (socketChannel.read(byteBuffer) > 0) {
                        byteBuffer.flip();
                        for (int i = 0, j = byteBuffer.limit(); i < j; i++) {
                            System.out.print((char) byteBuffer.get());
                        }
                        System.out.println();
                        byteBuffer.clear();
                    }
                }
                it.remove();
            }
        }
        if (socketChannel != null) {
            socketChannel.close();
        }
        serverSocketChannel.close();
    }

}

  • src: c.y.nio.NonBlockSocketClient
package com.yap.nio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * @author yap
 */
public class NonBlockSocketClient {
    public static void main(String[] args) throws IOException {
        String ip = "127.0.0.1";
        int port = 9002;
        SocketAddress socketAddress = new InetSocketAddress(ip, port);
        SocketChannel socketChannel = SocketChannel.open(socketAddress);
        socketChannel.configureBlocking(false);
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        System.out.println("please input your message...");
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String str;
        while ((str = br.readLine()) != null) {
            byteBuffer.put(("=> " + str).getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            byteBuffer.clear();
        }
        br.close();
        socketChannel.close();
    }
}