NIO 入门

1 概述

NIO  指是:非阻塞I/O 或  异步非阻塞I/O

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

Channel  是一个通道 是对传统IO中的流的模拟,读入或者写出的所有数据必须通过一个Channel对象。

Channel 是全双工,所以它可以比流更好的地映射底层操作系统API。

Buffer实质上是一个容器对象,从Channel中读取的任何数据都要读到Buffer中,同样发送给一个通道的所有数据都必须先放到Buffer中(数据总是从Channel读入到Buffer中,或者从Buffer中写出到Channel中)。

可以将数据直接写入或将数据 直接读到Stream对象中

Selector用于监听多个通道的事件(比如连接打开和数据到达),所以单个线程可以监听多个数据通道。

异步I/O也被称为AIO

NIO 提供了 SocketChannel 和ServerSocketChannel 两种不同的套接字通道实现,这两种新增的通道都支持 阻塞和非阻塞两种模式

传统的BIO编程

NIO和传统IO之间的一个最大区别是:IO是面向流的,NIO是面向缓冲区的。

IONIO
面向Stream流面向Buffer缓冲区
阻塞IO非阻塞IO
无选择器有选择器

2 为什么要使用 NIO?

NIO 的创建目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机代码。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。

3 Channel

3.1、 什么是Channel?

  Channel是一个对象,可以通过Channel读取和写入数据。Channel和传统IO中Stream流差不多,不过Stream流是单向的(如:InputStream、OutputStream),而Channel是双向的,既可以进行读操作,又可以进行写操作。

3.2 、NIO中Channel的主要实现

   NIO中的Channel主要实现有:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel,分别对应文件IO、UDP、TCP(Client和Server)。

 

4 Buffer

4.1、 什么是Buffer?

  Buffer是一个对象,包含一些要写入或者读出的数据。在NIO中,所有数据都是用缓冲区处理的,在读入数据时,数据直接读入到缓冲区中;在写出数据时,数据从缓冲区中被写出。缓冲区实质上是一个数组,但是缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问,而且可以跟踪系统的读写进程。

       最常用的缓冲区是ByteBuffer,一个ByteBuffer 提供了一个组功能用于操作byte数组。

       常用: ByteBuffer:字节缓冲区

                   CharBuffer:字符缓冲区

                   ShortBuffer:短整形缓冲区

                   CharBuffer:字符缓冲区

                   

 

  可以把Buffer理解为一组基本数据类型的元素列表,它通过4个变量来保存数据的当前位置状态:capacity、position、limit、mark,含义如下: 

索引说明
capacity缓冲区数组的总长度
position下一个要操作的数据元素的位置
limit缓冲区数组中下一个不可操作的元素的位置,limit <= capacity
mark用于记录当前position的前一个位置或者默认-1

  说明:

  1)position:position变量跟踪缓冲区已经写了多少数据,它指定了下一个字节要放到数组的哪一个元素中。如果从通道中读取3个字节放到缓冲区中,则缓冲区的position将会设置为3,指向数组中的第四个元素;同样,在将数据写入通道时,position值跟踪从缓冲区中获取了多少数据,position指定下一个写入到通道的字节来自数组的哪一个元素,如果从缓冲区写了5个字节到通道中,那么缓冲区的position将被设置为5,指向缓冲数组的第6个元素。

  2)limit:limit变量表明在从缓冲区写入通道时还有多少数据需要取出,或者在从通道读入缓冲区时还有多少空间可以存放数据。

  3)capacity:capacity变量指定了可以存储在缓冲区中的最大数据容量。

4.2 、NIO中Buffer类型

  NIO中的关键Buffer实现有:ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、CharBuffer,分别对应基本数据类型:byte、short、int、long、float、double、char。另外,NIO中还有MappedByteBuffer、HeapByteBuffer、DirectByteBuffer等类型。

4.3 、Buffer的使用

  通过以上案例可总结出使用Buffer缓冲区的一般步骤:

  1)分配缓冲空间,如:ByteBuffer buf = ByteBuffer.allocate(1024);

  2)写入数据到Buffer缓冲区,如:int bytesRead = fileChannel.read(buf);

  3)调用flip方法,如:buf.flip();

  4)从Buffer缓冲区中读取数据,如:System.out.println((char)buf.get())

  5)调用clear()方法或者compact()方法

4.4、 Buffer数据访问

