java之NIO基础

本文大纲如下:

  • 1、什么是NIO
  • 2、为什么使用NIO
  • 3、NIO的基本使用
  • 4、BIO、NIO、AIO区别以及总结
NIO大纲

一、什么是NIO

NIO是JDK1.4新加入的,为了解决BIO(原始IO的不足)的非阻塞IO模型。原始的IO流是单向的,如InputStream只能读取不能写入,而NIO是面向通道(Channel)和缓冲区(Buffer),NIO可以输入也可以输出。

二、为什么使用NIO

探讨NIO之前,肯定先说明BIO的不足,只有在BIO满足不了需求的时候,才会出现新的技术代替。如下图所示,因为BIO是一个阻塞的IO模型,所以需要为每个IO配备一个线程去监听调度,如果在高并发的情况下,开辟新的线程以及切换线程会带来巨大的损耗。但是对于比较简单的文件读写,还是使用BIO。


BIO模型

NIO的出现解决了BIO的低效率不能高并发的问题,所以NIO不是阻塞的IO模型。他只需要调用一个线程去轮询Selector,是否有新的事件出现即可。再通过事件找到具体的Channel,这样就可以将N的线程降低为一个线程了。如下图所示,只需要单个线程监听Selector,当对应的Channel有消息到达时候,再调用工作线程去处理,这样可以减少了数据未到达之前的等待时间。


NIO模型

无论是输入还是输出,NIO中Channel和Buffer是配对使用的。在输入(Input)时,Channel的数据读取到Buffer中,使用者再从Buffer中读取数据。输出反之。调度者直接调度的只能是Buffer。


数据流流程

三、NIO的基本用法

1)、Channel、Buffer、Selector组件的介绍

① 、Channel通道

Channel通道是 I/O 传输发生时通过的入口。当Buffer数据写入到Channel时,他就开始传输了。反之当Channel接收到数据时,Buffer就可以读取了。Channel是真正开始数据传输的。

对应的Channel主要有:

  • FileChannel 读写文件
  • DatagramChannel 读写UDP数据
  • SocketChannel 读写TCP数据
  • ServerSocketChannel 监听TCP链接,并为服务端创建一个SocketChannel

②、Buffer缓冲区

Buffer即缓冲区。在NIO模式下,数据并非立刻到达的,在数据没到达之前都是阻塞的,所以在NIO中才引入缓冲区概念,数据到达时先写入到Buffer中,再交给调用者处理缓冲区。这样处理者线程就不需要阻塞了。

对应的Buffer有:

  • ByteBuffer 以字节为单位
  • CharBuffer 以Char为单位
  • DoubleBuffer 以Double为单位
  • FloatBuffer 以Float为单位
  • IntBuffer 以Int为单位
  • LongBuffer 以Long为单位
  • ShortBuffer 以Short为单位
Buffer的属性:
  • private int mark = -1;
  • private int position = 0;
  • private int limit;
  • private int capacity;

Buffer中共有mask、position、limit、capacity属性,其中主要的是position、limit、capacity。

capacity: capacity是容器的大小,即缓冲区的大小。

limit : capacity意味着缓冲区的大小,但并不是所有的缓冲区,使用者都能用的上,使用者可以根据需要,划分出自己所需要的大小,但不能大于了capacity,这就是limit的意义。写的模式下,limit代表了,最多可以写多少。读模式下,意味着最多可以读到多少。当从写模式切换至读模式,即调用flip()函数后,limit=position,position=0;这样就可以开始读buffer了。

position:在写的模式下,position为0,position代表下一个写入的位置,所以position最多不能大于limit-1;在读模式下,position代表的是下一个读取的位置,大小同样不能大于limit-1。在初始状态下或者调用flip函数时,position会重新赋值等于0;

mark: mark从字面意思就是标记的意思,它就是当前position的一个快照。通过reset函数会让position重新赋值为mark。

Buffer中核心API:

