澳门新葡萄京官网首页 6

Java I/O 操作及优化建议

Java I/O

I/O,即 Input/Output(输入/输出) 的简称。就 I/O 而言,概念上有 5
种模型:blocking I/O,nonblocking I/O,I/O multiplexing (select and
poll),signal driven I/O (SIGIO),asynchronous I/O (the POSIX
aio_functions)。不同的操作系统对上述模型支持不同,UNIX 支持 IO
多路复用。不同系统叫法不同,freebsd 里面叫 kqueue,Linux 叫 epoll。而
Windows2000 的时候就诞生了 IOCP 用以支持 asynchronous I/O。

Java 是一种跨平台语言,为了支持异步 I/O,诞生了 NIO,Java1.4 引入的
NIO1.0 是基于 I/O 复用的,它在各个平台上会选择不同的复用方式。Linux 用的
epoll,BSD 上用 kqueue,Windows 上是重叠 I/O。

Java I/O 的相关方法如下所述:

  • 同步并阻塞 (I/O
    方法):服务器实现模式为一个连接启动一个线程,每个线程亲自处理 I/O
    并且一直等待 I/O
    直到完成,即客户端有连接请求时服务器端就需要启动一个线程进行处理。但是如果这个连接不做任何事情就会造成不必要的线程开销,当然可以通过线程池机制改善这个缺点。I/O
    的局限是它是面向流的、阻塞式的、串行的一个过程。对每一个客户端的
    Socket 连接 I/O
    都需要一个线程来处理,而且在此期间,这个线程一直被占用,直到 Socket
    关闭。在这期间,TCP
    的连接、数据的读取、数据的返回都是被阻塞的。也就是说这期间大量浪费了
    CPU 的时间片和线程占用的内存资源。此外,每建立一个 Socket
    连接时,同时创建一个新线程对该 Socket 进行单独通信
    (采用阻塞的方式通信)。这种方式具有很快的响应速度,并且控制起来也很简单。在连接数较少的时候非常有效,但是如果对每一个连接都产生一个线程无疑是对系统资源的一种浪费,如果连接数较多将会出现资源不足的情况;
  • 同步非阻塞 (NIO
    方法):服务器实现模式为一个请求启动一个线程,每个线程亲自处理
    I/O,但是另外的线程轮询检查是否 I/O 准备完毕,不必等待 I/O
    完成,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有
    I/O 请求时才启动一个线程进行处理。NIO
    则是面向缓冲区,非阻塞式的,基于选择器的,用一个线程来轮询监控多个数据传输通道,哪个通道准备好了
    (即有一组可以处理的数据) 就处理哪个通道。服务器端保存一个 Socket
    连接列表,然后对这个列表进行轮询,如果发现某个 Socket
    端口上有数据可读时,则调用该 Socket 连接的相应读操作;如果发现某个
    Socket 端口上有数据可写时,则调用该 Socket
    连接的相应写操作;如果某个端口的 Socket
    连接已经中断,则调用相应的析构方法关闭该端口。这样能充分利用服务器资源,效率得到大幅度提高;
  • 异步非阻塞 (AIO 方法,JDK7
    发布):服务器实现模式为一个有效请求启动一个线程,客户端的 I/O
    请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,每个线程不必亲自处理
    I/O,而是委派操作系统来处理,并且也不需要等待 I/O
    完成,如果完成了操作系统会另行通知的。该模式采用了 Linux 的 epoll
    模型。

在连接数不多的情况下,传统 I/O
模式编写较为容易,使用上也较为简单。但是随着连接数的不断增多,传统 I/O
处理每个连接都需要消耗一个线程,而程序的效率,当线程数不多时是随着线程数的增加而增加,但是到一定的数量之后,是随着线程数的增加而减少的。所以传统阻塞式
I/O 的瓶颈在于不能处理过多的连接。非阻塞式 I/O
出现的目的就是为了解决这个瓶颈。非阻塞 IO
处理连接的线程数和连接数没有联系,例如系统处理 10000 个连接,非阻塞 I/O
不需要启动 10000 个线程,你可以用 1000 个,也可以用 2000
个线程来处理。因为非阻塞 IO
处理连接是异步的,当某个连接发送请求到服务器,服务器把这个连接请求当作一个请求“事件”,并把这个“事件”分配给相应的函数处理。我们可以把这个处理函数放到线程中去执行,执行完就把线程归还,这样一个线程就可以异步的处理多个事件。而阻塞式
I/O 的线程的大部分时间都被浪费在等待请求上了。

一、概述

  从JDK1.4开始,Java提供了一系列改进的输入/输出处理的新特性,被统称为NIO(即New
I/O)。新增了许多用于处理输入输出的类,这些类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写,新增了满足NIO的功能。NIO采用内存映射文件的方式来处理输入输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件了。

  澳门新葡萄京官网首页 1

  其中,Channel(通道)、Buffer(缓冲)和Selectors(选择器)是NIO中的两个核心对象。

  Channel是对传统I/O系统的模拟,在NIO中所有数据都需要通过Channel传输。它与传统的I/O最大的区别在于它提供了一个map方法,通过该方法可以直接将“一块数据”映射到内存中,如果说传统I/O是面向流的处理,那么NIO就是面向块的处理。

  Buffer可以理解为一个容器,本质是一个数组,发送到Channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先读到Buffer中。

  服务器端和客户端各自维护一个管理通道的对象,称之为Selector,该对象能检测一个或多个Channel上的事件。以服务器为例,如果服务器上的selector上注册了读事件,某时刻客户端给服务器端发送了一些数据,阻塞I/O这时会调用read()方法阻塞地读取数据,而NIO的服务端会在Selector中添加一个事件,服务器端的处理线程会轮询的访问Selector,如果访问selector时发现有感兴趣的事件到达,则处理这些事件,如果没有感兴趣的事件到达,则处理线程会一直阻塞直到感兴趣的事件到达为止。

缓冲区操作:
缓冲区,以及缓冲区如何工作,是所有I/O的基础。所谓“输入/输出”讲的无非就是把数据移出货移进缓冲区。
进程执行I/O操作,归纳起来也就是向操作系统发出请求,让它要么把缓冲区里的数据排干,要么用数据把缓冲区填满。进程使用这一机制处理所有数据进出操作。
Java.nio中的类被特意的设计为支持级联调用。
Java NIO:
Java NIO是一个可以替代标准Java IO API的IO API,Java
NIO提供了与标准IO不同的IO工作方式。
Java NIO:Channels and Buffers(通道和缓冲区)
标准的IO基于字节流和字符流进行操作的,儿NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Java NIO:Non-blocking IO(非阻塞IO)
Java
NIO可以让你非阻塞的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情。当数据被写入到缓冲区时,线程可以继续处理它。从缓冲区写入到通道也类似。
Java NIO:Selectors(选择器)
Java
NIO引入了选择器的概念,选择器用于箭筒多个通道的时间(比如:链接打开,数据到达)。因此,单个的线程可以坚挺多个数据通道。
一、 Java NIO概述
Java NIO由以下几个核心部分组成:
 Channels
 Buffers
 Selectors
