本网站(662p.com)打包出售,且带程序代码数据,662p.com域名,程序内核采用TP框架开发,需要联系扣扣:2360248666 /wx:lianweikj
精品域名一口价出售:1y1m.com(350元) ,6b7b.com(400元) , 5k5j.com(380元) , yayj.com(1800元), jiongzhun.com(1000元) , niuzen.com(2800元) , zennei.com(5000元)
需要联系扣扣:2360248666 /wx:lianweikj
最完善的NIO分析
奔跑的男人 · 588浏览 · 发布于2019-06-06 +关注

正所谓工欲善其事,必先利其器,在学习NIO之前,我们先了解一些基本概念。


阻塞与非阻塞

阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式。

阻塞:往往需要等待缞冲区中的数据准备好过后才处理其他的事情,否則一直等待在那里

非阻塞:当访问数据缓冲区时,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也会直接返回


同步与异步

同步:应用程序要直接参与IO读写的操作。在处理IO事件的时候,必须阻塞在某个方法上靣等待IO事件完成。

异步:所有的IO读写交给操作系统去处理,应用程序只需要等待通知。这个时候,我们可以去做其他的事情,当搡作系统完成IO后.会给应用程序一个通知。


为什么要使用NIO

在Java1.4之前的I/O系统中,提供的都是面向流的I/O系统,系统一次一个字节地处理数据,但是面向流的I/O速度非常慢。而在Java 1.4中推出了NIO,这是一个面向块的I/O系统,系统以块的方式处理数据,比按字节处理数据快的多。


IO与NIO之间的区别

IO模型IONIO
方式从硬盘到内存从内存到硬盘
通信面向流(乡村公路)
面向缓存(高速公路,多路复用技术)
处理阻塞IO(多线程)
非阻塞IO(反应堆Reactor)
触发
选择器(轮询机制)

NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器),下面我们会对其进行分别介绍。


缓冲区

缓冲区实际上是一个容器对象,更直接的说,其实就是一个数组,在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流I/O系统中,所有数据都是直接写入或者直接将数据读取到Stream对象中。

01.png


在NIO中,所有的缓冲区类型都继承于抽象类Buffer,最常用的就是ByteBuffer,对于Java中的基本类型,基本都有一个具体Buffer类型与之相对应。

02.png



下面我们举个例子


import java.nio.IntBuffer;
public class Demo {
    public static void main(String[] args) {
    // 分配新的int缓冲区,参数为缓冲区容量
    IntBuffer buffer = IntBuffer.allocate(10);
   
    for (int i=0;i<buffer.capacity();i++) {
// 将给定整数写入此缓冲区的当前位置,当前位置递增
buffer.put(i-1);
}
   
    // 重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为0
    buffer.flip();
   
    // 查看在当前位置和限制位置之间是否有元素
    while (buffer.hasRemaining()) {
    // 读取此缓冲区当前位置的整数,然后当前位置递增
    int j = buffer.get();
    System.out.print(j + " ");
    }
    }
}


运行结果

-1 0 1 2 3 4 5 6 7 8


buffer的工作机制

capacity 缓冲区数组的总长度

position 下一个要操作的数据元素的位置

limit 缓冲区数组中不可操作的下一个元素的位置,limit<=capacity

1 当我们刚刚初始化buffer数组时

03.png

2 向buffer写入数据

04.png

3 准备把buffer中的数据写到channel管道中,调用buffer.flip();

05.png

这时知道刚刚写入的数据在position到limit之间


4 这时底层操作系统就可以从缓冲区中正确读取这 5 个字节数据并发送出去。在下一次写数据之前我们在调一下 clear() 方法。缓冲区的索引状态又回到初始位置。


通道

通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象(通道),通过它可以读取和写入数据,当然了所有数据都通过Buffer对象来处理。我们永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区。同样不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。


在NIO中,提供了多种通道对象,而所有的通道对象都实现了Channel接口。


NIO读取数据实例

任何时候读取数据,都不是直接从通道读取,而是从通道读取到缓冲区。所以NIO读取数据其实可以分为三个步骤