Buffer既然是个内存,它的API就是对该内存位置的管理标记,即对属性管理。下面只介绍部分方法

  • clear() 当该buffer中的数据处理完的时候,就可以调用该方法,他会使buffer变成初始状态。
  • compact() 于clear一样,compact()也是清除的作用,但是他只清除读取完的数据,把未读取的数据copy到首部,移动position和limit
  • hasRemaining() 代表是否有下一个位置可以处理。读时是不是读完了,写时为是不是写完了。通常是在这操作之前判断。

③、Selector选择器

当应用中有大量的Channel的时候,即IO连接时。还得有大量的线程去监听数据状态。为此NIO加入了Selector,由Channel向Selector注册,并支持一个Selector可以注册多个Channel。将监听 Channel换成监听Selector,从而减少线程使用。

由于Channel可以进行读或者写,所以向Selector注册时,需要表明是监听哪一种事件。最终轮询Selector即可知道哪种事件到达了。

Selector事件如下:

  • OP_ACCEPT TCP服务端接收到客户端连接事件
  • OP_CONNECT 该连接是否已经断开事件
  • OP_READ 可以读取该数据事件
  • OP_WRITE 可以写入数据事件

2)、NIO基本用法

① 、FileChannel

FileChannel是读取文件时的Channel

读取文件例子如下:

    public static void main(String[] args) throws IOException {
        RandomAccessFile file = new RandomAccessFile("D:\\proguard-rules.pro", "rw");
        FileChannel channel = file.getChannel();

        // 分配缓存空间
        ByteBuffer buf = ByteBuffer.allocate(48);
        // buf读取通道的数据 length为读到的数据长度
        int length = channel.read(buf);
        byte[] bytes = new byte[48];
        while (length != -1) {
            // buf由写的状态 切换至读状态
            buf.flip();
            while(buf.hasRemaining()){
                buf.get(bytes,0,length);
                System.out.print(new String(bytes));
            }
            // 清空buf数据
            buf.clear();
            length = channel.read(buf);
        }
        file.close();
    }

上诉代码只是简单的读取文件,虽然简单,但包含了NIO的基本用法流程。生产Buffer对象有两种方法:

  • ByteBuffer buf = ByteBuffer.allocate(48);
  • ByteBuffer buf = ByteBuffer.wrap(bytes,0,length);
transferTo(long position, long count,
                                    WritableByteChannel target)

现代的操作系统分为内核态和用户态,文件的copy是从内核态 ->用户态 ->内核态,它需要经过用户态才能copy。FileChannel中的拷贝transferTo(transferTo类似),直接从内核态到内核态,效率比较高。

② 、DatagramChannel

DatagramChannel是接收发送UDP的,而UDP是面向数据包,而非数据流的。

发送数据包:

        DatagramChannel channel = DatagramChannel.open();

        // 发送数据
        String data = "DatagramChannel...";
        ByteBuffer writeBuffer = ByteBuffer.allocate(48);
        writeBuffer.clear();
        writeBuffer.put(data.getBytes());
        writeBuffer.flip();
        int len = channel.send(writeBuffer, new InetSocketAddress("127.0.0.1", 5520));
        System.out.println(len);

接收数据包:

        DatagramChannel channel = DatagramChannel.open();
        // 接收绑定端口
        channel.socket().bind(new InetSocketAddress(5521));
        ByteBuffer readBuffer = ByteBuffer.allocate(48);

        channel.receive(readBuffer);
        readBuffer.flip();
        byte[] bytes = new byte[48];
        while (readBuffer.hasRemaining())  {
            readBuffer.get(bytes,0,readBuffer.limit());
            System.out.print(new String(bytes));
        }

③ 、ServerSocketChannel