虽然Java NIO中除此之外还有很多类和组件,但是我看来,Channel ,Buffer
,Selector构成了核心的API。其他组件。如Pipe和FileLock,只不过是与三个核心组件共同使用的工具类。因此,在概述中我将其中在这三个组件上。其他组件会在单独的章节中讲到。
Channels和Buffer
基本上所有的IO在NIO中都从一个Channel开始。Channel有点象流。数据可以从Channel读取到Buffer中,也可以从Buffer写到Channel中

Java NIO

Java.nio 包是 Java 在 1.4 版本之后新增加的包,专门用来提高 I/O
操作的效率。

表 1 所示是 I/O 与 NIO 之间的对比内容。

  IO VS NIO

  NIO可以让您只使用一个或几个单线程管理多个通道,但付出的代价是解析数据可能比从一个阻塞流中读取数据更为复杂。

  如果需要管理同时打开的成千上万个连接,这些连接每次只发送少量的数据,例如聊天服务器,实现NIO的服务器可能是一个优势。

  如果你有少量的连接使用非常高的带宽,一次发送大量数据,也许典型的I/O服务器实现可能更为契合。

 

表 1. I/O VS NIO
I/O NIO
面向流 面向缓冲
阻塞 IO 非阻塞 IO
选择器

NIO 是基于块 (Block) 的,它以块为基本单位处理数据。在 NIO
中,最为重要的两个组件是缓冲 Buffer 和通道
Channel。缓冲是一块连续的内存块,是 NIO
读写数据的中转地。通道标识缓冲数据的源头或者目的地,它用于向缓冲读取或者写入数据,是访问缓冲的接口。Channel
是一个双向通道,即可读,也可写。Stream 是单向的。应用程序不能直接对
Channel 进行读写操作,而必须通过 Buffer 来进行,即 Channel 是通过 Buffer
来读写数据的。

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

  1. 写入数据到 Buffer;
  2. 调用 flip() 方法;
  3. 从 Buffer 中读取数据;
  4. 调用 clear() 方法或者 compact() 方法。

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

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

Buffer 有多种类型,不同的 Buffer 提供不同的方式操作 Buffer 中的数据。

二、缓冲区

  Buffer是特定基本类型元素的线性有限序列。除内容外,Buffer的基本属性还包括容量、限制和位置:

  • 容量:是它所包含的元素的数量。缓冲区的容量不能为负并且不能更改。
  • 限制:是第一个不应该读取或写入的元素的索引。缓冲区的限制不能为负,并且不能大于其容量。
  • 位置:是下一个要读取或写入的元素的索引。缓冲区的位置不能为负,并且不能大于其限制。

澳门新葡萄京官网首页 2

  Buffer的主要作用就是装入数据,然后输出数据。开始时,Buffer的position为0,limit为capacity,程序调用put不断向Buffer中放入数据,每放入一些数据,position响应的向后移动。当Buffer装入数据结束后,调用filp方法,该方法将limit设置为position所在的位置,将position设为0,这样使得从Buffer中读数据时总是从0开始,读完刚刚装入的所有数据即结束。

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

  • 写入数据到Buffer
  • 调用flip()方法
  • 从Buffer中读取数据
  • 调用clear()方法或者compact()方法

eg:

public class TestMain
{
    public static void main(String[] args)
    {
        CharBuffer buff = CharBuffer.allocate(5);
        System.out.println("buff.capacity="+buff.capacity());
        System.out.println("buff.position="+buff.position());
        System.out.println("buff.limit="+buff.limit());
        buff.put('a');
        buff.put('b');
        System.out.println("向Buffer中添加元素后--------");
        System.out.println("buff.capacity="+buff.capacity());
        System.out.println("buff.position="+buff.position());
        System.out.println("buff.limit="+buff.limit());
        buff.flip();
        System.out.println("调用filp函数后------------");
        System.out.println("buff.capacity="+buff.capacity());
        System.out.println("buff.position="+buff.position());
        System.out.println("buff.limit="+buff.limit());
        System.out.println(buff.get(0));//使用绝对操作获取0位置上的元素,此时不改变position的值
    }
}

  运行结果:

澳门新葡萄京官网首页 3

  其中,filp()的源码如下图,表示缓冲填充数据结束,position回到起点,limit设为position,相当于把Buffer中没有数据的存储空间封印起来,避免读到null值。

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

  还有一点值得提出的是clear()方法,我们看它的源码是:

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

  很明显,这个方法并不会让buffer真正的清空,它只是让position回到起始位置,此时还是可以通过get方法获取指定位置元素,然后将limit置为capacity,仿佛是将buffer清空一样。这样做的目的是不必为了每次读写都创建新的缓冲区,那样做会降低性能。相反,要重用现在的缓冲区,在再次读取之前要清除缓冲区.

  对于每个非 boolean 基本类型,此类都有一个子类与之对应。

澳门新葡萄京官网首页 4

  每个子类都定义了两种获取和放置操作:

  相对:从Buffer的当前位置读取或写入数据,然后将位置的值按处理的个数增加。

  绝对:直接根据索引向Buffer中读取或写入数据,使用绝对方式访问Buffer时,不会影响position中的值。

 

图 1 Buffer 接口层次图

澳门新葡萄京官网首页 5

Buffer 写数据有两种情况:

  1. 从 Channel 写到 Buffer,如例子中 Channel 从文件中读取数据,写到
    Channel;
  2. 直接调用 put 方法,往里面写数据。

从 Buffer 中读取数据有两种方式:

  1. 从 Buffer 读取数据到 Channel;
  2. 使用 get() 方法从 Buffer 中读取数据。

Buffer 的 rewin 方法将 position 设回 0,所以你可以重读 Buffer
中的所有数据。limit 保持不变,仍然表示能从 Buffer
中读取多少个元素(byte、char 等)。

clear() 和 compact() 方法

一旦读完 Buffer 中的数据,需要让 Buffer 准备好再次被写入。可以通过
clear() 或 compact() 方法来完成。

