java-NIO 笔记

视频资源:链接: https://pan.baidu.com/s/1Ymjz83A2iweuKR5I8rl9YA?pwd=tksz 提取码: tksz

能结合源码和API文档看最好

一、简介

nio是java New IO的简称,在jdk1.4里提供的新api。Sun官方标榜的特性如下:
1、为所有的原始类型提供(Buffer)缓存支持。
2、字符集编码解码解决方案。
3、Channel是一个新的原始I/O抽象。
4、支持锁和内存映射文件的文件访问接口。
5、提供多路(non-bloking)非阻塞式的高伸缩性网络I/O。

二、NIO的特点、NIO与IO的对比

  • NIO将最耗时的I/O操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。

  • 原来的I/O库(在java.io.*中)与NIO最重要的区别是数据打包和传输的方式:

    • 原来的I/O以流的方式处理数据

      面向流的I/O系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的I/O通常相当慢。

    • NIO以块的方式处理数据

      面向块的I/O系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的I/O缺少一些面向流的I/O所具有的优雅性和简单性。

  • java.io.*包中的一些类包含以块的形式读写数据的方法,NIO库也实现了标准I/O功能。

与字节流、字符流的区别??输入输出流是对文件的IO操作

三、缓冲区(Buffer)

缓冲区的介绍

缓冲区是包在一个对象内的基本数据元素数组。Buffer类相比一个简单数组的优点是它将关于数据的数据内容和信息包含在一个单一的对象中。这个对象具有四个属性,来说明其所包含数据的信息:

  1. 容量(Capacity)

    缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被改变。

  2. 上界(Limit)

    缓冲区的第一个不能被读或写的元素。或者说,缓冲区中现存元素的计数。

  3. 位置(Position)

    下一个要被读或写的元素的索引。位置会自动由相应的get()和put()函数更新。

  4. 标记(Mark)

    一个备忘位置。调用mark()来设定mark=postion。调用reset()设定position=mark。标记在设定前是未定义的(undefined)。

缓冲区的分类

缓冲区的分类,用来接收和发送不同类型的数据:
1、字节缓冲区:ByteBuffer(二进制图片视频等)
2、字符缓冲区:CharBuffer
3、双精浮点型(double)缓冲区:DoubleBuffer
4、单精浮点型(float)缓冲区:FloatBuffer
5、整型(int)缓冲区:IntBuffer
6、长整型缓冲区:LongBuffer
7、短整型缓冲区:ShortBuffer

上述的各类型的缓冲区都提供了读和写的方法get和put方法,也提供了一些批量的put和get方法。

缓冲区的创建

  • 缓冲区可以通过allocation创建,此方法通过wrapping将一个现有(数据类型)数组包装到缓冲区中来为缓冲区内容分配空间

  • 通过创建现有字节缓冲区的视图来创建

  • 所有不同类型的缓冲区,都包含静态工厂方法用来创建相应类的新实例,由分配或包装操作创建:

    • 分配操作allocate(),创建一个缓冲区对象并分配一个私有的空间来储存容量大小的数据元素,举例:

      // 分配一个容量为100个char变量的Charbuffer
      CharBuffer charBuffer = CharBuffer.allocate(100);  
      
    • 包装操作wrap(),创建一个缓冲区对象但是不分配任何空间来储存数据元素。它使用提供的数组作为存储空间来储存缓冲区中的数据元素,举例:

      char [] myArray = new char [100];  // 提供自己的数组做缓冲区的备份处理器
      CharBuffer charbuffer = CharBuffer.wrap (myArray);
      
  • 缓冲区的hasArray()方法可以获知该缓冲区是否有一个可存取的备份数组,如果是true的话,缓冲区的array()方法可以获取该缓冲区可存取的备份数组的引用

  • 缓冲区的创建举例:

