Java NIO 三大核心(BufferChannelSelector)理解
Posted 傲骄鹿先生
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java NIO 三大核心(BufferChannelSelector)理解相关的知识,希望对你有一定的参考价值。
我是傲骄鹿先生,沉淀、学习、分享、成长。
如果你觉得文章内容还不错的话,希望不吝您的「一键三连」,文章里面有不足的地方希望各位在评论区补充疑惑、见解以及面试中遇到的奇葩问题
目录
一、Buffer 的机制及子类
1、Buffer(缓冲区)基本介绍
缓冲区本质上是一个可以读写数据的内存块,可以理解为是一个容器对象(含数组)
,该对象提供了一组方法
,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。
Channel 提供从文件、网络读取数据的渠道,但是读取或者都必须经过 Buffer。
在 Buffer 子类中维护着一个对应类型的数组,用来存放数据:
public abstract class IntBuffer extends Buffer implements Comparable<IntBuffer>
// These fields are declared here rather than in Heap-X-Buffer in order to
// reduce the number of virtual method invocations needed to access these
// values, which is especially costly when coding small buffers.
//
final int[] hb; // Non-null only for heap buffers
final int offset;
boolean isReadOnly; // Valid only for heap buffers
// Creates a new buffer with the given mark, position, limit, capacity,
// backing array, and array offset
//
IntBuffer(int mark, int pos, int lim, int cap, // package-private
int[] hb, int offset)
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
// Creates a new buffer with the given mark, position, limit, and capacity
//
IntBuffer(int mark, int pos, int lim, int cap) // package-private
this(mark, pos, lim, cap, null, 0);
Buffer 常用子类 | 描述 |
---|---|
ByteBuffer | 存储字节数据到缓冲区 |
ShortBuffer | 存储字符串数据到缓冲区 |
CharBuffer | 存储字符数据到缓冲区 |
IntBuffer | 存储整数数据据到缓冲区 |
LongBuffer | 存储长整型数据到缓冲区 |
DoubleBuffer | 存储浮点型数据到缓冲区 |
FloatBuffer | 存储浮点型数据到缓冲区 |
Buffer 中定义了四个属性来提供包含的数据元素。
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
capacity | 容量,即可以容纳的最大数据量;在缓冲区被创建时候就被指定,无法修改 |
limit | 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作,但极限是可以修改的 |
position | 当前位置,下一个要被读或者写的索引,每次读写缓冲区数据都会改变该值,为下次读写做准备 |
Mark | 标记当前 position 位置,当 reset 后回到标记位置。 |
二、Channel 的基本介绍
NIO 的通道类似于流,但有如下区别:
- 通道是双向的可以进行读写,而流是单向的只能读,或者写。
- 通道可以实现异步读写数据。
- 通道可以从缓冲区读取数据,也可以写入数据到缓冲区。
常用的 Channel 有:FileChannel、DatagramChannel、SocketChannel、SocketServerChannel。
1、FileChannel 类
FileChannel 主要用来对本地文件进行 IO 操作,常见的方法有:
- public int read(ByteBuffer dst) :从通道中读取数据到缓冲区中。
- public int write(ByteBuffer src):把缓冲区中的数据写入到通道中。
- public long transferFrom(ReadableByteChannel src,long position,long count):从目标通道中复制数据到当前通道。
- public long transferTo(long position,long count,WriteableByteChannel target):把数据从当前通道复制给目标通道。
2、使用 FileChannel 写入文本文件
public class NIOFileChannel
public static void main(String[] args) throws IOException
String str = "Hello,Java菜鸟程序员";
//创建一个输出流
FileOutputStream fileOutputStream = new FileOutputStream("hello.txt");
//获取通道
FileChannel channel = fileOutputStream.getChannel();
//创建缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(100);
//写入byteBuffer
byteBuffer.put(str.getBytes());
//切换模式
byteBuffer.flip();
//写入通道
channel.write(byteBuffer);
//关闭
channel.close();
fileOutputStream.close();
3、使用 FileChannel 读取文本文件
public class NIOFileChannel
public static void main(String[] args) throws IOException
FileInputStream fileInputStream = new FileInputStream("hello.txt");
FileChannel channel = fileInputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(100);
channel.read(byteBuffer);
System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));
//Hello,Java菜鸟程序员
channel.close();
fileInputStream.close();
4、使用 FileChannel 复制文件
public class NIOFileChannel
public static void main(String[] args) throws IOException
FileInputStream fileInputStream = new FileInputStream("hello.txt");
FileOutputStream fileOutputStream = new FileOutputStream("world.txt");
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1);
while (inChannel.read(byteBuffer) != -1)
byteBuffer.flip();
outChannel.write(byteBuffer);
//清空重置
byteBuffer.clear();
fileOutputStream.close();
fileInputStream.close();
5、使用 transferFrom 复制文件
public class NIOFileChannel
public static void main(String[] args) throws IOException
FileInputStream fileInputStream = new FileInputStream("hello.txt");
FileOutputStream fileOutputStream = new FileOutputStream("world.txt");
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
//从哪拷贝,从几开始到几结束 对应的还有transferTo()方法.
outChannel.transferFrom(inChannel, 0, inChannel.size());
outChannel.close();
inChannel.close();
fileOutputStream.close();
fileInputStream.close();
三、Channel 和 Buffer 的注意事项
- ByteBuffer 支持类型化的 put 和 get,put 放入什么数据类型,get 就应该使用相应的数据类型来取出,否则可能会产生 ByteUnderflowException 异常。
- 可以将一个普通的 Buffer 转换为只读的 Buffer:asReadOnlyBuffer()方法。
- NIO 提供了 MapperByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,而如何同步到文件由 NIO 来完成。
-
NIO 还支持通过多个 Buffer(即 Buffer 数组)完成读写操作,即Scattering(分散)和 Gathering(聚集)。
Scattering(分散)
:在向缓冲区写入数据时,可以使用 Buffer 数组依次写入,一个 Buffer 数组写满后,继续写入下一个 Buffer 数组。Gathering(聚集)
:从缓冲区读取数据时,可以依次读取,读完一个 Buffer 再按顺序读取下一个。
四、Selector
1、Selector 的基本介绍
- Java 的 NIO 使用了非阻塞的 I/O 方式。可以用一个线程处理若干个客户端连接,就会使用到 Selector(选择器)。
- Selector 能够检测到多个注册通道上是否有事件发生(多个 Channel 以事件的形式注册到同一个 selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。
- 只有在连接真正有读写事件发生时,才会进行读写,减少了系统开销,并且不必为每个连接都创建一个线程,不用维护多个线程。
- 避免了多线程之间上下文切换导致的开销。
2、Selector 特点
Netty 的 I/O 线程 NioEventLoop 聚合了 Selector(选择器 / 多路复用器),可以并发处理成百上千个客户端连接。
当线程从某客户端 Socket 通道进行读写时,若没有数据可用,该线程可以进行其他任务。
线程通常将非阻塞 I/O 的空闲时间用于其他通道上执行 I/O 操作,所以单独的线程可以管理多个输入输出通道。
由于读写操作都是非阻塞的,就可以充分提高 I/O 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构性能、弹性伸缩能力和可靠性都得到极大地提升。
3、Selector 常用方法
public abstract class Selector implement Closeable
public static Selector open(); //得到一个选择器对象
public int select(long timeout); //监控所有注册的通道,当其中的IO操作可以进行时,将对应的selectionkey加入内部集合并返回,参数设置超时时间
public Set<SelectionKey> selectionKeys(); //从内部集合中得到所有的SelectionKey
4、Selector 相关方法说明
selector.select()
://若未监听到注册管道中有事件,则持续阻塞selector.select(1000)
://阻塞 1000 毫秒,1000 毫秒后返回selector.wakeup()
://唤醒 selectorselector.selectNow()
: //不阻塞,立即返回
五、NIO 非阻塞网络编程过程分析
- 当客户端连接时,会通过 SeverSocketChannel 得到对应的 SocketChannel。
- Selector 进行监听,调用 select()方法,返回注册该 Selector 的所有通道中有事件发生的通道个数。
- 将 socketChannel 注册到 Selector 上,public final SelectionKey register(Selector sel, int ops),一个 selector 上可以注册多个 SocketChannel。
- 注册后返回一个 SelectionKey,会和该 Selector 关联(以集合的形式)。
- 进一步得到各个 SelectionKey,有事件发生。
- 再通过 SelectionKey 反向获取 SocketChannel,使用 channnel()方法。
- 可以通过得到的 channel,完成业务处理。
SelectionKey 中定义了四个操作标志位:OP_READ
表示通道中发生读事件;OP_WRITE
—表示通道中发生写事件;OP_CONNECT
—表示建立连接;OP_ACCEPT
—请求新连接。
NIO 非阻塞网络编程代码示例
public class Server
public static void main(String[] args) throws IOException
//创建serverSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//得到Selector对象
try (Selector selector = Selector.open())
//把ServerSocketChannel注册到selector,事件为OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//如果返回的>0,表示已经获取到关注的事件
while (selector.select() > 0)
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext())
//获得到一个事件
SelectionKey next = iterator.next();
//如果是OP_ACCEPT,表示有新的客户端连接
if (next.isAcceptable())
//给该客户端生成一个SocketChannel
SocketChannel accept = serverSocketChannel.accept();
accept.configureBlocking(false);
//将当前的socketChannel注册到selector,关注事件为读事件,同时给socket Channel关联一个buffer
accept.register(selector, SelectionKey.OP_READ,ByteBuffer.allocate(1024));
System.out.println("获取到一个客户端连接");
//如果是读事件
else if (next.isReadable())
//通过key 反向获取到对应的channel
SocketChannel channel = (SocketChannel) next.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer) next.attachment();
while (channel.read(buffer) != -1)
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
buffer.clear();
iterator.remove();
public class Client
public static void main(String[] args) throws IOException
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞
socketChannel.configureBlocking(false);
//提供服务器端的IP和端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if (!socketChannel.connect(inetSocketAddress))
while (!socketChannel.finishConnect())
System.out.println("连接需要时间,客户端不会阻塞...先去吃个宵夜");
//连接成功,发送数据
String str = "hello,Java菜鸟程序员";
ByteBuffer byteBuffer = ByteBuffer.wrap(str.getBytes());
socketChannel.write(byteBuffer);
socketChannel.close();
System.out.println("客户端退出");
运行结果
系列文章持续更新,微信搜一搜「傲骄鹿先生 」,回复【面试】有准备的一线大厂面试资料。
以上是关于Java NIO 三大核心(BufferChannelSelector)理解的主要内容,如果未能解决你的问题,请参考以下文章
Java NIO 三大核心(BufferChannelSelector)理解
Java NIO 三大核心(BufferChannelSelector)理解