NIO的学习总结

NIO和BIO

什么是NIO以及其与传统IO的区别

NIO即非阻塞I/O(Non-Blocking I/O),NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

NIO几个重要的部分

1. Channel

介绍

Channel(通道):Channel是一个对象,可以通过它读取和写入数据。可以把它看做是IO中的流,不同的是:

Channel是双向的,既可以读又可以写,而流是单向的

Channel可以进行异步的读写

对Channel的读写必须通过buffer对象

正如上面提到的,所有数据都通过Buffer对象处理,所以,您永远不会将字节直接写入到Channel中,相反,您是将数据写入到Buffer中;同样,您也不会从Channel中读取字节,而是将数据从Channel读入Buffer,再从Buffer获取这个字节。因为Channel是双向的,所以Channel可以比流更好地反映出底层操作系统的真实情况。

在Java NIO中的Channel主要有如下几种类型:

  • FileChannel:从文件读取数据的

  • DatagramChannel:读写UDP网络协议数据

  • SocketChannel:读写TCP网络协议数据

  • ServerSocketChannel:可以监听TCP连接

主要类
Channel接口

所有具体channel类均直接或间接实现了此接口

public boolean isOpen(); // 获取此channel是否是处于打开状态

public void close() throws IOException; // 关闭channel 且有io错误抛出
FileChannel类

在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个FileInputStream、FileOutputStream或RandomAccessFile来获取一个FileChannel实例

 protected FileChannel() {
  } //本类的构造器 , 不可被new 只能使用open方法或者其他的类方法获取实例

   /**
    * 两个open方法 返回FileChannel的实例对象
    * path:要打开或创建的文件的路径
    * options:指定文件打开方式的选项
    * attrs: 创建文件时要自动设置的文件属性的可选列表
    */
   public static FileChannel open(Path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {/**.......*/}
   public static FileChannel open(Path, OpenOption... options) throws IOException {/**...*/}

   /**
    * 从该通道读取一个字节序列到给定的缓冲区
    */
   public abstract int read(ByteBuffer dst) throws IOException;
   public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
   public final long read(ByteBuffer[] dsts) throws IOException {    /**.....*/}

   /**
    * 从给定缓冲区向该通道写入一个字节序列。
    */
   public abstract int write(ByteBuffer src) throws IOException;
   public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException;
   public final long write(ByteBuffer[] srcs) throws IOException {  /**...*/}

   // 获取当前channel的位置
   public abstract long position() throws IOException;

   // 根据 newPosition 参数得到的channel
   public abstract FileChannel position(long newPosition) throws IOException;

   // 返回此通道文件的当前大小。
   public abstract long size() throws IOException;

   //截断为指定大小
   public abstract FileChannel truncate(long size) throws IOException;

   // 是否强制写入存储设备
   // 出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法。force()方法有一个boolean类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。
   public abstract void force(boolean metaData) throws IOException;

   // 将数据传给所给的Channel中
   public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;

   // 吧数据从所给Channel中转到当前通道
   public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;

   // 将此Channel中的某段数据映射到内存中
   public abstract MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) throws IOException;

   // 获取某个区域的FileLock(文件锁,保证线程安全)
   public abstract FileLock lock(long position, long size, boolean shared) throws IOException;
   public final FileLock lock() throws IOException {/**.....*/}

   // 尝试获取FileLock区域  
   public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;

   // 获取整个文件的fileLock
   public final FileLock tryLock() throws IOException {/**...*/}

DatagramChannel类