public static void main(String[] args) {
        //创建指定长度的缓冲区
        IntBuffer buff = IntBuffer.allocate(10);
        int [] array = new int[]{3,5,1};
        //使用数组来创健一个缓冲区视图〔3,5,1]
        buff = buff.wrap(array);
        //利用数组的某一个区间来建视图
        //buff buff.wrap (array,0,2);
        //对缓冲区某个位置上面进行元素修改
        buff.put(0,7);
        //遍历缓冲区中数据
        System.out.println("缓冲区数据如下:");
        for(int i=0;i<buff.limit ();i++){
            System.out.print (buff.get()+"\t");
        }
        //遍历数组中元素,结果表明缓冲区的修改,也会直接影响函到原数组的数据。
        System.out.println("\n原始数据如下:");
        for(int a : array){
            System.out.print (a+"\t");
        }
        System.out.println("\nBuffer类信息:"+buff);
        buff.flip();  //对缓冲区进行反转,(limit=Pos;Pos=0)
        //buff.clear();
        //打印对象信息
        System.out.println("\nBuffer类信息:"+buff);

        //0 <= mark <= position <= limit <= capacity
    }

缓冲区的复制

Duplicate()函数创建了一个与原始缓冲区相似的新缓冲区。两个缓冲区共享数据元素,拥有同样的容量,但每个缓冲区拥有各自的位置,上界和标记属性。对一个缓冲区内的数据元素所做的改变会反映在另外一个缓冲区上。这一副本缓冲区具有与原始缓冲区同样的数据视图。如果原始的缓冲区为只读,或者为直接缓冲区,新的缓冲区将继承这些属性。
注意:复制一个缓冲区会创建一个新的Buffer对象,但并不复制数据。原始缓冲区和副本都会操作同样的数据元素

// 复制一个缓冲区,但并不复制数据
IntBuffer buff2 = buff.duplicate();
System.out.println ("复制的缓冲区:"+buff2);

缓冲区的存取

取:通过相关Buffer类的get方法进行操作

存:通过相关Bufferi类的put方法进行操作(还可以在缓冲区里put进缓冲区)

四、通道(Channel)

通道的概念

通道是一种可以用最小的总开销来访问操作系统本身I/O服务的途径。通道用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套字)之间有效地传输数据,即将信息填充到缓冲区(缓冲区是通道内部用来发送和接收数据的端点),将缓冲区“写”到通道中,信息就被传递到通道另一侧的I/O服务。

通道经常使用操作系统的本地代码去实现,不同的操作系统上通道实现(ChannelImplementation)会有根本性的差异,所以通道API仅仅描述了可以做什么。

对所有通道来说只有两种共同的操作:检查一个通道是否打开IsOpen()和关闭一个打开的通道close()

  • 单向通道:仅能进行读,或者仅能进行写的通道。一个Channel类,它可以只实现ReadableByteChannel接口以提供read()方法去读文件,它也可以只实现WriteableByteChannel接口以提供write()方法去写文件。
  • 双向通道:能进行读和写的通道。一个Channel类同时实现上方两个接口,可以双向传输数据。
  • 非阻塞模式(NonBlocking)的通道:非阻塞模式的通道永远不会让调用的线程休眠。请求的操作要么立即完成,要么返回一个结果表明未进行任何操作。只有面向流的(stream-oriented)的通道,如sockets和pipes(管道)才能使用非阻塞模式。
  • 阻塞模式(Blocking)的通道:可能会让调用通道的线程进行休眠

通道的类别

I/O可以分为广义的两大类别:File I/O 和 Stream I/O,所以通道也分成两种类型

  • 文件(file)通道

    • FileChannel
      • 文件通道都是阻塞模式的,但操作系统有缓存和预存机制,能够提升操作延迟。
      • 文件通道可以进行异步I/O,但得看系统是否具备该功能支持。异步I/O(asynchronous I/O),允许一个进程可以从操作系统请求一个或多个I/O操作而不必等待这些操作的完成,发起请求的进程之后会收到它请求的I/O操作已完成的通知。
  • 套接字(Socket)通道

    SocketChannel、ServerSocketChannel、DatagramChannel

