目录
1 概述
2 阻塞IO/NIO
2.1 阻塞IO
2.2 NIO
2.3 NIO核心组件
2.3.1 Channel
2.3.2 Buffer
2.3.3 Selector
3 Channel
3.1 FileChannel
3.1.1 将数据读取到buffer中
3.1.2 向fileChannel中写数据
3.1.3 FileChannel的其他方法
3.2 Socket通道
3.2.1 ServerSocketChannel
3.2.2 SocketChannel
3.2.3 DatagramChannel
4 Scatter/Gather
之前听朋友说,他们公司有一个业务场景对于IO的操作要求较高,项目组长让他用NIO来完成这个需求,他一听一脸茫然的问组长:啥是NIO啊?项目组长听后对他挥挥手说:“起开起开,我来”。朋友后来和我分享这个事情,对于都是菜鸡的我们来说,我也不知道啥叫NIO。于是虎年伊始,我决定来学学这个NIO。以免有一天我的项目组长对我说,你起开起开,我来。
Java NIO是Java1.4之后引入的一个全新的API,它可以替代标准的IO操作,NIO既支持面向缓冲区的操作,同时也是基于通道的操作 ,它可以用更高效的方式进行文件的读写操作。鉴于NIO的内容较多,我决定写几篇博客分别来记录它。
在进行同步I/O操作时,如果读取数据,代码会阻塞直至有可供读取的数据,同样写入数据将会阻塞直至数据能够写入。传统的Server/Client模式会基于TPR(Thread per Request),服务器会为每一个客户端建立一个线程,由该线程单独负责处理一个客户请求,这种模式带来的问题就是线程数量的增多会增大服务器的开销。大多数时候都会采用线程池模型,并且设置线程池的最大线程数量,但这同样不能够完全解决问题,如果最大线程数是100,而有100个用户在进行大文件下载,那么101个用户的请求就会被线程池拒绝处理(线程池会执行拒绝策略)
NIO中的非阻塞I/O采用了基于Reactor模式的工作方式,I/O调用不会被阻塞,相反是注册感兴趣的特定I/O事件,如可读数据到达,新的套接字连接等等,在发生特定的事件时,系统在通知我们。NIO中实现非阻塞I/O的核心对象就是Selector。Selector就是注册各种I/O事件地方,而且当我们感兴趣的事件发生时,就是这个对象发生我们的事件
如图:如果通道1,通道2,通道3等任何注册事件发生的时候,可以从Selector中获得相应的SelectorKey,同时从SelectorKey中可以找到发生的事件和发生具体的SelectableChannel,以此来获得客户端发送来的数据。
注意:非阻塞指的是IO事件本身不阻塞,但是获取IO事件的select()方法是需要阻塞等待的。区别在于阻塞的IO会阻塞在IO操作上,NIO的阻塞是发生在事件获取上,没有事件就没有IO,所以就说IO不阻塞了。也就是说IO阻塞其实是看IO是否发生,发生了才会阻塞,没有发生就说不上IO阻塞了。所以NIO的本质是延迟IO操作到真正发生IO的时候,而不是以前只要IO流打开了就一直等待IO操作。
Java NIO由Channels,Buffers,Selectors三个核心部分组成。虽然NIO中除此之外还有很多类和组件,但这三个是核心API。其它,如Pipe和File和Lock,只不过是与三个核心组件共同使用的工具类。
Channel和IO流中的stream流差不多是一个等级的,只不过stream是单向的,如:InputStream,outputStream,而Channel是双向的,既可以用来读操作,又可以用来写操作。NIO中的Channel主要实现有:FileChannel(文件IO)、DatagramChannel(UDP)、SocketChannel(TCP)和ServerSocketChannel(Server和Client)。
1)FileChannel(文件IO):从文件中读写数据
2)DatagramChannel(UDP):能通过UDP读写网络中的数据
3)SocketChannel(TCP):能通过TCP读写网络中的数据
4)ServerSocketChannel(Server和Client):可以监听新进来的TCP连接,像web服务器那样,对每一个新进来的连接都会创建一个SocketChannel
NIO中的关键Buffer实现有:ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer。分别对应基本数据类型byte,char,double,float,int,long,short。
Selector运行单线程处理多个Channel,如果你的应用打开了多个通道,但每个的流量都很低,使用Selector就会很方便。比如向Selector注册Channel,然后调用它的select(),这个方法会一直阻塞到某个注册的通道有事件就绪,一旦这个方法返回,线程就可以处理这些事情。
channel是一个通道,可以通过它读取和写入数据。通道和流的区别在于,通道是双向的,流是单向的(一个流必须是InputStream或者OutputStream的子类),而且通道可以用于读写或者同时用于读写。因为通道是全双工的,所以它可以比流更好的映射底层操作系统的API.
NIO中通过channel封装了对数据源的操作,通过channel我们可以操作数据源,但又可以不用关心数据源的物理结构。这个数据源可以是多种的,可以是文件,也可以是网络socket,在大多数应用中,channel与文件描述或者socket是一一对应的。channel可用于在字节缓冲区和位于通道另一侧的实体之间有效的传输数据。
channel是一个对象,可以通过它读取和写入数据,通道就像是流,所有的数据都通过Buffer对象来处理,永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区,同样,我们不会直接从通道中读取字节,而是将数据读入缓冲区,再从缓冲区写这个字节
FileChannel类可以实现常用的read,write,以及scatter/gather操作,同时也提供了很多专用于文件的新方法
自定义一个TXT文件01.txt
public static void main(String[] args) throws Exception { //创建FileChannel RandomAccessFile accessFile = new RandomAccessFile("C:\\Users\\LiuBuJun\\Desktop\\liubujun\\01.txt","rw"); FileChannel channel = accessFile.getChannel(); //创建buffer ByteBuffer buffer = ByteBuffer.allocate(1024); //读取数据到buffer中 int bytesRead = channel.read(buffer); // bytesRead = -1 到达文件末尾 while (bytesRead != -1 ) { System.out.println("读取了:"+bytesRead); //将数据从buffer取出来 .flip()反转读写模式 buffer.flip(); while (buffer.hasRemaining()) { System.out.println((char)buffer.get()); } //清除缓存区内容 buffer.clear(); bytesRead = channel.read(buffer); } accessFile.close(); System.out.println("over"); }
读取结果:
public static void main(String[] args) throws Exception { //创建FileChannel RandomAccessFile accessFile = new RandomAccessFile("C:\\Users\\LiuBuJun\\Desktop\\liubujun\\00.txt","rw"); FileChannel channel = accessFile.getChannel(); //创建buffer ByteBuffer buffer = ByteBuffer.allocate(1024); //准备要写入的数据 String str = "hello world"; buffer.clear(); //写入内容 buffer.put(str.getBytes()); buffer.flip(); //因不清楚能一次性写入多少数据,所以需要放在循序里面 while (buffer.hasRemaining()) { channel.write(buffer); } //关闭 channel.close(); System.out.println("写入完成"); }
写入结果:
position:如果需要在FileChannel的某个特定位置进行数据的读写,可以通过调用position()方法获取FileChannel的当前位置。也可以通过调用position(long pos)方法设置FileChannel的当前位置。
如果将位置设置在文件结束之后,然后试图从文件通道中读取数据,该方法将返回-1(文件结束标志)
如果将位置设置在文件结束之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙
size: FileChannel实例的size()方法将返回该实例所关联文件的大小
truncate:可以使用fileChannel.truncate()方法截取一个文件。截取文件时,文件指定长度的后面部分将会删除。如:fileChannel.truncate(1024)就是截取文件的前1024个字节
force: FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘。出于性能方面的考虑。操作系统会先将数据写在内存中,所以无法保证写入到FileChannel里的数据一定会及时的写到磁盘上。要保证这一点,需要调用force()。force()方法上有一个Boolean类型的参数,指名是否将文件元数据(权限信息)写到磁盘上。
transferTo和transferFrom:通道之间的数据传输,如果两个通道中有一个是FileChannel,那你可以将数据从一个channel传输到另外一个channel。
将fromChannel通道中的数据传输到toChannel时:transferFrom()方法
public static void main(String[] args) throws Exception { //创建两个FileChannel RandomAccessFile aFile = new RandomAccessFile("C:\\Users\\LiuBuJun\\Desktop\\liubujun\\00.txt","rw"); FileChannel fromChannel1 = aFile.getChannel(); RandomAccessFile bFile = new RandomAccessFile("C:\\Users\\LiuBuJun\\Desktop\\liubujun\\02.txt","rw"); FileChannel toChannel1 = bFile.getChannel(); //将fromChannel通道中的数据传输到toChannel long position = 0; long size = fromChannel1.size(); toChannel1.transferFrom(fromChannel1,position,size); aFile.close(); bFile.close(); System.out.println("over"); }
将fromChannel通道中的数据传输到toChannel时:transferFrom()方法
public static void main(String[] args) throws Exception { //创建两个FileChannel RandomAccessFile aFile = new RandomAccessFile("C:\\Users\\LiuBuJun\\Desktop\\liubujun\\00.txt","rw"); FileChannel fromChannel1 = aFile.getChannel(); RandomAccessFile bFile = new RandomAccessFile("C:\\Users\\LiuBuJun\\Desktop\\liubujun\\02.txt","rw"); FileChannel toChannel1 = bFile.getChannel(); //将fromChannel通道中的数据传输到toChannel long position = 0; long size = fromChannel1.size(); fromChannel1.transferTo(0,size,toChannel1); aFile.close(); bFile.close(); System.out.println("over"); }
新的socket通道类可以运行非阻塞模式并且是可选择的,可以激活大程序巨大的可伸缩性和灵活性。
socket通道特点:
1)DatagramChannel和SocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,它本身从不传输数据。
2)socket通道类在被实例化之前都会创建一个对等socket对象。对等socket可以通过调用socket()方法从一个通道上获取。
ServerSocketChannel是一个基于通道的socket监听器。它同我们所熟悉的Java.net.ServerSocket执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。
测试ServerSocketChannel监听链接:
public static void main(String[] args) throws Exception { //端口号 int port = 8888; ByteBuffer buffer = ByteBuffer.wrap("hello wrold".getBytes()); //打开ServerSocketChannel ServerSocketChannel ssc = ServerSocketChannel.open(); //绑定 ssc.socket().bind(new InetSocketAddress(port)); //设置非阻塞模式 ssc.configureBlocking(false); while (true) { System.out.println("正在等待链接"); //监听新进的链接 SocketChannel sc = ssc.accept(); if (sc == null ) { System.out.println("null"); Thread.sleep(2000); }else { System.out.println("链接来自:"+sc.socket().getRemoteSocketAddress()); buffer.rewind(); //指针0 sc.write(buffer); sc.close(); } } }
访问链接后:
java NiO中的SocketChannel是一个连接到TCP网络套接字的通道。
SocketChannel特征
1)SocketChannel是用来连接Socket套接字
2)SocketChannel主要用途用来处理网络I/O的通道
3)SocketChannel是基于TCP连接传输
4)SocketChannel实现了可选择通道,可以被多路复用的
DatagramChannel是一个无连接的,对应的是UDP。DatagramChannel可以发送单独数据报给不同的地址,同样也可以接收来自任意地址的数据包。UDP不存在真正意义上的连接。
package com.liubujun.nio; import org.junit.Test; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.nio.charset.Charset; /** * @Author: liubujun * @Date: 2022/2/13 14:49 */ public class DatagramChannelDemo { @Test public void sendDatagram() throws Exception { DatagramChannel sendChannel = DatagramChannel.open(); InetSocketAddress sendAddress = new InetSocketAddress("127.0.0.1", 9999); //发送 while (true) { ByteBuffer buffer = ByteBuffer.wrap("发送hello".getBytes("UTF-8")); sendChannel.send(buffer,sendAddress); System.out.println("发送完成"); Thread.sleep(1000); } } @Test public void receiveDatagram() throws Exception { DatagramChannel receiveChannel = DatagramChannel.open(); InetSocketAddress receiveAddress = new InetSocketAddress(9999); //绑定 receiveChannel.bind(receiveAddress); ByteBuffer receiveBuffer = ByteBuffer.allocate(1024); while (true) { receiveBuffer.clear(); SocketAddress socketAddress = receiveChannel.receive(receiveBuffer); receiveBuffer.flip(); System.out.println(socketAddress.toString()); System.out.println(Charset.forName("UTF-8").decode(receiveBuffer)); } } }
分散(scatter):从channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据分散到多个Buffer
聚集(Gather):写入Channel是指在写操作时将多个buffer的数据写入同一个Channel。因此,Channel将多个Buffer中的数据聚集后发送到Channel