Java NIO系列教程

1 篇文章 0 订阅

Java NIO基础教程

一、Java NIO 概述

Java NIO 由以下几个核心部分组成:

l Channels

l Buffers

l Selectors

虽然Java NIO 中除此之外还有很多类和组件,但在我看来,Channel,Buffer 和 Selector 构成了核心的API。其它组件,如Pipe和FileLock,只不过是与三个核心组件共同使用的工具类。

Channel 和 Buffer

基本上,所有的 IO 在NIO 中都从一个Channel 开始。Channel 有点象流。 数据可以从Channel读到Buffer中,也可以从Buffer 写到Channel中。

 Channel和Buffer有好几种类型。下面是JAVA NIO中的一些主要Channel的实现:

FileChannel

DatagramChannel

SocketChannel

ServerSocketChannel

这些通道涵盖了UDP 和 TCP 网络IO,以及文件IO。

以下是Java NIO里关键的Buffer实现:

ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer

这些Buffer覆盖了你能通过IO发送的基本数据类型:byte, short, int, long, float, double 和 char。

Java NIO 还有个 MappedByteBuffer,用于表示内存映射文件。

Selector

Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中。

要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。

二、Java NIO Channel

Java NIO的通道类似流,但又有些不同:

既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。

通道可以异步地读写。

通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。

Channel的实现

这些是Java NIO中最重要的通道的实现:

FileChannel

DatagramChannel

SocketChannel

ServerSocketChannel

FileChannel 从文件中读写数据。

DatagramChannel 能通过UDP读写网络中的数据。

SocketChannel 能通过TCP读写网络中的数据。

ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

基本的 Channel 示例

下面是一个使用FileChannel读取数据到Buffer中的示例:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt""rw");

FileChannel inChannel = aFile.getChannel();

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf);

while (bytesRead != -1) {

System.out.println("Read " + bytesRead);

buf.flip();

while(buf.hasRemaining()){

System.out.print((char) buf.get());

}

buf.clear();

bytesRead = inChannel.read(buf);

}

aFile.close();

注意 buf.flip() 的调用,首先读取数据到Buffer,然后反转Buffer,接着再从Buffer中读取数据。

三、Java NIO Buffer

Buffer的基本用法

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

l 写入数据到Buffer

l 调用flip()方法

l 从Buffer中读取数据

l 调用clear()方法或者compact()方法

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

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

Buffer的capacity,position和limit

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

  一、缓冲区(Buffer):在 Java NIO 中负责数据的存取。缓冲区就是数组。用于存储不同数据类型的数据

  根据数据类型不同(boolean 除外),提供了相应类型的缓冲区:

  ByteBuffer

  CharBuffer

  ShortBuffer

  IntBuffer

  LongBuffer

  FloatBuffer

  DoubleBuffer

  上述缓冲区的管理方式几乎一致,通过 allocate() 获取缓冲区

  二、缓冲区存取数据的两个核心方法:

  put() : 存入数据到缓冲区中

  get() : 获取缓冲区中的数据 

  三、缓冲区中的四个核心属性:

  capacity : 容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变。

  limit : 界限,表示缓冲区中可以操作数据的大小。(limit 后数据不能进行读写)

  position : 位置,表示缓冲区中正在操作数据的位置。

  mark : 标记,表示记录当前 position 的位置。可以通过 reset() 恢复到 mark 的位置

  0 <= mark <= position <= limit <= capacity

  四、直接缓冲区与非直接缓冲区:

  非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中

  直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高效率

//通道之间的数据传输(直接缓冲区)

public void test3() throws IOException{

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

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

 

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

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

 

inChannel.close();

outChannel.close();

}

//利用通道完成文件的复制(非直接缓冲区)

//①获取通道

FileInputStream fis = new FileInputStream("d:/1.mkv");

FileOutputStream fos = = new FileOutputStream("d:/2.mkv");

 

FileChannel inChannel= fis.getChannel();

FileChannel outChannel = fos.getChannel();

//②分配指定大小的缓冲区

ByteBuffer buf = ByteBuffer.allocate(1024);

//③将通道中的数据存入缓冲区中

while(inChannel.read(buf) != -1){

buf.flip(); //切换读取数据的模式

//④将缓冲区中的数据写入通道中

outChannel.write(buf);

buf.clear(); //清空缓冲区

}

//需要关闭流和channel

四、Java NIO Scatter / Gather

Java NIO开始支持分散/聚集,分散/聚集用于描述从通道中读取或者写入到通道的操作。
分散(分散)从通道中读取指在读操作时将读取的数据写入多个缓冲区中。因此,通道将从通道中读取的数据“分散(散)”到多个缓冲区中。
聚集(聚集)写入通道是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel将多个Buffer中的数据“聚集(gather)”后发送到Channel。

分散/聚集经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的缓冲区中,这样你可以方便的处理消息头和消息体。

Scattering Reads
Scattering Reads是指数据从一个通道读取到多个缓冲区中。

Java NIO:Scattering Read

代码示例如下:

ByteBuffer header = ByteBuffer.allocate(128);

ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);

注意缓冲区首先被插入到数组,然后再将数组作为channel.read()的输入参数.read()方法按照缓冲区在数组中的顺序将从通道中读取的数据写入缓冲区,当一个缓冲区写满后,信道紧接着向另一个缓冲区中写。

Scattering Reads在移动下一个缓冲区前,必须填满当前的缓冲区,这也意味着它不适用于动态消息(译者注:消息大小不固定)。句句说,如果存在消息头和消息体,消息头必须完成填充(例如128byte),Scattering Reads才能正常工作。

收集文字

Gathering Writes是指数据从多个缓冲区写入到同一个通道。

Java NIO:收集写入

代码示例如下:

ByteBuffer header = ByteBuffer.allocate(128);

ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

缓冲器数组是写()方法的入参,写()方法会按照缓冲在数组中的顺序,将数据写入到信道,注意只有位置和极限之间的数据才会被写入。因此,如果一个缓冲区容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,收集写入能较好的处理动态消息。

五、Java NIO Selector

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。

Selector的创建

通过调用Selector.open()方法创建一个Selector,如下:

Selector selector = Selector.open();

向Selector注册通道

为了将Channel和Selector配合使用,必须将channel注册到selector上。通过SelectableChannel.register()方法来实现,如下:

channel.configureBlocking(false);               //阻塞状态

SelectionKey key = channel.register(selector,Selectionkey.OP_READ);

注意register()方法的第二个参数。这是一个“interest集合”可以监听四种不同类型的事件

这四种事件用SelectionKey的四个常量来表示:

SelectionKey.OP_CONNECT

SelectionKey.OP_ACCEPT

SelectionKey.OP_READ

SelectionKey.OP_WRITE

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

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

select()方法:

1. int select()   2.int select(long timeout)   3.int selectNow()

select()阻塞到至少有一个通道在你注册的事件上就绪了。

select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数)。

selectNow()不会阻塞,不管什么通道就绪都立刻返回(译者注:此方法执行非阻塞的选择操作。如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零。)。

select()方法返回的int值表示有多少通道已经就绪。

demo

//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);

System.out.println("start!!!");

//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();

}}}

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值