Java IO与NIO区别

  • 一、BIO
  • 二、NIO
  • 1.定义
  • 2.NIO与IO的主要区别
  • 3.通道和缓冲区
  • 3.1 缓冲区(buffer)
  • 3.2 直接缓冲区与非直接缓冲区
  • 3.3 通道(Channel)
  • 4.非阻塞式网络通信
  • 4.1 选择器(Selector)
  • 4.2 套接字
  • 4.3 管道Pipe


IO详解

二、NIO

1.定义

  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
  • Java 1.4版本对NIO进行了API的扩展

2.NIO与IO的主要区别

IO

NIO

面向字节流

面向缓冲区

阻塞IO

非阻塞IO

无选择器

选择器

3.通道和缓冲区

  • Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理
  • Channel 负责传输, Buffer 负责存储
3.1 缓冲区(buffer)

缓冲区的常见子类

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

注意:通过allocate(int capacity)方法创建一个缓冲区

缓冲区的基本属性

  • 容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
  • 限制 (limit):第一个不应该读取或写入的数据的索引,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
  • 位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
  • 标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position.

标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity

     下面这段代码分析我们在Buffer缓冲区中写入数据时基本属性的变化

public static void main(String[] args) {

        ByteBuffer buffer = ByteBuffer.allocate(20);

        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());

        buffer.put(new String("abcde").getBytes());

        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());
    }

运行结果如下图:

io和nio的优缺点 java java io nio区别_数据


     通过结果我们我们可以分析出当写入缓冲区是position属性就是写入的字节个数,limit属性在初始化容量大小的位置,capacity为最大容量

下面我们切换为读出模式

buffer.flip();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());

运行结果如下图:

io和nio的优缺点 java java io nio区别_System_02


     通过结果我们我们可以分析出切换为读出模式时,position的位置变成了开始读的位置,limit的位置变为最大可读出的位置,capacity仍然为最大的容量

下面我们读出俩个字符看看这些值的变化

byte[] bytes = new byte[buffer.limit()];
        buffer.get(bytes,0,2);

        System.out.println(new String(bytes,0,2));
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());

运行结果如下图:

io和nio的优缺点 java java io nio区别_io和nio的优缺点 java_03


     通过结果我们我们可以分析出position变成了读出的位置,其他俩个不变

下面先mark标记一下position的位置,继续读俩个

buffer.mark();
        buffer.get(bytes,buffer.position(),2);
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());

运行结果如下图:

io和nio的优缺点 java java io nio区别_io和nio的优缺点 java_04


     通过结果我们我们可以分析出position变成了读出的位置,其他俩个不变

下面调用reset方法

buffer.reset();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());

运行结果如下图:

io和nio的优缺点 java java io nio区别_数据_05


     通过结果我们我们可以分析出position恢复到了标记的位置,其他俩个不变

下面调用rewind方法

buffer.rewind();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());

运行结果如下图:

io和nio的优缺点 java java io nio区别_System_06


     通过结果我们我们可以分析出这三个值都被初始化了,数据可以进行重读

下面调用clear方法

buffer.clear();
        System.out.println(buffer.position());
        System.out.println(buffer.limit());
        System.out.println(buffer.capacity());

运行结果如下图:

io和nio的优缺点 java java io nio区别_System_07


     通过结果我们我们可以分析出这三个值都恢复到刚刚创建的位置,清空了缓冲区

注意: 这时候位置被初始化了,但是之前存储在Buffer中的数据并没有被清空,只是变成了被遗忘数据,我们不知道它有多少,因此并不能读取

3.2 直接缓冲区与非直接缓冲区

直接缓冲区

非直接缓冲区

创建方式

allocateDirect(int capacity)

allocate(int capacity)

创建位置

建立在物理内存中,效率高

建立在 JVM 的内存中

判断是否是直接缓冲区

isisDirect()

true

false

  • 字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中
3.3 通道(Channel)
  • 由 java.nio.channels 包定义的。Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。

通道的四大实现类

  • FileChannel
  • SocketChannel
  • ServerSocketChannel
  • DategramChannel

获取通道

  • Java 针对支持通道的类提供了 getChannel() 方法

本地 IO

  • FileInputStream/FileOutputStream
  • RandomAccessFile

网络IO

  • Socket
  • ServerSocket
  • DatagramSocket

在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()

下面利用通道完成文件的复制—非直接缓冲区

