文章目录

  • NIO编程
  • 1. 概述
  • NIO和BIO的区别
  • NIO三大核心
  • 2. 文件IO
  • 1. 概述和核心API
  • 缓冲区Buffer
  • 通道Channel
  • 2. 案例
  • 3. 网络IO
  • 1. 概述和核心API
  • 1. Selector(选择器/多路复用器)
  • 2. SelectionKey
  • 3. ServerSocketChannel
  • 4. SocketChannel
  • 2. 入门案例
  • 4. NIO多人聊天案例


NIO编程

1. 概述

java.nio 全称 java non-blocking IO, 是指 JDK 提供的新 API。 从 JDK1.4 开始, Java 提供了
一系列改进的输入/输出的新特性, 被统称为 NIO(即 New IO)。 新增了许多用于处理输入输出
的类, 这些类都被放在 java.nio 包及子包下 。

Java是如何控制硬件设备执行指令的_数据

NIO和BIO的区别
  1. BIO以流的方式处理数据,NIO以通道channel的方式处理数据
  2. BIO以自定义的byte类型数组充当缓冲区,NIO直接提供了buffer
  3. BIO阻塞式,NIO非阻塞式。

反转的思考:其实之前一直是往buffer中put数据的,现在想要get数据到通道,相当于指针还停留在put的数据的末尾,直接get得到空值,将指针重置到最初的位置,这样才能get所有数据。

NIO三大核心

Channel(通道)、Buffer(缓冲区)、Selector(选择器)。NIO基于Channel和Buffer进行操作,数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中。Selector用于监听多个通道的事件(连接请求、数据到达等),因此使用单个线程就可以监听多个客户端通道。

2. 文件IO

1. 概述和核心API
缓冲区Buffer

和BIO相比,抽象出buffer,读写都是到buffer。
是个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区状态变化情况。

buffer读写模式的转变

写:capacity为理论容量,底层可以扩,limit为实际容量

Java是如何控制硬件设备执行指令的_NIO_02


读:limit指向原来position的位置,position指向0,参见buffer的flip方法

public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
}

Java是如何控制硬件设备执行指令的_数据_03

读和写由channel来操作,channel.read(buffer);channel.write(buffer); Channel提供了从文件、网络读取数据的渠道,但是读取或写入的数据必须经由Buffer。

Java是如何控制硬件设备执行指令的_NIO_04

在NIO中,Buffer是一个顶层父类,抽象类,常用的子类有:

  • ByteBuffer,存储字节数据到缓冲区
  • ShortBuffer,存储字符串数据到缓冲区
  • CharBuffer,存储字符数据到缓冲区
  • IntBuffer,存储整数数据到缓冲区
  • LongBuffer,存储长整型数据到缓冲区
  • DoubleBuffer,存储小数到缓冲区
  • FloatBuffer,存储小数到缓冲区

对于Java中的基本数据类型,都有一个Buffer类型与之对应,最常用的是ByteBuffer类,其主要方法为

  • public abstract ByteBuffer put(byte[] b): 存储字节数据到缓冲区
  • public abstract byte[] get(): 从缓冲区数据转换成字节数组
  • public final byte[] array(): 把缓冲区数据转换为字节数组
  • public static ByteBuffer allocate(int capacity): 把一个现成的数组放到缓冲区中使用
  • public final Buffer flip(): 翻转缓冲区,重置位置到初始位置
通道Channel

类似于BIO中的stream,例如FileInputStream对象,用来建立到目标(文件、网络套接字、硬件设备等)的一个连接,注意:BIO的stream是单向的,NIO的通道是双向的,可读可写操作.

客户端socketChannel先和服务端的ServerSocketChannel建立连接connect,之后ServerSocketChannel调用accept方法创建服务端的socketChannel。之后就是client和server的socketChannel之间进行数据的读写操作。

Java是如何控制硬件设备执行指令的_Java是如何控制硬件设备执行指令的_05


将这些交互抽象为事件,如server的ServerSocketChannel为accept事件,client的socketChannel为connect事件…

Java是如何控制硬件设备执行指令的_NIO_06

常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel

Java是如何控制硬件设备执行指令的_客户端_07

这里先讲解FileChannel类,主要用来对本地文件进行IO操作,主要方法如下

  • public int read(ByteBuffer dst): 从通道读取数据并放到缓冲区
  • public int write(ByteBuffer src):把缓冲区的数据写到通道中
  • public long transferFrom(ReadableByteChannel src, long position, long count):从目标通道中复制数据到当前通道
  • public long transferTo(long position, long count, WriteableByteChannel target):把数据从当前通道复制给目标通道