利用ServerSocketChannel监听TCP链接

       Selector selector = Selector.open();

        ServerSocketChannel server = ServerSocketChannel.open();
        // 设置为非阻塞,没有连接时也会返回null
        server.configureBlocking(false);
        // 绑定本地端口
        server.socket().bind(new InetSocketAddress(5510));
        // 注册客户端连接到达监听
        server.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            try {
                if (selector.select() == 0) {
                    continue;
                }

                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {

                    SelectionKey key = iterator.next();
                    iterator.remove();
                    // 客户端到达状态
                    if (key.isAcceptable()) {
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                        // 非阻塞状态拿到客户端连接
                        SocketChannel socketChannel = serverSocketChannel.accept();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

首先,生成ServerSocketChannel对象,并绑定本地端口。再用ServerSocketChannel向Selector注册SelectionKey.OP_ACCEPT。轮询Selector返回的事件,当有TCP连接时,则生成SocketChannel进行网络数据传输。

④ 、SocketChannel

SocketChannel专门就是接收发送TCP数据的,打开连接,将buffer中的数据写入到channel或者从channel读取数据到buffer中。举个简单的例子。

    // 客户端打开SocketChannel
    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.connect(new InetSocketAddress("127.0.0.1", 5510));

    // 开启非阻塞模式 异步模式下调用connect(), read() 和write() 可以立即返回无阻塞
    socketChannel.configureBlocking(false);
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    // 连接还在
    while (!socketChannel.finishConnect()) {
        // 从channel中读取数据
        socketChannel.read(byteBuffer);
        // buffer切换到读模式
        byteBuffer.flip();
        // channel发送buffer中数据
        socketChannel.write(byteBuffer);
    }

在高并发的情况下,需要借助Selector,利用轮询Selector方式,找到SocketChannel,再根据需求做特定的处理。

Selector selector = Selector.open();
        // 客户端打开SocketChannel
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 5510));

        // 开启非阻塞模式 异步模式下调用connect(), read() 和write() 可以立即返回无阻塞
        socketChannel.configureBlocking(false);
        socketChannel.register(selector,SelectionKey.OP_READ);
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 连接还在
        while (true) {
            if (selector.select() == 0) {
                continue;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                if (selectionKey.isValid()) {
                    // 取消对读事件的继续监听
                    selectionKey.interestOps(selectionKey.readyOps() & ~SelectionKey.OP_READ);
                    // 从channel中读取数据
                    socketChannel.read(byteBuffer);
                }
                iterator.remove();
            }
        }
    }

channel向Selector注册是SelectionKey key = channel.register(selector, SelectionKey.OP_READ);结合上述的介绍可以看出SelectionKey与Channel是一对一的关系,而Selector和Channel是一对多关系。

将SelectionKey作为key,value是处理数据的Runnable,注册的时候,存入Runnable。事件到达时,根据key取出Runnable,这样就避免为每个连接创建线程执行了。


Input和Output流程

四、BIO、NIO、AIO区别

  • BIO,同步阻塞的IO模型。当用InputStream去读取数据的时候,必须等到数据操作完成才能进行下一步操作,否则会阻塞当前的线程。这种方式比较简单,但短流量高并发的情况下,会造成效率低下。基本用于本地文件操作等简单业务。

  • NIO,同步非阻塞IO模型。NIO还不是异步,它将一个线程操作一次请求,只是有效的减少了线程。即每一个连接注册到多路复用器Selector中,轮询Selector中是否有事件到达,有事件就代表一次请求到达,再用一个线程处理该请求。相比较BIO,它只是将多个线程去等待数据到达减低为一个线程去等待。多用于低流量多连接的场景,如聊天服务器。

  • AIO,异步非阻塞IO模型。当进行读写操作时,只须直接调用API的read或write方法。该方法都是异步的,方法执行完成会回调回来。从方法签名中可以看出,read时候会注入一个CompletionHandler回调方法,通知该次操作的状态。调用者发送一个read或者write操作请求,操作完成后会得到一个通知,真正的IO操作由操作系统内核进行处理。AIO多用于长连接业务。

    public abstract <A> void read(ByteBuffer dst,
                                  long timeout,
                                  TimeUnit unit,
                                  A attachment,
                                  CompletionHandler<Integer,? super A> handler);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容