try {
            FileInputStream in = new FileInputStream("D:/1.mv");
            FileOutputStream out = new FileOutputStream("D:2.mv");
            FileChannel inChannel =  in.getChannel();
            FileChannel outChannel = out.getChannel();

            ByteBuffer buffer = ByteBuffer.allocate(1024);

            while (inChannel.read(buffer) != -1){
                buffer.flip();
                outChannel.write(buffer);
                buffer.clear();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

注意:使用结束之后别忘记将缓冲区和通道关闭

利用通道完成文件的复制—直接缓冲区

FileChannel inChannel = FileChannel.open(Paths.get("D:/1.mv"), StandardOpenOption.READ);

        FileChannel outChannel = FileChannel.open(Paths.get("D:/2.mv"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);

        //内存映射文件
        MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
        MappedByteBuffer outMappedBuf = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());

        //直接对缓冲区进行数据的读写操作
        byte[] dst = new byte[inMappedBuf.limit()];
        inMappedBuf.get(dst);
        outMappedBuf.put(dst);

        inChannel.close();
        outChannel.close();

     通过对比直接缓冲区的复制速度要快很多

通道之间的数据传输

  • transferFrom()
  • transferTo()

通道之间的数据传输

FileChannel inChannel = FileChannel.open(Paths.get("D:/1.mv"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("D:/2.mv"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);

//		inChannel.transferTo(0, inChannel.size(), outChannel);
        outChannel.transferFrom(inChannel, 0, inChannel.size());

        inChannel.close();
        outChannel.close();

分散(Scatter)与聚集(Gather)

  • 分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中
  • 聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中
RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");
		
		//1. 获取通道
		FileChannel channel1 = raf1.getChannel();
		
		//2. 分配指定大小的缓冲区
		ByteBuffer buf1 = ByteBuffer.allocate(100);
		ByteBuffer buf2 = ByteBuffer.allocate(1024);
		
		//3. 分散读取
		ByteBuffer[] bufs = {buf1, buf2};
		channel1.read(bufs);
		
		for (ByteBuffer byteBuffer : bufs) {
			byteBuffer.flip();
		}
		
		System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
		System.out.println("-----------------");
		System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
		
		//4. 聚集写入
		RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
		FileChannel channel2 = raf2.getChannel();
		
		channel2.write(bufs);

字符集:Charset

  • 编码:字符串 -> 字节数组
  • 解码:字节数组 -> 字符串
Charset cs1 = Charset.forName("GBK");
		
		//获取编码器
		CharsetEncoder ce = cs1.newEncoder();
		
		//获取解码器
		CharsetDecoder cd = cs1.newDecoder();
		
		CharBuffer cBuf = CharBuffer.allocate(1024);
		cBuf.put("hello world!");
		cBuf.flip();
		
		//编码
		ByteBuffer bBuf = ce.encode(cBuf);
		
		for (int i = 0; i < 12; i++) {
			System.out.println(bBuf.get());
		}
		
		//解码
		bBuf.flip();
		CharBuffer cBuf2 = cd.decode(bBuf);
		System.out.println(cBuf2.toString());
		
		System.out.println("------------------------------------------------------");
		
		Charset cs2 = Charset.forName("GBK");
		bBuf.flip();
		CharBuffer cBuf3 = cs2.decode(bBuf);
		System.out.println(cBuf3.toString());

4.非阻塞式网络通信

  • 通道(Channel):负责连接

java.nio.channels.Channel 接口:
SelectableChannel

  • SocketChannel----TCP
  • ServerSocketChannel----TCP
  • DatagramChannel----UDP
  • Pipe.SinkChannel
  • Pipe.SourceChannel
  • 缓冲区(Buffer):负责数据的存取
  • 选择器(Selector):是 SelectableChannel 的多路复用器。用于监控 SelectableChannel 的 IO 状况
4.1 选择器(Selector)
  • 选择器(Selector) 是 SelectableChannle 对象的多路复用器
  • Selector 可以同时监控多个SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个 Channel
  • Selector 是非阻塞 IO 的核心。
  • 选择器的应用
  • 创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。
  • 向选择器注册通道:SelectableChannel.register(Selector sel, int ops)
  • 当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。
  • 读 : SelectionKey.OP_READ
  • 写 : SelectionKey.OP_WRITE
  • 连接 : SelectionKey.OP_CONNECT
  • 接收 : SelectionKey.OP_ACCEPT
4.2 套接字

下面介绍一下它的使用

  • SockeChannel—阻塞
//客户端
	@Test
	public void client() throws IOException{
		SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
		
		FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
		
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		while(inChannel.read(buf) != -1){
			buf.flip();
			sChannel.write(buf);
			buf.clear();
		}
		
		sChannel.shutdownOutput();
		
		//接收服务端的反馈
		int len = 0;
		while((len = sChannel.read(buf)) != -1){
			buf.flip();
			System.out.println(new String(buf.array(), 0, len));
			buf.clear();
		}
		
		inChannel.close();
		sChannel.close();
	}
	
	//服务端
	@Test
	public void server() throws IOException{
		ServerSocketChannel ssChannel = ServerSocketChannel.open();
		
		FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
		
		ssChannel.bind(new InetSocketAddress(9898));
		
		SocketChannel sChannel = ssChannel.accept();
		
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		while(sChannel.read(buf) != -1){
			buf.flip();
			outChannel.write(buf);
			buf.clear();
		}
		
		//发送反馈给客户端
		buf.put("服务端接收数据成功".getBytes());
		buf.flip();
		sChannel.write(buf);
		
		sChannel.close();
		outChannel.close();
		ssChannel.close();
	}
  • SockeChannel—非阻塞
//客户端
	@Test
	public void client() throws IOException{
		//1. 获取通道
		SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
		
		//2. 切换非阻塞模式
		sChannel.configureBlocking(false);
		
		//3. 分配指定大小的缓冲区
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		//4. 发送数据给服务端
		Scanner scan = new Scanner(System.in);
		
		while(scan.hasNext()){
			String str = scan.next();
			buf.put((new Date().toString() + "\n" + str).getBytes());
			buf.flip();
			sChannel.write(buf);
			buf.clear();
		}
		
		//5. 关闭通道
		sChannel.close();
	}

	//服务端
	@Test
	public void server() throws IOException{
		//1. 获取通道
		ServerSocketChannel ssChannel = ServerSocketChannel.open();
		
		//2. 切换非阻塞模式
		ssChannel.configureBlocking(false);
		
		//3. 绑定连接
		ssChannel.bind(new InetSocketAddress(9898));
		
		//4. 获取选择器
		Selector selector = Selector.open();
		
		//5. 将通道注册到选择器上, 并且指定“监听接收事件”
		ssChannel.register(selector, SelectionKey.OP_ACCEPT);
		
		//6. 轮询式的获取选择器上已经“准备就绪”的事件
		while(selector.select() > 0){
			
			//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
			Iterator<SelectionKey> it = selector.selectedKeys().iterator();
			
			while(it.hasNext()){
				//8. 获取准备“就绪”的是事件
				SelectionKey sk = it.next();
				
				//9. 判断具体是什么事件准备就绪
				if(sk.isAcceptable()){
					//10. 若“接收就绪”,获取客户端连接
					SocketChannel sChannel = ssChannel.accept();
					
					//11. 切换非阻塞模式
					sChannel.configureBlocking(false);
					
					//12. 将该通道注册到选择器上
					sChannel.register(selector, SelectionKey.OP_READ);
				}else if(sk.isReadable()){
					//13. 获取当前选择器上“读就绪”状态的通道
					SocketChannel sChannel = (SocketChannel) sk.channel();
					
					//14. 读取数据
					ByteBuffer buf = ByteBuffer.allocate(1024);
					
					int len = 0;
					while((len = sChannel.read(buf)) > 0 ){
						buf.flip();
						System.out.println(new String(buf.array(), 0, len));
						buf.clear();
					}
				}
				
				//15. 取消选择键 SelectionKey
				it.remove();
			}
		}
	}
4.3 管道Pipe
  • Java NIO 管道是2个线程之间的单向数据连接
  • Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
  • 使用如下
public void test1() throws IOException{
		//1. 获取管道
		Pipe pipe = Pipe.open();
		
		//2. 将缓冲区中的数据写入管道
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		Pipe.SinkChannel sinkChannel = pipe.sink();
		buf.put("通过单向管道发送数据".getBytes());
		buf.flip();
		sinkChannel.write(buf);
		
		//3. 读取缓冲区中的数据
		Pipe.SourceChannel sourceChannel = pipe.source();
		buf.flip();
		int len = sourceChannel.read(buf);
		System.out.println(new String(buf.array(), 0, len));
		
		sourceChannel.close();
		sinkChannel.close();
	}