获取Channel
创建Buffer
将数据从Channel读取到Buffer中
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class Demo {
    public static void main(String[] args) throws IOException {
    FileInputStream fin = new FileInputStream("d:\\my.txt");
   
    // 获取通道
        FileChannel fc = fin.getChannel();
        
        // 创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        
        // 读取数据到缓冲区
        fc.read(buffer);
        
        // 重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为0
        buffer.flip();
        
        while (buffer.remaining()>0) {
            byte b = buffer.get();
            System.out.print(((char)b));
        }
        
        fin.close();
    }
}


NIO写入数据示例

与读取数据的过程类似,三个步骤与读取数据一模一样


import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class Demo {
static private final byte message[] = {1,2,3,4,5,6,7,8,9,10};
 
static public void main( String args[] ) throws Exception {
FileOutputStream fout = new FileOutputStream( "d:\\my.txt" );
        
FileChannel fc = fout.getChannel();
        
    ByteBuffer buffer = ByteBuffer.allocate( 1024 );
        
    for (byte me : message) {
            buffer.put( me );
        }
     
    buffer.flip();
        
    fc.write( buffer );
        
    fout.close();
}
}

选择器

关于选择器的学习参考这篇文章:Java NIO 的前生今世 之四 NIO Selector 详解


选择器允许一个单一的线程来操作多个 Channel. 如果我们的应用程序中使用了多个 Channel, 那么使用 Selector 很方便的实现这样的目的, 但是因为在一个线程中使用了多个 Channel, 因此也会造成了每个 Channel 传输效率的降低。

06.png

为了使用 Selector, 我们首先需要将 Channel 注册到 Selector 中, 随后调用 Selector 的 select()方法, 这个方法会阻塞, 直到注册在 Selector 中的 Channel 发送可读写事件. 当这个方法返回后, 当前的这个线程就可以处理 Channel 的事件了。


创建选择器

Selector selector = Selector.open();

将 Channel 注册到选择器中

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);


注意:如果一个 Channel 要注册到 Selector 中, 那么这个 Channel 必须是非阻塞的。 因此,FileChannel 是不能够使用选择器的, 因为 FileChannel 都是阻塞的。


在使用 Channel.register()方法时, 第二个参数指定了我们对 Channel 的什么类型的事件感兴趣, 这些事件有:


Connect, 即连接事件(TCP 连接), 对应于SelectionKey.OP_CONNECT

Accept, 即确认事件, 对应于SelectionKey.OP_ACCEPT

Read, 即读事件, 对应于SelectionKey.OP_READ, 表示 buffer 可读

Write, 即写事件, 对应于SelectionKey.OP_WRITE, 表示 buffer 可写

一个 Channel发出一个事件也可以称为 对于某个事件, Channel 准备好了. 因此一个 Channel 成功连接到了另一个服务器也可以被称为 connect ready


SelectionKey

当我们使用 register 注册一个 Channel 时, 会返回一个 SelectionKey 对象, 这个对象包含了如下内容:


  • interest set,,即我们感兴趣的事件集, 即在调用 register 注册 channel 时所设置的 interest set

  • ready set,代表了 Channel 所准备好了的操作

  • channel

  • selector

  • attached object, 可选的附加对象


通过 Selector 选择 Channel

我们可以通过 Selector.select()方法获取对某件事件准备好了的 Channel, 即如果我们在注册 Channel 时, 对其的可写事件感兴趣, 那么当 select()返回时, 我们就可以获取 Channel 了。


获取可操作的 Channel

如果 select()方法返回值表示有多个 Channel 准备好了, 那么我们可以通过 Selected key set 访问这个 Channel:


Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    
    SelectionKey key = keyIterator.next();
    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }
    keyIterator.remove();
}

注意, 在每次迭代时, 我们都调用 “keyIterator.remove()” 将这个 key 从迭代器中删除, 因为 select() 方法仅仅是简单地将就绪的 IO 操作放到 selectedKeys 集合中, 因此如果我们从 selectedKeys 获取到一个 key, 但是没有将它删除, 那么下一次 select 时, 这个 key 所对应的 IO 事件还在 selectedKeys 中。


Selector 的基本使用流程

1 通过 Selector.open() 打开一个 Selector


2 将 Channel 注册到 Selector 中, 并设置需要监听的事件(interest set)


3 不断重复:


3.1 调用 select() 方法


3.2 调用 selector.selectedKeys() 获取 selected keys


3.3 迭代每个 selected key:


从 selected key 中获取 对应的 Channel 和附加信息(如果有的话)

