本篇文章是观看尚硅谷NIO视频,整理而出的笔记内容。
Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
NIO与IO的区别
NIO | IO |
---|---|
面向缓冲区(Buffer Oriented) | 面向流(Stream Oriented) |
非阻塞IO(Non Blocking IO) | 阻塞IO(Blocking IO) |
选择器(Selectors) | 无 |
@Test
public void testBuffer() {
// 分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("------allocate()-------");
//System.out.println(byteBuffer.mark());
System.out.println(byteBuffer.position()); // 0
System.out.println(byteBuffer.limit()); // 1024
System.out.println(byteBuffer.capacity()); // 1024
// put() 存入数据
String content = "abcde";
byteBuffer.put(content.getBytes());
System.out.println("------put()--------");
System.out.println(byteBuffer.position()); // 5
System.out.println(byteBuffer.limit()); // 1024
System.out.println(byteBuffer.capacity()); // 1024
// 切换到读模式 flip 将 limit = position,同时 position = 0
byteBuffer.flip();
System.out.println("------flip()--------");
System.out.println(byteBuffer.position()); // 0
System.out.println(byteBuffer.limit()); // 5
System.out.println(byteBuffer.capacity()); // 1024
// get() 读取数据
byte[] dest = new byte[byteBuffer.limit()];
byteBuffer.get(dest);
System.out.println("------get()--------");
System.out.println("读取到数据:" +new String(dest));
System.out.println(byteBuffer.position()); // 5
System.out.println(byteBuffer.limit()); // 5
System.out.println(byteBuffer.capacity()); // 1024
// rewind position = 0 ,可重复读
System.out.println("------rewind()--------");
byteBuffer.rewind();
System.out.println(byteBuffer.position()); // 0
System.out.println(byteBuffer.limit()); // 5
System.out.println(byteBuffer.capacity()); // 1024
// clear 清空缓冲区(缓冲区的数据还在,只是处于一种"被遗忘的状态",不知道缓冲区寸了多少数据) 回复初始状态
System.out.println("------clear()--------");
byteBuffer.clear();
System.out.println(byteBuffer.position()); // 0
System.out.println(byteBuffer.limit()); // 1024
System.out.println(byteBuffer.capacity()); // 1024
System.out.println((char)byteBuffer.get());// a
}
常用方法总结
方法 | 描述 |
---|---|
Buffer clear() | 清空缓冲区并返回对缓冲区的引用(put的数据并不会被清除) |
Buffer flip() | 将缓冲区的界限设置为当前位置,并将当前位置充值为 0 |
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 |
非直接缓冲区: 通过 allocate() 方法分配缓冲区,将缓冲区建立在JVM的内存中。
直接缓冲区: 通过allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。
ByteBuffer direct = ByteBuffer.allocateDirect(1024);
// isDirect() 可判断 缓冲区是否是直接缓冲区
System.out.println(direct.isDirect());// true
通道: 表示IO源目标打开的连接,类似传统IO的流,只不过,channel不能直接访问数据。通道负责传输,缓冲区负责存储数据。
java.nio.channels.Channel 有几个重要的实现类
java.nio.channels.FileChannel
java.nio.channels.SocketChannel : 适用于TCP
java.nio.channels.ServerSocketChannel: 适用于TCP
java.nio.channels.DatagramChannel: 适用于UDP传输
JAVA对支持通道的类提供了 getChannel() 方法,用于获取通道。
本地IO :
FileInputStream
FileOutputStream
RandomAccessFile
网络IO:
Socket
ServerSocket
DatagramSocket
途径二: jdk 1.7中的NIO.2提供了静态方法 open()
途径三: jdk 1.7 NIO.2 Files工具类 newByteChannel() 方法
案例: 本地IO实现文件复制
@Test
public void testChannel() throws IOException{
FileInputStream fis= new FileInputStream("002.jpg");
FileOutputStream fos = new FileOutputStream("2.jpg");
// 获取通道
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel();
// 指定缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将通道中的数据 写入 缓冲区
while (inChannel.read(buffer) != -1) {
buffer.flip();//切换到 读模式
//将缓冲区的数据写入通道
outChannel.write(buffer);
buffer.clear();
}
// 关闭通道
outChannel.close();
inChannel.close();
fos.close();
fis.close();
}
采用直接缓冲区,内存映射的方式实现复制
@Test
public void test2() throws IOException{
//采用直接缓冲区
//创建通道
FileChannel inChannel = FileChannel.open(Paths.get("002.jpg"), StandardOpenOption.READ);
// StandardOpenOption.CREATE_NEW 目标文件不存在就创建,存在会报错
// StandardOpenOption.CREATE 文件覆盖
FileChannel outChannel = FileChannel.open(Paths.get("3.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);
// 内存映射文件
MappedByteBuffer intMappedBuffer = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
// 直接对缓冲区 进行读写操作
byte[] dest = new byte[intMappedBuffer.limit()];
intMappedBuffer.get(dest);
outMappedBuffer.put(dest);
outChannel.close();
inChannel.close();
}
transferTo
transferFrom
//通道间的数据传输(直接缓冲区)
@Test
public void test3() throws IOException{
FileChannel inChannel = FileChannel.open(Paths.get("002.jpg"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("3.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.close();
inChannel.close();
}
分散读取: 将通道中的数据分散到多个缓冲区中(按照缓冲区的顺序,一次将缓冲区填满)。
聚集写入: 将多个缓冲区中的数据聚集到通道中(按照缓冲区的顺序,将position到limit之间的数据写入到通道)。
案例:
@Test
public void test4() throws IOException{
RandomAccessFile randomAccessFile = new RandomAccessFile("快捷指令.txt","rw");
// 获取通道
FileChannel channel = randomAccessFile.getChannel();
ByteBuffer bf1 = ByteBuffer.allocate(100);
ByteBuffer bf2 = ByteBuffer.allocate(1024);
// 分散读取
ByteBuffer[] bfs = new ByteBuffer[]{bf1,bf2};
channel.read(bfs);
for (ByteBuffer byteBuffer : bfs) {
byteBuffer.flip();
}
System.out.println(new String(bfs[0].array(),0,bfs[0].limit()));
System.out.println("=======");
System.out.println(new String(bfs[1].array(),0,bfs[1].limit()));
channel.close();
// 聚集写入
RandomAccessFile randomAccessFile2 = new RandomAccessFile("聚集写入.txt","rw");
FileChannel channel2 = randomAccessFile2.getChannel();
channel2.write(bfs);
channel2.close();
}
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。
// 获取选择器
Selector selector = Selector.open();
向选择器注册通道:SelectableChannel.register(Selector sel, int ops)
当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。
可以监听的事件类型(可使用 SelectionKey 的四个常量表示):
若注册时不止监听一个事件,则可以使用“位或”操作符连接。
int selectionKeys = SelectionKey.OP_READ|SelectionKey.OP_ACCEPT;
SelectionKey:表示 SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。
方法 | 描述 |
---|---|
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测 Channal 中读事件是否就绪 |
boolean isWritable() | 检测 Channal 中写事件是否就绪 |
boolean isConnectable() | 检测 Channel 中连接是否就绪 |
boolean isAcceptable() | 检测 Channel 中接收是否就绪 |
Selector 的常用方法
方法 | 描述 |
---|---|
Set keys() | 所有的 SelectionKey 集合。代表注册在该Selector上的Channel |
selectedKeys() | 被选择的 SelectionKey 集合。返回此Selector的已选择键集 |
int select() | 监控所有注册的Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应得的 SelectionKey 加入被选择的SelectionKey 集合中,该方法返回这些 Channel 的数量 |
int select(long timeout) | 可以设置超时时长的 select() 操作 |
int selectNow() | 执行一个立即返回的 select() 操作,该方法不会阻塞线程 |
Selector wakeup() | 使一个还未返回的 select() 方法立即返回 |
void close() | 关闭该选择器 |
案例演示,网络通信
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.time.LocalDateTime;
import java.util.Iterator;
import org.junit.Test;
public class NonBlockNIO {
@Test
public void client() throws IOException {
// 获取通道
SocketChannel scockChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
//切换成非阻塞模式
scockChannel.configureBlocking(false);
// 分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 发送数据给服务端
buffer.put(LocalDateTime.now().toString().getBytes());
buffer.flip();
scockChannel.write(buffer);
buffer.clear();
// 关闭通道
scockChannel.close();
}
@Test
public void server() throws IOException {
// 获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 切换到非阻塞模式
serverSocketChannel.configureBlocking(false);
// 绑定连接
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1",9898));
// 获取选择器
Selector selector = Selector.open();
// 将通道注册到选择器上,指定监听事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//一个选择器上可以注册多个通道
// 所以 轮询获取选择器上已经"准备就绪"的事件
while(selector.select() > 0) {
// 获取选择器 已就绪的监听事件
Iterator iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if(selectionKey.isAcceptable()) {// 接收事件
// 获取客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
// 切换 非阻塞
socketChannel.configureBlocking(false);
// 通道 注册到选择器
socketChannel.register(selector, selectionKey.OP_READ);
}else if(selectionKey.isReadable()) {// 读事件
//获取 "读"就绪的通道
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = 0;
while((len = channel.read(buffer)) > 0) {
buffer.flip();
System.out.println(new String(buffer.array(),0,len));
buffer.clear();
}
}else if(selectionKey.isWritable()) {// 写事件
}else if(selectionKey.isConnectable()) {// 连接事件
}else if(selectionKey.isValid()) {//是否有效
}
}
// 取消选择键
iterator.remove();
}
}
}
Java NIO中的DatagramChannel是一个能收发UDP包的通道。
操作步骤:
public class TestDatagramChannel {
@Test
public void client() throws IOException {
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello world".getBytes());
buffer.flip();
datagramChannel.send(buffer, new InetSocketAddress("127.0.0.1", 9898));
buffer.clear();
}
@Test
public void server() throws IOException {
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
datagramChannel.bind(new InetSocketAddress(9898));
Selector selector = Selector.open();
datagramChannel.register(selector, SelectionKey.OP_READ);
while(selector.select() > 0) {
Iterator iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if(selectionKey.isAcceptable()) {// 接收事件
}else if(selectionKey.isReadable()) {// 读事件
ByteBuffer buffer = ByteBuffer.allocate(1024);
datagramChannel.receive(buffer);
buffer.flip();
System.out.println(new String(buffer.array(),0,buffer.limit()));
buffer.clear();
}else if(selectionKey.isWritable()) {// 写事件
}else if(selectionKey.isConnectable()) {// 连接事件
}else if(selectionKey.isValid()) {//是否有效
}
}
// 取消选择键
iterator.remove();
}
}
}
Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
@Test
public void testPipe() throws IOException {
//获取管道
Pipe pipe = Pipe.open();
//分配缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
SinkChannel sink = pipe.sink();
buffer.put("单向管道pipe 发送数据".getBytes());
buffer.flip();
// 写入管道
sink.write(buffer);
//读取缓冲区
SourceChannel source = pipe.source();
buffer.flip();
int read = source.read(buffer);
System.out.println(new String(buffer.array(),0,read));
source.close();
sink.close();
}