通道的创建

  • 文件通道的创建:通过在一个打开的RandomAccessFile、FileInputStream或FileOutputStream对象上调用getChannel()方法来获取文件通道。getChannel()方法会返回一个连接到该文件的FileChannel对象(文件通道对象)且该FileChannel对象具有与file对象相同的访问权限,然后您就可以使用该通道对象来利用强大的FileChannel API了。
  • 套接字通道:有可以直接创建新套接字通道的工厂方法。或者静态的open()方法。

通道的使用

使用通道操作文件时,需要注意系统中文件本身定义的操作权限,只读文件不能写,只写文件不能读,或者有些不能读写的文件等。

文件通道(FileChannel)
  • FileChannel对象(文件通道对象)是线程安全(thread-safe)的,可以进行并发访问,不过影响通道位置或者影响文件大小的操作都是单线程的(single-threaded)【多线程multithreaded】。如果有一个线程已经在执行会影响通道位置或文件大小的操作,那么其他尝试进行此类操作之一的线程必须等待。并发行为也会受到底层的操作系统或文件系统影响。

  • FileChannel API

    • read()读取文件
    • write()写入数据
    • position()获取文件中哪一个位置的数据接下来被读或者写,返回该位置position。
      • FileChannel位置(position)是从底层的文件描述符获得的,一个对象对该position的更新可以被另一个对象看到
      • position能够决定文件中哪一处的数据接下来将被读或者写。类似于缓冲区的get()和put()方法,当字节被read()或write()方法传输时,文件position会自动更新
      • 如果position值达到了文件大小的值,read()方法会返回一个文件尾条件值(-1)。可是,不同于缓冲区的是,如果实现write()方法时position前进到超过文件大小的值,该文件会扩展以容纳新写入的字节
    • size()返回文件大小的值
  • 举例:读取一个文件,并且在文件末尾追加文字

    public static void fileChannelDemo(){
            try {
                //定义缓冲区对象
                //分配操作allocate(),创建一个缓冲区对象并分配一个私有的空间来储存容量大小的数据元素
                ByteBuffer buff = ByteBuffer.allocate(1024);
                //通过文件输入流获得文件通道对象(读取操作)
                FileChannel inFc = new FileInputStream("D:\\tmp\\a.txt").getChannel();
                //追加写入文件,true是追加,false是覆盖
                FileChannel outFc = new FileOutputStream("D:\\tmp\\a.txt",true).getChannel();
                //使用文件通道读取文件数据到缓冲区
                buff.clear();
                int len = inFc.read(buff);
                //缓冲区的hasArray()方法可以获知该缓冲区是否有一个可存取的备份数组
                //缓冲区的array()方法可以获取该缓冲区可存取的备份数组的引用
                System.out.println(new String(buff.array(),0,len));
                //使用文件通道将缓冲区的数据写入文件
                //包装操作wrap(),创建一个缓冲区对象,使用提供的数组作为存储空间来储存缓冲区中的数据元素
                ByteBuffer buf2 = ByteBuffer.wrap("jack".getBytes());
                outFc.write(buf2);
                //关闭资源
                outFc.close();
                inFc.close();
            }catch (Exception e){
                e.printStackTrace();
            }
    }
    
