java IO----BIO、NIO、AIO

IO

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

BIO(同步阻塞IO)

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

在NIO中Buffer是一个顶层抽象父类,常用的子类有:ByteBuffer,ShortBuffer等。

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)是双向的,既可以用来进行读操作,也可以用来进行写操作

常用的 Channel 类有:

  • FileChannel 用于文件的数据读写
  • DatagramChannel 用于 UDP 的数据读写
  • ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写

常用的方法,以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)把数据从当前通道复制给目标通道(特别适合复制大文件)
例子

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

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服务器,通用的几种模式
  • 一个客户端连接用一个线程
    优点:编码简单
    缺点:如果连接的客户端比较多,则 分配的线程也很多,就会导致服务器资源耗尽而崩溃

  • 每一个客户端连接交给一个拥有固定数量线程的连接池
    优点:编码简单,可处理大量连接
    缺点:线程开销很大,如果连接比较多,则排队现象比较严重

  • 使用java中的NIO,用非阻塞IO的方式处理,这种模式可以用一个线程处理大量的客户端连接

选择器(Selector)

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

常用方法:

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

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

  • int OP_ACCEPT:有新的网络连接可以 accept,值为 16
  • int OP_CONNECT:代表连接已经建立,值为 8
  • int OP_READ 和 int OP_WRITE:代表了读、写操作,值为 1 和 4
常用方法说明
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

作用: 用于在服务器端监听一个新的连接

常用方法说明
public abstract Selector selector()得到一个 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

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

常用方法说明
public static SocketChannel open()得到一个 SocketChannel 通道
public final SelectableChannel configureBlocking(boolean block)设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
Pblic 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()关闭通道
例子

创建一个客户端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();
    }
}

创建一个服务器端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();
            }
        }
    }
}

AIO(异步非阻塞)

与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。
在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel
    其中的read/write方法,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数

总结

  • BIO是同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。

  • NIO是同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

  • AIO是异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

BIO、NIO、AIO适用场景分析
  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高
  • NIO方式适用于连接数目多且连接比较短的架构,可充分利用服务器资源
  • AIO方式使用于连接数目多且连接比较长的架构,充分调用OS参与并发操作
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值