NIO中的Channel和Buffer

前言

打算输出一系列Netty源码分析与实践的文章,也作为后端开发学习过程中的沉淀。写作风格会遵循目标导向,关注核心,抽离出知识的Pattern,无价值细节决不花时间。
此文章为第三篇,和大家一块了解下NIO中的核心组件ChannelBuffer

Channel Buffer

NIO中的Channel和Buffer_第1张图片

  • Buffer顾名思义,本质上就是一个内存缓冲区,作为存储数据的一块内存而已。对于每个非布尔原始数据类型都有一个缓冲区类,不过底层数据存储本质上都是字节数组,只不过给我们提供了方便的特定类型的put(), get()。可以类比JDK中字符流和字节流联系。
  • Channel是对“连接”的一种抽象,比如和硬件设备的连接,网络socket连接,同时它具备了IO操作的能力,比如从网络中读取数据,写出数据到对端。通过channel读取数据的过程是先要从网络中读取数据到Buffer,写数据依然是会通过channrl先写入到Buffer中。
    为什么NIO要搞一个Buffer呢?
    • 传统IO中,流是基于字节的方式进行读写的,一个字节一个字节的读取和写入
    • NIO基于Buffer可以实现异步的数据读写,并且可以基于一整块(chunk)字节数据进行处理
      其实我们会对Buffer越来越有感觉的,从操作系统设备块缓冲到上层各种Java应用框架设计,导出可见基于Buffer的设计

Buffer特性

下图是对Buffer概念模型:

NIO中的Channel和Buffer_第2张图片

Buffer的实现主要依赖下面的四个属性:

  • Capacity: Buffer所能存储的最大字节数量,一旦定义好buffer后就再不可变。一旦容量写满了,只能重新创建Buffer或者清空现有Buffer
  • Limit: 对于Buffer的操作,存在读和写两种模式,在写模式下 limit等于 capacity,表示当前最大可以写入的上限, 在读模式下,代表当前可以从buffer中最多可以读到的数据
  • Position: 当前操作buffer的位置指针,表示下一个可以读或者可以写的位置,在buffer创建时被默认置为0,每当对buffer进行get(), put()操作时,会启动加1
  • Mark: 调用mark()方法会记录下mark的buffer下标位置,当后续调用reset(), 就会回到当时曾mark的位置,回到此位置继续操作buffer。

Buffer常用方法

Buffer使用

Buffer创建

  1. allocate(int capacity) : 在堆内存中分配指定容量大小的Buffer
  2. allocateDirect(int capacity): 在堆外内存分配指定大小的Buffer,JVM会通过native I/O方式操作这块分配在堆外的内存,这样少了堆内和堆外内存的拷贝,未来我们会看到很多地方都会出现这玩意。
  3. wrap(byte[] array): 包装一个字节数据作为buffer

下面是一个创建capacity == 256 大小的buffer示例:

ByteBuffer buffer = ByteBuffer.allocate(256);

写入数据到Buffer

写入数据到buffer可通过以下方式:

  1. 直接使用buffer本身的put()方法
  2. 使用channel的read() 方法

直接基于buffer操作时,put()方法有多个重载版本: put(byte b), put(byte[] src), put(byte[] src, int offset, int length), 以及 put(ByteBuffer src)

NIO中的Channel和Buffer_第3张图片

myBuffer.put("QUOTE".getBytes());

下面这个例子,我们使用Channel的read()方法,它会从某连接中读取字节序列写入到buffer,概念模型如下:
NIO中的Channel和Buffer_第4张图片

int bytesRead = inChannel.read(myBuffer);

在这里我们可以形象的看到Channel所体现的对文件系统建立连接的抽象。

下面给出一个完整的通过NIO Channel 和
Buffer将字符串数据写入到文本文件的程序:

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

import org.apache.log4j.Logger;