4.4.1、get()方法

  ByteBuffer类中有4个get()方法:

  1)byte get()

  2)ByteBuffer get(byte[] dst)

  3)ByteBuffer get(byte[] dst, int offset, int length)

  4)byte get(int index)

  方法1)获取单个字节;方法2)和方法3)将一组字节读入到一个数组中;方法4)从缓冲区中的特定位置获取字节;返回类型为ByteBuffer的方法返回的是调用该方法的缓冲区的this值。

  另外,方法1)、2)、3)是相对的,方法4)是绝对的。相对意味着get操作服从position和limit的值,即数据从当前position读取,在get操作之后position的值会增加;绝对意味着get操作会忽略position和limit的值,并不会Buffer缓冲区的参数产生影响。

4.4.2、put()方法

  ByteBuffer中有5个put()方法

  1)ByteBuffer put(byte b)

  2)ByteBuffer put(byte[] src)

  3)ByteBuffer put(byte[] src, int offset, int length)

  4)ByteBuffer put(ByteBuffer src)

  5)ByteBuffer put(int index, byte b)

  方法1)写入(put)单个字节;方法2)、3)写入来自源数组的一组字节;方法4)将数据从数据从一个给定的源ByteBuffer写入当前ByteBuffer;方法5)将字节写入Buffer缓冲区中特定的位置;返回类型为ByteBuffer的方法返回的是调用该方法的缓冲区的this值。

  另外,与get()方法一样,把put()方法划分为相对的和绝对的。方法1)、2)、3)、4)是相对的,方法5)是绝对的。

4.4.3、类型化的get()和put()方法

  除了前文描述的get()和put()方法,ByteBuffer还有用于读写不同类型值得其他方法,如:

get()方法put()方法
getByte()putByte()
getShort()putShort()
getInt()putInt()
getLong()putLong()
getFloat()putFloat()
getDouble()putDouble()
getChar()putChar()

 

 

 

 

 

 

 

 

5 选择器(Selector)

Selector一般称为选择器,用来作为SelectableChannel通道的多路复用器

SelectableChannel 的多路复用器,用于监控 SelectableChannel 的 IO 状况。

与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字实现。这两种新增的通道都支持阻塞和非阻塞模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式则正好相反。一般来说,低负载、低并发的应用程序可以选择同步阻塞IO以降低编程复杂度;对于高负载,高并发的网络应用,需要使用NIO的非阻塞模式进行开发。

简单地讲Selector会不断地轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的IO操作。

一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端

Selector 是非阻塞IO 的核心。

SelectableChannle 的结构如下图:

1. Selector的创建

通过调用Selector.open()方法创建一个Selector对象,如下:

Selector selector = Selector.open();

这里需要说明一下

2. 注册Channel到Selector

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

Channel必须是非阻塞的
所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SelectableChannel。Socket channel可以正常使用。

SelectableChannel抽象类 有一个 configureBlocking() 方法用于使通道处于阻塞模式或非阻塞模式。

abstract SelectableChannel configureBlocking(boolean block)  

注意:

SelectableChannel抽象类configureBlocking() 方法是由 AbstractSelectableChannel抽象类实现的,SocketChannel、ServerSocketChannel、DatagramChannel都是直接继承了 AbstractSelectableChannel抽象类 。
大家有兴趣可以看看NIO的源码,各种抽象类和抽象类上层的抽象类。我本人暂时不准备研究NIO源码,因为还有很多事情要做,需要研究的同学可以自行看看。

register() 方法的第二个参数。这是一个“ interest集合 ”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:

  • Connect
  • Accept
  • Read
  • Write

通道触发了一个事件意思是该事件已经就绪。比如某个Channel成功连接到另一个服务器称为“ 连接就绪 ”。一个Server Socket Channel准备好接收新进入的连接称为“ 接收就绪 ”。一个有数据可读的通道可以说是“ 读就绪 ”。等待写数据的通道可以说是“ 写就绪 ”。

3. SelectionKey介绍

一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。

key.attachment(); //返回SelectionKey的attachment,attachment可以在注册channel的时候指定。
key.channel(); // 返回该SelectionKey对应的channel。
key.selector(); // 返回该SelectionKey对应的Selector。
key.interestOps(); //返回代表需要Selector监控的IO操作的bit mask
key.readyOps(); // 返回一个bit mask,代表在相应channel上可以进行的IO操作。

key.interestOps():

我们可以通过以下方法来判断Selector是否对Channel的某种事件感兴趣

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

key.readyOps()

ready 集合是通道已经准备就绪的操作的集合。JAVA中定义以下几个方法用来检查这些操作是否就绪.

//创建ready集合的方法
int readySet = selectionKey.readyOps();
//检查这些操作是否就绪的方法
key.isAcceptable();//是否可读,是返回 true
boolean isWritable()://是否可写,是返回 true
boolean isConnectable()://是否可连接,是返回 true
boolean isAcceptable()://是否可接收,是返回 true

