目录
1 概述
之前听朋友说,他们公司有一个业务场景对于IO的操作要求较高,项目组长让他用NIO来完成这个需求,他一听一脸茫然的问组长:啥是NIO啊?项目组长听后对他挥挥手说:“起开起开,我来”。朋友后来和我分享这个事情,对于都是菜鸡的我们来说,我也不知道啥叫NIO。于是虎年伊始,我决定来学学这个NIO。以免有一天我的项目组长对我说,你起开起开,我来。
Java NIO是Java1.4之后引入的一个全新的API,它可以替代标准的IO操作,NIO既支持面向缓冲区的操作,同时也是基于通道的操作 ,它可以用更高效的方式进行文件的读写操作。鉴于NIO的内容较多,我决定写几篇博客分别来记录它。
2 阻塞IO/NIO
2.1 阻塞IO
在进行同步I/O操作时,如果读取数据,代码会阻塞直至有可供读取的数据,同样写入数据将会阻塞直至数据能够写入。传统的Server/Client模式会基于TPR(Thread per Request),服务器会为每一个客户端建立一个线程,由该线程单独负责处理一个客户请求,这种模式带来的问题就是线程数量的增多会增大服务器的开销。大多数时候都会采用线程池模型,并且设置线程池的最大线程数量,但这同样不能够完全解决问题,如果最大线程数是100,而有100个用户在进行大文件下载,那么101个用户的请求就会被线程池拒绝处理(线程池会执行拒绝策略)
2.2 NIO
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操作。
2.3 NIO核心组件
Java NIO由Channels,Buffers,Selectors三个核心部分组成。虽然NIO中除此之外还有很多类和组件,但这三个是核心API。其它,如Pipe和File和Lock,只不过是与三个核心组件共同使用的工具类。
2.3.1 Channel
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
2.3.2 Buffer
NIO中的关键Buffer实现有:ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer。分别对应基本数据类型byte,char,double,float,int,long,short。
2.3.3 Selector
Selector运行单线程处理多个Channel,如果你的应用打开了多个通道,但每个的流量都很低,使用Selector就会很方便。比如向Selector注册Channel,然后调用它的select(),这个方法会一直阻塞到某个注册的通道有事件就绪,一旦这个方法返回,线程就可以处理这些事情。
3 Channel
channel是一个通道,可以通过它读取和写入数据。通道和流的区别在于,通道是双向的,流是单向的(一个流必须是InputStream或者OutputStream的子类),而且通道可以用于读写或者同时用于读写。因为通道是全双工的,所以它可以比流更好的映射底层操作系统的API.
NIO中通过channel封装了对数据源的操作,通过channel我们可以操作数据源,但又可以不用关心数据源的物理结构。这个数据源可以是多种的,可以是文件,也可以是网络socket,在大多数应用中,channel与文件描述或者socket是一一对应的。channel可用于在字节缓冲区和位于通道另一侧的实体之间有效的传输数据。
channel是一个对象,可以通过它读取和写入数据,通道就像是流,所有的数据都通过Buffer对象来处理,永远不会将字节直接写入通道中,相反是将数据写入包含一个或者多个字节的缓冲区,同样,我们不会直接从通道中读取字节,而是将数据读入缓冲区,再从缓冲区写这个字节
3.1 FileChannel
FileChannel类可以实现常用的read,write,以及scatter/gather操作,同时也提供了很多专用于文件的新方法
3.1.1 将数据读取到buffer中
自定义一个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");
}
读取结果:
3.1.2 向fileChannel中写数据
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("写入完成");
}
写入结果:
3.1.3 FileChannel的其他方法
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");
}
3.2 Socket通道
新的socket通道类可以运行非阻塞模式并且是可选择的,可以激活大程序巨大的可伸缩性和灵活性。
socket通道特点:
1)DatagramChannel和SocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,它本身从不传输数据。
2)socket通道类在被实例化之前都会创建一个对等socket对象。对等socket可以通过调用socket()方法从一个通道上获取。
3.2.1 ServerSocketChannel
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();
}
}
}
访问链接后:
3.2.2 SocketChannel
java NiO中的SocketChannel是一个连接到TCP网络套接字的通道。
SocketChannel特征
1)SocketChannel是用来连接Socket套接字
2)SocketChannel主要用途用来处理网络I/O的通道
3)SocketChannel是基于TCP连接传输
4)SocketChannel实现了可选择通道,可以被多路复用的
3.2.3 DatagramChannel
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));
}
}
}
4 Scatter/Gather
分散(scatter):从channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据分散到多个Buffer
聚集(Gather):写入Channel是指在写操作时将多个buffer的数据写入同一个Channel。因此,Channel将多个Buffer中的数据聚集后发送到Channel
以上只是部分内容,为了维护方便,本文已迁移到新地址:java NIO-Channel – 编程屋