2. 案例

往本地文件中写数据、读数据和复制操作

public class TestNio {
    @Test //往本地文件中写数据
    public void test01()throws Exception{
        // 1.创建输出流
        FileOutputStream fos = new FileOutputStream("basic.txt");
        // 2. 从流中得到一个通道
        FileChannel fc = fos.getChannel();
        // 3. 提供一个缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 4. 往缓冲区存入数据
        String str = "hello,nio";
        buffer.put(str.getBytes());
        // 5. 翻转缓冲区
        // 翻转的文档:After a sequence of channel-read or put operations,
        // invoke this method to prepare for a sequence of channel-write or relative get operations.
        buffer.flip();
        // 6. 把缓冲区写入到通道
        fc.write(buffer);
        // 7. 关闭
        fos.close();
    }

    @Test // 从本地文件读取数据
    public void test02() throws Exception{
        File file = new File("basic.txt");
        // 1. 创建输入流
        FileInputStream fis = new FileInputStream(file);
        // 2. 得到一个通道
        FileChannel fc = fis.getChannel();
        // 3. 准备一个缓冲区
        ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
        // 4. 从通道中读取数据并存到缓冲区
        fc.read(buffer);
        // buffer.array() Returns the byte array数组 that backs this buffer
        System.out.println(new String(buffer.array()));
        // 5.关闭
        fis.close();
    }

    @Test //使用NIO实现文件复制,特别适合复制大文件
    public void test03() throws Exception{
        // 1. 创建两个流
        FileInputStream fis = new FileInputStream("basic.txt");
        FileOutputStream fos = new FileOutputStream("target.txt");
        // 2. 得到两个通道
        FileChannel sourceFC = fis.getChannel();
        FileChannel destFC = fos.getChannel();
        // 3. 复制
        destFC.transferFrom(sourceFC,0,sourceFC.size());
        // 4. 关闭
        fis.close();
        fos.close();
    }
}

3. 网络IO

1. 概述和核心API

FileChannel并不支持非阻塞操作,学习NIO主要就是进行网络IO,java NIO的网络通道是非阻塞IO的实现,基于事件驱动,非常适用于服务器需要维持大量连接,但是数据交换量不大的情况,如一些即时通信的服务。

在java中编写Socket服务器,通常有以下几种模式:

  • 一个客户端连接用一个线程。如果连接非常多,分配线程也非常多,服务器可能因为资源耗尽而崩溃。
  • 每个客户端连接交给固定数量线程的连接池。可以处理大量的连接,但线程开销很大,连接如果非常多,排队现象比较严重
  • 使用NIO,非阻塞方式处理,可以一个线程,处理大量客户端连接
1. Selector(选择器/多路复用器)

解决了服务端需要起多个独立的socketChannel线程跟client的socketChannel交互的问题。

selector不断轮询注册在它上面的Channel,如果serverSocketChannel有accept事件,就再注册新的socketChannel,注册到selector;如果某个socketChannel发生读写事件,就处于就绪状态,被轮询出来,通过selectedKeys获取就绪channel的集合,进行后续IO操作。

这是IO模型,还有线程模型,是一个线程处理,还是怎么分配,为reactor 线程模型。

Java是如何控制硬件设备执行指令的_非阻塞_08


能够检测多个注册的通道上是否有事件发生,有,获取事件相应处理。这样就可以一个单线程管理多个通道,也就是多个连接。这样,只有真正有读写事件发生时,调用函数读写,减少系统开销,不用维护多个线程,避免线程间上下文切换的开销。

Java是如何控制硬件设备执行指令的_Java是如何控制硬件设备执行指令的_09

常用方法如下:

  • public static Selector open():得到一个选择器对象
  • public int select(long timeout): 监控所有注册的通道,当其中有IO操作时,将对应的SelectionKey加入到内部集合中并返回,参数超时时间
  • public Set<SelectionKey> selectionKeys():从内部集合中得到所有的SelectionKey
2. SelectionKey

代表Selector网络通道注册关系,有四种

  • int OP_ACCEPT:有新的网络连接可以accept,值为16
  • int OP_CONNECT:代表连接已经建立,值为8
  • int OP_READint OP_WRITE:代表读写操作,值为1和4