从SelectionKey访问Channel和Selector很简单。如下:

Channel channel = key.channel();
Selector selector = key.selector();
key.attachment();

可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:

key.attach(theObject);
Object attachedObj = key.attachment();

还可以在用register()方法向Selector注册Channel的时候附加对象。如:

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

4. 从Selector中选择channel(Selecting Channels via a Selector)

选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中.

Selector维护的三种类型SelectionKey集合:

  • 已注册的键的集合(Registered key set)

    所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。

  • 已选择的键的集合(Selected key set)

    所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。

  • 已取消的键的集合(Cancelled key set)

    已注册的键的集合的子集,这个集合包含了 cancel() 方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。

    注意:
    当键被取消( 可以通过isValid( ) 方法来判断)时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。当再次调用 select( ) 方法时(或者一个正在进行的select()调用结束时),已取消的键的集合中的被取消的键将被清理掉,并且相应的注销也将完成。通道会被注销,而新的SelectionKey将被返回。当通道关闭时,所有相关的键会自动取消(记住,一个通道可以被注册到多个选择器上)。当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化(取消)。一旦键被无效化,调用它的与选择相关的方法就将抛出CancelledKeyException。

select()方法介绍:

在刚初始化的Selector对象中,这三个集合都是空的。 通过Selector的select()方法可以选择已经准备就绪的通道 (这些通道包含你感兴趣的的事件)。比如你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。下面是Selector几个重载的select()方法:

  • int select():阻塞到至少有一个通道在你注册的事件上就绪了。
  • int select(long timeout):和select()一样,但最长阻塞时间为timeout毫秒。
  • int selectNow():非阻塞,只要有通道就绪就立刻返回。

select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。例如:首次调用select()方法,如果有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。

一旦调用select()方法,并且返回值不为0时,则 可以通过调用Selector的selectedKeys()方法来访问已选择键集合 。如下:
Set selectedKeys=selector.selectedKeys();
进而可以放到和某SelectionKey关联的Selector和Channel。如下所示:

Set selectedKeys = selector.selectedKeys();
Iterator 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();
}

5. 停止选择的方法

选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在select()方法中阻塞的线程。

  • wakeup()方法 :通过调用Selector对象的wakeup()方法让处在阻塞状态的select()方法立刻返回
    该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作,那么下一次对select()方法的一次调用将立即返回。
  • close()方法 :通过close()方法关闭Selector,
    该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有的键将被取消,但是Channel本身并不会关闭。

 

6实列: 

 服务端

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class NIOServer {

    /*标识数字*/
    private  int flag = 0;
    private int port = 8080;
    private Selector selector = null;
    /*缓冲区大小*/
    private  int Block = 4096;
    //接受数据缓冲区
    private ByteBuffer sendbuffer=ByteBuffer.allocate(Block);
    //发送数据缓冲区
    private  ByteBuffer receivebuffer=ByteBuffer.allocate(Block);


    public  NIOServer(int port) throws IOException
    {

            //第一步  打开ServerSocketChannel 用于监听客户端的连接
            ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
            //第二步  绑定监听端口,设置连接为非阻塞模试
            serverSocketChannel.socket().bind(new InetSocketAddress(port));
            serverSocketChannel.configureBlocking(false);

            //第三步  创建 Reactor线程 创建多路复用器并 启动线程
            // 通过open()方法找到Selector
             selector= Selector.open();

            //第四步 将ServerSocketChannel 注册到Reactor 线程 的多路得用器Selector 上 监听ACCEPT事件
            // 注册到selector,等待连接
            serverSocketChannel. register(selector, SelectionKey.OP_ACCEPT);

           System.out.println("服务已启动,监听端口是:" + port);

    }


    //第五 步 多路复用器在线程 方法的无限循环体内轮询准务就绪的Key
    private void  listener() throws  IOException{

        while (true)
        {
         int wait=selector.select();
         if(wait==0) continue;
         Set<SelectionKey> keys=selector.selectedKeys();
         Iterator<SelectionKey> iterable=keys.iterator();
         while ((iterable.hasNext()))
         {

             SelectionKey key= iterable.next();
             iterable.remove();
             Process(key);
         }


        }
    }

    //多咱得用器监听到有新客户端接入,处理新的接入请求 完成TCP 三次握手,建立物理链路
    public void Process(SelectionKey key) throws IOException
    {
     String sendText;
     SocketChannel client=null;
     int readNumber=0;
      if(key.isAcceptable())
      {
          ServerSocketChannel server=(ServerSocketChannel)key.channel();
          //第六步 多咱得用器监听到有新客户端接入,处理新的接入请求 完成TCP 三次握手,建立物理链路
           client=server.accept();
          //第七步 设置非阻塞模式
          client.configureBlocking(false);
          //第八步 将新接入的客户端连接注册到 Reactor 线程的多路复用器上
          client.register(selector,SelectionKey.OP_READ);


      }
      //第九步 异步读取客户端请求消息到 缓冲区
      if(key.isReadable())
      {
          //创建此 通道
          client=(SocketChannel)key.channel();
          //清除发送数据缓冲区
          receivebuffer.clear();
          readNumber=client.read(receivebuffer);
          if(readNumber>0)
          {
              System.out.println(new String(receivebuffer.array(),0,readNumber));
               client.register(selector,SelectionKey.OP_WRITE);
          }


      }
      //第十步 对ByteBuffer 进行编解码
      else if(key.isWritable())
      {

          sendbuffer.clear();
          //创建此 通道
          client=(SocketChannel)key.channel();
          sendText="message from server"+flag;
          sendbuffer.put(sendText.getBytes());
          sendbuffer.flip();
          //第十一 调用 Socketchannel 的异步write接口 将消息异步发送给客户端
          client.write(sendbuffer);
          System.out.println("服务器端向客户端发送数据--:"+sendText);
          client.register(selector,SelectionKey.OP_READ);
      }

    }

    public static void main(String[] arg) throws IOException
    {
        int port=8090;
        NIOServer server=new NIOServer(port);
        server.listener();


    }
}