DatagramChannel是一个能收发UDP包的通道

   protected DatagramChannel(SelectorProvider provider) //不可通过new来获取实例对象

   // 获取DatagramChannel实例 ProtocolFamily 为协议类型(可选IPv4,IPv6)
   public static DatagramChannel open() throws IOException { /**....*/}
   public static DatagramChannel open(ProtocolFamily family) throws IOException {/**...*/}

   // 返回确定此频道支持的操作的操作集。
   public final int validOps() {/**...*/}

   // 用于socket的操作
   public abstract DatagramChannel bind(SocketAddress local)throws IOException;
   public abstract <T> DatagramChannel setOption(SocketOption<T> name, T value)throws IOException;
   public abstract DatagramSocket socket();// 返回与此channel有关的socket
   public abstract boolean isConnected();// socket是否连接
   public abstract DatagramChannel connect(SocketAddress remote)throws IOException;// 连接socket
   public abstract DatagramChannel disconnect() throws IOException;// 关闭连接
   public abstract SocketAddress getRemoteAddress() throws IOException;// 获取已连接的socket的地址
   public abstract SocketAddress receive(ByteBuffer dst) throws IOException;// 接受数据
   public abstract int send(ByteBuffer src, SocketAddress target)throws IOException;// 发送数据

   // 读取数据的Read Write方法
   // 。。。。。。。。。

   public abstract SocketAddress getLocalAddress() throws IOException;// 获取本地的socket地址
SocketChannel类

SocketChannel主要是用来基于TCP通信的通道

protected SocketChannel(SelectorProvider provider)// 不可new

// 获取实例
public static SocketChannel open() throws IOException {}
public static SocketChannel open(SocketAddress remote)throws IOException{}

/**   同 DatagramChannnel 的Socket操作方法*/
// ......zh

   public abstract boolean isConnectionPending();// 判断管道上的连接操作是否正在进行

// 在不关闭通道的情况下关闭连接以进行读取/写入。
public abstract SocketChannel shutdownInput() throws IOException;
public abstract SocketChannel shutdownOutput() throws IOException;

public abstract boolean finishConnect() throws IOException;//结束连接
ServerSocketChannel类

ServerSocketChannel是一个可以监听新进来的TCP连接的通道

ServerSocketChannel类的方法主要进行Socket的连接的监听,接受,方法与前面的DatagramChannel和SocketChannel内的方法基本一致。

ServerSocketChannel

2. Buffer

Buffer是一个对象,它包含一些要写入或者读到Stream对象的。应用程序不能直接对 Channel 进行读写操作,而必须通过 Buffer 来进行,即 Channel 是通过 Buffer 来读写数据的。

在NIO中,所有的数据都是用Buffer处理的,它是NIO读写数据的中转池。Buffer实质上是一个数组,通常是一个字节数据,但也可以是其他类型的数组。但一个缓冲区不仅仅是一个数组,重要的是它提供了对数据的结构化访问,而且还可以跟踪系统的读写进程。

使用 Buffer 读写数据一般遵循以下四个步骤:

  1. 写入数据到 Buffer;

  2. 调用 flip() 方法;

  3. 从 Buffer 中读取数据;

  4. 调用 clear() 方法或者 compact() 方法。

当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip() 方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 Buffer 的所有数据。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。

Buffer主要有如下几种:

  • ByteBuffer

  • CharBuffer

  • DoubleBuffer

  • FloatBuffer

  • IntBuffer

  • LongBuffer

  • ShortBuffer

Buffer类的主要方法
Buffer(int mark, int pos, int lim, int cap) {  /**.......*/ }构造器,进行初始化
public final int capacity() // 获取缓冲区容量
public final int position() // 获取当前缓冲区位置
public final Buffer position(int newPosition) // 设置buffer的位置,并返回设置后的buffer
public final int limit() // 获取缓冲区的限制
public final Buffer limit(int newLimit) // 设置缓冲区的限制
public final Buffer mark() //在此位置设置此缓冲区的标记
public final Buffer reset() // 将缓冲位置设置为先去标记的位置
public final Buffer clear() // 清除缓冲区 position设置为0 limit设置为容量 mark被清除
public final Buffer flip() // 模式切换limit设置为当前position 且position设置为0 mark被清除
public final Buffer rewind() // 倒回这个缓冲区。position设置为零,mark被清除
public final int remaining() // 返回limit-position的元素数
public final boolean hasRemaining() // 是否有剩余的元素
public abstract boolean isReadOnly()// 判断此缓冲区是否为只读
public abstract boolean hasArray()// 判断此缓冲区是否由可访问数组支持
public abstract Object array()//返回支持此数组的数组
public abstract int arrayOffset()//返回第一个缓冲区的后备数组中的偏移量
public abstract boolean isDirect()//判断此字节缓冲区是否是直接的。

3. Scatter / Gather