套接字通道(SocketChannel、ServerSocketChannel和DatagramChannel)
  • 可以选择是否以非阻塞模式运行

    configureBlocking()方法设置或重新设置一个通道的阻塞模式,传递参数值为true则设为阻塞模式,参数值为false值设为非阻塞模式。

    isBlocking()方法判断某个socket通道当前处于哪种模式。

  • 有对等的socket对象。全部套接字通道类(SocketChannel、ServerSocketChannel和DatagramChannel)在被实例化时都会创建一个对等socket对象(Socket、ServerSocket和DatagramSocket [来自java.net]),socket对象通过对应通道的socket()方法获取。

    这三个对等的socket对象也都有getChannel()获取通道对象的方法。但是如果用传统方式(直接实例化)创建了一个Socket对象,它就不会有关联的SocketChannel并且它的getChannel()方法将总是返回null。

    • SocketChannel

      • Socket和SocketChannel类封装点对点、有序的网络连接,类似于TCP/IP络连接。SocketChannel扮演客户端发起监听服务器的连接。直到连接成功,它才能从连接到的地址接收数据。

      • 每个SocketChannel对象创建时都是同一个对等的java.net.Socket对象串联。

        • 静态的open()方法可以创建一个新的SocketChannel对象;
        • 在新创建的SocketChannel上调用socket()方法能返回它对等的Socket对象;
        • 在该Socket上调用getChannel()方法则能返回最初的那个SocketChannel。
      • 新创建的SocketChannel虽已打开却是未连接的。在一个未连接的SocketChannel对象上尝试一个I/O操作会导致NotYetConnectedException异常。这时需要调用connect()方法连接上。

        • 在通道上直接调用connect()方法,或在通道关联的Socket对象上调用connect(),来将该socket通道连接。

          在通道上调用连接,如果通道处于默认模式(阻塞模式),那么线程在连接建立好或超时过期前都保持阻塞,跟在Socket对象上建立连接是一样的。

          在通道上调用连接,如果通道处于非阻塞模式,通道对象提供并发连接,连接上返回true,不能立即连接上则返回false并且并发的继续连接。

        • 没有一种connect()方法可以指定超时时间

        • 一旦一个socket通道被连接,它将保持连接状态直到被关闭。

        • 通过调用布尔型的isConnected()方法来测试某个SocketChannel当前是否已连接。

        • 假如某个SocketChannel上当前正有一个并发连接,isConnectPending()方法就会返回true
          值。

      • 调用finishConnect()方法来完成连接过程,该方法任何时候都可以安全地进行调用。假如在一个非阻塞模式的SocketChannel对象上调用finishConnect()方法,将可能出现下列情形之一:

        1、connect()方法尚未被调用。那么将产生NoConnectionPendingException异常
        2、连接建立过程正在进行,尚未完成。那么什么都不会发生,finishConnect()方法会立即返回false值。
        3、在非阻塞模式下调用connect()方法之后,SocketChannel又被切换回了阻塞模式。那么如果有必要的话,调用线程会阻塞直到连接建立完finishConnect()方法接着就会返回true值。
        4、在初次调用connect()或最后一次调用finishConnect()之后,连接建立过程已经完成。那么SocketChannel对象的内部状态将被更新到已连接状态,finishConnect()方法会返回true值,然后SocketChannel对象就可以被用来传输数据了。
        5、连接已经建立。那么什么都不会发生,finishConnect()方法会返回true值

    • ServerSocketChannel

      ServerSocketChannel通过socket()获取的ServerSocket对象,ServerSocket对象需要绑定到端口。

      在serverSocket对象上accept()监听接收的内容,会阻塞并返回Socket对象。在ServerSocketChannel对象上accept()监听接收的内容,会收到SocketChannel对象。如果非阻塞模式且没有可返回的对象时,会返回null。

      它又可以检查是否有对象来连接又不会阻塞,所以可以在有连接的时候通知,所以可以做出选择,便有了选择器。

  • 举例:socket通道的客户端服务端操作,客户端发送两个数字,服务端返回两个数字加法运算结果。

    • 目录结构

    • 服务端代码

      package com.cxq.remotecar.nio;
      
      import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.nio.ByteBuffer;
      import java.nio.IntBuffer;
      import java.nio.channels.ServerSocketChannel;
      import java.nio.channels.SocketChannel;
      
      /**
       * NIO服务端:对客户端发来的两个数据进行相加
       */
      public class NioChannelServer {
          private ByteBuffer buff = ByteBuffer.allocate(1024); //分配
          //创健一个1七线冲区的视图此缓冲区内容的更改在新线冲区中是可见的,反之亦然。
          private IntBuffer intBuff = buff.asIntBuffer();
          private SocketChannel clientChannel = null;
          private ServerSocketChannel serverChannel = null;
      
          /**
           * 打开服务端的通道
           * @throws Exception
           */
          public void openChannel() throws Exception{
              //建立一个ServerSocketChannel新的连接的通道
              serverChannel = ServerSocketChannel.open();
              //为新的通道设置访问的端口
              serverChannel.socket().bind(new InetSocketAddress(8888));
              System.out.println("服务器通道已打开");
          }
      
          /**
           * 等待客户端的请求链接
           * @throws IOException
           */
          public void waitReqConn() throws IOException {
              while(true){
                  //在ServerSocketChannel对象上accept()监听接收的内容,会收到SocketChannel对象
                  //如果非阻塞模式且没有可返回的对象时,会返回null
                  clientChannel = serverChannel.accept();
                  if(null!=clientChannel){
                      System.out.println("新的连接加入!");
                  }
                  processReq();
                  clientChannel.close();
              }
          }
      
          /**
           * 处理请求过来的数据(对客户端发来的两个数据进行相加)
           * @throws IOException
           */
          public void processReq() throws IOException{
              System.out.println("开始读取和处理客户端数据");
              buff.clear(); //把当前位置设置为0,上限值修改为容量的值
              clientChannel.read(buff); //将客户端的数据读取并放到缓冲区
              int result = intBuff.get(0)+intBuff.get(1); //buff视图intbuff的数据跟buff的数据一样
              buff.flip();
              buff.clear();
              intBuff.put(0,result);//buff的视图发生修改,buff也同样会被修改
              clientChannel.write(buff);
              System.out.println("读取和处理客户端数据完成");
          }
      
          public void start(){
              try {
                  //打开服务通道
                  openChannel();
                  //监听等待客户端请求
                  waitReqConn();
                  //关闭客户端
                  clientChannel.close();
                  System.out.println("服务端处理完毕");
              }catch (Exception e){
      
              }
          }
      
          public static void main(String[] args) {
              new NioChannelServer().start();
          }
          
      }
      
    • 客户端代码

      package com.cxq.remotecar.nio;
      
      import java.io.IOException;
      import java.net.InetSocketAddress;
      import java.nio.ByteBuffer;
      import java.nio.IntBuffer;
      import java.nio.channels.SocketChannel;
      
      public class NioChannelClient {
      
          private SocketChannel channel = null;
          private ByteBuffer buff = ByteBuffer.allocate(8);
          private IntBuffer intBuff = buff.asIntBuffer();
      
          /**
           * 与服务端建立连接通道
           * @return
           * @throws IOException
           */
          public SocketChannel connect() throws IOException{
              //静态的open()方法可以创建一个新的SocketChannel对象
              return SocketChannel.open(new InetSocketAddress("127.0.0.1",8888));
          }
      
          /**
           * 客户端用通道把缓冲区的数据发送到服务端
           * @param a
           * @param b
           * @throws IOException
           */
          public void sendRequest(int a,int b) throws IOException{
              buff.clear();
              intBuff.put(0,a);
              intBuff.put(1,b);
              channel.write(buff);
              System.out.println("客户端发送加法请求("+a+"+"+b+")");
          }
      
          /**
           * 用缓冲区接收服务端通过通道返回的数据
           * @return
           * @throws IOException
           */
          public int receiveResult() throws IOException{
              buff.clear();
              channel.read(buff);
              return intBuff.get(0);
          }
      
          /**
           * 获得加法运算的结果
           * @param a
           * @param b
           * @return
           */
          public int getSum(int a,int b){
              int result = 0;
              try{
                  channel = connect();
                  sendRequest(a,b);
                  result = receiveResult();
              }catch (Exception e){
                  e.printStackTrace();
              }
              return result;
          }
      
          public static void main(String[] args) {
              int result = new NioChannelClient().getSum(56,34);
              System.out.println("服务端加法运算的结果为:"+result);
          }
      }
      

