NIO

Java NIO 的前生今世 之四 NIO Selector 详解

1. BIO的问题

BIO是阻塞IO,那么阻塞到底发生在哪里?
ServserSocket.accept()是阻塞的
② 所有输入流和输出流都是阻塞的,接收方等待发送方发送消息的时候是阻塞等待的,如果发送方一直不发送消息,那么接收方就要一直阻塞等待干不了其他事
③ 在BIO多人聊天室的案例中,由于BIO输出输出的阻塞,我们不得不为他们分配不同的线程

2. NIO非阻塞式IO

--使用Channel代替Stream
--使用Selector监控多条Channel,由于Channel的读的方法可以是非阻塞的,所以我一直监控Channel的数据是否准备好
--可以在一个线程处理Channel I/O

3. ChannleBuffer

对应BIO来说,完成程序与磁盘的读写需要在磁盘到程序建立一个管道,就像现实生活中的水管,这样源节点的字节数据就像水流一样直接流到程序

而NIO的Channel替代BIO的流,我们可以通过Channel读写,但是真正完成读写我们需要一个Buffer,他对应的是内存中一块可以读写的区域,也就是说Channel就是程序和磁盘中间的通道,我们可以把他理解为生活当作的铁路,只是用于连接,铁路自己本身不能完成运输,想完成运输要依赖于火车,也就是NIO中的缓冲区了,把数据装到缓冲区从铁路传输缓冲区到目的地,channel是双向的,Buffer可以双向移动(可读可写)

3.1 Buffer

Buffer在Java NIO 中负责数据的存取,缓冲区低层就是数组,用于存储不同数据类型的数据,根据数据类型的不同(boolean除外),提供了相应类型的缓冲区ByteBuffer/ CharBuffer/ ShortBuffer/ IntBuffer/ LongBuffer/ FloatBuffer/ DoubleBuffer,缓冲区的管理方式几乎一致,通过allocate()获取缓冲区

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
① 缓冲区的四个核心属性

capacity 容量,表示缓冲区中最大存储数据的容量,一但声明不能改变。(因为底层是数组,数组一但被创建就不能被改变)

limit 界限,表示缓冲区中可以操作数据的大小。(limit后数据不能进行读写)

position 位置,表示缓冲区中正在操作数据的位置position <= limit <= capacity

mark 标记,表示记录当前position的位置,可以通过reset()恢复到mark的位置

② 使用方法

allocate():分配缓冲区

ByteBuffer byteBuffer = ByteBuffer.allocate(8);

在这里插入图片描述
put():将数据存入缓冲区

byte[] data = new byte[] {'H','E','L','L','O'};
byteBuffer.put(data);

在这里插入图片描述
由于Buffer是双向的,可以读可以写,所以他有读写两种模式

flip(): 切换到读取数据的模式

byteBuffer.flip();

在这里插入图片描述
get():读取数据

 byte[] data1 = new byte[3];
 byteBuffer.get(data1);

读的最远位置就是limit的位置,保证了读到的数据就是刚刚写入的数据
在这里插入图片描述
clear():清空缓冲区,但是缓冲区中的数据依然存在,只是处于一种“被遗忘“的状态,所以的指针回到了最初的位置

compact():如果我上一次没有读完,读到一半就切换到了写模式,我想把没读的那一部分不清除,而是保存起来
compact会把未读的数据拷贝到缓冲区的头部,position指向移动后未读取的数据的后一个位置,然后再他后面进行写
在这里插入图片描述
rewind():重复读,使position归0

mark():标记。mark会记录当前的position之后使用reset将此缓冲区的位置重置为先前标记的位置

reset():position恢复到mark记录的位置

hasRemaining(),判断缓冲区是不是还要没读的,告诉当前位置和极限之间是否存在任何元素

remaining(),还有几个没读的,返回当前位置和限制之间的元素数

3.2 Channel

Channel用于源节点与目标节点的连接。在Java NIO中负责缓冲区中数据的传输。Channel本身并不存储数据,因此需要配合Buffer一起使用,每一个Channel也可以向另一个Channel进行数据交换

  • 我们可以在同一个 Channel 中执行读和写操作, 然而同一个 Stream 仅仅支持读或写.

  • Channel 可以异步地读写, 而 Stream 是阻塞的同步读写.

  • Channel 总是从 Buffer 中读取数据, 或将数据写入到 Buffer

java.nio.channels.Channel接口:

用于本地数据传输:
​ |-- FileChannel

用于网络数据传输:
​ |-- SocketChannel //TCP 操作|-- ServerSocketChannel //TCP 操作, 使用在服务器端|-- DatagramChannel //UDP 操作
① FileChannel

打开Channel

Java 针对支持通道的类提供了一个 getChannel() 方法

FileChannel fin = null;//与源文件之间的通道
fin = new FileInputStream(source).getChannel();

文件大小

我们可以通过 channel.size()获取关联到这个 Channel 中的文件的大小. 注意, 这里返回的是文件的大小, 而不是 Channel 中剩余的元素个数.

channel.size()

从 FileChannel 中读取数据

因为Buffer是在管道运输,所以从FileChannel读实际上是把读的数据写到缓冲区

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);//如果 read()返回 -1表示读完了

写入数据

