BIO编程

IO 有的称之为 basic(基本) IO,有的称之为 block(阻塞) IO,主要应用于文件 IO 和网络 IO,
这里不再说文件 IO, 大家对此都非常熟悉,本次主要讲解网络 IO。
在 JDK1.4 之前,我们建立网络连接的时候只能采用 BIO,需要先在服务端启动一个ServerSocket,然后在客户端启动 Socket 来对服务端进行通信,默认情况下服务端需要对每个请求建立一个线程等待请求,而客户端发送请求后,先咨询服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝,如果有的话,客户端线程会等待请求结束后才继续执行,这就是阻塞式 IO。
接下来通过一个例子复习回顾一下 BIO 的基本用法(基于 TCP)。

package com.bestqiang.bio;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

//BIO 服务器端程序
public class TCPServer {
    public static void main(String[] args) throws Exception {
        //1.创建 ServerSocket 对象
        ServerSocket ss = new ServerSocket(9999);
        while (true) {
            //2.监听客户端
            Socket s = ss.accept(); //阻塞
            //3.从连接中取出输入流来接收消息
            InputStream is = s.getInputStream(); //阻塞
            byte[] b = new byte[10];
            is.read(b);
            String clientIP = s.getInetAddress().getHostAddress();
            System.out.println(clientIP + "说:" + new String(b).trim());
            //4.从连接中取出输出流并回话
            OutputStream os = s.getOutputStream();
            os.write("没钱".getBytes());
            //5.关闭
            s.close();
        }
    }
}

上述代码编写了一个服务器端程序,绑定端口号 9999,accept 方法用来监听客户端连接,
如果没有客户端连接,就一直等待,程序会阻塞到这里。

package com.bestqiang.bio;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

//BIO 客户端程序
public class TCPClient {
    public static void main(String[] args) throws Exception {
        while (true) {
            //1.创建 Socket 对象
            Socket s = new Socket("127.0.0.1", 9999);
            //2.从连接中取出输出流并发消息
            OutputStream os = s.getOutputStream();
            System.out.println("请输入:");
            Scanner sc = new Scanner(System.in);
            String msg = sc.nextLine();
            os.write(msg.getBytes());
            //3.从连接中取出输入流并接收回话
            InputStream is = s.getInputStream(); //阻塞
            byte[] b = new byte[20];
            is.read(b);
            System.out.println("老板说:" + new String(b).trim());
            //4.关闭
            s.close();
        }
    }
}

上述代码编写了一个客户端程序,通过 9999 端口连接服务器端,getInputStream 方法用来
等待服务器端返回数据,如果没有返回,就一直等待,程序会阻塞到这里。运行效果如下图
所示:

NIO编程

什么是NIO?

java.nio 全称 java non-blocking IO,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO)。新增了许多用于处理输入输出的类,这些类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写,新增了满足 NIO 的功能。

NIO 和 BIO 有着相同的目的和作用,但是它们的实现方式完全不同,BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多。另外,NIO 是非阻塞式的,这一点跟 BIO 也很不相同,使用它可以提供非阻塞式的高伸缩性网络。NIO 主要有三大核心部分:Channel(通道)Buffer(缓冲区), Selector(选择器)。传统的 BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。

文件IO

概述和核心 API

缓冲区(Buffer):实际上是一个容器,是一个特殊的数组,缓冲区对象内置了一些机
制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,
但是读取或写入的数据都必须经由 Buffer,如下图所示:

在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类

常用的 Buffer 子类有:

  • ByteBuffer,存储字节数据到缓冲区
  • ShortBuffer,存储字符串数据到缓冲区
  • CharBuffer,存储字符数据到缓冲区
  • IntBuffer,存储整数数据到缓冲区
  • LongBuffer,存储长整型数据到缓冲区
  • DoubleBuffer,存储小数到缓冲区
  • FloatBuffer,存储小数到缓冲区

对于 Java 中的基本数据类型,都有一个 Buffer 类型与之相对应,最常用的自然是ByteBuffer 类(二进制数据),该类的主要方法如下所示:

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

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

这里我们先讲解 FileChannel 类,该类主要用来对本地文件进行 IO 操作,主要方法如下所示:

  • 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),把数据从当前通道复制给目标通道

下面来使用NIO进行简单编程,熟悉API基本用法

package com.bestqiang.nio.file;

import org.junit.Test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/** * @author BestQiang */
// 通过NIO实现文件IO
public class TestNIO  {

    @Test //向本地文件中写数据
    public void test1() throws Exception {
        //1. 创建输出流
        FileOutputStream fileOutputStream = new FileOutputStream("basic.txt");
        //2. 从流中得到一个通道
        FileChannel fileChannel = fileOutputStream.getChannel();
        //3. 提供一个缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //4. 向缓冲区中存入数据
        String str = "Hello,NIO";
        buffer.put(str.getBytes());
        //** 反转缓冲区
        buffer.flip();
        //5. 把缓冲区写到通道中
        fileChannel.write(buffer);
        //6. 关闭
        fileOutputStream.close();
    }

    @Test // 从本地文件中读取数据
    public void test2() throws Exception{
        File file = new File("basic.txt");
        // 1. 创建输入流
        FileInputStream inputStream = new FileInputStream(file);
        // 2.从流中得到一个通道
        FileChannel channel = inputStream.getChannel();
        // 3.获取缓冲区
        ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
        // 4.从通道取得数据到缓冲区
        int read = channel.read(buffer);
        System.out.println(new String(buffer.array()));
        inputStream.close();
    }

    @Test // 使用NIO实现文件复制
    public void test3() throws Exception{
        // 1.创建两个流
        FileInputStream inputStream = new FileInputStream("basic.txt");
        FileOutputStream outputStream = new FileOutputStream("basic2.txt");
        // 2.得到两个通道
        FileChannel inputStreamChannelchannel = inputStream.getChannel();
        FileChannel outputStreamChannelchannel = outputStream.getChannel();
        // 3.复制
        inputStreamChannelchannel.transferTo(0, inputStreamChannelchannel.size(),outputStreamChannelchannel);

        inputStream.close();
        outputStream.close();
    }
}

NIO 中的通道是从输出流对象里通过 getChannel 方法获取到的,该通道是双向的,既可以读,又可以写。在往通道里写数据之前,必须通过 put 方法把数据存到 ByteBuffer 中,然后通过通道的 write 方法写数据。在 write 之前,需要调用 flip 方法翻转缓冲区,把内部重置到初始位置,这样在接下来写数据时才能把所有数据写到通道里。

关于buffer.flip()方法,jdk的源码已经说得很清楚

上面说翻转这个缓冲区,将极限位置设置为当前位置,将当前位置设置为0,如果mark被定义,就终止。
在经过put操作后,应该翻转缓冲区,然后再进行使用。