NIO 输入输出

  NIO 是java14 API 提供的一种新输入输出流,一套用于标准IO的文件读写,一套用于网络编程。

1. NIO 与IO 的区别

  • IO流以字节流输入输出,一次以一个字节进行数据操作,效率慢; NIO以块的方式处理数据,面向缓冲区,一次产生或消费一个数据块,速度快
  • IO流是阻塞IO,当客户端连接会处于阻塞状态;NIO是非阻塞IO,可以设置属性进行多路复用处理
  • NIO可以通过选择器Selctors进行多路复用

2. 核心组件

  • ByteBuffer 数据缓冲区
    • 对数据进行处理,本质是一个数组,用来保存类型不同的数据
    • Buffer是一个抽象类,有很多子类,最常用的是ByteBuffer
    • 创建(静态方法调用)
      • (最常用)在堆中创建缓冲区  allocate(int capacity)
      • 在系统内存创建 allocateDirect(int capacity)
      • 普通数组创建 wrap(byte[] arr)
    • 常用方法
      • 添加元素,写处理  put(byte b)
      • 获取元素,读处理  get(byte b)
      • 将缓冲区转换成数组  对象名.array()
      • 切换读模式,将position=0;limit=position   flip()
      • 清空数据  clear(),数据只是被遗忘,但仍存在在缓冲区,不能在通过下标获取元素,position=0;limit=capacity
    • 重要属性
      • capacity 容量,缓冲区能容纳的最大数据元素数量,一经设定不可改变
      • limit 界限,缓冲区可以操作的数据大小,limit后面的元素都不可操作,初始化为capacity的值
      • position 位置,下一个要被操作的数据位置,初始化为0, put 和get 操作都会影响limit和position的值
      • mark 标记,初始化为-1,记录上一次position的位置
        package task04;
        
        import java.io.FileInputStream;
        import java.nio.ByteBuffer;
        import java.util.Arrays;
        
        public class ByteBufferTest {
            public static void main(String[] args) {
                //创建
                ByteBuffer bf1=ByteBuffer.allocate(10);
                ByteBuffer bf2=ByteBuffer.allocateDirect(20);
                byte[] bytes=new byte[1024];
                ByteBuffer bf3=ByteBuffer.wrap(bytes);
                //属性值
                System.out.println(bf1.capacity()); //10
                System.out.println(bf1.limit());//10
                System.out.println(bf1.position());//0
                System.out.println(bf1.mark());//java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
                System.out.println("-----------");
        
                //添加获取元素
                byte[] arr=new byte[]{1,2,3,4,5};
                bf1.put(arr);
                System.out.println(bf1.get());//0  因为此时position在5的位置
                bf1.flip();
                System.out.println(bf1.get());//1,在ByteBuffer 下标为0
                System.out.println(bf1.get(2));//3,在ByteBuffer 下标为2,这里并不会设置position的值
                System.out.println(bf1.get());//2 position=2
                System.out.println(bf1.get());//3 position=2
                
                System.out.println("-----------");
                //属性值
                System.out.println(bf1.capacity());//10
                System.out.println(bf1.limit());//6
                System.out.println(bf1.position());//3
        
            }
        }
  • Channel通道
    • 用于传输数据,与数据处理无关,双向传输,在多路复用时要进行注册操作
    • Channel接口通过以下实现类实现
      • FileChannel:用于读取、写入、映射和操作文件的通道。
      package task04;
      
      import java.io.FileInputStream;
      import java.io.FileNotFoundException;
      import java.io.FileOutputStream;
      import java.io.IOException;
      import java.nio.ByteBuffer;
      import java.nio.channels.Channel;
      import java.nio.channels.FileChannel;
      
      public class FileTest {
          public static void main(String[] args) throws IOException {
              //建立输入输出流
              FileInputStream fis=new FileInputStream("D:\\a.txt");
              FileOutputStream fos=new FileOutputStream("D:\\b.txt");
              //获取连接
              FileChannel fc1=fis.getChannel();
              FileChannel fc2=fos.getChannel();
              //缓冲区
              ByteBuffer bf=ByteBuffer.allocate(1024);
      
              //读取
              while (fc1.read(bf)!=-1){
                  bf.flip();
                  fc2.write(bf);
                  bf.clear();
              }
              //关流
              fis.close();
              fos.close();
      
          }
      }
      • DatagramChannel:通过 UDP 读写网络中的数据通道。
      • SocketChannel:通过 TCP 读写网络中的数据。
      • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来 的连接都会创建一个SocketChannel
    • 网络通信常用open方法生成实例对象
    • SocketChannel:通过connect方法建立连接
    • ServerSocketChannel:通过bind方法绑定端口号,accept方法建立连接
    • 设置非阻塞方式:configureBlocking(false)
    package task04;
    
    import java.io.IOException;
    import java.net.InetAddress;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SocketChannel;
    //客户端
    public class ClientTest {
        public static void main(String[] args) throws Exception {
            SocketChannel sc = SocketChannel.open();
            System.out.println("建立连接");
            sc.connect(new InetSocketAddress("127.0.0.1",9999));
            System.out.println("成功连接服务端");
            //创建缓冲区
            ByteBuffer bf=ByteBuffer.allocate(1024);
            bf.put("哈哈哈".getBytes());
            bf.flip();
            sc.write(bf);
            //关闭
            sc.close();
        }
    }
    package task04;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.net.ServerSocket;
    import java.nio.ByteBuffer;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    //服务器端
    public class ServerTest {
        public static void main(String[] args) throws Exception {
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.bind(new InetSocketAddress(9999));
            System.out.println("等待连接");
            //设置非阻塞
            ssc.configureBlocking(false);
    
            while (true){
                SocketChannel accept = ssc.accept(); //阻塞IO
                if(accept!=null){
                    System.out.println("连接成功");
                    //读取数据
                    ByteBuffer bf=ByteBuffer.allocate(1024);
                    int read = accept.read(bf);
                    System.out.println(new String(bf.array(),0,read));
                    accept.close();
                    ssc.close();
                    break;
                }else {
                    System.out.println("嘻嘻嘻");
                    Thread.sleep(2000);
                }
    
            }
    
    
        }
    }
  • Selector 选择器,多路复用器
    • 一个选择器可以同时监听多个服务器端口,帮多个服务器端口同时等待客户端访问
    • Selector是channel通道的多路复用器,同时监控多个通道的IO状况,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。
    • 可选择通道SelectableChannel ,继承了抽象类SelectableChanne的Channel可以被复用,否则不能
    • 通道和选择器之间的关系,使用注册的方式完成。SelectableChannel可以被注册到Selector对象上,在注册的时候,需要指定通道的哪些操作,是Selector感兴趣的
      • Channel.registerSelector selint ops)方法,将一个通道注册到一个选择器时。
        • 第一个参数:指定通道要注册的选择器是谁
        • 第二个参数:指定选择器需要查询的通道操作
          • 可读 : SelectionKey.OP_READ
          • 可写 : SelectionKey.OP_WRITE
          • 连接 : SelectionKey.OP_CONNECT
          • 接收 : SelectionKey.OP_ACCEPT
      • select() :选择器等待客户端连接的方法
      • selectedKeys() :选择器会把被连接的服务端对象放在Set集合中,这个方法就是返回一个Set集合
  • 使用selector   
    • 创建Selector,Selector对象是通过调用静态工厂方法open()来实例化的
    • 创建channel,Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出异常iIlegalBlockingModeException
    • 设置非阻塞
    • 绑定连接
    • 制定监听事件
    • 通过Selectorselect() 方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,包存在一个元素是SelectionKey对象的Set集合中。select()方法返回的int值,表示有多少通道已经就绪