通道的关闭

通过调用通道的close()方法进行关闭,但是可能会导致关闭底层I/O服务时发生阻塞(非阻塞模式和阻塞模式都有可能关闭时阻塞),可以通过isopen()方法来测试通道的开放状态,如果返回true,那么说明通道可以使用。反之,说明通道已经关闭,不能使用。

五、选择器(Selector)

选择器的概念

选择器即通道管理器,选择器利用了操作系统检查通道就绪状态并通知的能力,可获知通道是否已准备好执行每个I/O,而不用非阻塞模式一直轮询。

  • 选择器(Selector)

    [select选择,跟sql语句中的查询的select一样] 选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。

  • 可选择通道(SelectableChannel)

    这个抽象类提供了通道的可选择性所需要的公共方法。FileChannel对象不是可选择的,因为它们没有继承SelectableChannel。所有soclet通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。可选择通道可以被注册到选择器对象上,一个通道可以被注册到多个选择器上,但一个通道只能在一个选择器里面注册一次。

  • 选择键(SelectionKey)

    选择键封装了通道与选择器的注册关系[4种]。选择键对象在通道对象通过SelectableChannel.register(选择器)方法注册到选择器时会返回,并提供一个表示这种注册关系的标记。每个通道的注册,将定义它自己的选择键类。在register()方法中可以构造它并将它传递给所提供的选择器对象。

    在选择键中,用静态常量定义了四种IO操作:读OP_READ=1、写OP_WRITE=4、
    连接OP_CONNECT=8、接收消息OP_ACCEPT=16,这4个值任何2、3、4个相加结果都不相同,因此可以用validOps()方法返回值确定通道注册到选择器时选择的选择键。