其常用方法如下:

  • public abstract Selector selector():得到与之关联的Selector对象
  • public abstract SelectableChannel channel():得到与之关联的通道
  • public final Object attachment():得到与之关联的共享数据
  • public abstract SelectionKey interestOps(int ops):设置或改变监听事件
  • public final boolean isAcceptable():是否可以accept
  • public final boolean isReadable():是否可以读
  • public final boolean isWritable():是否可写
3. ServerSocketChannel

用来在服务器端监听新的客户端Socket连接,常用方法

  • public static ServerSocketChannel open():得到一个ServerSocketChannel通道
  • public final ServerSocketChannel bind(SocketAddress local):设置服务器端端口号
  • public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,false表示非阻塞
  • public SocketChannel accept():接收一个连接,返回代表该连接的通道对象
  • public final SelectionKey register(Selector sel,int ops):注册一个连接器并设置监听事件
4. SocketChannel

网络IO通道,负责具体读写操作。NIO总是把缓冲区的数据写入到通道,或者把通道的数据读到缓冲区。常用方法

  • public static SocketChannel open():得到一个SocketChannel通道
  • public final SelectableChannel configureBlocking(boolean block):设置阻塞或非阻塞模式,false非阻塞
  • public boolean connect(SocketAddress remote):连接服务器
  • public boolean finishConnect():如果上面的方法连接失败,接下来就要通过该方法完成连接
  • public init write(ByteBuffer src):往通道写数据
  • public int read(ByteBuffer dst):从通道读数据
  • public final SelectionKey register(Selector sel,int ops,Object att):注册一个选择器并设置监听事件,最后一个参数可设置共享数据
  • public final void close():关闭通道

Java是如何控制硬件设备执行指令的_数据_10

2. 入门案例

以服务器端和客户端的数据通信为例

Java是如何控制硬件设备执行指令的_NIO_11

客户端

public class NIOClient {
    public static void main(String[] args) throws Exception{
        // 1. 得到一个网络通道
        SocketChannel channel = SocketChannel.open();
        // 2. 设置非阻塞方式
        channel.configureBlocking(false);
        // 3. 提供服务器端的IP地址和端口号
        InetSocketAddress address = new InetSocketAddress("127.0.0.1",9999);
        // 4. 连接服务器端
        if(!channel.connect(address)){
            while(!channel.finishConnect()){//NIO作为非阻塞的优势
                System.out.println("Client:连接服务器端的同时,我还可以做别的事情");
            }
        }

        // 5. 得到一个缓冲区并存入数据
        String msg = "hello,Server";
        ByteBuffer writeBuf = ByteBuffer.wrap(msg.getBytes());
        // 6. 发送数据
        channel.write(writeBuf);
        // 暂时不能关,否则服务器会报异常

        System.in.read();//临时措施
    }
}

服务器端

public class NIOServer {
    public static void main(String[] args) throws Exception{

        // 1.得到一个ServerSocketChannel对象,老大
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        // 2. 得到一个Selector对象 ,间谍
        Selector selector = Selector.open();

        // 3. 绑定端口号
        serverSocketChannel.bind(new InetSocketAddress(9999));

        // 4. 设置非阻塞方式
        serverSocketChannel.configureBlocking(false);

        // 5. 把serverSocketChannel对象注册给Selector
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 6. 干活
        while(true){
            // 6.1 监控客户端
            if(selector.select(2000)==0){//表示被监控的客户端通道有几个
                System.out.println("Server:没有客户端搭理我,我干点别的事");
                continue;
            }

            // 6.2 监控事件,得到SelectionKey,判断通道里的时间
             // selectedKeys() returns this selector's selected-key set.
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
            while(keyIterator.hasNext()){
                SelectionKey key = keyIterator.next();
            /*}
            Set<SelectionKey> keys = selector.selectedKeys();
            for (SelectionKey key : keys) {*/
                if(key.isAcceptable()){// 客户端连接事件
                    System.out.println("OP_ACCEPT");
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if(key.isReadable()){//读取客户端数据事件
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    channel.read(buffer);
                    System.out.println("客户端发来数据:"+new String(buffer.array()));
                }

                // 6. 手动从集合中移除当前的key
                keyIterator.remove();
            }

        }
    }
}

4. NIO多人聊天案例

服务器端

public class ChatServer {
    private ServerSocketChannel listenerChannel; //监听通道 老大
    private Selector selector; //选择器对象  间谍
    private static final int PORT = 9999;  //服务器端口

