三、NIO
1、NIO三大核心部分:
Channel:通道,可以理解为是铁路;
Buffer:缓冲区,可以理解为火车;程序不是直接通过channel读写数据,而是通过buffer。这很好理解,火车装着货物跑到铁路上,对应了buffer装着数据跑在channel上;
Selector:选择器,就是上面NIO模型图中的selector,selector发现这个通道有内容要读取,就处理这个通道,如果这个通道没啥事儿,它不会阻塞在这里等这个通道,而是去看别的通道有没有内容要读取,如果都没有,管理selector的这个线程还可以去做别的事。
selector、buffer、channel之间的关系:
每个channel都会对应一个buffer;一个channel可以理解为就是一个连接;
一个selector对应一个线程;一个selector对应多个channel;
程序切换到哪个channel是由事件决定;
selector会根据不同的事件,在各通道上切换;
buffer底层是一个数组;
数据的读取和写入是通过buffer来完成的;BIO的读取和写入是通过输入输出流,不能双向,而buffer是双向的;
2、buffer:
buffer有四个重要的属性:
capacity:容量,该buffer能够容纳的最大数据量,缓冲区创建时被设定且不能修改;
limit:缓冲区当前的终点,不能对缓冲区超出终点的位置进行读写,limit是可变的;
position:下一个要被读或写的元素的索引,每次读写都会改变该值;
mark:标记;
buffer属性读取数据的时候可以设置position和limit,表示从哪儿开始读,读到哪儿结束。
3、channel:
channel类似BIO的流,但是有些区别,如下:
channel是一个接口,用得比较多的实现有如下几个:
---
看几个实操案例:
public class NioFileChannel01 {
public static void main(String[] args) throws IOException {
String str = "带你去爬山啊";
FileOutputStream fos = new FileOutputStream("C:\\Users\\14751\\Desktop\\test01.txt");
// 1. 通过FileOutputStream获取对应的FileChannel
FileChannel fc = fos.getChannel();
// 2. 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 3. 将str放入buffer中
buffer.put(str.getBytes());
// 4. 切换写数据模式
buffer.flip();
// 5. 将buffer数据写入到通道
fc.write(buffer);
// 6. 关闭资源
fos.close();
fc.close();
}
}
public class NioFileChannel02 {
public static void main(String[] args) throws IOException {
// 1. 读取test01.txt文件
File file = new File("C:\\Users\\14751\\Desktop\\test01.txt");
// 2. 将file转成FileInputStream
FileInputStream fis = new FileInputStream(file);
// 3. 获取通道
FileChannel channel = fis.getChannel();
// 4. 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate((int)file.length());
// 5. 将通道数据读到buffer中
channel.read(buffer);
System.out.println(new String(buffer.array()));
// 6. 关闭资源
fis.close();
channel.close();
}
}
public class NioFileChannel03 {
public static void main(String[] args) throws IOException {
// 1. 读取源文件
FileInputStream fis = new FileInputStream("C:\\Users\\14751\\Desktop\\test01.txt");
// 2. 获取通道
FileChannel sourceChannel = fis.getChannel();
// 3. 加载目标文件
FileOutputStream fos = new FileOutputStream("C:\\Users\\14751\\Desktop\\test02.txt");
// 4. 获取通道
FileChannel targetChannel = fos.getChannel();
// 5. 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
// 6. 标志位复位,一定不能漏了这步,否则死循环
buffer.clear();
// 7. 读取数据
int read = sourceChannel.read(buffer);
if (read == -1) {
break;
}
// 8. 切换到写数据模式,并将buffer中的数据写入到targetChannel
buffer.flip();
targetChannel.write(buffer);
}
// 9. 关闭资源
fis.close();
sourceChannel.close();
fos.close();
targetChannel.close();
}
}
public class NioFileChannel04 {
public static void main(String[] args) throws IOException {
// 1. 读取源文件
FileInputStream fis = new FileInputStream("C:\\Users\\14751\\Desktop\\test01.txt");
// 2. 获取通道
FileChannel sourceChannel = fis.getChannel();
// 3. 加载目标文件
FileOutputStream fos = new FileOutputStream("C:\\Users\\14751\\Desktop\\test03.txt");
// 4. 获取通道
FileChannel targetChannel = fos.getChannel();
// 5. 使用transferFrom完成拷贝
targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
// 6. 关闭资源
fis.close();
sourceChannel.close();
fos.close();
targetChannel.close();
}
}
public class NioFileChannel05 {
public static void main(String[] args) throws IOException {
// 1. 加载文件
RandomAccessFile file = new RandomAccessFile("C:\\Users\\14751\\Desktop\\test01.txt", "rw"); // rw表示读写
// 2. 获取文件通道
FileChannel channel = file.getChannel();
// 3. 获取MappedByteBuffer,这三个参数,第一个表示读写模式,第二个表示直接修改的起始位置,第三个表示映射到内存中的大小
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
// 4. 对test01.txt进行修改
buffer.put(0, (byte)'A'); // 第一个字符改成A
buffer.put(1, (byte)'B'); // 第二个字符改成B
// 5. 关闭资源
file.close();
channel.close();
}
}
/**
* scattering:将数据写入到buffer时,可以采用buffer数组,依次写入
* gathering:从buffer读数据的时候,可以采用buffer数组,依次读取
* @author zhu
*
*/
public class NioFileChannel06 {
public static void main(String[] args) throws IOException {
// 1. 创建channel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 2. 绑定端口并启动
InetSocketAddress address = new InetSocketAddress(6666);
serverChannel.socket().bind(address);
// 3. 创建buffer数组
ByteBuffer[] buffers = new ByteBuffer[2];
buffers[0] = ByteBuffer.allocate(5);
buffers[1] = ByteBuffer.allocate(4);
// 4. 等待客户端连接
SocketChannel channel = serverChannel.accept();
// 5. 循环读取
// 假设客户端会发送8个字节
int len = 8;
while (true) {
int read = 0;
while (read < len) {
long byteNum = channel.read(buffers);
read += byteNum;
System.out.println("读取到的字节数:" + read);
}
// 6. 切换模式
Arrays.asList(buffers).forEach(buffer -> buffer.flip());
// 7. 将读取到的数据显示到客户端
long writeLen = 0;
while (writeLen < len) {
long byteNum = channel.write(buffers);
writeLen += byteNum;
}
// 8. 将所有buffer进行clear
Arrays.asList(buffers).forEach(buffer -> buffer.clear());
}
}
}
4、selector:
selector能够检测多个通道是否有事件要发生,多个channel以事件的方式可以注册到同一个selector中。主要工作流程如下:
当客户端连接时,会通过severSocket channel得到对应的socketChannel,并且将socketChannel通过register方法注册到selector中,注册后返回一个selectionKey;
selector通过集合关联这个selectionKey;
selector通过select方法进行监听(select方法是阻塞的,也可以传入超时时间,阻塞指定的时间,还可以用selectNow方法,这个就是非阻塞的;NIO的非阻塞也就体现在这里),返回有事件发生的通道的个数;
selector可以得到有事件发生的通道的selectionKey;
通过selectionKey,就可以得到它对应的通道,然后就可以完成业务操作了。
---
看一个实操案例:用NIO实现服务端和客户端的通讯:
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1. 创建NIOServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 2. 得到Selector对象
Selector selector = Selector.open();
// 3. 绑定端口,进行监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
// 4. 设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 5. 把serverSocketChannel注册到selector中,设置关心事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6. 循环等待客户端连接
while (true) {
if (selector.select(1000) == 0) { // 没有事件
System.out.println("服务器等待了1秒钟,没有事件发生");
continue;
} else { // 有事件
// 7. 有事件发生,就拿到selectionKey的集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 8. 通过selectionKeys得到channel
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 9. 根据key的不同事件,做对应的处理
if (key.isAcceptable()) { // 如果是OP_ACCEPT连接事件
// 10. 为该客户端生成一个SocketChannel并设置成非阻塞
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 11. 将当前socketChannel也注册到selector中,关注事件为OP_READ,并且关联一个Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
}
if (key.isReadable()) { // 如果是OP_READ读取事件
// 12. 通过key得到channel
SocketChannel channel = (SocketChannel) key.channel();
// 13. 获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 14. 将channel中的数据读到buffer中去
channel.read(buffer);
System.out.println("客户端发送的数据:" + new String(buffer.array()));
}
// 15. 移除当前的selectionKey,防止重复操作
keyIterator.remove();
}
}
}
}
}
public class NIOClient {
public static void main(String[] args) throws IOException {
// 1. 设置ip和端口
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
// 2. 创建SocketChannel并设置成非阻塞
SocketChannel socketChannel = SocketChannel.open(address);
socketChannel.configureBlocking(false);
// 3. 连接服务器
String str = "hello world";
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
// 4. 将数据写入channel
socketChannel.write(buffer);
System.in.read();
}