通道在被注册到一个选择器上之前,必须通过调用configureBlocking(false)设置该通道为非阻塞模式。否则可能出现以下三个异常:

1、如果试图注册一个处于阻塞状态的通道的话,register()方法将抛出未检查的IllegalBlockingModeException异常;

2、如果试图令已注册的通道回到阻塞状态的话,将在调用configureBlocking()方法时将抛出IllegalBlockingModeException异常;

3、如果试图注册一个已经关闭的SelectableChannel实例的话,也将抛出losedChannelException异常。

选择器的使用

如果我们要使用非阻塞I/O编写服务器处理程序,可使用选择器监听通道的不同事件,即不同选择键类型:
1、向Selector对象注册感兴趣的事件。
2、从Selector中获取感兴趣的事件。
3、根据不同的事件进行相应的处理。

  • 选择器的API

    API 描述
    abstract void close() 关闭此选择器
    abstract boolean isOpen() 判断选择器是否已打开
    static Selector open() 打开一个选择器
    abstract SelectorProvider provider() 返回创建此通道的提供者
    abstract int select() 返回键的个数,其相应的通道已为I/O操作准备就绪
    abstract int select(long timeout) 同上,但指定了阻塞时间,select(0)是无限期阻塞
    abstract int selectNow() select()方法的非阻塞形式,不等于select(0)
    abstract Set keys() 返回此选择器的键集。
    abstract Set selectedKeys() 返回此选择器以上相应的通道I/O操作准备就续的选择键集
    abstract Selector wakeup() 使尚来返回的第一个选择操作立即返回
    • 通过keys()方法已注册键的集合,集合可能是空的。这个已注册的键的集合不是可以直接修改的,试图这么做的话将java.lang.UnsupportedOperationException
    • 当通道关闭时,所有相关的键会自动取消。当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化(取消)。一旦键被无效化,调用它的与选择相关的方法就将抛出CancelledKeyException
    • 唤醒在select()方法 [该方法返回准备好的通道个数int,readyCount selector.select(10000),阻塞方法,睡眠,直到过了十秒或者至少有一个通道的I/O操作准备好] 中睡眠的线程的三种方法:
      • 调用Selector对象的wakeup()方法将使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有在进行中的选择,那么下一次对select()方法的一种形式的调用将立即返回。后续的选择操作将正常进行。在选择操作之间多次调用wakeup()方法与调用它一次没有什么不同。
      • 如果选择器的close()方法被调用,那么任何一个在选择操作中阻塞的线程都将被唤醒,就像wakeup(0方法被调用了一样。但是,与选择器相关的通道将被注销,键将被取消。
      • 如果睡眠中的线程的interrupt()方法被调用,它的返回状态将被设置。如果被唤醒的线程试图在通道上执行I/O操作,通道将立即关闭,然后线程将捕捉到一个异常。选择器对象将捕捉InterruptedException异常并调用wakeup()方法。
  • 选择键的API

    API 描述
    Object attach(Object ob) 将给定的对象附加到此键
    Object attachment() 获取当前的附加对象
    abstract void cancel() 请求取消此键的通道到其选择器的注册
    abstract boolean isValid() 告知此健是否有效
    abstract SelectableChannel channel() 返回与此健相关的通道
    abstract Selector selector() 返回为此选择器创建的健
    abstract Int InterestOps() 获取此键的Interest(选择器感兴趣的)集合,是通道被注册时传进来的值
    abstract SelectionKey interestOps(int ops) 将此键的Interest设置为给定值(OP_READ=1、OP_WRITE=4、OP_CONNECT=8、OP_ACCEPT=16)
    abstract int readyOps() 获取此键的ready(选择器已经准备好的)操作集合,为Interest的子集
    boolean isAcceptable() 测试此键的通道是否已准备好接受新的套接字连接
    boolean isConnectable() 测试此键的通通是否已完成其套接字连接操作
    boolean isReadable() 测试此键的通道是否已准备好进行读取
    boolean isWritable() 测试此键的通调是否已准备好进行写入
  • 举例:客户端服务端互发字符串消息,使用选择器监听通道事件的方式实现

    • 服务端
    package com.cxq.remotecar.nio;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.*;
    import java.util.Iterator;
    
    /**
     * NIO选择器实例-服务端
     */
    public class SelcetorServerTest {
    
        //选择器
        private Selector selector;
        //服务端套接字通道
        private ServerSocketChannel serverChannel = null;
        //选择键 的数量
        private int keys = 0;
    
        /**
         * 初始化服务端通道和选择器,及注册事件
         * @throws IOException
         */
        public void initServer() throws IOException{
            this.selector = Selector.open();
            serverChannel = ServerSocketChannel.open();
            serverChannel.socket().bind(new InetSocketAddress("127.0.0.1",8888));
            serverChannel.configureBlocking(false);  // 通道设置为非阻塞,才能注册到选择器中
            // 把通道注册到选择器上,感兴趣的事件为OP_ACCEPT监听通道接收消息,返回选择键对象
            SelectionKey key = serverChannel.register(this.selector, SelectionKey.OP_ACCEPT);
        }
    
        public void listen() throws Exception{
            System.out.println("服务端已经启动");
            while (true){
                //让选择器至少选择一个通道
                //返回键的个数,其相应的通道已为I/O操作准备就绪
                keys = this.selector.select();
                if(keys>0){
                    //返回此选择器以上相应的通道I/O操作准备就续的选择键集
                    Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();
                    //对选择键集进行轮询
                    while (it.hasNext()){
                        SelectionKey key = it.next();
                        it.remove();
                        //这个选择键是客户端连接事件
                        if(key.isAcceptable()){
                            //选择键channel()方法返回与此健相关的通道
                            serverChannel = (ServerSocketChannel) key.channel();
                            //获取客户端主动连接到服务端的通道
                            //在ServerSocketChannel对象上accept()监听接收的内容,会收到SocketChannel对象。
                            //如果非阻塞模式且没有可返回的对象时,会返回null。
                            SocketChannel channel = serverChannel.accept();
                            //客户端通道设置为非阻塞模式
                            channel.configureBlocking(false);
                            //通过客户端通道,给客户端发送消息
                            channel.write(ByteBuffer.wrap(new String("hello client.").getBytes()));
                            //客户端通道在选择器注册一个读取事件
                            channel.register(this.selector,SelectionKey.OP_READ);
                        }else if(key.isReadable()){
                            read(key);
                        }
                    }
                }else{
                    System.out.println("选择器没有监听的事件");
                }
            }
        }
    
        /**
         * 根据选择键对象(选择器监听到的事件对象)来读取客户端发送到通道里的数据
         * @throws Exception
         */
        public void read(SelectionKey key) throws Exception{
            //返回与该事件相关的通道
            SocketChannel channel = (SocketChannel) key.channel();
            //设置缓冲区,接收客户端发来的消息
            ByteBuffer buff = ByteBuffer.allocate(1024);
            int len = channel.read(buff);
            String msg = "服务端收到的消息为:"+new String(buff.array(),0,len);
            System.out.println(msg);
        }
    
    
        /**
         * 启动服务
         */
        public void start(){
            try {
                SelcetorServerTest ns = new SelcetorServerTest();
                ns.initServer();
                ns.listen();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            new SelcetorServerTest().start();
        }
    }
    
    • 客户端
    package com.cxq.remotecar.nio;
    
    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;
    
    /**
     * NIO选择器实例-客户端
     */
    public class SelectorClientTest {
    
        private Selector selector; //选择器
        private ByteBuffer outBuff = ByteBuffer.allocate(1024);
        private ByteBuffer inBuff = ByteBuffer.allocate(1024);
        private int keys = 0;  //选择键个数
        private SocketChannel channel = null; //客户端通道
    
    
        /**
         * 初始化客户端
         * @throws IOException
         */
        public void initClient() throws IOException{
            //获得一个socket通道,并没有进行连接
            channel = SocketChannel.open();
            //获得一个选择器
            selector = Selector.open();
            //设置为非阻塞
            channel.configureBlocking (false);
            //连接服务端
            channel.connect(new InetSocketAddress("127.0.0.1",8888));
            //再选择器注册客户端通道连接服务器的事件
            channel.register (this.selector, SelectionKey.OP_CONNECT);
        }
    
        /**
         * 监听通道在选择器上注册时,说明需要监听的事件
         * @throws IOException
         */
        public void listen() throws IOException{
            while (true){
                keys = this.selector.select(); //获取准备好的事件(选择键)个数
                if(keys>0){
                    //获得选择器事件注册的集合
                    Iterator it = this.selector.selectedKeys().iterator();
                    while (it.hasNext()){
                        SelectionKey key = (SelectionKey)it.next();
                        //如果是连接事件
                        if(key.isConnectable()){
                            // 获得与服务端相连的通道,连接事件注册监听的那个通道
                            SocketChannel channel = (SocketChannel) key.channel();
                            //如果正在连接,就连接完成
                            if(channel.isConnectionPending()){
                                channel.finishConnect();
                                System.out.println("完成连接");
                            }
                            //连接成功后,客户端要向服务端写数据,先注册下
                            channel.register(this.selector,SelectionKey.OP_WRITE);
                        }else if(key.isWritable()){
                            //有客户端通道写的操作的选择键!在通道上进行写作
                            SocketChannel channel = (SocketChannel) key.channel();
                            outBuff.clear();
                            System.out.println("客户端正在写数据……");
                            channel.write(outBuff.wrap("我是ClientA".getBytes()));
                            channel.register(this.selector,SelectionKey.OP_READ);
                            System.out.println("客户端写数据完成");
                        } else if (key.isReadable()) {
                            //在通道上进行读取
                            SocketChannel channel = (SocketChannel) key.channel();
                            inBuff.clear();
                            System.out.println("客户端开始读取消息");
                            int len = channel.read(inBuff);
                            System.out.println("==>"+new String(inBuff.array(),0,len));
                            System.out.println("客户端读取完消息");
                        }
                    }
                }else{
                    System.out.println("选择器没有准备好的选择键(准备好的感兴趣的事件)");
                }
    
            }
        }
    
        /**
         * 启动程序
         */
        public void start(){
            try {
                initClient();
                listen();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            new SelectorClientTest().start();
        }
    }
    

六、扩展

posted @ 2022-07-31 10:10  菜小七  阅读(110)  评论(0编辑  收藏  举报