JavaIO进阶系列——NIO day1-3
NIO
Java NlO (New lO)也有人称之为java non-blocking lO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。
NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写
注意点(*)
这里我特地提到最上面,这个注意点是一个很容易犯的错误
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buffer)) != -1) {
....
}
当你们看到上面这段代码的时候是否感到十分熟悉?
没错,这就是读取字节流最常用的操作,之前我们在写的时候几乎天天写这个,判断条件就是(len = sChannel.read(buffer)) != -1
,但在NIO的通道(channel)的读取上这个地方却是错误的!
原因
channel按照缓冲区读取数据:
- 有数据,返回数据的长度
- 无数据,返回
0
没错,就是在无数据的时候,返回的是0,也就是说你前面用来接收的len永远无法!= -1
这样就会导致你从程序在读取的时候进行无限循环!
解决
方法1:> 0
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buffer)) > 0) {
....
}
方法二:!= 0
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buffer)) != 0) {
....
}
NIO三大核心部分
- Channel(通道)
- Buffer(缓冲区)
- Selector(选择器)
NIO非阻塞模式
Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有1000个请求过来,根据实际情况,可以分配20或者80个线程来处理。不像之前的阻塞IO那样,非得分配1000个。
NIO和BIO比较
NIO | BIO |
---|---|
块方式处理数据 | 流方式处理数据 |
效率高 | 效率低 |
非阻塞 | 阻塞 |
基于Channel,Buffer | 基于字节流或字符流 |
NIO中数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中
NIO中Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
NIO三大核心部分详细
每个selector对应多个channel,每个channel都有一个buffer,selector的切换需要事件进行驱使
Channel通道
Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区也支持异步地读写
通道表示IO源与目标打开的连接,本身不能直接访问数据,而是与Buffer进行交互
Channel的特点
- 可以同时进行读写
- 可以实现异步读写数据
- 从缓冲读数据,也可以写数据到缓冲中
Channel类结构
主要的实现
- FileChannel:用于读取、写入、映射和操作文件的通道
- DatagramChannel:通过UDP读写网络中的数据通道
- SocketChannel:通过TCP读写网络中的数据
- ServerSocketChannel:可以监听新进来的TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。 【ServerSocketChanne类似 ServerSocket , SocketChannel类似Socket】
FileChannel常用方法
除了熟悉的byte的读、写、关闭操作 通道,这个类定义了以下文件特定的操作:
- 文件的一个区域可能是 mapped直接进入内存; 对于大文件,这通常更有效 比调用通常的 read或者 write方法。
- 对文件所做的更新可能是 forced out到底层存储设备,确保数据不 在系统崩溃的情况下丢失。
- 可以从文件传输字节 to some other channel, 和 vice versa, 以一种可以被许多操作系统优化的方式 直接与文件系统缓存进行非常快速的传输。
- 文件的一个区域可能是 locked 防止其他程序访问
方法 | 说明 |
---|---|
abstract void force(boolean metaData) | 强制将此通道文件的任何更新写入存储 包含它的设备。 |
FileLock lock() | 获取此频道文件的排他锁。 |
abstract MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) | 将此通道文件的一个区域直接映射到内存中。 |
static FileChannel open(Path path, OpenOption… options) | 打开或创建文件,返回文件通道以访问文件。 |
abstract long position() | 返回此通道的文件位置。 |
abstract int read(ByteBuffer dst) | 从此通道中读取一个字节序列到给定的缓冲区中。 |
long read(ByteBuffer[] dsts) | 从此通道中读取一个字节序列到给定的缓冲区中。 |
abstract long read(ByteBuffer[] dsts, int offset, int length) | 从该通道读取一个字节序列到该通道的子序列中 给定的缓冲区。 |
abstract long size() | 返回此通道文件的当前大小。 |
abstract long transferFrom(ReadableByteChannel src, long position, long count) | 将字节从给定的可读字节传输到该通道的文件中 渠道。 |
abstract long transferTo(long position, long count, WritableByteChannel target) | 将此通道文件中的字节传输到给定的可写字节 渠道。 |
abstract FileChannel truncate(long size) | 将此通道的文件截断为给定大小。 |
abstract int write(ByteBuffer src) | 从给定的缓冲区将字节序列写入此通道。 |
long write(ByteBuffer[] srcs) | 从给定的缓冲区将字节序列写入此通道。 |
abstract long write(ByteBuffer[] srcs, int offset, int length) | 将字节序列从 给定的缓冲区。 |
abstract int write(ByteBuffer src, long position) | 从给定的缓冲区将字节序列写入此通道, 从给定的文件位置开始。 |
FileChannel实例(写数据)
package test.channel;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
public class ChannelTest {
public static void main(String[] args) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("file_channel1.txt");
//获取字节输出流对应的通道
FileChannel channel = fileOutputStream.getChannel();
//分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//设置数据
buffer.put("file_channel_test01".getBytes(StandardCharsets.UTF_8));
//设置缓冲区为写模式
buffer.flip();
//写数据
channel.write(buffer);
//关闭通道
channel.close();
}
}
FileChannel实例(读数据)
package test.channel;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ChannelTest02 {
public static void main(String[] args) throws IOException {
//获取目标文件
FileInputStream fileInputStream = new FileInputStream("file_channel1.txt");
//获取通道
FileChannel channel = fileInputStream.getChannel();
//设置缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//从channel中读取数据到缓冲区中
channel.read(buffer);
//设置为写模式
buffer.flip();
//读取并输出
String s = new String(buffer.array(), 0, buffer.remaining());
System.out.println(s);
}
}
FileChannel实例(文件复制)
package test.channel;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ChannelTest03 {
public static void main(String[] args) throws IOException {
//定义文件
String path = "file_channel1.txt";
String targetPath = "copy_file_channel1.txt";
//获取字节输出或输入流
FileInputStream fileInputStream = new FileInputStream(path);
//设置输出流
FileOutputStream fileOutputStream = new FileOutputStream(targetPath);
//获取通道
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
//分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true){
//需要先清空缓冲区
buffer.clear();
//写入数据到buffer中
int read = inChannel.read(buffer);
//判断数据是否读完
if (read==-1){
break;
}
buffer.flip();
//从缓冲区写数据到文件中
outChannel.write(buffer);
}
outChannel.close();
inChannel.close();
}
}
FileChannel实例(分散读取与聚集写入)
即:分配多个缓冲区读取数据并最后将读取的数据聚集起来写到目标中
分散读取:Scatter,指将Channel通道的数据读入多个Buffer中
聚集写入:Gather,指将多个Buffer中的数据聚集到Channel中
package test.channel;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ChannelTest04 {
public static void main(String[] args) throws IOException {
String path = "file_channel1.txt";
String targetPath = "copy_file_channel2.txt";
FileInputStream fileInputStream = new FileInputStream(path);
FileOutputStream fileOutputStream = new FileOutputStream(targetPath);
//定义多个缓冲区
ByteBuffer buffer1 = ByteBuffer.allocate(4);
ByteBuffer buffer2 = ByteBuffer.allocate(10);
ByteBuffer buffer3 = ByteBuffer.allocate(11);
//定义数组
ByteBuffer[] buffers = {buffer1,buffer2,buffer3};
//获取输入输出流通道
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
//从通道中读取数据分散到各个缓冲区
inChannel.read(buffers);
//从每个缓冲区查询是否有数据,进行分散读取
for (ByteBuffer buffer : buffers) {
//切换读取模式
buffer.flip();
System.out.println(new String(buffer.array(),0,buffer.remaining()));
}
//进行聚集写入
outChannel.write(buffers);
outChannel.close();
inChannel.close();
System.out.println("over");
}
}
*FileChannel实例(复制文件,使用复制通道数据方法)
用这种方式更加简单,方便
从目标通道中复制原通道数据使用transferFrom()
方法
从原通道复制数据到目标通道使用transferTo()
方法
transferFrom:
package test.channel;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
public class ChannelTest05 {
public static void main(String[] args)throws IOException {
String path = "file_channel1.txt";
String targetPath = "copy_file_channel3.txt";
FileInputStream fileInputStream = new FileInputStream(path);
FileOutputStream fileOutputStream = new FileOutputStream(targetPath);
//获取输入输出流通道
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
//从原始通道复制数据
outChannel.transferFrom(inChannel, inChannel.position(), inChannel.size());
outChannel.close();
inChannel.close();
System.out.println("over");
}
}
transferTo:
package test.channel;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
public class ChannelTest06 {
public static void main(String[] args) throws IOException {
String path = "file_channel1.txt";
String targetPath = "copy_file_channel4.txt";
FileInputStream fileInputStream = new FileInputStream(path);
FileOutputStream fileOutputStream = new FileOutputStream(targetPath);
//获取输入输出流通道
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
//从原始通道复制数据
inChannel.transferTo(inChannel.position(), inChannel.size(), outChannel);
outChannel.close();
inChannel.close();
System.out.println("over");
}
}
测试
Buffer缓冲区
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer API更加容易操作和管理
存在于Java.nio包下,所有缓冲区是Buffer抽象类的子类,用于NIO通道交互
Buffer类结构图
我们可以看到下面有各种原始类型的Buffer
Buffer的四个组成
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
- mark:标记,标记其实是个索引,通过Buffer的mark方法指定一个特定的位置(position)为mark,后续我们可以使用reset方法对position进行恢复
- position:位置,其实就是下一个要被读取或写入的数据的索引,位置不能为负数或大于限制
- limit:限制,表示缓冲区可以操作的数据的大小的最大值,超过最大值的数据无法进行读写,同时limit不为负数,且不能大于缓冲区的容量,在写入模式中,限制等于buffer的容量。读取模式下,limit等于写入的数据量
- capacity:容量,表示一个内存的大小,容量必须大于等于0,并且创建之后不能修改容量的大小
即:0 <= mark <= position <= limit <=capacity
当我们需要读数据时需要使用flip方法重置position
Buffer对象中的方法
方法 | 说明 |
---|---|
get() | 读取单个字节 |
put() | 写入缓冲区 |
Buffer clear() | 清空缓冲区并返回对缓冲区的引用 |
Buffer flip( ) | 为将缓冲区的界限设置为当前位置,并将当前位置充值为Оint capacity返回 Buffer 的 capacity大小 |
boolean hasRemaining( ) | 判断缓冲区中是否还有元素int limit(返回 Buffer 的界限(limit)的位置 |
Buffer limit(int n) | 将设置缓冲区界限为 n,并返回一个具有新limit的缓冲区对象 |
Buffer mark() | 对缓冲区设置标记 |
int position() | 返回缓冲区的当前位置 position |
Buffer position(int n) | 将设置缓冲区的当前位置为n ,并返回修改后的Buffer 对象 |
int remaining() | 返回 position和 limit 之间的元素个数 |
Buffer reset() | 将位置position转到以前设置的 mark所在的位置 |
Buffer rewind() | 将位置设为0,取消设置的 mark |
Buffer读写数据步骤
- 写入数据到Buffer
- 使用
flip()
方法,转换为读取模式 - 从Buffer中读取数据
- 调用
clear()
方法或compact()
方法清除缓冲区
常用方法实例
package test;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class Test {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(256);
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
System.out.println(buffer.mark());
System.out.println("================");
buffer.put("element1".getBytes(StandardCharsets.UTF_8));
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
System.out.println(buffer.mark());
//使用flip
buffer.flip();
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.mark());
//读取数据,先执行flip方法
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
String s = new String(bytes);
System.out.println(s);
}
}
Selector选择器
Selector是一个Java NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率
选择器(Selector)是SelectableChannle对象的多路复用器,Selector可以同时监控多个SelectableChannel的lO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel。Selector是非阻塞IO的核心
选择器可以监测多个注册通道上是否有事件的发生,若事件发生,获取事件然后进行相应处理。以达到单线程管理多通道(管理多个连接和请求),只有在连接通道时有读写事件发生时,才会进行读写,大大减少了系统的开销,并不必要为了每个连接都去创建一个线程,无需维护多个线程,避免了多线程之间的上下文切换导致的开销问题
使用场景
当NIO中使用单线程但处理多个客户端连接时,就会使用Selector选择器了
创建选择器
使用open
方法进行创建
Selector open = Selector.open();
将通道注册到选择器中
package test.channel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
public class SelectorDemo1 {
public static void main(String[] args) throws IOException {
//创建通道
ServerSocketChannel sChannel = ServerSocketChannel.open();
//切换为非阻塞模式
sChannel.configureBlocking(false);
//绑定连接,设置端口
sChannel.bind(new InetSocketAddress(9999));
//获取选择器
Selector selector = Selector.open();
//通道注册
sChannel.register(selector, SelectionKey.OP_ACCEPT);
//接下来就可以开启轮询获取事件了
}
}
关于SelectionKey
是监听事件类型
- OP_READ = 1 << 0:表示1,读操作
- OP_WRITE = 1 << 2:表示4,写操作
- OP_CONNECT = 1 << 3:表示8,连接操作
- OP_ACCEPT = 1 << 4:表示16,接收操作
Selector实例
Server
package test.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* NIO非阻塞通信服务端
*/
public class Server {
public static void main(String[] args) throws Exception {
//获取通道
ServerSocketChannel channel = ServerSocketChannel.open();
//切换非阻塞模式
channel.configureBlocking(false);
//提供端口进行绑定,开放给客户端
channel.bind(new InetSocketAddress(9999));
//获取选择器
Selector selector = Selector.open();
//将通道注册到选择器中,指定监听事件
channel.register(selector, SelectionKey.OP_ACCEPT);
//使用选择器进行轮询准备好的事件,使用select函数,大于0表示有事件
try{
while (selector.select() > 0) {
//获取选择器中所有注册通道的就绪好的事件
//使用迭代器进行获取,选择器的selectedKeys().iterator()
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
//遍历事件
while (it.hasNext()) {
//提取当前事件
SelectionKey selectionKey = it.next();
//判断事件类型
//判断是否为接收事件
if (selectionKey.isAcceptable()) {
//获取当前接入的客户端通道
SocketChannel sChannel = channel.accept();
//切换为非阻塞模式
sChannel.configureBlocking(false);
//将客户端通道注册到选择器中,注册读事件(客户端是写,服务端是读)
sChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
//若是读事件,获取客户端通道
SocketChannel sChannel = (SocketChannel) selectionKey.channel();
//读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len = sChannel.read(buffer)) > 0) {
//切换读取模式
buffer.flip();
System.out.println(new String(buffer.array(), 0, len));
//归位
buffer.clear();
}
}
//处理完毕,进行移除,否则会重复进行处理
it.remove();
}
}
}catch (Exception e){
System.out.println("connect end");
}
}
}
Client
package test.nio;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* NIO非阻塞客户端
*/
public class Client {
public static void main(String[] args) throws Exception {
//获取服务端提供的通道进行连接,对应IP地址以及端口号
SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
//切换非阻塞模式
channel.configureBlocking(false);
//分配缓冲区大小
ByteBuffer buffer = ByteBuffer.allocate(1024);
//发送数据到服务端
Scanner scanner = new Scanner(System.in);
while (true){
System.out.print("client:");
String str = scanner.nextLine();
buffer.put(("client:"+str).getBytes(StandardCharsets.UTF_8));
buffer.flip();
//写到缓冲区中
channel.write(buffer);
//清除,归位
buffer.clear();
//指定退出
if ("bye".equals(str)){
break;
}
}
}
}
测试
直接缓冲区与非直接缓冲区
byte ,buffer可以是两种类型,一种是基于直接内存(也就是非堆内存),另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理。
从数据流的角度,非直接内存是下面这样的作用链:
直接内存:
很明显,在做IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。
直接内存使用allocateDirect
创建,但是它比申请普通的堆内存需要耗费更高的性能。不过,这部分的数据是在JVM之外的,因此它不会占用应用的内存。
所以呢,当你有很大的数据要缓存,并且它的生命周期又很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐直接使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其isDirect()方法来确定。
使用直接内存
ByteBuffer buffer1 = ByteBuffer.allocateDirect(1024);
//isDirect()判断是否为直接内存
System.out.println(buffer1.isDirect());