写入实际上是从buffer中读

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}
使用NIO完成文件的拷贝
   public void nioBufferCopy(File source, File target){
        //通道
        FileChannel fin = null;//与源文件之间的通道
        FileChannel fout = null;//与目标文件之间的通道
        try {
            fin = new FileInputStream(source).getChannel();
            fout = new FileOutputStream(target).getChannel();
            //一个Buffer又读又写
            ByteBuffer buffer = ByteBuffer.allocate(1024);//最大容量为1024
            //fin-->buffer-->fout
            while (fin.read(buffer)!=-1){//写到buffer中
                //buffer转换为读模式
                buffer.flip();
                while(buffer.hasRemaining()){//只要buffer里面有可以读的就一直读
                    fout.write(buffer);
                }
                //清空转换为写模式
                buffer.clear();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                fin.close();
                fout.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
使用NIO直接让两个Channel交互完成文件的拷贝
 public void nioTransferCopy(File source, File target){
        //通道
        FileChannel fin = null;//与源文件之间的通道
        FileChannel fout = null;//与目标文件之间的通道
        try {
            fin = new FileInputStream(source).getChannel();
            fout = new FileOutputStream(target).getChannel();
            //直接让两个Channel交互完成文件的拷贝
            long transfer = 0;
            long size = fin.size();
            while (transfer!=size) {
                transfer += fin.transferTo(0, fin.size(), fout);//返回结果是这次一共传输了多少字节
                
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                fin.close();
                fout.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

对比所以的文件拷贝方法,包括以前的流的方法,使用缓冲区的效率要远远大于不使用缓冲区,NIO的性能实际上和传统IO差不多,因为在JDK1.4以后,传统IO的底层也进行了改进,所以性能和NIO差不多

SocketChannel

SocketChannel 是一个客户端用来进行 TCP 连接的 Channel.
创建一个 SocketChannel 的方法有两种:

  • 打开一个 SocketChannel, 然后将其连接到某个服务器中

  • 当一个 ServerSocketChannel 接受到连接请求时, 会返回一个 SocketChannel 对象.

打开 SocketChannel

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));

读取数据
如果 read()返回 -1, 那么表示连接中断了.

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);

写入数据

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}
非阻塞模式

我们可以设置 SocketChannel 为异步模式, 这样我们的 connect, read, write 都是异步的了

连接

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://example.com", 80));

while(! socketChannel.finishConnect() ){
    //wait, or do something else...    
}

在异步模式中, 或许连接还没有建立, connect 方法就返回了, 因此我们需要检查当前是否是连接到了主机, 因此通过一个 while 循环来判断.

读写
在异步模式下, 读写的方式是一样的.
在读取时, 因为是异步的, 因此我们必须检查 read 的返回值, 来判断当前是否读取到了数据

ServerSocketChannel

ServerSocketChannel 顾名思义, 是用在服务器为端的, 可以监听客户端的 TCP 连接, 例如:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true){
    SocketChannel socketChannel =
            serverSocketChannel.accept();

    //do something with socketChannel...
}
非阻塞模式

在非阻塞模式下, accept()是非阻塞的, 因此如果此时没有连接到来, 那么 accept()方法会返回null:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);

while(true){
    SocketChannel socketChannel =
            serverSocketChannel.accept();

    if(socketChannel != null){
        //do something with socketChannel...
        }
}

4. ChannleSelector

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

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

创建选择器

通过 Selector.open()方法, 我们可以创建一个选择器

Selector selector = Selector.open();
Channel 注册到选择器中

为了使用选择器管理 Channel, 我们需要将 Channel 注册到选择器中:

channel.configureBlocking(false);

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

注意, 如果一个 Channel 要注册到 Selector 中, 那么这个 Channel 必须是非阻塞的, 即channel.configureBlocking(false);

因为 Channel 必须要是非阻塞的, 因此 FileChannel 是不能够使用选择器的, 因为 FileChannel 都是阻塞的.

注意到, 在使用 Channel.register()方法时, 第二个参数指定了我们对 Channel 的什么类型的事件感兴趣,也就是我们让Selector监听Channel的什么状态,这些事件有:

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

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

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

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

可以使用或运算|来组合多个事件
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey

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

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

我们可以通过如下方式获取 interest set:

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE; 

--------------------------------------------------------------------------------------   
ready set:代表了 Channel 所准备好了的操作

int readySet = selectionKey.readyOps();

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
--------------------------------------------------------------------------------------   
channel: 获取相对应的 Channel 

Channel  channel  = selectionKey.channel();
--------------------------------------------------------------------------------------   
selector:获取相对应的 Selector:

Selector selector = selectionKey.selector();  
--------------------------------------------------------------------------------------   
attached object, 可以在selectionKey中附加一个对象:
通过 Selector 选择 Channel

我们可以通过 Selector.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 中.

例如此时我们收到 OP_ACCEPT 通知, 然后我们进行相关处理, 但是并没有将这个 Key 从 SelectedKeys 中删除, 那么下一次 select() 返回时 我们还可以在 SelectedKeys 中获取到 OP_ACCEPT 的 key.

注意, 我们可以动态更改 SekectedKeys 中的 keyinterest set. 例如在 OP_ACCEPT 中, 我们可以将 interest set 更新为 OP_READ, 这样 Selector 就会将这个 Channel 的 读 IO 就绪事件包含进来了

Selector 的基本使用流程

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

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

不断重复以下步骤

① 调用 select() 方法

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

③ 迭代每个 selected key:

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

⑤ 判断是哪些 IO 事件已经就绪了, 然后处理它们. 如果是 OP_ACCEPT 事件, 则调用 "SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()" 获取 SocketChannel, 并将它设置为 非阻塞的, 然后将这个 Channel 注册到 Selector 中.

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

⑦ 将已经处理过的 keyselected keys 集合中删除

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

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();
                }
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值