client端代码:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NIOClient {

    /*标识数字*/
    private static int flag = 0;
    /*缓冲区大小*/
    private static int BLOCK = 4096;
    /*接受数据缓冲区*/
    private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
    /*发送数据缓冲区*/
    private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
    /*服务器端地址*/
    private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
            "localhost", 8090);

    public static void main(String[] args) throws IOException {
        // TODO Auto-generated method stub
        // 打开socket通道
        SocketChannel socketChannel = SocketChannel.open();
        // 设置为非阻塞方式
        socketChannel.configureBlocking(false);
        // 打开选择器
        Selector selector = Selector.open();
        // 注册连接服务端socket动作
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        // 连接
        socketChannel.connect(SERVER_ADDRESS);
        // 分配缓冲区大小内存

        Set<SelectionKey> selectionKeys;
        Iterator<SelectionKey> iterator;
        SelectionKey selectionKey;
        SocketChannel client;
        String receiveText;
        String sendText;
        int count=0;

        while (true) {
            //选择一组键,其相应的通道已为 I/O 操作准备就绪。
            //此方法执行处于阻塞模式的选择操作。
            selector.select();
            //返回此选择器的已选择键集。
            selectionKeys = selector.selectedKeys();
            //System.out.println(selectionKeys.size());
            iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                selectionKey = iterator.next();
                if (selectionKey.isConnectable()) {
                    System.out.println("client connect");
                    client = (SocketChannel) selectionKey.channel();
                    // 判断此通道上是否正在进行连接操作。
                    // 完成套接字通道的连接过程。
                    if (client.isConnectionPending()) {
                        client.finishConnect();
                        System.out.println("完成连接!");
                        sendbuffer.clear();
                        sendbuffer.put("Hello,Server".getBytes());
                        sendbuffer.flip();
                        client.write(sendbuffer);
                    }
                    client.register(selector, SelectionKey.OP_READ);
                } else if (selectionKey.isReadable()) {
                    client = (SocketChannel) selectionKey.channel();
                    //将缓冲区清空以备下次读取
                    receivebuffer.clear();
                    //读取服务器发送来的数据到缓冲区中
                    count=client.read(receivebuffer);
                    if(count>0){
                        receiveText = new String( receivebuffer.array(),0,count);
                        System.out.println("客户端接受服务器端数据--:"+receiveText);
                        client.register(selector, SelectionKey.OP_WRITE);
                    }

                } else if (selectionKey.isWritable()) {
                    sendbuffer.clear();
                    client = (SocketChannel) selectionKey.channel();
                    sendText = "message from client--" + (flag++);
                    sendbuffer.put(sendText.getBytes());
                    //将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
                    sendbuffer.flip();
                    client.write(sendbuffer);
                    System.out.println("客户端向服务器端发送数据--:"+sendText);
                    client.register(selector, SelectionKey.OP_READ);
                }
            }
            selectionKeys.clear();
        }
    }
}

结果:

客户端向服务器端发送数据--:message from client--236304
客户端接受服务器端数据--:message from server0
客户端向服务器端发送数据--:message from client--236305
客户端接受服务器端数据--:message from server0
客户端向服务器端发送数据--:message from client--236306
客户端接受服务器端数据--:message from server0

。。。。。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值