1.NIO的基本概念和三大核心组件

2.NIO的缓冲区(Buffer)介绍

3.NIO的通道(Channel)介绍

4.NIO的选择器(Selector)介绍

5.NIO的原理分析图

6.NIO的编程案例

1.NIO的基本概念和三大核心组件

1.1)NIO的基本概念
在我们学习过前面的Netty网络编程——Netty的基本介绍与BIO以后,我们来学习NIO。
NIO的概念:同步非阻塞

在NIO的模式中,一切都是非阻塞的

也就是说,客户端调用服务器端,仅仅能获取到目前可用的数据,如果有数据则返回数据,如果没有数据直接返回空而不是保持线程阻塞,非要等到服务器端处理完成再返回客户端。所以直至数据变得可读以前,该线程可以做其它的事情。
image.png

而且,在NIO模式中,一个线程是可以处理多个操作的
当一个客户端与服务器进行链接,这个socket连接就会加入到一个数组中,隔一段时间遍历一次,这样一个线程就能处理多个客户端的链接和数据了,不像之前的阻塞IO那样,一个连接分配一个线程。
image.png

1.2)NIO的三大核心组件

其实NIO是面向缓冲区,或者面向块编程的。把数据读取到缓冲区,然后让客户端线程从缓冲区读取数据,这就增加了处理过程中的灵活性。

NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)。

我们先来看一张图:
image.png

1.2.1)服务端和客户端进行连接之后,通过buffer(缓冲区)进行数据的交互。
1.2.2)服务端和客户端进行连接之后,通过各自的channel(通道)进行操作缓冲区。
1.2.3)在一个Channel有事件发出后(读、写、连接等等),selector就会切换到对应的channel进行处理数据。
1.2.4)Selector会根据不同的事件,在各个通道上进行切换
1.2.5)Buffer就是一个内存块,底层是一个数组。
1.2.6)Buffer可以读也可以写,需要调用flip方法进行切换。

2.NIO的缓冲区(Buffer)介绍
缓冲区(Buffer):
缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个包含数字的容器对象,该对象提供了一组api,可以让开发者轻松地使用内存块。同时缓冲区也设置了一些机制,能够跟踪和记录缓冲区的状态变化情况。
image.png

2.1)Buffer类及其子类
在Buffer中,Buffer是一个顶层的父类。
image.png
它拥有各种类型的buffer,比如ByteBuffer,是存储字节数组的缓冲区。
我们可以得出java中的基本数据类型除了boolean以外,都有一个Buffer类型与之对应。最常用的当然是ByteBuffer。

2.2)Buffer的重要属性
Buffer定义了四个属性来提供包含的数据元素的信息:
image.png

属性描述
mark标记
position位置,下一个要被读写的元素的索引,每次读写缓冲区数据时值都会改变,为下次读写做准备。
limit表示缓冲区当前的终点,不能对超过缓冲区极限的位置进行读写操作,极限是可以修改的。
capacity容量,就是能容纳的最大数据量,在缓冲区创建的时候就不能改变。

我们可能感觉positionlimit的概念理解有点不太容易区分,其实有以下的区别

当往buffer对象里数据的时候,limit一直等于capacity,表示最多能往buffer对象里写的数据

当调用buffer对象的flip()方法后(切换模式,改为buffer读模式),想从buffer对象读取数据的时候,position会被置为0,limit会被设置为之前是position,这个时候limit表示最多能从Buffer读多少数据。

2.3)Buffer的api
image.png

2.4)ByteBuffer的api
ByteBuffer作为最常用的Buffer,其api的主要方法如下:
image.png

3.NIO的通道(Channel)介绍
channel代表连接每个client连接都会建立一个channnel,channel可以对buffer进行操作。

同时,channel可以从buffer中读数据,也可以将数据写入buffer。

常用的channel类有FileChannel 、 DatagramChannel 、 ServerSocketChannel 和 SocketChannel。

我们可以先拿一个常见的FileChannel(文件数据的读写)进行举例:

3.1)使用FileChannel进行本地文件写数据

我们对一个本地文件进行写入操作。

public class NIOFileChannel01 {

    public static void main(String[] args) throws Exception {
        String msg = "hello,netty!";
        //创建一个输出流
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");

        //通过fileOutPutStream获得对应的CileChannel
        //这个fileChannel真实类型是FileChannelImpl
        FileChannel fileChannel = fileOutputStream.getChannel();

        //创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //把数据放入缓冲区
        byteBuffer.put(msg.getBytes());

        //对byteBuffer进行flip flip之后,就可以进行写操作
        byteBuffer.flip();

        //将byteBuffer写入到fileChannel
        fileChannel.write(byteBuffer);
        fileOutputStream.close();

    }

}

3.2)使用FileChannel进行本地文件读数据

@Slf4j
public class NIOFileChannel02 {

    public static void main(String[] args) throws Exception {
        //创建一个输入
        FileInputStream fileInputStream = new FileInputStream("d:\\file01.txt");

        //通过fileInputStream获得对应的CileChannel
        //这个fileChannel真实类型是FileChannelImpl
        FileChannel fileChannel = fileInputStream.getChannel();

        //创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //将channel的数据读入缓冲区
        fileChannel.read(byteBuffer);
        log.info(new String(byteBuffer.array()));

        fileInputStream.close();

    }

}