如果调用的是 clear() 方法,position 将被设回 0,limit 被设置成 capacity
的值。换句话说,Buffer 被清空了。Buffer
中的数据并未清除,只是这些标记告诉我们可以从哪里开始往 Buffer 里写数据。

如果 Buffer 中有一些未读的数据,调用 clear()
方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。如果
Buffer
中仍有未读的数据,且后续还需要这些数据,但是此时想要先写些数据,那么使用
compact() 方法。compact() 方法将所有未读的数据拷贝到 Buffer
起始处。然后将 position 设到最后一个未读元素正后面。limit 属性依然像
clear() 方法一样,设置成 capacity。现在 Buffer
准备好写数据了,但是不会覆盖未读的数据。

Buffer 参数

Buffer 有 3 个重要的参数:位置 (position)、容量 (capacity) 和上限
(limit)。

capacity 是指 Buffer 的大小,在 Buffer 建立的时候已经确定。

limit 当 Buffer
处于写模式,指还可以写入多少数据;处于读模式,指还有多少数据可以读。

position 当 Buffer
处于写模式,指下一个写数据的位置;处于读模式,当前将要读取的数据的位置。每读写一个数据,position+1,也就是
limit 和 position 在 Buffer 的读/写时的含义不一样。当调用 Buffer 的 flip
方法,由写模式变为读模式时,limit(读)=position(写),position(读) =0。

散射&聚集

NIO 提供了处理结构化数据的方法,称之为散射 (Scattering) 和聚集
(Gathering)。散射是指将数据读入一组 Buffer
中,而不仅仅是一个。聚集与之相反,指将数据写入一组 Buffer
中。散射和聚集的基本使用方法和对单个 Buffer
操作时的使用方法相当类似。在散射读取中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个,在某种意义上,缓冲区数组就像一个大缓冲区。在已知文件具体结构的情况下,可以构造若干个符合文件结构的
Buffer,使得各个 Buffer
的大小恰好符合文件各段结构的大小。此时,通过散射读的方式可以一次将内容装配到各个对应的
Buffer
中,从而简化操作。如果需要创建指定格式的文件,只要先构造好大小合适的
Buffer 对象,使用聚集写的方式,便可以很快地创建出文件。清单 1 以
FileChannel 为例,展示如何使用散射和聚集读写结构化文件。

三、通道

  Channel类似于传统的流对象,但它们之间有两个主要区别:

  1. Channel可以直接将指定文件中的数据部分或全部直接映射成Buffer。
  2. 程序不能直接访问Channel中的数据,读写都不行,必须先用Buffer从Channel中取得数据,然后让程序从Buffer中取出这些数据。写数据同样要将数据先放入Buffer中,再将Buffer里的输入写入到Channel中。

  Channel(通道)表示到实体如硬件设备、文件、网络套接字或可以执行一个或多个不同I/O操作的程序组件的开放的连接。所有的Channel都不是通过构造器创建的,而是通过传统的节点InputStream、OutputStream的getChannel方法来返回响应的Channel。

  Channel中最常用的三个类方法就是map、read和write,其中map方法用于将Channel对应的部分或全部数据映射成ByteBuffer,而read或write方法有一系列的重载形式,这些方法用于从Buffer中读取数据或向Buffer中写入数据。eg:

public class TestMain
{
    public static void main(String[] args) throws UnknownHostException, IOException
    {
        FileChannel outChannel=null;
        File file=new File("E:\hello.txt");
        FileInputStream inputStream=new FileInputStream(file);
        FileChannel fileInChannel=inputStream.getChannel();
        //将FileInChannel中的全部数据映射成ByteBuffer
        MappedByteBuffer buffer=fileInChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
        Charset charset=Charset.forName("GBK");
        //以文件输出流形式创建FileBuffer,用以控制输出
        outChannel=new FileOutputStream("E://hello_Copy.txt").getChannel();
        outChannel.write(buffer);
        buffer.clear();
        CharsetDecoder decoder=charset.newDecoder();
        CharBuffer charBuffer=decoder.decode(buffer);
        System.out.println(charBuffer);
        inputStream.close();
        fileInChannel.close();
        outChannel.close();
    }
}

  运行结果:

  澳门新葡萄京官网首页 6

  上面的代码中,从FileInputStream获取的FileChannel只能读,而FileOutStream获取的FileChannel只能写。程序先将指定Channel中的全部数据映射成ByteBuffer,然后直接将整个ByteBuffer的全部数据写入一个输出FileChannel中,这样就完成了文件的复制。

  文件的复制还有一种更为简单的方式,就是直接将数据从一个channel传输到另外一个channel。eg:

public class TestMain
{
    public static void main(String[] args) throws UnknownHostException, IOException
    {
        RandomAccessFile fromFile = new RandomAccessFile("E://hello.txt", "rw");  
        FileChannel      fromChannel = fromFile.getChannel();  
        RandomAccessFile toFile = new RandomAccessFile("E://toFile.txt", "rw");  
        FileChannel      toChannel = toFile.getChannel();  
        long position = 0;  
        long count = fromChannel.size();  
        toChannel.transferFrom(fromChannel, position, count);  
        fromFile.close();
        toFile.close();
    }
}

 

