java中的三种IO(BIO、NIO、AIO)

IO

阻塞和非阻塞主要指的是访问 IO 的线程是否会阻塞(或者说是等待)
线程访问资源,该资源是否准备就绪的一种处理方式

BIO(传统的IO)

  1. BIO是同步阻塞式的IO,以流的方式处理数据(效率低)

    Socket编程就是BIO,一个socket连接处理一个线程。当多个socket请求与服务端建立连接时,服务端不能提供相应数量的处理线程,没有分配到处理线程的连接自然就会阻塞或者是被拒绝了。

    • 创建一个服务器端Serve类

      public class Server {
          public static void main(String[] args) throws IOException {
              ServerSocket serverSocket = new ServerSocket(9999);
              while (true) {
                  System.out.println("哈哈");
                  //接受请求(阻塞)
                  Socket socket = serverSocket.accept();
                  System.out.println("阻塞1");
                  //获取输入流(阻塞)
                  BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                  System.out.println("阻塞2");
                  //获取输出流
                  System.out.println(reader.readLine());
                  PrintStream printStream = new PrintStream(socket.getOutputStream());
                  printStream.println("找我什么事?");
                  socket.close();
              }
      
          }
      }
      
      
    • 创建一个客户端Clientr类

      public class Client {
          public static void main(String[] args) throws IOException {
              Socket socket = new Socket("127.0.0.1", 9999);
              System.out.println("请输入:");
              Scanner scanner = new Scanner(System.in);
              String line = scanner.nextLine();
              PrintStream printStream = new PrintStream(socket.getOutputStream());
              printStream.println(line);
              //获取输入流(阻塞)
              BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
              System.out.println(reader.readLine());
              socket.close();
          }
      }
      
      
    • 测试1:只启动服务端,控制台只输出了“哈哈”,说明serverSocket.accept();是阻塞的

    • 测试2:启动客户端,此时服务端输出了“阻塞1”,没有输出“阻塞2‘’。

    • 测试3:在客户端输入“你在吗”,服务端输出“找我什么事?”说明socket.getInputStream()也是阻塞的

NIO(同步非阻塞式IO)

概念