3.3)使用一个 Buffer 完成文件读取、写入

@Slf4j
public class NIOFileChannel03 {

    public static void main(String[] args) throws Exception {
        //创建一个输入
        FileInputStream fileInputStream = new FileInputStream("d:\\file01.txt");
        FileChannel inputChannel = fileInputStream.getChannel();

        //创建一个输出
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file02.txt");
        FileChannel outputChannel = fileOutputStream.getChannel();

        //创建一个buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(512);

        while(true){
            byteBuffer.clear();
            //把数据读入buffer
            int read = inputChannel.read(byteBuffer);
            log.info("read="+read);
            if(read==-1){
                //读完
                break;
            }
            //转化为写
            byteBuffer.flip();
            outputChannel.write(byteBuffer);

        }

        //关闭流
        fileInputStream.close();
        fileOutputStream.close();


    }

}

3.4)使用拷贝文件 transferFrom 方法

@Slf4j
public class NIOFileChannel04 {

    public static void main(String[] args) throws Exception {
        //创建一个输入
        FileInputStream fileInputStream = new FileInputStream("d:\\file01.txt");
        FileChannel inputChannel = fileInputStream.getChannel();

        //创建一个输出
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file03.txt");
        FileChannel outputChannel = fileOutputStream.getChannel();

       //使用transferForm完成拷贝
        outputChannel.transferFrom(inputChannel,0,inputChannel.size());

        //关闭流
        fileInputStream.close();
        fileOutputStream.close();

    }

}

4.NIO的选择器(Selector)介绍

我们在学习BIO的时候,发现每多创建一个客户端连接,都需要创建一个线程,那么NIO时如何办到一个工作线程处理多了连接的呢?那就是Selector

Selector可以使用一个线程,处理多个客户端连接。首先创建的连接都会被注册到selector上,selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以用一个单线程去管理多个通道,也就是一个线程管理多个链接请求。如图所示:

image.png

优点:
1)可以不用为每一个连接都创建一个线程,一个线程就可以处理N个客户端的链接和读写操作,从根本上解决了传统同步阻塞IO,一链接一线程的模型。
2)只有在连接真正有事件发生的时候(事件驱动),才会进行读写,就大大减少了系统开销。
3)避免了多线程之间的上下文切换开销。
4)当没有任务可用的时候,该线程就可以执行其它任务。

大致常用方法:
image.png

我们会在下面进行使用。

5.NIO的原理分析图
image.png

6.NIO的编程案例

服务器端:

@Slf4j
public class NIOServer {

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

        //创建ServerSocketChannel
        //客户端连接 通过serverSocketChannel获取socketChannel,再和selector绑定
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //绑定一个端口,在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6677));
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);

        //获得一个selector对象
        Selector selector = Selector.open();
        //把这个channel注册到selector上
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        log.info("注册后的selectionKey数量="+selector.keys().size());

        //循环等待客户端连接
        while(true){
            //当没有连接发生的时候
            if(selector.select(1000) == 0){
                log.info("服务器等待一秒,无连接");
                continue;
            }
            //当有链接发生的时候
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            log.info("selectionKeys数量="+selectionKeys.size());

            //遍历Set 使用迭代器
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while(iterator.hasNext()){
                //得到key
                SelectionKey key = iterator.next();
                //根据key的对应通道发生的事件做相应处理
                //发生连接事件
                if(key.isAcceptable()){
                    //如果是有新客户端连接
                    //通过该客户端生成socketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    log.info("客户端连接成功,生成了一个socketChannel"+socketChannel.hashCode());

                    //将socketChannel设置为非阻塞
                    socketChannel.configureBlocking(false);

                    //将socketChannel注册到selector,关注事件为read,同时关联一个buffer
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));

                    log.info("客户端连接后,注册的selectionKey数量"+selector.keys().size());

                }
                //发生读事件
                if(key.isReadable()){
                    //通过key 反向获取到对应channel
                    SocketChannel channel = (SocketChannel)key.channel();

                    //获取key绑定的buffer
                    ByteBuffer buffer = (ByteBuffer)key.attachment();
                    channel.read(buffer);
                    log.info("客户端发送信息="+new String(buffer.array()));

                }
                iterator.remove();
            }

        }


    }

}

客户端:

@Slf4j
public class NIOClient {

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

        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //提供服务器端的ip和端口
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6677);

        if(!socketChannel.connect(address)){
            while(!socketChannel.finishConnect()){
                log.info("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
            }
        }

        String msg = "hello,netty!";

        ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
        //发送数据,将 buffer写入channel
        socketChannel.write(buffer);
        System.in.read();
    }
}

流程:
5.1)创建ServerSocketChannel,监听端口
5.2)获得一个selector对象,把这个channel注册到这个selector上
5.3)循环select,查看是否有事件发生。
5.4)根据不同的事件做相应的处理。
5.5)移除key,防止无限循环。


苏凌峰
73 声望38 粉丝

你的迷惑在于想得太多而书读的太少。