清单 1. 使用散射和聚集读写结构化文件
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NIOScatteringandGathering {
 public void createFiles(String TPATH){
 try {
 ByteBuffer bookBuf = ByteBuffer.wrap("java 性能优化技巧".getBytes("utf-8"));
ByteBuffer autBuf = ByteBuffer.wrap("test".getBytes("utf-8"));
int booklen = bookBuf.limit();
int autlen = autBuf.limit();
ByteBuffer[] bufs = new ByteBuffer[]{bookBuf,autBuf};
File file = new File(TPATH);
if(!file.exists()){
try {
file.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
FileOutputStream fos = new FileOutputStream(file);
FileChannel fc = fos.getChannel();
fc.write(bufs);
fos.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

ByteBuffer b1 = ByteBuffer.allocate(booklen);
ByteBuffer b2 = ByteBuffer.allocate(autlen);
ByteBuffer[] bufs1 = new ByteBuffer[]{b1,b2};
File file1 = new File(TPATH);
try {
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
fc.read(bufs1);
String bookname = new String(bufs1[0].array(),"utf-8");
String autname = new String(bufs1[1].array(),"utf-8");
System.out.println(bookname+" "+autname);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

 }

 public static void main(String[] args){
 NIOScatteringandGathering nio = new NIOScatteringandGathering();
 nio.createFiles("C://1.TXT");
 }
}

输出如下清单 2 所示。

四、选择器

  Selector是Java
NIO中能够检测一到多个NIO通道,并能知晓通道是否为诸如读写事件做好准备的组件。

  可以通过调用此类的open()方法创建选择器,该方法将使用系统的默认选择器提供者创建新的选择器。通过选择器的close()方法关闭选择器之前,它一直保持打开状态。

  通过SelectionKey对象表示可选择通道到选择器的注册,选择器维护了三种选择键集:

  • 键集:包含的键表示当前通道到此选择器的注册,此集合由keys方法返回。
  • 已选择键集:在前一次选择操作期间,检测每个键的通道是否已经至少为该键的相关操作所标识的一个操作准备就绪。此集合由selectorKeys方法返回。
  • 已取消键集:是已被取消但其通道尚未注销的键的集合,不可直接访问此集合。

  通过某个通道的register方法注册该通道时,就向选择器的键集中添加了一个键,在选择操作期间从键集中移除已取消的键。

  与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。 
  register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件: 

  • Connect
  • Accept
  • Read
  • Write

  通道触发了一个事件意思是该事件已经就绪。所以,某个channel成功连接到另一个服务器称为“连接就绪”。一个server
socket
channel准备好接收新进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。 
  这四种事件用SelectionKey的四个常量来表示: 

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

  如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来。

Channel和Buffer有好几种类型。下面是JAVA
NIO中的一些主要Channel的实现,这些通道涵盖了UDP和RCP网络IO,以及文件IO:
 FileChannel
 DatagramChannel
 SockekChannel
 ServerSocketChannel
Java
NIO中关键的Buffer的实现,这些Buffer覆盖了你能通过IO发送的基本类型数据:byte,short,int,long,float,double和char:
 ByteBuffer
 CharBuffer
 DoubleBuffer
 FloatBuffer
 IntBuffer
 LongBuffer
 ShortBuffer
Java NIO 还有一个MappedByBuffer,用于表示内存映射文件,
Selector:
Selector允许单线程处理多个Channel,如果你的应用打开了多个连接(通道),但是每个连接的流量都很低,使用Selector就会很方便。
例如在聊天服务器中,以下是一个单线程中使用Selector处理3个Channel的图示:

清单 2. 运行结果
java 性能优化技巧 test

清单 3 所示代码对传统 I/O、基于 Byte 的 NIO、基于内存映射的 NIO
三种方式进行了性能上的对比,使用一个有 400
万数据的文件的读、写操作耗时作为评测依据。

要使用Selector,得向Selector注册Channel,然后调用他的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。
二、 Channel
 Java NIO的通道类似流,但又有些不同:
 既可以从同道中读取数据,又可以写数据到通道。但流的读写同城是单向的。
 通道可以异步地读写。
 通道中的数据总是要读到一个Buffer,或者总是从一个Buffer中写入。

清单 3. I/O 的三种方式对比试验
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class NIOComparator {
 public void IOMethod(String TPATH){
 long start = System.currentTimeMillis();
 try {
DataOutputStream dos = new DataOutputStream(
 new BufferedOutputStream(new FileOutputStream(new File(TPATH))));
for(int i=0;i<4000000;i++){
dos.writeInt(i);//写入 4000000 个整数
}
if(dos!=null){
dos.close();
}
 } catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
 } catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
 }
 long end = System.currentTimeMillis();
 System.out.println(end - start);
 start = System.currentTimeMillis();
 try {
DataInputStream dis = new DataInputStream(
 new BufferedInputStream(new FileInputStream(new File(TPATH))));
for(int i=0;i<4000000;i++){
dis.readInt();
}
if(dis!=null){
dis.close();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

 end = System.currentTimeMillis();
 System.out.println(end - start);
 }

 public void ByteMethod(String TPATH){
 long start = System.currentTimeMillis();
 try {
FileOutputStream fout = new FileOutputStream(new File(TPATH));
FileChannel fc = fout.getChannel();//得到文件通道
ByteBuffer byteBuffer = ByteBuffer.allocate(4000000*4);//分配 Buffer
for(int i=0;i<4000000;i++){
byteBuffer.put(int2byte(i));//将整数转为数组
}
byteBuffer.flip();//准备写
fc.write(byteBuffer);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
 long end = System.currentTimeMillis();
 System.out.println(end - start);

 start = System.currentTimeMillis();
 FileInputStream fin;
try {
fin = new FileInputStream(new File(TPATH));
FileChannel fc = fin.getChannel();//取得文件通道
ByteBuffer byteBuffer = ByteBuffer.allocate(4000000*4);//分配 Buffer
fc.read(byteBuffer);//读取文件数据
fc.close();
byteBuffer.flip();//准备读取数据
while(byteBuffer.hasRemaining()){
byte2int(byteBuffer.get(),byteBuffer.get(),byteBuffer.get(),byteBuffer.get());//将 byte 转为整数
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
 end = System.currentTimeMillis();
 System.out.println(end - start);
 }

 public void mapMethod(String TPATH){
 long start = System.currentTimeMillis();
 //将文件直接映射到内存的方法
 try {
FileChannel fc = new RandomAccessFile(TPATH,"rw").getChannel();
IntBuffer ib = fc.map(FileChannel.MapMode.READ_WRITE, 0, 4000000*4).asIntBuffer();
for(int i=0;i<4000000;i++){
ib.put(i);
}
if(fc!=null){
fc.close();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
 long end = System.currentTimeMillis();
 System.out.println(end - start);

 start = System.currentTimeMillis();
 try {
FileChannel fc = new FileInputStream(TPATH).getChannel();
MappedByteBuffer lib = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
lib.asIntBuffer();
while(lib.hasRemaining()){
lib.get();
}
if(fc!=null){
fc.close();
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
 end = System.currentTimeMillis();
 System.out.println(end - start);

 }

 public static byte[] int2byte(int res){
 byte[] targets = new byte[4];
 targets[3] = (byte)(res & 0xff);//最低位
 targets[2] = (byte)((res>>8)&0xff);//次低位
 targets[1] = (byte)((res>>16)&0xff);//次高位
 targets[0] = (byte)((res>>>24));//最高位,无符号右移
 return targets;
 }

 public static int byte2int(byte b1,byte b2,byte b3,byte b4){
 return ((b1 & 0xff)<<24)|((b2 & 0xff)<<16)|((b3 & 0xff)<<8)|(b4 & 0xff);
 }

 public static void main(String[] args){
 NIOComparator nio = new NIOComparator();
 nio.IOMethod("c://1.txt");
 nio.ByteMethod("c://2.txt");
 nio.ByteMethod("c://3.txt");
 }
}

清单 3 运行输出如清单 4 所示。

Channel的实现:
 FileChannel:从文件中读写数据
 DatagramChannel:能通过UDP读写网络中的数据
 SocketChannel:能通过TCP读写网络中的数据

ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel.
基本Channel使用的示例:
package com.slp.nio;

清单 4. 运行输出
1139
906
296
157
234
125

除上述描述及清单 3 所示代码以外,NIO 的 Buffer
还提供了一个可以直接访问系统物理内存的类 DirectBuffer。DirectBuffer
继承自 ByteBuffer,但和普通的 ByteBuffer 不同。普通的 ByteBuffer 仍然在
JVM 堆上分配空间,其最大内存受到最大堆的限制,而 DirectBuffer
直接分配在物理内存上,并不占用堆空间。在对普通的 ByteBuffer
访问时,系统总是会使用一个“内核缓冲区”进行间接的操作。而 DirectrBuffer
所处的位置,相当于这个“内核缓冲区”。因此,使用 DirectBuffer
是一种更加接近系统底层的方法,所以,它的速度比普通的 ByteBuffer
更快。DirectBuffer 相对于 ByteBuffer
而言,读写访问速度快很多,但是创建和销毁 DirectrBuffer 的花费却比
ByteBuffer 高。DirectBuffer 与 ByteBuffer 相比较的代码如清单 5 所示。

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

清单 5. DirectBuffer VS ByteBuffer
import java.nio.ByteBuffer;

public class DirectBuffervsByteBuffer {
 public void DirectBufferPerform(){
 long start = System.currentTimeMillis();
 ByteBuffer bb = ByteBuffer.allocateDirect(500);//分配 DirectBuffer
 for(int i=0;i<100000;i++){
 for(int j=0;j<99;j++){
 bb.putInt(j);
 }
 bb.flip();
 for(int j=0;j<99;j++){
 bb.getInt(j);
 }
 }
 bb.clear();
 long end = System.currentTimeMillis();
 System.out.println(end-start);
 start = System.currentTimeMillis();
 for(int i=0;i<20000;i++){
 ByteBuffer b = ByteBuffer.allocateDirect(10000);//创建 DirectBuffer
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);
 }

 public void ByteBufferPerform(){
 long start = System.currentTimeMillis();
 ByteBuffer bb = ByteBuffer.allocate(500);//分配 DirectBuffer
 for(int i=0;i<100000;i++){
 for(int j=0;j<99;j++){
 bb.putInt(j);
 }
 bb.flip();
 for(int j=0;j<99;j++){
 bb.getInt(j);
 }
 }
 bb.clear();
 long end = System.currentTimeMillis();
 System.out.println(end-start);
 start = System.currentTimeMillis();
 for(int i=0;i<20000;i++){
 ByteBuffer b = ByteBuffer.allocate(10000);//创建 ByteBuffer
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);
 }

 public static void main(String[] args){
 DirectBuffervsByteBuffer db = new DirectBuffervsByteBuffer();
 db.ByteBufferPerform();
 db.DirectBufferPerform();
 }
}

运行输出如清单 6 所示。

/**
* Created by sanglp on 2017/2/28.
* Read48
* package com.slp;

清单 6. 运行输出
920
110
531
390

由清单 6 可知,频繁创建和销毁 DirectBuffer
的代价远远大于在堆上分配内存空间。使用参数-XX:MaxDirectMemorySize=200M
–Xmx200M 在 VM Arguments 里面配置最大 DirectBuffer
和最大堆空间,代码中分别请求了 200M
的空间,如果设置的堆空间过小,例如设置 1M,会抛出错误如清单 7 所示。

* import java.util.concurrent.Read48
* locks.Lock;

清单 7. 运行错误
Error occurred during initialization of VM
Too small initial heap for new size specified

DirectBuffer 的信息不会打印在 GC 里面,因为 GC
只记录了堆空间的内存回收。可以看到,由于 ByteBuffer
在堆上分配空间,因此其 GC 数组相对非常频繁,在需要频繁创建 Buffer
的场合,由于创建和销毁 DirectBuffer 的代码比较高昂,不宜使用
DirectBuffer。但是如果能将 DirectBuffer
进行复用,可以大幅改善系统性能。清单 8 是一段对 DirectBuffer
进行监控代码。

* public class Main {

清单 8. 对 DirectBuffer 监控代码
import java.lang.reflect.Field;

public class monDirectBuffer {

public static void main(String[] args){
try {
Class c = Class.forName("java.nio.Bits");//通过反射取得私有数据
Field maxMemory = c.getDeclaredField("maxMemory");
maxMemory.setAccessible(true);
Field reservedMemory = c.getDeclaredField("reservedMemory");
reservedMemory.setAccessible(true);
synchronized(c){
Long maxMemoryValue = (Long)maxMemory.get(null);
Long reservedMemoryValue = (Long)reservedMemory.get(null);
System.out.println("maxMemoryValue="+maxMemoryValue);
System.out.println("reservedMemoryValue="+reservedMemoryValue);
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}
}

运行输出如清单 9 所示。

* publicRead48
* static void main(String[] args) {
// write yoRead26
* ur code here

清单 9. 运行输出
maxMemoryValue=67108864
reservedMemoryValue=0

由于 NIO 使用起来较为困难,所以许多公司推出了自己封装 JDK NIO
的框架,例如 Apache 的 Mina,JBoss 的
Netty,Sun 的 Grizzly
等等,这些框架都直接封装了传输层的 TCP 或 UDP 协议,其中 Netty 只是一个
NIO 框架,它不需要 Web 容器的额外支持,也就是说不限定 Web 容器。

}
}
*/
public class FileChannelUse {
public static void main(String []args){
try {
//为了以可读可写的方式打开文件使用RandomAccessFile来创建文件
以下两个相当于:FileChannel fc = new
RandomAccessFile(“D:\Project\InterviewPlan\src\com\slp\Main.java”,”rw”).getChannel();
RandomAccessFile accessFile = new
RandomAccessFile(“D:\Project\InterviewPlan\src\com\slp\Main.java”,”rw”);
FileChannel fileChannel = accessFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = fileChannel.read(buffer);
while (bytesRead!=-1){
System.out.println(“Read”+bytesRead);
buffer.flip();//使用buffer.flip()首先读取数据到Buffer,然后又反转Buffer,接着从Buffer中读取数据
while (buffer.hasRemaining()){
System.out.print((char)buffer.get());
}
buffer.clear();
bytesRead = fileChannel.read(buffer);
}
fileChannel.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

Java AIO

AIO 相关的类和接口:

  • java.nio.channels.AsynchronousChannel:标记一个 Channel 支持异步 IO
    操作;
  • java.nio.channels.AsynchronousServerSocketChannel:ServerSocket 的
    AIO 版本,创建 TCP 服务端,绑定地址,监听端口等;
  • java.nio.channels.AsynchronousSocketChannel:面向流的异步 Socket
    Channel,表示一个连接;
  • java.nio.channels.AsynchronousChannelGroup:异步 Channel
    的分组管理,目的是为了资源共享。一个 AsynchronousChannelGroup
    绑定一个线程池,这个线程池执行两个任务:处理 IO 事件和派发
    CompletionHandler。AsynchronousServerSocketChannel
    创建的时候可以传入一个 AsynchronousChannelGroup,那么通过
    AsynchronousServerSocketChannel 创建的 AsynchronousSocketChannel
    将同属于一个组,共享资源;
  • java.nio.channels.CompletionHandler:异步 IO
    操作结果的回调接口,用于定义在 IO 操作完成后所作的回调工作。AIO 的
    API 允许两种方式来处理异步操作的结果:返回的 Future 模式或者注册
    CompletionHandler,推荐用 CompletionHandler 的方式,这些 handler
    的调用是由 AsynchronousChannelGroup
    的线程池派发的。这里线程池的大小是性能的关键因素。

这里举一个程序范例,简单介绍一下 AIO 如何运作。

三、 Buffer
Java
NIO中的Buffer用于和nio通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的额内存,这块内存被包装成NIO
Buffer对象,并提供了一组方法,用来方便的访问该块内存。
1、 Buffer的基本用法
使用Buffer读写数据一般遵循以下4个步骤:
 写入数据到Buffer
 调用flip()方法
 从Buffer中读取数据
 调用clear()方法或者compact()方法
当向buffer写入数据时,buffer会记录下写了多少数据。一旦要读取数据,需要通过flip()方法将Buffer从写模式切入到度模式。在读模式下,可以读取之前写入到buffer的所有数据。
一旦读完了所有的数据,需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或者compact()方法,clear()会清空整个缓冲区,compact()方法只会清楚已经度过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读取数据的后面。
public class FileChannelUse {
public static void main(String []args){
try {
//为了以可读可写的方式打开文件使用RandomAccessFile来创建文件
以下两个相当于:FileChannel fc = new
RandomAccessFile(“D:\Project\InterviewPlan\src\com\slp\Main.java”,”rw”).getChannel();
RandomAccessFile accessFile = new
RandomAccessFile(“D:\Project\InterviewPlan\src\com\slp\Main.java”,”rw”);
FileChannel fileChannel = accessFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(48);//create buffer with
capacity of 48 bytes
int bytesRead = fileChannel.read(buffer);
//read into buffer
while (bytesRead!=-1){
System.out.println(“Read”+bytesRead);
// make buffer ready for read
buffer.flip();//使用buffer.flip()首先读取数据到Buffer,然后又反转Buffer,接着从Buffer中读取数据
while (buffer.hasRemaining()){
System.out.print((char)buffer.get());//read 1 byte at a time
}
buffer.clear();//make buffer ready for writing
bytesRead = fileChannel.read(buffer);
}
fileChannel.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

清单 10. 服务端程序
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;

public class SimpleServer {
public SimpleServer(int port) throws IOException { 
final AsynchronousServerSocketChannel listener = 
 AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(port));
//监听消息,收到后启动 Handle 处理模块
listener.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
public void completed(AsynchronousSocketChannel ch, Void att) { 
listener.accept(null, this);// 接受下一个连接 
handle(ch);// 处理当前连接 
}

@Override
public void failed(Throwable exc, Void attachment) {
// TODO Auto-generated method stub

} 

});
}

public void handle(AsynchronousSocketChannel ch) { 
ByteBuffer byteBuffer = ByteBuffer.allocate(32);//开一个 Buffer 
try { 
 ch.read(byteBuffer).get();//读取输入 
} catch (InterruptedException e) { 
 // TODO Auto-generated catch block 
 e.printStackTrace(); 
} catch (ExecutionException e) { 
 // TODO Auto-generated catch block 
 e.printStackTrace(); 
} 
byteBuffer.flip(); 
System.out.println(byteBuffer.get()); 
// Do something 
} 

}

2、 Buffer的capacity,position和limit
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存,这块内存被包装成NIO
Buffer对象,并提供了一组方法,用来方便的访问该块内存。
Buffer的单个属性:capacity position limit。
Position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。

清单 11. 客户端程序
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class SimpleClientClass {
private AsynchronousSocketChannel client; 
public SimpleClientClass(String host, int port) throws IOException, 
                                    InterruptedException, ExecutionException { 
 this.client = AsynchronousSocketChannel.open(); 
 Future<?> future = client.connect(new InetSocketAddress(host, port)); 
 future.get(); 
} 

public void write(byte b) { 
 ByteBuffer byteBuffer = ByteBuffer.allocate(32);
 System.out.println("byteBuffer="+byteBuffer);
 byteBuffer.put(b);//向 buffer 写入读取到的字符 
 byteBuffer.flip();
 System.out.println("byteBuffer="+byteBuffer);
 client.write(byteBuffer); 
} 

}

Capacity:作为一个内存块,Buffer有一个固定的大小值,也叫capacity你只能往里写个byte
long char
等类型,一旦Buffer满了,需要将其清空才能继续写数据往里写数据。
Position:当写数据到Buffer中时,position表示当前的位置,初始的position值为0,当一个byte
long等数据写到Buffer后,position会向钱移动到下一个科插入数据的Buffer单元。Position最大可为capacity-1
当读取数据时,也是从某个特定位置读,当将Buffer从写模式切换到读模式,position会被重置为0,当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
Limit:在写模式下,Buffer的limit表示你最多能往buffer里写多少数据,写模式下,limit等于Buffer的capacity.
当切换Buffer到读模式时,limit表示最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据。

清单 12.Main 函数
import java.io.IOException;
import java.util.concurrent.ExecutionException;

import org.junit.Test;

public class AIODemoTest {

@Test
public void testServer() throws IOException, InterruptedException { 
 SimpleServer server = new SimpleServer(9021); 
 Thread.sleep(10000);//由于是异步操作,所以睡眠一定时间,以免程序很快结束
} 

@Test 
public void testClient() throws IOException, InterruptedException, ExecutionException { 
SimpleClientClass client = new SimpleClientClass("localhost", 9021); 
 client.write((byte) 11); 
}

public static void main(String[] args){
AIODemoTest demoTest = new AIODemoTest();
try {
demoTest.testServer();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
demoTest.testClient();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

后续会专门出文章具体深入介绍 AIO 的源代码、设计理念、设计模式等等。

3、 Buffer的类型
 ByteBuffer
 MappedByteBuffer
 CharBuffer
 DoubleBuffer
 FloatBuffer
 IntBuffer
 LongBuffer
 ShortBuffer
4、 Buffer的分配
要想获得一个Buffer对象首先要进行分配,每一个Buffer都有一个allocate方法。
ByteBuffer buf = ByteBuffer.allocate(48);//ByteBuffer的例子
CharBuffer buf = CharBuffer.allocate(1024);//CharBuffer的例子

结束语

I/O 与 NIO 一个比较重要的区别是我们使用 I/O
的时候往往会引入多线程,每个连接使用一个单独的线程,而 NIO
则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。而由于 NIO
的非阻塞需要一直轮询,比较消耗系统资源,所以异步非阻塞模式 AIO
就诞生了。本文对 I/O、NIO、AIO
等三种输入输出操作方式进行一一介绍,力求通过简单的描述和实例让读者能够掌握基本的操作、优化方法。

5、 向Buffer中写数据
写数据到Buffer有两种方式:
 从Channel写到Buffer
int bytesRead = inChannel.read(buf);

 通过Buffer的put()方法写到Buffer里
buf.put(127);

6、 Flip方法
Flip方法将Buffer从写模式切换到读模式,调用flip()方法会将position设回0,并将limit设置成之前的position的值。
换句话说,position现在用于标记读的位置,limit表示之前写进了多少个byte
char
7、 向Buffer中读取数据
 从Buffer读取数据到Channel
int bytesWritten = inChannel.write(buf);

 使用get()方法从Buffer中读取数据
byte abyte = buf.get();

Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。Limit保持不变,仍然表示能从Buffer中读取多少个元素。
8、 Clear()和compact()方法
一旦Buffer中的数据读完,需要让Buffer准备好再次被写入。可以通过clear()或compact()方法来完成。Clear()方法position将被设置为0,limit被设置成capacity的值。Compact()方法将所有未读的数据拷贝到Buffer的起始处。
9、 Mark()和reset()方法
通过调用Buffer.mark()方法可以标记Buffer中的一个特定position,之后可以通过调用Buffer.reset()方法恢复到这个position
10、 equals()和compareTo()方法
满足如下条件时,说明两个Buffer相等:
 有相同的类型
 Buffer中剩余的byte char等的个数相等
 Buffer中所有剩余的byte char等都相同
CompareTo()方法比较两个Buffer的剩余元素如果满足如下则认为一个Buffer小于另一个Buffer:
 第一个不相等的元素小于另一个Buffer中对应的元素
 所有元素都相等,但是一个Buffer比另一个先耗尽
四、 Scatter/Gather
Java
NIO开始支持scatter/gather,scatter/gather用于描述从Channel中读取或者写入到Channel的操作。
分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此Channel将从channel中读取到的数据分散到多个Buffer
中。
聚集(gather)写入Channel是指在写操作时将多个Buffer的数据写入同一个Channel中因此,Channel将多个Buffer中的数据聚集后发送到Channel。
Scatter/gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息透和消息体组成的消息,你可能会将消息体和消息透分散到不同的buffer中,这样你可以方便的的处理消息头和消息体。
Scattering Reads :是指数据从一个channel读取到多个buffer中
ByteBuffer header= ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = {header,body};
Channel.read(bufferArray)
注意buffer首先被插入到数组,然后再将数组作为channel.read()的输入参数。Read()方法按照buffer在数组中的顺序将从channel中读取到的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer写。
Scattering
Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着他不适合用于动态消息。换句话说,如果存在消息头和消息体,消息头必须完成填充,Scattering
Reads才能正常工作。
Gathering Writes:是指数据从多个Buffer写入到同一个channel
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray ={header,body};
Channel.write(bufferArray);
Buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58bytes的数据,那么这58byte的数据将被写入到channel中,因此与Scattering
Reads相反,Gathering Writes能较好的处理动态消息。
五、 通道之间的数据传输
在Java
NIO中,如果两个通道中有一个是FileChannel,那你可以直接将数据从一个channel传输到另一个channel
TransferFrom():FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中。
RandomAccessFile fromFile = new RandomAccessFie(“fromFile.txt”,”rw”);
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile(“toFile.txt”,”rw”);
FileChannel toChannel = toFile.getChannel();
long position =0;
long count = fromChannel.size();
toChannel.transferFrom(position,count,fromChannel)
transferTo():将数据从FileChannel传输到其他的channel中
RandomAccessFile fromFile = new RandomAccessFile(“fromFile.txt”,”rw”);
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile= new RandomAccessFile(“toFile.txt”,”rw”);
FileChannel toChannel = toFile.getChannel();
long position =0;
long count = fromChannel.size();
fromChannel.transferTo(position,count,toChannel);
六、 Selector
Selector(选择器)是Java
nio中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个chnnel,从而管理多个网络连接。
1、 为什么使用selector
仅用单个线程来处理多个Channels的好处是,值需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源。因此,使用的线程越少越好。
2、 Selector的创建
Selector selector = Selector.open();

3、 向selector注册通道
Channel.configureBlocking(false);
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
与selector一起使用时channel必须处于非阻塞模式。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。

4、 SelectionKey
当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了
 Interest集合:
Interest集合是你所选择的感兴趣的事件集合
Int interestSet = selectionKey.interestOps();
Boolean isInterestedInAccept = (interestSet &
SelectionKey.OP_ACCEPT)==SelectionKey.OP_ACCEPT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInConnect = interestSet &
SelectionKey.OP_CONNECT;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

 Ready集合
通道已经准备就绪的操作的集合。在一次选择之后,你会首先访问这个ready
set
Int readySet = selectionKey.readyOps();
可以像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪
SelectionKey.isAcceptable()
SelectionKey.isConnectable();
SelectionKey.isReadable();
SelectionKey.isWritable();

 Channel
 Selector
从selectionkey访问Channel和Selector的方法
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();

5、 通过Selector选择通道
一旦向Selector注册了一个或多个通道,就可以调用接重载的select()方法,这些方法返回你所感兴趣的事件(连接
接受
读或写)已经准备就绪的那些通道,换句话说,如果你对读继续的通道感兴趣,select()方法会返回读事件已经就绪的那些通道
 Int select():阻塞到至少有一个通道在你注册的事件上就绪了
 Int select(long timeout):除了最长会阻塞timeout毫秒
 Int selectNow():不会阻塞,不管什么通道阻塞都立刻返回
Select()方法返回的int值表示有多少通道已经就绪,也就是自上次调用select()方法后有多少通道变成就绪状态,如果调用select()方法,因为有一个通道变成就绪状态返回了1,若再次调用select()方法,如果返回另一个通道就绪了,他会再次返回1如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但咋每次select()方法调用之间,只有一个通道就够了。
6、
Wakeup():某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。只要让其他线程在第一个线程调用select()方法的那个对象上调用wakeup()方法就可以
7、
Close();用完selector后调用close()方法会关闭该Selector,且使注册到该selector上的所有selectionKey实例无效。通道本身并不会关闭。
8、 完整的示例
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,SelectionKey.OP_READ);
While(true){
Int readyChannels = selector.select();
If(readyChannels==0) continue;
Set selectedKeys = selector.selectdKeys();
Iterator keyIterator =selectedKeys.iterator();
While(keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
If(key.isAcceptable()){

}else if(key.isConnectable()){

}else if(key.isReadable()){
}else if(key.isWritable()){
}
KeyIterator.remove();
}}
七、 FileChannel
Java
nio的FileChannel是一个连接到文件的通道。可以通过文件通道的读写操作。
FileChannel无法设置为非阻塞模式他总是允许在阻塞模式下。
打开FileChannel:在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个InputStream
OutputStream RandomAccessFile来获取一个FileChannel实例。
RandomAccessFile afile = new RandomAccessFile(“”,”rw”);
FileChannel inchannel = afile.getChannel();

从FileChannel读取数据:
调用多个read()方法之一从FileChannel读取数据:
ByteBuffer buf = ByteBuffer.allocate(48);
Int byteRead = inChannel.read(buf);
首先,分配一个Buffer,从FileChannel中读取的数据将被读到Buffer中。
然后,调用FileChannel.read()方法,该方法将数据从FileChannel读取到Buffer中,read()方法返回的int值表示有多少个字节被读取到了buffer中。如果是-1表示到了文件末尾。
向FileChannel写数据:
使用FileChannel.write()方法向FileChannel写数据,该方法的参数是一个Buffer
String newData = “ACCCCCCCC”;
System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
Buf.clear();
Buf.put(newData.getbytes());
Buf.flip();
While(buf.hasRemaining()){
Channel.write(buf);
}
关闭FileChannel:channel.close();
FileChannel的position方法
有时可能需要在FileChannel的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取FileChannel的当前位置。
也可以通过调用position(long pos)方法设置FileChannel的当前位置。
这里有两个例子:
1 long pos = channel.position();
2 channel.position(pos +123);
如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1
—— 文件结束标志。
如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。
FileChannel的size方法
FileChannel实例的size()方法将返回该实例所关联文件的大小。如:
1 long fileSize = channel.size();
FileChannel的truncate方法
可以使用FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。如:
1 channel.truncate(1024);
这个例子截取文件的前1024个字节。
FileChannel的force方法
FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法。
force()方法有一个boolean类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。
八、 SocketChannel
Java
NIO中的SocketChannel是一个连接到TCP网络套接字的通道。可以通过如下两个方式进行创建:
 打开一个SocketChannel并连接到互联网上的某台服务器
 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel
打开SocketChannel:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(“));
关闭SocketChannel:
socketChannel.close();
从SocketChannel读取数据:
//从SocketChannel读取数据
ByteBuffer buffer =
ByteBuffer.allocate(48);//首先分配一个Buffer,从SocketChannel读取到的数据将会放到Buffer中
int bytesRead
=socketChannel.read(buffer);//调用read()方法,该方法将数据从SocketChannel读到Buffer中。返回的int表示读取了多少字节到Buffer里
写入SocketChannel:
//写入到SocketChannel
String newData = “ASDFFF”;
System.currentTimeMillis();
ByteBuffer buffer1 = ByteBuffer.allocate(48);
buffer1.clear();
buffer1.put(newData.getBytes());
buffer1.flip();
while (buffer1.hasRemaining()){
socketChannel.write(buffer1);
}
非阻塞模式:
可以设置SocketChannel为非阻塞模式,设置之后,就可以异步模式下调用connect(),read()
write()了。
 connect():
如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定是否连接,可以调用finishConnect()的方法。
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(“));
while (!socketChannel.finishConnect()){
}
 write()
非阻塞模式下,write()方法尚未写出任何内容时可能就返回了,所以需要重复调用。
 read()
非阻塞模式下,read()方法尚未读取到任何数据时可能就返回了,所以需要关注他的返回int的值。
非阻塞模式与选择器:
非阻塞模式与选择器搭配会工作的更好,通过将一或多个SocketChannel注册到Selector,可以询问选择器哪个通道已经准备好了读取。写入等。
九、 ServerSocketChannel
Java
NIO中的ServerSocketChannel是一个可以坚挺新进来的TCP连接的通道,就像标准IO中的ServerSocket一样。ServerSocketChannel类在java.nio.chennels包中
//打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while (true){
//监听新进来的连接,当accept()返回的时候他返回一个包含新进来的连接的SocketChannel.所以accept()会一直阻塞到有新连接到达。
SocketChannel socketChannel = serverSocketChannel.accept();

}

非阻塞模式:
ServerSocketChannel可以设置成非阻塞模式。在非阻塞模式下,accept()方法会立刻返回,如果还没有新进来的连接,返回的将是null,因此需要检查返回的SocketChannel是否为null
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while (true){
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel!=null){

}
}
十、 DatagramChannel
DatagramChannel十一个接收UDP包的通道,因为UDP是无连接的网络协议,所以不能像其他通道那样读取和写入,他发送和接收的都是数据包。
//打开DatagramChannel, 可以在UDP端口9999上接收数据包
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.socket().bind(new InetSocketAddress(9999));
//接收数据
receive()方法将会接收到的数据包内容复制到指定的Buffer,如果Buffer容不下收到的数据,多出的数据将被丢弃。
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.clear();
datagramChannel.receive(buffer);
//发送数据
String newData = “ASDFGGG”;
System.currentTimeMillis();
ByteBuffer buffer1 = ByteBuffer.allocate(48);
buffer1.clear();
buffer1.put(newData.getBytes());
buffer1.flip();
int bytesSent = datagramChannel.send(buffer1,new
InetSocketAddress(“baidu.com”,80));
十一、Pipe
Java
NIO管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道,数据会被写到sink通道,从source通道读取。

//打开通道
Pipe pipe = Pipe.open();
//向管道写数据
Pipe.SinkChannel sinkChannel = pipe.sink();
String newData = “New String to write to file…” +
System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
sinkChannel.write(buf);
}
//读取数据
Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead =
sourceChannel.read(buffer);//read()方法返回的int告诉我们多少字节读进了缓冲区

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注