public class MyBufferWriteExample {
  private static final Logger logger
                    = Logger.getLogger(MyBufferWriteExample.class);
  private static final int BUFFER_SIZE = 1024;
  private static final String FILE_NAME = "c:\\tmp\\output.txt";
  private static final String QUOTE
      = "If your actions inspire others to dream more, learn "
      + "more, do  more and become more, you are a leader.";

  public static void main(String[] args) throws IOException {
    logger.info("Starting MyBufferWriteExample...");
    FileOutputStream fileOS = new FileOutputStream(FILE_NAME);
    FileChannel channel = fileOS.getChannel();

    try {
      ByteBuffer myBuffer = ByteBuffer.allocate(BUFFER_SIZE);
      myBuffer.put(QUOTE.getBytes());
      myBuffer.flip();

      int bytesWritten = channel.write(myBuffer);
      logger.info(
        String.format("%d bytes have been written to disk...",
        bytesWritten));
      logger.info(
        String.format("Current buffer position is %d",
        myBuffer.position()));
    } finally  {
      channel.close();
      fileOS.close();
  }
}

从Buffer中读取数据

NIO中的Channel和Buffer_第5张图片

上图是基于Buffer本身的API get()操作。

NIO中的Channel和Buffer_第6张图片
上图展示了应用程序会先将数据写到Buffer,Channel基于write()操作的调用,将数据从buffer读出刷到文件系统的过程

int bytesWritten = channel.write(myBuffer);

下面给出一个完整的从文本文件读取数据的NIO程序:

package com.avaldes.tutorial;

import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

import org.apache.log4j.Logger;

public class MyBufferReadExample {
  private static final Logger logger
            = Logger.getLogger(MyBufferReadExample.class);
  private static final int BUFFER_SIZE = 1024;
  private static final String FILE_NAME = "c:\\tmp\\output.txt";

  public static void main(String[] args) throws IOException {
    logger.info("Starting MyBufferReadExample...");
    FileInputStream fileIS = new FileInputStream(FILE_NAME);
    FileChannel inChannel = fileIS.getChannel();

    ByteBuffer myBuffer = ByteBuffer.allocate(BUFFER_SIZE);
    while (inChannel.read(myBuffer) > 0) {
      myBuffer.flip();

      while (myBuffer.hasRemaining()) {
        System.out.print((char) myBuffer.get());
      }
      myBuffer.clear();
    }

    inChannel.close();
    fileIS.close();
  }
}

更多的Buffer内部操作

这一部分介绍一些Buffer中内置的其它一些操作,所有的子类都会继承这些方法

flip()

会将Buffer由写模式切换到读模式,flip操作时会将limit设置为当前position,mark重置为-1, position指针置为0,表示从0可以开始读,最大可读到limit位置:

 public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

mark() 和reset()

mark()方法会记录当前buffer的position位置,在未来的某个时间点,使得我们能够通过调用reset(), 来将当前position重新设定为之前mark的position。

 public final Buffer mark() {
        mark = position;
        return this;
}
public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

Warning: If the mark has not set, invoking reset() will cause an exception called InvalidMarkException.

clear()

clear()方法会设置position为0,设置limit等于buffer的capacity,mark重置为-1。这是将buffer置为写模式的准备

rewind()

rewind() 会设置position为0,并丢弃mark(将mark=-1),一般用于从0position开始重新读。

public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
}
public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
}

总结

本篇文章我们分析了NIO中的两大组件Channel和Buffer,围绕它是什么,存在的意义。通过概念模型展示了所扮演的角色,讲述了基本的使用操作。一旦我们对知识建立了顶层抽象认知,对细节的探索就更有效率和针对性。所以至于JDK是如何实现的这些东西,打开IDEA,对着代码傻子都能看懂吧!

下一篇我们会分析Netty的ByteBuf,嗯? 又一个buffer?

参考

Java NIO Buffers

NIO中的Channel和Buffer_第7张图片

你可能感兴趣的:(秒懂Netty源码分析系列)