判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT 事件, 则调用 “SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()” 获取

SocketChannel, 并将它设置为 非阻塞的, 然后将这个 Channel 注册到 Selector 中

根据需要更改 selected key 的监听事件

将已经处理过的 key 从 selected keys 集合中删除


关闭 Selector

当调用了 Selector.close()方法时, 我们其实是关闭了 Selector 本身并且将所有的 SelectionKey 失效, 但是并不会关闭 Channel


完整的 Selector 例子

public class NioEchoServer {
    private static final int BUF_SIZE = 256;
    private static final int TIMEOUT = 3000;
    public static void main(String args[]) throws Exception {
        // 打开服务端 Socket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 打开 Selector
        Selector selector = Selector.open();
        // 服务端 Socket 监听8080端口, 并配置为非阻塞模式
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        // 将 channel 注册到 selector 中.
        // 通常我们都是先注册一个 OP_ACCEPT 事件, 然后在 OP_ACCEPT 到来时, 再将这个 Channel 的 OP_READ
        // 注册到 Selector 中.
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            // 通过调用 select 方法, 阻塞地等待 channel I/O 可操作
            if (selector.select(TIMEOUT) == 0) {
                System.out.print(".");
                continue;
            }
            // 获取 I/O 操作就绪的 SelectionKey, 通过 SelectionKey 可以知道哪些 Channel 的哪类 I/O 操作已经就绪.
            Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                // 当获取一个 SelectionKey 后, 就要将它删除, 表示我们已经对这个 IO 事件进行了处理.
                keyIterator.remove();
                if (key.isAcceptable()) {
                    // 当 OP_ACCEPT 事件到来时, 我们就有从 ServerSocketChannel 中获取一个 SocketChannel,
                    // 代表客户端的连接
                    // 注意, 在 OP_ACCEPT 事件中, 从 key.channel() 返回的 Channel 是 ServerSocketChannel.
                    // 而在 OP_WRITE 和 OP_READ 中, 从 key.channel() 返回的是 SocketChannel.
                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                    clientChannel.configureBlocking(false);
                    //在 OP_ACCEPT 到来时, 再将这个 Channel 的 OP_READ 注册到 Selector 中.
                    // 注意, 这里我们如果没有设置 OP_READ 的话, 即 interest set 仍然是 OP_CONNECT 的话, 那么 select 方法会一直直接返回.
                    clientChannel.register(key.selector(), OP_READ, ByteBuffer.allocate(BUF_SIZE));
                }
                if (key.isReadable()) {
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer buf = (ByteBuffer) key.attachment();
                    long bytesRead = clientChannel.read(buf);
                    if (bytesRead == -1) {
                        clientChannel.close();
                    } else if (bytesRead > 0) {
                        key.interestOps(OP_READ | SelectionKey.OP_WRITE);
                        System.out.println("Get data length: " + bytesRead);
                    }
                }
                if (key.isValid() && key.isWritable()) {
                    ByteBuffer buf = (ByteBuffer) key.attachment();
                    buf.flip();
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    clientChannel.write(buf);
                    if (!buf.hasRemaining()) {
                        key.interestOps(OP_READ);
                    }
                    buf.compact();
                }
            }
        }
    }
}


NIO与IO

IO是以最基础的字节流来处理数据的,而NIO是以块的方式处理数据,因此NIO效率比IO效率会高出很多。NIO不是和IO一样用流的形式来进行处理数据,但又是基于这种流的形式,采用了通道和缓冲区的形式来进行处理数据的。NIO的通道是可以双向的,但是IO中的流只能是单向的。NIO的缓冲区(其实也就是一个字节数组)还可以进行分片,可以建立只读缓冲区、直接缓冲区和间接缓冲区。



相关推荐

PHP实现部分字符隐藏

沙雕mars · 1312浏览 · 2019-04-28 09:47:56
Java中ArrayList和LinkedList区别

kenrry1992 · 896浏览 · 2019-05-08 21:14:54
Tomcat 下载及安装配置

manongba · 957浏览 · 2019-05-13 21:03:56
JAVA变量介绍

manongba · 953浏览 · 2019-05-13 21:05:52
什么是SpringBoot

iamitnan · 1077浏览 · 2019-05-14 22:20:36
加载中

0评论

评论
分类专栏
小鸟云服务器
扫码进入手机网页