NIO是对BIO的改进,它是同步非阻塞的IO,以块的方式处理数据(效率高)。NIO基于通道和缓冲区进行数据操作,数据总是从通道读取到缓冲区中,或者从缓冲区中写到通道中。Selector(选择器)用于监听多个通道的时间,(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。

NIO的三大核心

NIO的三大核心:Channel(通道)、Buffer(缓冲区)、Selector(选择器)

文件IO(不支持非阻塞的操作)

缓冲区(buffer)

实际就是一个容器,是一个特殊的数组,缓冲区内部内置了一些机制,能够跟踪和记录缓冲区的状态和变化。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。

  1. 在NIO中Buffer是一个顶层抽象父类,常用的子类有:

    • ByteBuffer
    • ShortBuffer
    • ······
  2. ByteBuffer·····类中的一些常用方法,这里以ByteBuffer类为例

    • public abstract ByteBuffer put(byte[] b):存储字节数据到缓冲区
    • public abstract byte[] get():从缓冲区获得字节数据
    • public final byte[] array():把缓冲区数据转换成字节数组
    • public static ByteBuffer allocate(int capacity):设置缓冲区的初始容量
    • public final Buffer flip(): 翻转缓冲区,重置位置到初始位置

通道(Channel)

类似于 BIO 中的 stream,例如 FileInputStream 对象,用来建立到目标(文件,网络套接字,硬件设备等)的一个连接,但是需要注意:BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,既可以用来进行读操作,也可以用来进行写操作。

  1. 常用的 Channel 类有:

    • FileChannel 用于文件的数据读写,
    • DatagramChannel 用于 UDP 的数据读写,
    • ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。
  2. 常用的方法,以FileChannel类为例

    • public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
    • public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
      public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道(特别适合复制大文件)
    • public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道(特别适合复制大文件)

入门demo

向文件中写入数据、从文件中读取数据、复制文件

public class TestNIO {
    @Test
    //向文件中写数据
    public void test1() throws Exception {
        //创建文件输出流
        FileOutputStream fos = new FileOutputStream("basic.txt");
        //获取通道
        FileChannel channel = fos.getChannel();
        //获取缓冲数组
        ByteBuffer byteBuffer = ByteBuffer.allocate(2048);
        //往缓区写入字节数组
        String str = "hello nio";
        byteBuffer.put(str.getBytes());
        //翻转缓冲区
        byteBuffer.flip();
        //把缓冲区写到通道中
        channel.write(byteBuffer);
        //关闭流
        fos.close();
    }


    @Test
    //从文件中读数据
    public void test2() throws Exception {
        File file = new File("basic.txt");
        //创建文件输入流
        FileInputStream fis = new FileInputStream(file);
        //获取通道
        FileChannel channel = fis.getChannel();
        //获取缓区
        ByteBuffer  byteBuffer = ByteBuffer.allocate((int) file.length());
        //从通道中读取数据到缓冲区中
        channel.read(byteBuffer);
        System.out.println(new String(byteBuffer.array()));
        //关闭流
        fis.close();
    }


    @Test
    //复制文件
    public void test3() throws Exception {
        //创建文件输入流
        FileInputStream fis = new FileInputStream("basic.txt");
        //创建文件输出流
        FileOutputStream fos = new FileOutputStream("I:\\test\\test.txt");
        //获取两个通道
        FileChannel fisChannel = fis.getChannel();
        FileChannel fosChannel = fos.getChannel();
        //把fisChannel通道中的数据复制到fosChannel通道中
        fosChannel.transferFrom(fisChannel, 0, fisChannel.size());
        //关闭流
        fis.close();
        fos.close();
    }
}

注意:使用文件IO时,把缓冲区中的数据写到Channel通道之前一定要调用Buffer类的flip()方法,翻转缓冲区

网络IO

概述

文件IO中的Channel并不支持非阻塞操作,NIO主要就是进行网络IO,java中网络IO是非阻塞IO。基于事件驱动,非常适用于服务器需要大量连接,但数据量不大的情况。

java中常用的编写Socket服务器,通用的几种模式

  1. 一个客户端连接用一个线程

    • 优点:编码简单
    • 缺点:如果连接的客户端比较多,则 分配的线程也很多,就会导致服务器资源耗尽而崩溃
  2. 把每一个客户端连接交给一个拥有固定数量线程的连接池

    • 优点:编码简单,可处理大量连接
    • 缺点:线程开销很大,如果连接比较多,则排队现象比较严重
  3. 使用java中的NIO,用非阻塞IO的方式处理,这种模式可以用一个线程处理大量的客户端连接

Selector(选择器)

  1. 作用:

    能够检测多个注册服务上的通道是否有事件发生,如果有事件发生,便可以获取到事件然后针对每个事件进行相应的处理。这样就可以使用单线程管理多个客户端连接,这样使得只有真正的读写事件发生时,才会调用函数进行读写。减少了系统的开销,并且不必为每一个连接都创建一个线程,不用去维护多个线程。

  2. 该类常用的方法

    • public static Selector open(),得到一个选择器对象
    • public int select(long timeout),监控所有注册的通道,当其中有 IO 操作可以进行时,将
      对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
    • public Set selectedKeys(),从内部集合中得到所有的 SelectionKey

SelectionKey

  1. 作用:代表了Selector和网络通道的四种注册关系,一共有四种。

    • int OP_ACCEPT:有新的网络连接可以 accept,值为 16
    • int OP_CONNECT:代表连接已经建立,值为 8
    • int OP_READ 和 int OP_WRITE:代表了读、写操作,值为 1 和 4
  2. 常用方法

    • public abstract Selector selector(),得到与之关联的 Selector 对象
    • public abstract SelectableChannel channel(),得到与之关联的通道
    • public final Object attachment(),得到与之关联的共享数据
    • public final boolean isAcceptable(),是否可以 accept
    • public final boolean isReadable(),是否可以读
    • public final boolean isWritable(),是否可以写

ServerSocketChannel

  1. 作用:用于在服务器端监听一个新的连接
  2. 常用方法:
    • public static ServerSocketChannel open(),得到一个 ServerSocketChannel 通道
    • public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号
    • public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,
      取值 false 表示采用非阻塞模式
    • public SocketChannel accept(),接受一个连接,返回代表这个连接的通道对象
    • public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件

SocketChannel

  1. 作用:网络IO通道,负责读写操作,负责从网络中读取数据到缓冲区中,或者把数据写入到缓冲区中

  2. 常用法法:

    • public static SocketChannel open(),得到一个 SocketChannel 通道
    • public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,
      取值 false 表示采用非阻塞模式
    • public boolean connect(SocketAddress remote),连接服务器
    • public boolean finishConnect(),如果上面的方法连接失败,接下来就要通过该方法完成
      连接操作
    • public int write(ByteBuffer src),往通道里写数据
    • public int read(ByteBuffer dst),从通道里读数据
    • public final SelectionKey register(Selector sel, int ops, Object att),注册一个选择器并设置
      监听事件,最后一个参数可以设置共享数据
    • public final void close(),关闭通道

入门小demo

  1. 创建一个客户端NIOClient类

    public class NIOClient {
        public static void main(String[] args) throws Exception {
            //获取通道
            SocketChannel channel = SocketChannel.open();
            //设置非阻塞方式
            channel.configureBlocking(false);
            //提供服务器端IP和端口号
            InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
            //尝试连接服务器
            if (!channel.connect(address)) {
                while (!channel.finishConnect()) {//体现了nio非阻塞的优势
                    System.out.println("Client连接客户端的同时,可以做别的事情");
                }
            }
            String str = "你好啊,我是NIO客户端";
            //获取缓冲区,并向其中写入数据
            ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
            //发送数据
            channel.write(byteBuffer);
            System.in.read();
        }
    }
    
    
  2. 创建一个服务器端NIOServer类

    public class NIOServer {
        public static void main(String[] args) throws Exception {
            //获取通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    
            //绑定端口号
            serverSocketChannel.bind(new InetSocketAddress(9999));
    
            //设置非阻塞方式
            serverSocketChannel.configureBlocking(false);
    
            //获取选择器
            Selector selector = Selector.open();
    
            //将ServerSocketChannel对象注册给Selector对象
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//register()方法的第二个参数是设置的监听事件
    
            //干活
            while (true) {
                //监控客户端
                if (selector.select(2000)==0) {//nio非阻塞的优势
                    System.out.println("没有客户端请求,我可以做别的");
                    continue;
                }
                //得到selectionKey
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
    
                //遍历selectionKey,判断通道里的事件
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {//判断是否有连接请求
                    SelectionKey selectionKey = iterator.next();
                    if (selectionKey.isAcceptable()) {//客户端请求事件
                        System.out.println("OP_ACCEPT");
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    }
                    if (selectionKey.isReadable()) {//读取客户端数据事件
                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
                        channel.read(byteBuffer);
                        System.out.println("收到客户端数据:"+new String(byteBuffer.array()));
                    }
                    //手动从集合中移除当前selectionKey,防止重复处理
                    iterator.remove();
                }
    
            }
        }
    }
    
    

小案例

使用NIO完成一个简易的聊天案例。要求:客户端能够给服务器端发送消息,服务器接收到消息时候,能够将消息广播给其他所有的客户端。

  1. 创建一个聊天客户端类ChatClient

    public class ChatClient {
    
        private final String IP = "127.0.0.1";
        private int port = 9999;
        private SocketChannel socketChannel;           //网络通道
        private String userName;
    
        public ChatClient() throws IOException {
            //获取网络通道
            socketChannel = SocketChannel.open();
            //设置非阻塞方式
            socketChannel.configureBlocking(false);
    
            if (!socketChannel.connect(new InetSocketAddress(IP,port))) {
                while (!socketChannel.finishConnect()) {//体现了NIO非阻塞的优势
                    System.out.println("连接服务器的同事还可以做别的事");
                }
            }
            //得到客户端IP作为用户名
            userName = socketChannel.getLocalAddress().toString().substring(1);
            System.out.println("------------------"+userName+"is ready---------------");
        }
    
    
        //向服务端发送数据
        public void sendMsg(String message) throws IOException {
            //如果从键盘录入的为“bye”则关闭socketChannel,退出聊天
            if (message.equalsIgnoreCase("bye")) {
                socketChannel.close();
                return;
            }
    
            String msg = userName + "说:" + message;
            //获取缓冲区
            ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
            socketChannel.write(byteBuffer);
        }
    
        //从服务端接收数据
        public void receiveMsg() throws IOException {
            //获取缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //将接收到的数据写到缓冲区中
            int count = socketChannel.read(byteBuffer);
            if (count > 0) {
                System.out.println(new String(byteBuffer.array()).trim());
            }
        }
    }
    
  2. 创建一个聊天客户端类ChatServer

    public class ChatServer {
        private ServerSocketChannel serverSocketChannel;
    
        private Selector selector;
    
        //获取客户端连接
        public ChatServer() throws IOException {
            //获取ServerSocketChannel通道
            serverSocketChannel = ServerSocketChannel.open();
            //绑定端口号
            serverSocketChannel.bind(new InetSocketAddress(9999));
            //设置非阻塞方式
            serverSocketChannel.configureBlocking(false);
            //获取Selector
            selector = Selector.open();
            //把serverSockerChannel注册到服务器
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        }
    
    
        //监控客户端连接
        public void start() throws IOException {
            //干活
            while (true) {
                if (selector.select(2000) == 0) {//体现了NIO非阻塞的优势
                    System.out.println("没有客户端连接,我可以做别的");
                    continue;
                }
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    if (selectionKey.isAcceptable()) {
                        //接收请求,得到SocketChannel
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        //设置非阻塞方式
                        socketChannel.configureBlocking(false);
                        //将socketChannel注册到selector中
                        socketChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println(socketChannel.getRemoteAddress().toString().substring(1)+"上线了...");
                    }
    
                    if (selectionKey.isReadable()) {
                        //读取客户端发来的数据
                        readMsg(selectionKey);
                    }
                    //一定要把当前key删掉,防止重复处理
                    iterator.remove();
                }
            }
        }
    
        //读取客户端发送来的数据并且进行广播
        private void readMsg(SelectionKey selectionKey) throws IOException {
            //获取通道
            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
            //获取缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //读取客户端发送的数据
            int count = socketChannel.read(byteBuffer);
            if (count >0) {
                String message = new String(byteBuffer.array());
                printMsg(message);
                //将客户端发来的消息进行广播
                broadCast(message,socketChannel);
            }
    
        }
    
        private void broadCast(String message,SocketChannel socketChannel) throws IOException {
            //得到所有已经就绪的Channel
            for (SelectionKey key : selector.keys()) {
                Channel channel = key.channel();
                if (channel instanceof SocketChannel && channel != socketChannel) {
                    SocketChannel targetChannel = (SocketChannel) channel;
                    //获取缓冲区
                    ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());
                    targetChannel.write(byteBuffer);
                }
            }
        }
    
    
        private void printMsg(String msg) {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            System.out.println(dateFormat.format(new Date())+"      "+msg);
        }
    
    
        public static void main(String[] args) throws IOException {
            new ChatServer().start();
    
        }
    }
    
    
  3. 创建聊天测试类TestChat

    public class TestChat {
        public static void main(String[] args) throws IOException {
            final ChatClient client = new ChatClient();
            new Thread() {
                public void run() {
                    while (true) {
                        try {
                            client.receiveMsg();
                            Thread.sleep(2000);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }.start();
    
    
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String msg = scanner.nextLine();
                client.sendMsg(msg);
            }
        }
    
    }
    
    

    AIO(异步非阻塞)

    先由操作系统完成客户端的请求,再通知服务器去启动线程进行处理。

IO的对比

对比总结 BIO NIO AIO
IO方式 同步阻塞 同步非阻塞 异步非阻塞
API使用难度 简单 复杂 复杂
可靠性
吞吐量

BIO

  • 适用于连接较少,对服务器资源消耗很大,但是编程简单。是同步阻塞的。

  • 举例:你到餐馆点餐,然后在那儿等着,什么也做不了,只要饭还没有好,就要必须等着

NIO

  • 使用于连接数量比较多且连接时间比较短的架构,比如聊天服务器,编程比较复杂。是同步非阻塞的

  • 举例:你到餐馆点完餐,然后就可以去玩儿了,玩一会儿就回饭馆问一声,饭好了没。

AIO

  • 适用于连接数量多而且连接时间长的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂。是异步非阻塞的。

  • 举例:饭馆打电话给你说,我们知道你的位置,待会儿给您送来,你安心的玩儿就可以了。类似于外卖。

你可能感兴趣的:(java)