    public ChatServer(){
        try {
            // 1. 得到监听通道  老大
            listenerChannel = ServerSocketChannel.open();
            // 2.得到选择器  间谍
            selector = Selector.open();
            // 3. 绑定端口
            listenerChannel.bind(new InetSocketAddress(PORT));
            // 4. 设置非阻塞模式
            listenerChannel.configureBlocking(false);
            // 5. 将选择器绑定到监听通道监听accept事件
            listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("Chat Server is ready....");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 6.干活
    public void start() throws Exception{
        try {
            while(true){
                if(selector.select(2000)==0){
                    System.out.println("Server:没有客户客户端找我,我就干别的事情");
                    continue;
                }
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while(iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    if(key.isAcceptable()){//连接请求事件
                        SocketChannel sc = listenerChannel.accept();//接收通道
                        sc.configureBlocking(false);//非阻塞
                        sc.register(selector,SelectionKey.OP_READ);//监听读事件
                        System.out.println(sc.getRemoteAddress().toString().substring(1)+"上线了");//打印客户端

                    }
                    if(key.isReadable()){
                        readMsg(key);
                    }
                    // 一定要把当前key删掉,防止重复处理
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 读取客户端发来的消息并广播出去
    public void readMsg(SelectionKey key) throws Exception{
        SocketChannel channel = (SocketChannel) key.channel();//获取通道
        ByteBuffer buffer = ByteBuffer.allocate(1024); //获取缓冲区
        int count = channel.read(buffer);//从缓冲区读取数据,count>0说明有数据
        if(count>0){
            String msg = new String(buffer.array()); // 缓冲区数据转换为字节数组,转换为字符串
            printInfo(msg);
            broadcast(channel,msg);//广播,排除发送者,发送msg
        }
    }

    // 给所有的客户端发广播
    public  void broadcast(SocketChannel except,String msg) throws Exception{
        System.out.println("服务器发送广播了。。。");
        for (SelectionKey key : selector.keys()) {
            // 获取每个selectionKey
            SelectableChannel targetChannel = key.channel();//每个通道
// 通道是SocketChannel的实例并且不是except
            if(targetChannel instanceof SocketChannel && targetChannel!=except){
                SocketChannel destChannel = (SocketChannel)targetChannel;
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());//wrap获取buffer
                destChannel.write(buffer); //写数据
            }
        }

    }

    private void printInfo(String str){// 往控制台打印消息
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("["+sdf.format(new Date())+"] -> "+str);

    }

    public static void main(String[] args) throws Exception{
        new ChatServer().start();
    }
}

客户端

public class ChatClient {
    private final String HOST = "127.0.0.1";//服务器地址
    private int PORT = 9999;//服务器端口
    private SocketChannel socketChannel;//网络通道
    private String userName; // 聊天用户名

    public ChatClient() throws IOException{
        // 1. 得到一个网络通道
        socketChannel = SocketChannel.open();
        // 2. 设置非阻塞方式
        socketChannel.configureBlocking(false);
        // 3. 提供服务器端的IP地址和端口号
        InetSocketAddress address = new InetSocketAddress(HOST,PORT);
        // 4. 连接服务器端
        if(!socketChannel.connect(address)){
            while (!socketChannel.finishConnect()){//NIO的优势
                System.out.println("Client:连接服务器的同时,我还可以干别的事情");
            }
        }
        // 5. 得到客户端IP地址和端口信息,作为聊天用户名使用
        userName = socketChannel.getLocalAddress().toString().substring(1);
        System.out.println("----------Client(" +userName+" ) is ready---------------");
    }

    // 向服务器端发送数据
    public void sendMsg(String msg) throws Exception{
        if(msg.equalsIgnoreCase("bye")){
            socketChannel.close();
            return;
        }
        msg = userName + "说:"+msg;
        ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
        socketChannel.write(buffer);
    }

    // 从服务器端接收数据
    public void receiveMsg() throws Exception{
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int size = socketChannel.read(buffer);
        if(size>0){
            String msg = new String(buffer.array());
            System.out.println(msg.trim());//trim为了只显示简单的话,不带空格
        }
    }

}

客户端启动

public class TestChat {
    public static void main(String[] args) throws Exception{
        final ChatClient chatClient = new ChatClient();
        new Thread(){ //单独开个线程,不停接收数据
            @Override
            public void run() {
                while(true){
                    try {
                        chatClient.receiveMsg();
                        Thread.sleep(2000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNextLine()){
            String msg = scanner.nextLine();
            chatClient.sendMsg(msg);
        }
    }
}