package task04;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class SelectServer {
    public static void main(String[] args) throws IOException {
        //创建服务端对象
        ServerSocketChannel ssc1 = ServerSocketChannel.open();
        ServerSocketChannel ssc2 = ServerSocketChannel.open();
        ServerSocketChannel ssc3 = ServerSocketChannel.open();
        //设置非阻塞
        ssc1.configureBlocking(false);
        ssc2.configureBlocking(false);
        ssc3.configureBlocking(false);
        //创建服务端对象
        ssc1.bind(new InetSocketAddress(9999));
        ssc2.bind(new InetSocketAddress(8888));
        ssc3.bind(new InetSocketAddress(7777));

        //创建选择器对象
        Selector selcetor = Selector.open();
        //两个服务器都要交给选择器来管理
        ssc1.register(selcetor, SelectionKey.OP_ACCEPT);
        ssc2.register(selcetor, SelectionKey.OP_ACCEPT);
        ssc3.register(selcetor, SelectionKey.OP_ACCEPT);

        while (selcetor.select() > 0) {
            //获取集合
            //selectedKeys() :返回集合,集合作用存放的是被连接的服务对象的key
            Set<SelectionKey> selectionKeys = selcetor.selectedKeys();
            // 6、采用轮询的方式,查询获取"准备就绪"的注册过的操作
            // 7、获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            // 8、获取"准备就绪"的事件
            while (iterator.hasNext()) {
                SelectionKey next = iterator.next();
                // 9、获取ServerSocketChannel
                ServerSocketChannel channel = (ServerSocketChannel) next.channel();
                // 10、接受客户端发来的数据
                SocketChannel accept = channel.accept();
                ByteBuffer bf = ByteBuffer.allocate(1024);
                // 11、读取数据
                int length = 0;
                while ((length = accept.read(bf)) != -1) {
                    bf.flip();
                    System.out.println(new String(bf.array(), 0, length));
                    bf.clear();
                }
                accept.close();
            }
            // 12、移除选择键
            iterator.remove();
        }
        // 13、关闭连接
        ssc1.close();
        ssc2.close();
        ssc3.close();

    }
}
package task04;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class SelectClient {
    public static void main(String[] args) throws IOException {
        //创建客户端
        SocketChannel sc=SocketChannel.open();
        //指定要连接的服务器ip和端口
        sc.connect(new InetSocketAddress("127.0.0.1",9999));
        //创建缓冲输出
        ByteBuffer bf=ByteBuffer.allocate(1024);
        //给数组添加数据
        bf.put("hahaha".getBytes());
        //切换
        bf.flip();
        //输出数据服务端
        sc.write(bf);
        //关闭资源
        sc.close();
    }


}

 

posted @ 2021-04-14 09:17  forever_fate  阅读(181)  评论(0)    收藏  举报