3.1 Java NIO简介
在1.4版本之前,Java IO类库是阻塞IO;从1.4版本开始,引进了新的异步IO库,被称为Java New IO类库,简称为Java NIO。更多的人喜欢称Java NIO为非阻塞IO(Non-Blocking IO),称“老的”阻塞式Java IO为OIO(Old IO)。
Java NIO类库包含以下三个核心组件:
- Channel(通道)
- Buffer(缓冲区)
-
3.1.1 NIO和OIO的对比
OIO是面向流(Stream Oriented)的,NIO是面向缓冲区(Buffer Oriented)的
- OIO的操作是阻塞的,而NIO的操作是非阻塞的。
OIO没有选择器(Selector)的概念,而NIO有选择器的概念。
3.1.2 通道
在OIO中,同一个网络连接会关联到两个流:一个是输入流(Input Stream),另一个是输出流(Output Stream)。Java应用程序通过这两个流不断地进行输入和输出的操作。
在NIO中,一个网络连接使用一个通道表示,所有NIO的IO操作都是通过连接通道完成的。一个通道类似于OIO中两个流的结合体,既可以从通道读取数据,也可以向通道写入数据。3.1.3 选择器
选择器可以理解为一个IO事件的监听与查询器。通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。
3.1.4 缓冲区
所谓通道的读取,就是将数据从通道读取到缓冲区中;所谓通道的写入,就是将数据从缓冲区写入通道中。缓冲区的使用是面向流进行读写操作的OIO所没有的,也是NIO非阻塞的重要前提和基础之一。
3.2 详解NIO Buffer类及其属性
NIO的Buffer本质上是一个内存块,既可以写入数据,也可以从中读取数据。Java NIO中代表缓冲区的Buffer类是一个抽象类,位于java.nio包中。
Buffer类是一个非线程安全类。3.2.1 Buffer类
Buffer类是一个抽象类,对应于Java的主要数据类型。在NIO中,有8种缓冲区类,分别是ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedByteBuffer。前7种Buffer类型覆盖了能在IO中传输的所有Java基本数据类型,第8种类型是一种专门用于内存映射的ByteBuffer类型。
3.2.2 Buffer类的重要属性
为了记录读写的状态和位置,Buffer类额外提供了一些重要的属性,其中有三个重要的成员属性:capacity(容量)、position(读写位置)和limit(读写的限制)。
capacity属性
Buffer类的capacity属性表示内部容量的大小。一旦写入的对象数量超过了capacity,缓冲区就满了,不能再写入了。
- position属性
Buffer类的position属性表示当前的位置。position属性的值与缓冲区的读写模式有关。在不同的模式下,position属性值的含义是不同的,在缓冲区进行读写的模式改变时,position值会进行相应的调整。
在写模式下,position值的变化规则如下:
(1)在刚进入写模式时,position值为0,表示当前的写入位置为从头开始。
(2)每当一个数据写到缓冲区之后,position会向后移动到下一个可写的位置。
(3)初始的position值为0,最大可写值为limit-1。当position值达到limit时,缓冲区就已经无空间可写了。
在读模式下,position值的变化规则如下:
(1)当缓冲区刚开始进入读模式时,position会被重置为0。
(2)当从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动到下一个可读的位置。
(3)在读模式下,limit表示可读数据的上限。position的最大值为最大可读上限limit,当position达到limit时表明缓冲区已经无数据可读。
模式的切换,可以调用flip()方法
在从写模式到读模式的翻转过程中,position和limit属性值会进行调整,具体的规则是:
(1)limit属性被设置成写模式时的position值,表示可以读取的最大数据位置。
(2)position由原来的写入位置变成新的可读位置,也就是0,表示可以从头开始读。
- limit属性
Buffer类的limit属性表示可以写入或者读取的数据最大上限,其属性值的具体含义也与缓冲区的读写模式有关。在不同的模式下,limit值的含义是不同的,具体分为以下两种情况:
(1)在写模式下,limit属性值的含义为可以写入的数据最大上限。在刚进入写模式时,limit的值会被设置成缓冲区的capacity值,表示可以一直将缓冲区的容量写满。
(2)在读模式下,limit值的含义为最多能从缓冲区读取多少数据。
总结:
| 属性 | 说明 |
|---|---|
| capacity | 容量,即可以容纳的最大数据量,在缓冲区创建时设置并且不能改变 |
| limit | 读写的限制,缓冲区中当前的数据量 |
| position | 读写位置,缓冲区中下一个要被读或写的元素的索引 |
| mark | 调用mark()方法来设置mark=position在调用reset()让position恢复到mark标记的位置 |
3.3 详解NIO Buffer类的重要方法
3.3.1 allocate()
在使用Buffer实例之前,我们首先需要获取Buffer子类的实例对象,并且分配内存空间。需要获取一个Buffer实例对象时,并不是使用子类的构造器来创建,而是调用子类的allocate()方法。
package com.crazymakercircle.bufferDemo;import com.crazymakercircle.util.Logger;import java.nio.IntBuffer;public class UseBuffer{//一个整型的Buffer静态变量static IntBuffer intBuffer = null;public static void allocateTest(){//创建一个intBuffer实例对象intBuffer = IntBuffer.allocate(20);Logger.debug("------------after allocate------------------");Logger.debug("position=" + intBuffer.position());Logger.debug("limit=" + intBuffer.limit());Logger.debug("capacity=" + intBuffer.capacity());}//省略其他代码}//allocatTest |> ------------after allocate------------------//allocatTest |> position=0//allocatTest |> limit=20//allocatTest |> capacity=20
3.3.2 put()
在调用allocate()方法分配内存、返回了实例对象后,缓冲区实例对象处于写模式,可以写入对象,如果要把对象写入缓冲区,就需要调用put()方法。put()方法很简单,只有一个参数,即需要写入的对象,只不过要求写入的数据类型与缓冲区的类型保持一致。
package com.crazymakercircle.bufferDemo;//省略importpublic class UseBuffer{//一个整型的Buffer静态变量static IntBuffer intBuffer = null;//省略了创建缓冲区的代码,具体查看前面小节的内容和随书源码public static void putTest(){for (int i = 0; i < 5; i++){//写入一个整数到缓冲区intBuffer.put(i);}//输出缓冲区的主要属性值Logger.debug("------------after putTest------------------");Logger.debug("position=" + intBuffer.position());Logger.debug("limit=" + intBuffer.limit());Logger.debug("capacity=" + intBuffer.capacity());}//省略其他代码}//输出结果putTest |> ------------after putTest------------------putTest |> position=5putTest |> limit=20putTest |> capacity=20
3.3.3 flip()
向缓冲区写入数据之后,是否可以直接从缓冲区读取数据呢?不能!这时缓冲区还处于写模式,如果需要读取数据,要将缓冲区转换成读模式。flip()翻转方法是Buffer类提供的一个模式转变的重要方法,作用是将写模式翻转成读模式。
package com.crazymakercircle.bufferDemo;//省略importpublic class UseBuffer{//一个整型的Buffer静态变量static IntBuffer intBuffer = null;//省略了缓冲区的创建、写入数据的代码,具体查看前面小节的内容和随书源码public static void flipTest(){//翻转缓冲区,从写模式翻转成读模式intBuffer.flip();//输出缓冲区的主要属性值Logger.info("------------after flip ------------------");Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());}//省略其他代码}//输出结果flipTest |> ------------after flipTest ------------------flipTest |> position=0flipTest |> limit=5flipTest |> capacity=20
在读取完成后,如何再一次将缓冲区切换成写模式呢?
答案是:可以调用Buffer.clear()清空或者Buffer.compact()压缩方法,它们可以将缓冲区转换为写模式。
3.3.4 get()
调用flip()方法将缓冲区切换成读模式之后,就可以开始从缓冲区读取数据了。读取数据的方法很简单,可以调用get()方法每次从position的位置读取一个数据,并且进行相应的缓冲区属性的调整。
package com.crazymakercircle.bufferDemo;//省略importpublic class UseBuffer{//一个整型的Buffer静态变量static IntBuffer intBuffer = null;//省略了缓冲区的创建、写入、翻转的代码,具体查看前面小节的内容和随书源码public static void getTest(){//先读2个数据for (int i = 0; i< 2; i++){int j = intBuffer.get();Logger.info("j = " + j);}//输出缓冲区的主要属性值Logger.info("---------after get 2 int --------------");Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());//再读3个数据for (int i = 0; i< 3; i++){int j = intBuffer.get();Logger.info("j = " + j);}//输出缓冲区的主要属性值Logger.info("---------after get 3 int ---------------");Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());}//…}//省略其他代码}getTest |> ------------after get 2 int ------------------getTest |> position=2getTest |> limit=5getTest |> capacity=20getTest |> ------------after get 3 int ------------------getTest |> position=5getTest |> limit=5getTest |> capacity=20
缓冲区是不是可以重复读呢?
答案是可以的,既可以通过倒带方法rewind()去完成,也可以通过mark()和reset()两个方法组合实现。
3.3.5 rewind()
已经读完的数据,如果需要再读一遍,可以调用rewind()方法。rewind()也叫倒带,就像播放磁带一样倒回去,再重新播放。
package com.crazymakercircle.bufferDemo;//省略importpublic class UseBuffer{//一个整型的Buffer静态变量static IntBuffer intBuffer = null;//省略了缓冲区的写入和读取等代码,具体查看前面小节的内容和随书源码public static void rewindTest() {//倒带intBuffer.rewind();//输出缓冲区属性Logger.info("------------after rewind ------------------");Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());}//省略其他代码}rewindTest |> ------------after rewind ------------------rewindTest |> position=0rewindTest |> limit=5rewindTest |> capacity=20
从JDK中可以查阅Buffer.rewind()方法的源代码,具体如下:
public final Buffer rewind() {position = 0; //重置为0,所以可以重读缓冲区中的所有数据mark = -1; //mark被清理,表示之前的临时位置不能再用了return this;}
重复读取的示例代码如下:
package com.crazymakercircle.bufferDemo;//省略importpublic class UseBuffer{//一个整型的Buffer静态变量static IntBuffer intBuffer = null;//省略了缓冲区的写入和读取、倒带等代码,具体查看前面小节的内容和随书源码public static void reRead() {for (int i = 0; i< 5; i++) {if (i == 2) {//临时保存,标记一下第3个位置intBuffer.mark();}//读取元素int j = intBuffer.get();Logger.info("j = " + j);}//输出缓冲区的属性值Logger.info("------------after reRead------------------");Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());}//省略其他代码}
3.3.6 mark()和reset()
mark()和reset()两个方法是配套使用的:Buffer.mark()方法将当前position的值保存起来放在mark属性中,让mark属性记住这个临时位置;然后可以调用Buffer.reset()方法将mark的值恢复到position中。
package com.crazymakercircle.bufferDemo;//省略importpublic class UseBuffer{//一个整型的Buffer静态变量static IntBuffer intBuffer = null;//省略了缓冲区的倒带、重复读取等代码,具体查看前面小节的内容和随书源码//演示前提://在前面的reRead()演示方法中,已经通过mark()方法暂存了position值public static void afterReset() {Logger.info("------------after reset------------------");//把前面保存在mark中的值恢复到position中intBuffer.reset();//输出缓冲区的属性值Logger.info("position=" + intBuffer.position());Logger.info("limit=" + intBuffer.limit());Logger.info("capacity=" + intBuffer.capacity());//读取并且输出元素for (int i =2; i< 5; i++) {int j = intBuffer.get();Logger.info("j = " + j);}}//省略其他代码}afterReset |> ------------after reset------------------afterReset |> position=2afterReset |> limit=5afterReset |> capacity=20afterReset |> j = 2afterReset |> j = 3afterReset |> j = 4
3.3.7 clear()
在读模式下,调用clear()方法将缓冲区切换为写模式。此方法的作用是:
(1)将position清零。
(2)limit设置为capacity最大容量值,可以一直写入,直到缓冲区写满。
package com.crazymakercircle.bufferDemo;//省略importpublic class UseBuffer{//一个整型的Buffer静态变量static IntBuffer intBuffer = null;//省略了缓冲区的创建、写入、读取等代码,具体查看前面小节的内容和随书源码public static void clearDemo() {Logger.info("------------after clear------------------");//清空缓冲区,进入写模式intBuffer.clear();//这个示例程序运行之后,结果如下:main |>清空clearDemo |> ------------after clear------------------clearDemo |> position=0clearDemo |> limit=20clearDemo |> capacity=20
3.3.8 使用Buffer类的基本步骤
(1)使用创建子类实例对象的allocate()方法创建一个Buffer类的实例对象。
(2)调用put()方法将数据写入缓冲区中。
(3)写入完成后,在开始读取数据前调用Buffer.flip()方法,将缓冲区转换为读模式。
(4)调用get()方法,可以从缓冲区中读取数据。
(5)读取完成后,调用Buffer.clear()方法或Buffer.compact()方法,将缓冲区转换为写模式,可以继续写入。
3.4 详解NIO Channel类
前面提到,Java NIO中一个socket连接使用一个Channel来表示。从更广泛的层面来说,一个通道可以表示一个底层的文件描述符,例如硬件设备、文件、网络连接等。然而,远不止如此,Java NIO的通道可以更加细化。例如,不同的网络传输协议类型,在Java中都有不同的NIO Channel实现。
最为重要的四种Channel实现:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。
对于以上四种通道,说明如下:
(1)FileChannel:文件通道,用于文件的数据读写。
(2)SocketChannel:套接字通道,用于套接字TCP连接的数据读写。
(3)ServerSocketChannel:服务器套接字通道(或服务器监听通道),允许我们监听TCP连接请求,为每个监听到的请求创建一个SocketChannel通道。
(4)DatagramChannel:数据报通道,用于UDP的数据读写。
3.4.1 FileChannel
FileChannel(文件通道)是专门操作文件的通道。通过FileChannel,既可以从一个文件中读取数据,也可以将数据写入文件中。特别申明一下,FileChannel为阻塞模式,不能设置为非阻塞模式。
- 获取FileChannel
可以通过文件的输入流、输出流获取FileChannel
//创建一个文件输入流FileInputStream fis = new FileInputStream(srcFile);//获取文件流的通道FileChannel inChannel = fis.getChannel();//创建一个文件输出流FileOutputStream fos = new FileOutputStream(destFile);//获取文件流的通道FileChannel outchannel = fos.getChannel();
也可以通过RandomAccessFile(文件随机访问)类来获取FileChannel实例
//创建RandomAccessFile随机访问对象RandomAccessFile rFile = new RandomAccessFile("filename.txt","rw");//获取文件流的通道(可读可写)FileChannel channel = rFile.getChannel();
- 读取FileChannel
在大部分应用场景中,从通道读取数据都会调用通道的int read(ByteBuffer buf)方法,它把从通道读取的数据写入ByteBuffer缓冲区,并且返回读取的数据量。
RandomAccessFile aFile = new RandomAccessFile(fileName, "rw");//获取通道(可读可写)FileChannel channel = aFile.getChannel();//获取一个字节缓冲区ByteBuffer buf = ByteBuffer.allocate(CAPACITY);int length = -1;//调用通道的read()方法,读取数据并写入字节类型的缓冲区while ((length = channel.read(buf)) != -1) {//省略buf中的数据处理}
以上代码中channel.read(buf)读取通道的数据时,对于通道来说是读模式,对于ByteBuffer缓冲区来说是写入数据,这时ByteBuffer缓冲区处于写模式。
- 写入FileChannel
把数据写入通道,在大部分应用场景中都会调用通道的write(ByteBuffer)方法,此方法的参数是一个ByteBuffer缓冲区实例,是待写数据的来源。
write(ByteBuffer)方法的作用是从ByteBuffer缓冲区中读取数据,然后写入通道自身,而返回值是写入成功的字节数。
//如果buf处于写模式(如刚写完数据),需要翻转buf,使其变成读模式buf.flip();int outlength = 0;//调用write()方法,将buf的数据写入通道while ((outlength = outchannel.write(buf)) != 0) {System.out.println("写入的字节数:" + outlength);}
在以上的outchannel.write(buf)调用中,对于入参buf实例来说,需要从其中读取数据写入outchannel通道中,所以入参buf必须处于读模式,不能处于写模式。
- 关闭通道
通道使用完成后,必须将其关闭。关闭非常简单,调用close()方法即可。
//关闭通道channel.close();
- 强制刷新到磁盘
在将缓冲区写入通道时,出于性能的原因,操作系统不可能每次都实时地将写入数据落地(或刷新)到磁盘,完成最终的数据保存。
在将缓冲区数据写入通道时,要保证数据能写入磁盘,可以在写入后调用一下FileChannel的force()方法。
//强制刷新到磁盘channel.force(true);
3.4.2 使用FileChannel完成文件复制的实战案例
下面是一个简单的实战案例:使用FileChannel复制文件。具体的功能是使用FileChannel将原文件复制一份,把原文件中的数据都复制到目标文件中。
package com.crazymakercircle.iodemo.fileDemos;//省略import,具体请参见源代码工程public class FileNIOCopyDemo {public static void main(String[] args) {//演示复制资源文件nioCopyResouceFile();}/*** 复制两个资源目录下的文件*/public static void nioCopyResouceFile() {//源String sourcePath = NioDemoConfig.FILE_RESOURCE_SRC_PATH;String srcPath = IOUtil.getResourcePath(sourcePath);Logger.info("srcPath=" + srcPath);//目标String destPath = NioDemoConfig.FILE_RESOURCE_DEST_PATH;String destDecodePath = IOUtil.builderResourcePath(destPath);Logger.info("destDecodePath=" + destDecodePath);//复制文件nioCopyFile(srcDecodePath, destDecodePath);}/*** NIO方式复制文件* @param srcPath 源路径* @param destPath目标路径*/public static void nioCopyFile(String srcPath, String destPath){File srcFile = new File(srcPath);File destFile = new File(destPath);try {//如果目标文件不存在,则新建if (!destFile.exists()) {destFile.createNewFile();}long startTime = System.currentTimeMillis();FileInputStream fis = null;FileOutputStream fos = null;FileChannel inChannel = null; //输入通道FileChannel outchannel = null; //输出通道try {fis = new FileInputStream(srcFile);fos = new FileOutputStream(destFile);inChannel = fis.getChannel();outchannel = fos.getChannel();int length = -1;//新建buf,处于写模式ByteBufferbuf = ByteBuffer.allocate(1024);//从输入通道读取到bufwhile ((length = inChannel.read(buf)) != -1) {//buf第一次模式切换:翻转buf,从写模式变成读模式buf.flip();int outlength = 0;//将buf写入输出的通道while ((outlength = outchannel.write(buf)) != 0) {System.out.println("写入的字节数:" + outlength);}//buf第二次模式切换:清除buf,变成写模式buf.clear();}//强制刷新到磁盘outchannel.force(true);} finally {//关闭所有的可关闭对象IOUtil.closeQuietly(outchannel);IOUtil.closeQuietly(fos);IOUtil.closeQuietly(inChannel);IOUtil.closeQuietly(fis);}long endTime = System.currentTimeMillis();Logger.info("base复制毫秒数:" + (endTime - startTime));} catch (IOException e) {e.printStackTrace();}}
3.4.3 SocketChannel
在NIO中,涉及网络连接的通道有两个:一个是SocketChannel,负责连接的数据传输;另一个是ServerSocketChannel,负责连接的监听。其中,NIO中的SocketChannel传输通道与OIO中的Socket类对应,NIO中的ServerSocketChannel监听通道对应于OIO中的ServerSocket类。
ServerSocketChannel仅应用于服务端,而SocketChannel同时处于服务端和客户端。所以,对于一个连接,两端都有一个负责传输的SocketChannel。
无论是ServerSocketChannel还是SocketChannel,都支持阻塞和非阻塞两种模式。如何进行模式的设置呢?调用configureBlocking()方法,具体如下:
(1)socketChannel.configureBlocking(false)设置为非阻塞模式。
(2)socketChannel.configureBlocking(true)设置为阻塞模式。
在非阻塞模式下,通道的操作是异步、高效的,这也是相对于传统OIO的优势所在。下面详细介绍在非阻塞模式下通道的获取、读写和关闭等操作。
- 获取SocketChannel传输通道
在客户端,先通过SocketChannel静态方法open()获得一个套接字传输通道,然后将socket设置为非阻塞模式,最后通过connect()实例方法对服务器的IP和端口发起连接。
//获得一个套接字传输通道SocketChannel socketChannel = SocketChannel.open();//设置为非阻塞模式socketChannel.configureBlocking(false);//对服务器的IP和端口发起连接socketChannel.connect(new InetSocketAddress("127.0.0.1", 80));
在非阻塞情况下,与服务器的连接可能还没有真正建立,socketChannel.connect()方法就返回了,因此需要不断地自旋,检查当前是否连接到了主机:
while(! socketChannel.finishConnect() ){//不断地自旋、等待,或者做一些其他的事情}
在连接建立的事件到来时,服务端的ServerSocketChannel能成功地查询出这个新连接事件,并且通过调用服务端ServerSocketChannel监听套接字的accept()方法来获取新连接的套接字通道:
//新连接事件到来,首先通过事件获取服务器监听通道ServerSocketChannel server = (ServerSocketChannel) key.channel();//获取新连接的套接字通道SocketChannel socketChannel = server.accept();//设置为非阻塞模式socketChannel.configureBlocking(false);
NIO套接字通道主要用于非阻塞的传输场景。所以,基本上都需要调用通道的configureBlocking(false)方法,将通道从阻塞模式切换为非阻塞模式。
- 读取SocketChannel传输通道
调用read()方法,将数据读入缓冲区ByteBuffer。
ByteBufferbuf = ByteBuffer.allocate(1024);int bytesRead = socketChannel.read(buf);
在读取时,因为是异步的,所以我们必须检查read()的返回值,以便判断当前是否读取到了数据。read()方法的返回值是读取的字节数,如果是-1,那么表示读取到对方的输出结束标志,即对方已经输出结束,准备关闭连接。实际上,通过read()方法读数据本身是很简单的,比较困难的是在非阻塞模式下如何知道通道何时是可读的。
- 写入SocketChannel传输通道
大部分应用场景都会调用通道的int write(ByteBufferbuf)方法。
//写入前需要读取缓冲区,要求ByteBuffer是读模式buffer.flip();socketChannel.write(buffer);
- 关闭SocketChannel传输通道
在关闭SocketChannel传输通道前,如果传输通道用来写入数据,则建议调用一次shutdownOutput()终止输出方法,向对方发送一个输出的结束标志(-1)。然后调用socketChannel.close()方法,关闭套接字连接。
//调用终止输出方法,向对方发送一个输出的结束标志socketChannel.shutdownOutput();//关闭套接字连接IOUtil.closeQuietly(socketChannel);
3.4.4 使用SocketChannel发送文件的实战案例
下面的实战案例是使用FileChannel读取本地文件内容,然后在客户端使用SocketChannel把文件信息和文件内容发送到服务器。客户端的完整代码如下:
package com.crazymakercircle.iodemo.socketDemos;//…public class NioSendClient {private Charset charset = Charset.forName("UTF-8");/*** 向服务端传输文件*/public void sendFile(){try{String sourcePath = NioDemoConfig.SOCKET_SEND_FILE;String srcPath = IOUtil.getResourcePath(sourcePath);Logger.debug("srcPath=" + srcPath);String destFile = NioDemoConfig.SOCKET_RECEIVE_FILE;Logger.debug("destFile=" + destFile);File file = new File(srcPath);if (!file.exists()){Logger.debug("文件不存在");return;}FileChannel fileChannel = new FileInputStream(file).getChannel();SocketChannel socketChannel = SocketChannel.open();socketChannel.socket().connect(new InetSocketAddress("127.0.0.1",18899));socketChannel.configureBlocking(false);Logger.debug("Client 成功连接服务端");while (!socketChannel.finishConnect()){//不断地自旋、等待,或者做一些其他的事情}//发送文件名称和长度ByteBuffer buffer = sengFileNameAndLength(destFile, file, socketChannel);//发送文件内容int length = sendContent(file, fileChannel, socketChannel, buffer);if (length == -1){IOUtil.closeQuietly(fileChannel);socketChannel.shutdownOutput();IOUtil.closeQuietly(socketChannel);}Logger.debug("======== 文件传输成功 ========");} catch (Exception e){e.printStackTrace();}}//方法:发送文件内容public int sendContent(File file, FileChannel fileChannel,SocketChannel socketChannel,ByteBuffer buffer) throws IOException{//发送文件内容Logger.debug("开始传输文件");int length = 0;long progress = 0;while ((length = fileChannel.read(buffer)) > 0){buffer.flip();socketChannel.write(buffer);buffer.clear();progress += length;Logger.debug("| " + (100 * progress / file.length()) + "% |");}return length;}//方法:发送文件名称和长度public ByteBuffer sengFileNameAndLength(String destFile,File file,SocketChannel socketChannel) throws IOException{//发送文件名称ByteBuffer fileNameByteBuffer = charset.encode(destFile);ByteBuffer buffer = ByteBuffer.allocate(NioDemoConfig.SEND_BUFFER_SIZE);//发送文件名称长度int fileNameLen = fileNameByteBuffer.capacity();buffer.putInt(fileNameLen);buffer.flip();socketChannel.write(buffer);buffer.clear();Logger.info("Client 文件名称长度发送完成:", fileNameLen);//发送文件名称socketChannel.write(fileNameByteBuffer);Logger.info("Client 文件名称发送完成:", destFile);//发送文件长度buffer.putLong(file.length());buffer.flip();socketChannel.write(buffer);buffer.clear();Logger.info("Client 文件长度发送完成:", file.length());return buffer;}}
3.4.5 DatagramChannel
在Java NIO中,使用DatagramChannel来处理UDP的数据传输。
- 获取DatagramChannel
获取数据报通道的方式很简单,调用DatagramChannel类的open()静态方法即可。然后调用configureBlocking(false)方法,设置成非阻塞模式。
//获取DatagramChannelDatagramChannel channel = DatagramChannel.open();//设置为非阻塞模式datagramChannel.configureBlocking(false);
如果需要接收数据,还需要调用bind()方法绑定一个数据报的监听端口,具体如下:
//调用bind()方法绑定一个数据报的监听端口channel.socket().bind(new InetSocketAddress(18080));
- 从DatagramChannel读取数据
当DatagramChannel通道可读时,可以从DatagramChannel读取数据。和前面的SocketChannel读取方式不同,这里不调用read()方法,而是调用receive(ByteBufferbuf)方法将数据从DatagramChannel读入,再写入ByteBuffer缓冲区中。
//创建缓冲区ByteBuffer buf = ByteBuffer.allocate(1024);//从DatagramChannel读入,再写入ByteBuffer缓冲区SocketAddress clientAddr= datagramChannel.receive(buf);
通道读取receive(ByteBufferbuf)方法虽然读取了数据到buf缓冲区,但是其返回值是SocketAddress类型,表示返回发送端的连接地址(包括IP和端口)。
- 写入DatagramChannel
向DatagramChannel发送数据,和向SocketChannel通道发送数据的方法是不同的。这里不是调用write()方法,而是调用send()方法。
//把缓冲区翻转为读模式buffer.flip();//调用send()方法,把数据发送到目标IP+端口dChannel.send(buffer, new InetSocketAddress("127.0.0.1",18899));//清空缓冲区,切换到写模式buffer.clear();
- 关闭DatagramChannel
//简单关闭即可dChannel.close();
3.4.6 使用DatagramChannel发送数据的实战案例
下面是一个使用DatagramChannel发送数据的客户端示例程序,功能是获取用户的输入数据,通过DatagramChannel将数据发送到远程的服务器。 ```java package com.crazymakercircle.iodemo.udpDemos; //… public class UDPClient { public void send() throws IOException {
} public static void main(String[] args) throws IOException {//获取DatagramChannelDatagramChannel dChannel = DatagramChannel.open();//设置为非阻塞dChannel.configureBlocking(false);ByteBuffer buffer = ByteBuffer.allocate(NioDemoConfig.SEND_BUFFER_SIZE);Scanner scanner = new Scanner(System.in);Print.tcfo("UDP客户端启动成功!");Print.tcfo("请输入发送内容:");while (scanner.hasNext()) {String next = scanner.next();buffer.put((Dateutil.getNow() + " >>" + next).getBytes());buffer.flip();//通过DatagramChannel发送数据dChannel.send(buffer,new InetSocketAddress("127.0.0.1",18899));buffer.clear();}//关闭DatagramChanneldChannel.close();
} }new UDPClient().send();
接下来看看在服务端应该如何使用DatagramChannel接收数据。<br />服务端是通过DatagramChannel绑定一个服务器地址(IP+端口),接收客户端发送过来的UDP数据报。服务端的完整代码如下:```javapackage com.crazymakercircle.iodemo.udpDemos;//…public class UDPServer {public void receive() throws IOException {//获取DatagramChannelDatagramChannel datagramChannel = DatagramChannel.open();//设置为非阻塞模式datagramChannel.configureBlocking(false);//绑定监听地址datagramChannel.bind(new InetSocketAddress("127.0.0.1",18899));Print.tcfo("UDP服务器启动成功!");//开启一个通道选择器Selector selector = Selector.open();//将通道注册到选择器datagramChannel.register(selector, SelectionKey.OP_READ);//通过选择器查询IO事件while (selector.select() > 0) {Iterator<SelectionKey> iterator =selector.selectedKeys().iterator();ByteBuffer buffer =ByteBuffer.allocate(NioDemoConfig.SEND_BUFFER_SIZE);//迭代IO事件while (iterator.hasNext()) {SelectionKeyselectionKey = iterator.next();//可读事件,有数据到来if (selectionKey.isReadable()) {//读取DatagramChannel数据SocketAddress client = datagramChannel.receive(buffer);buffer.flip();Print.tcfo(new String(buffer.array(), 0, buffer.limit()));buffer.clear();}}iterator.remove();}//关闭选择器和通道selector.close();datagramChannel.close();}public static void main(String[] args) throws IOException {new UDPServer().receive();}}
3.5 详解NIO Selector
3.5.1 选择器与注册
简单地说,选择器的使命是完成IO的多路复用,其主要工作是通道的注册、监听、事件查询。一个通道代表一条连接通路,通过选择器可以同时监控多个通道的IO(输入输出)状况。选择器和通道的关系是监控和被监控的关系。
选择器提供了独特的API方法,能够选出(select)所监控的通道已经发生了哪些IO事件,包括读写就绪的IO操作事件。
在NIO编程中,一般是一个单线程处理一个选择器,一个选择器可以监控很多通道。所以,通过选择器,一个单线程可以处理数百、数千、数万甚至更多的通道。在极端情况下(数万个连接),只用一个线程就可以处理所有的通道,这样会大量地减少线程之间上下文切换的开销。
通道和选择器之间的关联通过register(注册)的方式完成。调用通道的Channel.register(Selector sel,int ops)方法,可以将通道实例注册到一个选择器中。register方法有两个参数:第一个参数指定通道注册到的选择器实例;第二个参数指定选择器要监控的IO事件类型。
可供选择器监控的通道IO事件类型包括以下四种:
(1)可读:SelectionKey.OP_READ。
(2)可写:SelectionKey.OP_WRITE。
(3)连接:SelectionKey.OP_CONNECT。
(4)接收:SelectionKey.OP_ACCEPT。
//监控通道的多种事件,用“按位或”运算符来实现int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;
什么是IO事件?
这里的IO事件不是对通道的IO操作,而是通道处于某个IO操作的就绪状态,表示通道具备执行某个IO操作的条件。
3.5.2 SelectableChannel
并不是所有的通道都是可以被选择器监控或选择的。判断一个通道能否被选择器监控或选择有一个前提:判断它是否继承了抽象类SelectableChannel(可选择通道),如果是,就可以被选择,否则不能被选择。
3.5.3 SelectionKey
通道和选择器的监控关系注册成功后就可以选择就绪事件,具体的选择工作可调用Selector的select()方法来完成。通过select()方法,选择器可以不断地选择通道中所发生操作的就绪状态,返回注册过的那些感兴趣的IO事件。
通过SelectionKey不仅可以获得通道的IO事件类型,还可以获得发生IO事件所在的channel,另外,还可以获得selector
3.5.4 选择器使用流程
获取选择器实例。选择器实例是通过调用静态工厂方法open()来获取的
//调用静态工厂方法open()来获取Selector实例Selector selector = Selector.open();
Selector的类方法open()的内部是向选择器SPI发出请求,通过默认的SelectorProvider(选择器提供者)对象获取一个新的选择器实例。Java中的SPI(Service Provider Interface,服务提供者接口)是一种可以扩展的服务提供和发现机制。Java通过SPI的方式提供选择器的默认实现版本。也就是说,其他的服务提供者可以通过SPI的方式提供定制化版本的选择器的动态替换或者扩展。
将通道注册到选择器实例。要实现选择器管理通道,需要将通道注册到相应的选择器上 ```java //获取通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //设置为非阻塞 serverSocketChannel.configureBlocking(false); //绑定连接 serverSocketChannel.bind(new InetSocketAddress(18899)); //将通道注册到选择器上,并指定监听事件为“接收连接” serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
FileChannel不能与选择器一起使用,因为FileChannel只有阻塞模式,不能切换到非阻塞模式;而socket相关的所有通道都可以<br />一个通道并不一定支持所有的四种IO事件。例如,服务器监听通道ServerSocketChannel仅支持Accept(接收到新连接)IO事件,而传输通道SocketChannel则不同,它不支持Accept类型的IO事件。<br />可以在注册之前通过通道的validOps()方法来获取该通道支持的所有IO事件集合。3. 选出感兴趣的IO就绪事件(选择键集合)。```java//轮询,选择感兴趣的IO就绪事件(选择键集合)while (selector.select() > 0) {Set selectedKeys = selector.selectedKeys();Iterator keyIterator = selectedKeys.iterator();while(keyIterator.hasNext()) {SelectionKey key = keyIterator.next();//根据具体的IO事件类型执行对应的业务操作if(key.isAcceptable()) {//IO事件:ServerSocketChannel服务器监听通道有新连接} else if (key.isConnectable()) {//IO事件:传输通道连接成功} else if (key.isReadable()) {//IO事件:传输通道可读} else if (key.isWritable()) {//IO事件:传输通道可写}//处理完成后,移除选择键keyIterator.remove();}}
处理完成后,需要将选择键从SelectionKey集合中移除,以防止下一次循环时被重复处理。SelectionKey集合不能添加元素,如果试图向SelectionKey中添加元素,则将抛出java.lang.UnsupportedOperationException异常。<br />用于选择就绪的IO事件的select()方法有多个重载的实现版本,具体如下:
- select():阻塞调用,直到至少有一个通道发生了注册的IO事件。
- select(long timeout):和select()一样,但最长阻塞时间为timeout指定的毫秒数。
selectNow():非阻塞,不管有没有IO事件都会立刻返回。
3.5.5 使用NIO实现Discard服务器的实战案例
Discard服务器的功能很简单:仅读取客户端通道的输入数据,读取完成后直接关闭客户端通道,并且直接抛弃掉(Discard)读取到的数据。 ```java package com.crazymakercircle.iodemo.NioDiscard; //… public class NioDiscardServer { public static void startServer() throws IOException {
//1.获取选择器Selector selector = Selector.open();//2.获取通道ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//3.设置为非阻塞serverSocketChannel.configureBlocking(false);//4.绑定连接serverSocketChannel.bind(new InetSocketAddress(18899));Logger.info("服务器启动成功");//5.将通道注册的“接收新连接”IO事件注册到选择器上serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//6.轮询感兴趣的IO就绪事件(选择键集合)while (selector.select() > 0) {//7.获取选择键集合Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();while (selectedKeys.hasNext()) {//8.获取单个的选择键,并处理SelectionKey selectedKey = selectedKeys.next();//9.判断key是具体的什么事件if (selectedKey.isAcceptable()) {//10.若选择键的IO事件是“连接就绪”,就获取客户端连接SocketChannel socketChannel = serverSocketChannel.accept();//11.将新连接切换为非阻塞模式socketChannel.configureBlocking(false);//12.将新连接的通道的可读事件注册到选择器上socketChannel.register(selector, SelectionKey.OP_READ);} else if (selectedKey.isReadable()) {//13.若选择键的IO事件是“可读”,则读取数据SocketChannel socketChannel = (SocketChannel) selectedKey.channel();//14.读取数据,然后丢弃ByteBufferbyteBuffer = ByteBuffer.allocate(1024);int length = 0;while ((length = socketChannel.read(byteBuffer)) >0){byteBuffer.flip();Logger.info(new String(byteBuffer.array(), 0, length));byteBuffer.clear();}socketChannel.close();}//15.移除选择键selectedKeys.remove();}}//16.关闭连接serverSocketChannel.close();
} public static void main(String[] args) throws IOException {
startServer();
} }
客户端首先建立到服务器的连接,发送一些简单的数据,然后直接关闭连接。客户端的DiscardClient代码更加简单```javapackage com.crazymakercircle.iodemo.NioDiscard;//…public class NioDiscardClient {public static void startClient() throws IOException {InetSocketAddress address =new InetSocketAddress("127.0.0.1",18899);//1.获取通道SocketChannel socketChannel = SocketChannel.open(address);//2.切换成非阻塞模式socketChannel.configureBlocking(false);//不断地自旋、等待连接完成,或者做一些其他的事情while (!socketChannel.finishConnect()) {}Logger.info("客户端连接成功");//3.分配指定大小的缓冲区ByteBuffer byteBuffer = ByteBuffer.allocate(1024);byteBuffer.put("hello world".getBytes());byteBuffer.flip();//发送到服务器socketChannel.write(byteBuffer);socketChannel.shutdownOutput();socketChannel.close();}public static void main(String[] args) throws IOException {startClient();}}