scatter / gather是通过通道读写数据的两个概念。

Scattering read指的是从通道读取的操作能把数据写入多个buffer,也就是sctters代表了数据从一个channel到多个buffer的过程。

gathering write则正好相反,表示的是从多个buffer把数据写入到一个channel中。

Scatter/gather在有些场景下会非常有用,比如需要处理多份分开传输的数据。举例来说,假设一个消息包含了header和body,我们可能会把header和body保存在不同独立buffer中,这种分开处理header与body的做法会使开发更简明

Scattering Read

Scattering Read

用代码表示

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);

我们把多个buffer写在了一个数组中,然后把数组传递给channel.read()方法。read()方法内部会负责把数据按顺序写进传入的buffer数组内。一个buffer写满后,接着写到下一个buffer中。

实际上,scattering read内部必须写满一个buffer后才会向后移动到下一个buffer,因此这并不适合消息大小会动态改变的部分,也就是说,如果你有一个header和body,并且header有一个固定的大小(比如128字节),这种情形下可以正常工作。

Gathering Writes

Gathering Writes

用代码表示

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

类似的传入一个buffer数组给write,内部机会按顺序将数组内的内容写进channel,这里需要注意,写入的时候针对的是buffer中position到limit之间的数据。也就是如果buffer的容量是128字节,但它只包含了58字节数据,那么写入的时候只有58字节会真正写入。因此gathering write是可以适用于可变大小的message的,这和scattering reads不同。

4. Selector

Selector是Java NIO中的一个组件,用于检查一个或多个NIO Channel的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。

为什么使用Selector

用单线程处理多个channels的好处是我需要更少的线程来处理channel。实际上,你甚至可以用一个线程来处理所有的channels。从操作系统的角度来看,切换线程开销是比较昂贵的,并且每个线程都需要占用系统资源,因此占用线程越少越好。

需要留意的是,现代操作系统和CPU在多任务处理上已经变得越来越好,所以多线程带来的影响也越来越小。如果一个CPU是多核的,如果不执行多任务反而是浪费了机器的性能。不过这些设计讨论是另外的话题了。简而言之,通过Selector我们可以实现单线程操作多个channel。

主要方法
public static Selector open() throws IOException //获取Selector实例
public abstract boolean isOpen();// 获取此selector是否打开
public abstract SelectorProvider provider() // 返回SelectorProvider 
public abstract Set<SelectionKey> keys();// 返回此selector的key的集合 这里面的key不能直接修改,key只有在他被取消或者channel被注销后才会被移除。同时这个set非线程安全
public abstract Set<SelectionKey> selectedKeys();//返回 selected-key的集合
/**
* select()方法在返回channel之前处于阻塞状态。 select(long timeout)和select做的事一样,不过他的阻塞有一个超时限制。
* selectNow()不会阻塞,根据当前状态立刻返回合适的channel
* select()方法的返回值是一个int整形,代表有多少channel处于就绪了。也就是自上一次select后有多少channel进入就绪
*/
public abstract int selectNow() throws IOException;//根据当前状态立刻返回合适的channel
public abstract int select(long timeout)throws IOException;
public abstract int select() throws IOException;

public abstract Selector wakeup();//由于调用select而被阻塞的线程,可以通过调用Selector.wakeup()来唤醒即便此时已然没有channel处于就绪状态。具体操作是,在另外一个线程调用wakeup,被阻塞与select方法的线程就会立刻返回。
public abstract void close() throws IOException;// 关闭selector

5. Pipe

一个Java NIO的管道是两个线程间单向传输数据的连接。一个管道(Pipe)有一个source channel和一个sink channel。我们把数据写到sink channel中,这些数据可以同过source channel再读取出来。

管道的示意图

Pipe

向管道写入数据
//向管道写入数据需要访问他的sink channel
Pipe.SinkChannel sinkChannel = pipe.sink();

//接下来就是调用write()方法写入数据了:
String newData = "New String to write to file "

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    sinkChannel.write(buf);
}
从管道读取数据
//访问他的source channel
Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buf = ByteBuffer.allocate(48);

//调用read()方法读取数据
int bytesRead = inChannel.read(buf);

欢迎关注 小海博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值