大多数人肯定了解Java IO, 但是对于NIO一般是陌生的,但是Java NIO是一个高频知识点,又不得不学,所以本文通过图文+代码的方式,保姆级别的讲述Java NIO的各个知识点。觉得写得好的,希望点个赞,给个收藏。
Java IO 与 Java NIO读取文件的差别
普通Java IO
public static void ioReadFile(String fileName) throws Exception {
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File(fileName)));
int len = 0;
byte [] bytes = new byte[32];
while((len = bis.read(bytes)) != -1){
for(int i = 0; i < len; i++) {
System.out.print((char) bytes[i]);
}
}
bis.close();
}
Java NIO
public static void nioReadFile(String fileName) throws Exception{
RandomAccessFile read = new RandomAccessFile(fileName, "r");
FileChannel channel = read.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(32);
while(channel.read(byteBuffer) > 0) {
byteBuffer.flip();
while(byteBuffer.hasRemaining()){
System.out.print((char) byteBuffer.get());
}
byteBuffer.clear();
}
read.close();
channel.close();
}
Java IO与Java NIO主要有三个区别
普通IO是面向流(stream
)的处理,而NIO是面向缓冲区(buffer
)的处理
面向流
Java IO面向流意味着每次从流中读取一个或多个字节,直至读取所有字节,字节数据没有被缓存在任何地方,不能前后移动在流中读取数据的位置。
面向缓冲区
Java NIO面向缓冲区,数据读取到一个稍后处理的缓冲区,需要时可在缓冲区中前后移动。增加了处理过程中的灵活性。
普通IO是阻塞IO,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。而NIO是非阻塞IO,Java 一个线程从某通道发送请求读取数据,如果无可读取数据,其不会被阻塞,在数据变得可读取之前可以做其他事情
普通IO不支持selector,而NIO支持selector.(selector下文会详细解释).通过Selector对象,可以实现IO复用,下文会详细讲述。
从上面NIO读取文件的代码,可以看出,NIO方式读取文件,首先需要获取一个传输数据的通道
channel
,之后构建一个承载数据的buffer
.打个形象点的比喻就是channel
相当于铁路,而buffer
相当于货运火车,通过铁路,货运火车可以源源不断将货物,从出发点运抵目的地。其实还有一个selector
,构成NIO的三个核心。
channel的读写是双向的,既可以从通道读数据,又可以往通道写数据,而流一般是单向的,比如输入流,输出流,同时channel也支持异步得读写
channel主要有一下四种实现
buffer其实本质是一块可以写入数据,也可以读取数据的内存
其工作流程主要分为一下几个步骤
buffer主要有三个属性capacity
,position
,limit
,通过分析其三个属性,可以详细了解其工作原理
capacity
:buffer的大小,对于ByteBuffer,就是规定其能存储的最大byte数
buffer分为读模式和写模式,如下图所示
position
:下一个要被读或写的元素的位置。
写数据时,初始的position值为0.当一个byte、long等数据写到Buffer后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity-1.
读数据时,position也是从0开始,向前读取数据。写模式向读模式切换时,会将position重置为0
limit
:缓冲区里的数据总数。
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据。
flip方法:从写模式切换到读模式,position置为0,limit设置为之前写模式的position值。
rewind方法:将position设置为0,但limit不变
clear方法:position设置为0,limit设置为capacity的值
compact方法:将所有未读的数据拷贝到Buffer起始处,然后将position设到最后一个未读元素正后面。limit属性依然像clear方法一样设置成capacity.现在Buffer准备好了写数据,但是不会覆盖未读数据。
mark、reset方法:通过调用Buffer.mark()方法,可以标记Buffer的一个特定position。之后可以通过调用Buffer.reset方法恢复到这个position
Buffer.mark();
//call Buffer.get() a couple of times, e.g. during parsing.
Buffer.reset(); //set position back to mark.
Selector是channel的管理器,Selector允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如,在一个聊天服务器中。这是在一个单线程中使用一个Selector处理3个Channel的图示:
要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。
java NIO复用,主要是通过Selector对象实现的,通过Selector对象,注册监听多个通道的多个事件,事件主要有四种,如下所示
OP_ACCEPT
: 监听连接事件,服务器监听客户端的连接请求
OP_CONNECT
:连接就绪事件,客户端与服务器已经连接成功
OP_READ
:读就绪事件,通道中已有可读数据,可以指向读操作
OP_WRITE
:写就绪事件,可以向通道中写数据
下面代码主要实现客户端向服务端传送图片数据的功能
client代码
public class NoBlockClient {
public static void main(String[] args) throws IOException {
//创建一个SocketChannel对象,通过从TCP读取数据,并绑定相应服务端ip和port
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
//设置为非阻塞IO
socketChannel.configureBlocking(false);
//为需要传送的图片文件建立channel
FileChannel fileChannel = FileChannel.open(Paths.get("C:\\Users\\LENOVO\\Desktop\\1.png"), StandardOpenOption.READ);
//创建运载数据所需的ByteBuffer对象
ByteBuffer buffer = ByteBuffer.allocate(1024);
//从FileChannel中读取数据到ByteBuffer
//将ByteBuffer对象写入SocketChannel
while (fileChannel.read(buffer) != -1) {
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
}
//关闭相应通道
fileChannel.close();
socketChannel.close();
}
}
Server代码
public class NoBlockServer {
public static void main(String[] args) throws IOException {
//创建一个ServerSocketChannel对象
ServerSocketChannel server = ServerSocketChannel.open();
//设置为非阻塞IO
server.configureBlocking(false);
//绑定监听端口
server.bind(new InetSocketAddress(6666));
//建立Selector对象,并注册OP_ACCEPT事件
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
//selector.select()方法一直会阻塞,除非监听的事件就绪
while (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//有新的连接
if (selectionKey.isAcceptable()) {
//为连接创建一个SocketChannel
SocketChannel client = server.accept();
client.configureBlocking(false);
//监听读事件
client.register(selector, SelectionKey.OP_READ);
//可读
} else if (selectionKey.isReadable()) {
SocketChannel client = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
//创建一个FileChannel,接受网络数据
FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
//从SocketChannel读取数据,并通过ByteBuffer,向FileChannel对象传输
while (client.read(buffer) > 0) {
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
client.close();
}
//处理完了的就绪事件,一定要删除,否则会反复处理
iterator.remove();
}
}
}
}
如何学习Java的NIO?