Java NIO 中的 selector channel buffer
一 buffer 缓冲区
NIO的Buffer(缓冲区)本质上是一个内存块,既可以写入数据,也可以从中读取数据。 实际上是一个容器,一个连续数组。读写的数据都必须经过Buffer。
Buffer类是一个抽象类,且 Buffer 类是一个非线程安全类。
缓冲区类:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、 MappedByteBuffer。
-
-
- 前7种Buffer类型,覆盖了Java基本数据类型。第8种类型 MappedByteBuffer是专门用于内存映射的一种ByteBuffer类型。
- 不同的Buffer子类,其能操作 的数据类型能够通过名称进行判断,比如IntBuffer只能操作Integer类型的对象。
- 实际上,使用最多的还是ByteBuffer二进制字节缓冲区类型。
-
Buffer类 中 重要的成员属性:
capacity(容量):表示内部容量的大小。 属性一旦初始化,就不能再改变。
position(读写位置):表示当前的位置。position属性的值与缓冲区的读写模式有关。 在不同的模式下,position属性值的含义是不同的,在缓冲区进行读写的模式改变时,position 值会进行相应的调整。使用(即调用)flip() 翻转方法。
在写模式下:(1)进入到写入模式时,position值为0,表示当前的写入位置为从头开始。 (2)每当一个数据写到缓冲区之后,position会向后移动到下一个可写的位置。 (3)初始的position值为0,最大可写值为limit–1。当position值达到limit时,缓冲区就 已经无空间可写了。
在读模式下:(1)当缓冲区刚开始进入到读取模式时,position会被重置为0。 (2)从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动到 下一个可读的位置。 (3)在读模式下,limit表示可以读上限。position的最大值,为最大可读上限limit,当 position达到limit时,表明缓冲区已经无数据可读。
在从写入模式到读取模式的flip翻转过程中,position和limit属性值会进行调整,具体的 规则是: (1)limit属性被设置成写入模式时的position值,表示可以读取的最大数据位置; (2)position由原来的写入位置,变成新的可读位置,也就是0,表示可以从头开始读。
limit(读写的限制):表示可以写入或者读取的最大上限。
mark (读写位置的临时备份):读位置或者写位置的一个备份,供后续恢复时使用。
以下是简单的代码示例:
//buffer的示例 public static void nioBufferEg01(){ //创建一个intbuffer //allocate() 为创建缓冲区,大小capacity为10, IntBuffer intBuffer = IntBuffer.allocate(10); //put 方法添加元素 intBuffer.put(10); intBuffer.put(11); intBuffer.put(12); intBuffer.put(13); //intBuffer.put(0); //读写切换 intBuffer.flip(); //读出数据 //用get方法每次从position的位置读取一个数据 可以指定下标读取哪一个元素 get(i) //hasRemaining() 方法表示 当前位置和限制之间是否有任何元素。当且仅当此缓冲区中至少剩余一个元素时为true。 while (intBuffer.hasRemaining()) { System.out.println(intBuffer.get()); } //clear 方法是将 position 与 limit 值 变为初始化的 0 与 10 与 数组中的 原有元素无关 intBuffer.clear(); //这里只是替换了第一个元素的值 intBuffer.put(20); intBuffer.flip(); //翻转后 再读取也只是读取 第一个元素,如果手动修改 limit 值为2,则会获取到 原有元素下标为1的 11 while (intBuffer.hasRemaining()) { System.out.println(intBuffer.get()); } }
capacity大小 为固定10 ,存入4个 也是有10个元素,只是 翻转 读取的时候 limit 变为 position 的值 ,position变为0 从头开始读取。
这里可以看到 执行 clear 方法后 将 position 与 limit 值 变为初始化的 0 与 10 ,put 一个元素 翻转后 只是 limit 变为了 1
这时候手动修改 limit 值 2 就可以 取到 下标为 0,1 的 两个元素,一个是新值,一个是旧值。所以 clear 方法与原数组内容无关
总体来说,使用JavaNIOBuffer类的基本步骤如下:
(1)使用创建子类实例对象的allocate()方法,创建一个Buffer类的实例对象。
(2)调用put()方法,将数据写入到缓冲区中。
(3)写入完成后,在开始读取数据前,调用Buffer.flip()方法,将缓冲区转换为读模式。
(4)调用get()方法,可以从缓冲区中读取数据。
(5)读取完成后,调用Buffer.clear()方法,将缓冲区转换为写入 模式,可以继续写入。
二 channel 通道
Channel 和 BIO中 Stream(流) 是差不多的。在BIO中,一个网络连接会关联到两个流:输入流(InputStream),输出流(OutputStream),通过这两个流,进行输入和输出的操作。
在NIO中,一个网络连接使用一个Channel(通道),IO操作都是通过 连接通道完成。一个通道类似于BIO中两个流的结合,既可以读取数据,也可以写入数据,所以 Channel(通道)双向的。
JavaNIO中,一个socket连接使用一个Channel(通道),不同网络传输协议类型,在Java中都有不同的 NIO Channel(通道)相对应。
JavaNIO 中 Channel 主要实现:
1. FileChannel 用于文件IO操作 ;
2.DatagramChannel 数据报通道,用于UDP协议的数据读写;
3.SocketChannel 用于Socket套接字TCP连接的数据读写;
4.ServerSocketChannel 服务器套接字通道(或服务器监听通道),允许我们监听TCP 连接请求,为每个监听到的请求,创建一个SocketChannel套接字通道;
代码示例1:File Channel 的使用,使用一个buffer 完成文件的 读写
public static void nioFileChannelEg01() throws Exception { //文件inputStream 流 FileInputStream fileInputStream = new FileInputStream("hello.txt");//这里文件可以随意定义 //获取 channel FileChannel fileChannel01 = fileInputStream.getChannel(); //文件outputStream 流 FileOutputStream fileOutputStream = new FileOutputStream("JavaHello.txt");//这里文件可以随意定义 //获取 channel FileChannel fileChannel02 = fileOutputStream.getChannel(); //buffer 缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(128); //循环读取文件内容 while (true) { //这里需要 buffer.clear 重置一下,如果没有重置 position 与limit 会相等为0 不会进入read ==-1 的判断 byteBuffer.clear(); //将 input channel 中的内容读到 buffer 中 int read = fileChannel01.read(byteBuffer); System.out.println("read" + read);//这里观察一下 read 每次读多少 ,验证buffer.clear方法 if (read == -1) { break; } //翻转buffer byteBuffer.flip(); System.out.println(new String(byteBuffer.array()));//将buffer 内容输出到控制台 //将 buffer 中的内容写到 output channel 中 fileChannel02.write(byteBuffer); } //关闭相关的 流 fileChannel02.close(); fileInputStream.close(); }
代码示例2:使用 channel 的 transferFrom 方法 进行文件的 复制
public static void nioFileChannelCopyEg02() throws Exception { //文件inputStream 流 FileInputStream fileInputStream = new FileInputStream("hello.txt"); //获取 channel FileChannel fileInputChannel = fileInputStream.getChannel(); //文件outputStream 流 FileOutputStream fileOutputStream = new FileOutputStream("HelloJava.txt"); //使用 channel 的 transferFrom 进行文件 copy(第一个参数是 目标,第二个参数是 从0开始,第三个参数是 在哪里结束) fileOutputStream.getChannel().transferFrom(fileInputChannel, 0, fileInputChannel.size()); //关闭相关的 流 fileInputStream.close(); fileOutputStream.close(); }
补充代码 mappedByteBuffer 的简单使用 :可以让文件 直接在内存(堆外内存)修改,操作系统不需要拷贝一次(具体内容后续补充)
// mappedByteBuffer 可以让文件 直接在内存(堆外内存)修改,操作系统不需要拷贝一次 public static void nioMappedByteBuffer() throws Exception { RandomAccessFile rw = new RandomAccessFile("hello.txt", "rw"); FileChannel channel = rw.getChannel(); //参数一:FileChannel.MapMode.READ_WRITE 使用读写模式。 参数二:直接修改的起始位置 0开始。参数三 :映射到内存的大小,就是 修改的范围是0-4 MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); mappedByteBuffer.put(0,(byte) 72); //写入 H 的 ASCII 码 mappedByteBuffer.put(4,(byte) '4');//最多修改到这里 //mappedByteBuffer.put(5,(byte) '8');//这里会 java.lang.IndexOutOfBoundsException mappedByteBuffer.force();//手动将内存数据刷新到 磁盘 rw.close(); System.out.println("success!"); }
示例代码3:ServerSocketChannel 与 SocketChannel 通过 buff[] 数组的形式 分散聚合 数据( 代码只适用于演示,不能做生产用途,存在 bug )
/** * Scattering:将数据写入到buffer时:可以采用buffer数组,依次写入 [分散] * Gathering: 从buffer读取数据时,可以采用buffer数组,依次读 */ public static void nioBufferScatteringAndGatheringDemo() throws Exception { //使用 ServerSocketChannel和 SocketChannel 网络socket ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress(7000); //绑定端口到socket,并启动 serverSocketChannel.socket().bind(inetSocketAddress); System.out.println("serverSocket 启动 端口 7000!"); //创建buffer数组 ByteBuffer[] byteBuffers = new ByteBuffer[2]; byteBuffers[0] = ByteBuffer.allocate(5); byteBuffers[1] = ByteBuffer.allocate(2); //等待连接(telnet测试连接) SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("连接请求:" + socketChannel.getRemoteAddress()); int messageLength = 0; while (true) {//循环读取 long read = socketChannel.read(byteBuffers); System.out.println("read" + read);//当前读了多少个元素 messageLength += read; System.out.println(new String(byteBuffers[0].array())); System.out.println(new String(byteBuffers[1].array())); //输出元素位置 观察 position limit Arrays.stream(byteBuffers).map(buffer -> "position=" + buffer.position() + ",limit=" + buffer.limit()).forEach(System.out::println); //将所有的 buff 翻转 Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.flip()); //将数据 读出 显示到客户端 long w = socketChannel.write(byteBuffers);//回送 //当前写了多少个元素 System.out.println("byteWrite = " + w); //将所有的buffer 进行clear Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear()); //当前循环读了多少个元素, 写了多少个元素, 消息总长度是多少。 System.out.println("byteRead=" + read + ",byteWrite=" + w + ",messageLength= " + messageLength); } }
结果:平台输出
三 selector 选择器
1. selector 选择器的使命是完成IO的多路复用,其主要工作是通道的注册、监听、事件查询。一个 channel 通道代表一条连接通路 ,通过选择器可以同时监控多个通道的IO(输入输出) 状况。
2. 选择器和通道的关系,是监控和被监控的关系。 选择器提供了独特的API方法,能够选出(select)所监控的通道已经发生了哪些IO事件。
在NIO编程中,一般是一个单线程处理一个选择器,一个选择器可以监控很多通道。所 以,通过选择器,一个单线程可以处理数百、数千、数万、甚至更多的通道。这样会大量地减少线程之间上下文 切换的开销。
通道和选择器之间,通过register(注册)的方式完成。调用通道的 Channel.register (Selector sel,int ops)方法,可以将通道实例注册到一个选择器中。
register方法有 两个参数:第一个参数,指定通道注册到的选择器实例;第二个参数,指定选择器要监控的IO事 件类型。
3. 可供选择器监控的通道IO事件类型,包括以下四种:
(1)可读:SelectionKey.OP_READ
(2)可写:SelectionKey.OP_WRITE
(3)连接:SelectionKey.OP_CONNECT
(4)接收:SelectionKey.OP_ACCEPT
以上的事件类型常量定义在SelectionKey类中。如果选择器要监控通道的多种事件,用“位或”运算符来实现( int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE )。
4.SelectionKey选择键,Selector不直接管理Channel,而是直接管理SelectionKey,通过SelectionKey与 Channel发生关系。两核心成员keys、selectedKeys定义在Selector的抽象实现类SelectorImpl中。
一个Channel 最多能向 Selector 注册一次,注册 之后就形成了唯一的SelectionKey,被Selector管理起来。
核心成员keys, 专门管理注册的 SelectionKey,Channel注册到Selector后 创建一个唯一的 SelectionKey,添加在 keys 成员中,(这是一个HashSet类型的集合)。
核心成员selectedKeys,用于存放已经发生了IO事件的SelectionKey。
SelectionKey是IO事件的记录者(或存储者),SelectionKey 有两个核心成员, 存储着自己关联的Channel上的感兴趣IO事件和已经发生的IO事件。int interestOps(),int readyOps(),通过比特位来完成记录多个事件。
具体的IO事件所占用的哪一个比特位, 通过常量的方式定义在SelectionKey中。
5.SelectableChannel可选择通道。不是所有的通道,都是可以被选择器监控或选择的
FileChannel 就不能被选择器复用。判断一个通道能否被选择器监控或选择,有一个前提:判断它是否继承 了抽象类SelectableChannel(可选择通道),如果是则可以被选择,否则不能。
简单地说,一条通道若能被选择,必须继承SelectableChannel类。 它提供了实现通道的可选择性所需要的公共方法。
Java NIO中所有网络链接Socket套接字通道,都继承了SelectableChannel类,是可选择的。 而FileChannel文件通道,没有继承SelectableChannel,因此不是可选择通道。
6. 选择器使用流程 : (1)获取选择器实例; (2)将通道注册到选择器中; (3)轮询感兴趣的IO就绪事件(选择键集合)。
第一步:获取选择器实例。选择器实例是通过调用静态工厂方法open()来获取的,具体 如下:
1.//调用静态工厂方法open()来获取Selector实例
Selector selector = Selector.open();
第二步:将通道注册到选择器实例。要实现选择器管理通道,需要将通道注册到相应的 选择器上,
// 2.获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4.绑定连接
serverSocketChannel.bind(new InetSocketAddress(18899));
// 5.将通道注册到选择器上,并制定监听事件为:“接收连接”事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
第三步:选出感兴趣的IO就绪事件(选择键集合)。通过Selector选择器的select()方法,选出已经注册的、已经就绪的IO事件,并且保存到SelectionKey选择键集合中。
SelectionKey 集合保存在选择器实例内部,其元素为SelectionKey类型实例。调用选择器的selectedKeys() 方法,可以取得选择键集合。 接下来,需要迭代集合的每一个选择键,根据具体IO事件类型,执行对应的业务操作。
大致的处理流程如下:
//轮询,选择感兴趣的IO就绪事件(选择键集合)
while (selector.select() > 0) {
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
//根据具体的IO事件类型,执行对应的业务操作
if(key.isAcceptable()) {
// IO事件:ServerSocketChannel服务器监听通道有新连接
} else if (key.isConnectable()) { // IO事件:传输通道连接成功
} else if (key.isReadable()) { // IO事件:传输通道可读
} else if (key.isWritable()) { // IO事件:传输通道可写 }
//处理完成后,移除选择键
keyIterator.remove();
}}
处理完成后,需要将选择键从这个SelectionKey集合中移除,防止下一次循环的时候, 被重复的处理。
SelectionKey集合不能添加元素,如果试图向SelectionKey选择键集合中添加 元素,则将抛出java.lang.UnsupportedOperationException异常。
用于选择就绪的IO事件的select()方法,有多个重载的实现版本,具体如下:
(1)select():阻塞调用,一直到至少有一个通道发生了注册的IO事件。
(2)select(long timeout):和select()一样,但最长阻塞时间为timeout指定的毫秒数。
(3)selectNow():非阻塞,不管有没有IO事件,都会立刻返回。 select()方法的返回值的是整数类型(int),表示发生了IO事件的数量。
更准确地说,是 从上一次select到这一次select之间,有多少通道发生了IO事件,更加准确地说,是指发生了 选择器感兴趣(注册过)的IO事件数。
代码示例 1: 读取客户端通道的输入数据,读取完成后直接关闭
服务端代码
public static void selectorSeverDemo() throws Exception { // 1.获取选择器 Selector selector = Selector.open(); // 2.获取通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 3.设置为非阻塞 serverSocketChannel.configureBlocking(false); // 4.绑定连接 serverSocketChannel.bind(new InetSocketAddress(5555)); System.out.println("服务器启动成功"); // 5.将通道注册的“接收新连接”IO事件,注册到选择器上 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 6.轮询感兴趣的IO就绪事件(选择键集合) while (selector.select() > 0) { // 7.获取选择键集合 Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator(); while (selectedKeys.hasNext()) { // 8.获取单个的选择键,并处理 SelectionKey selectedKey = selectedKeys.next(); // 9.判断key是具体的什么事件 if (selectedKey.isAcceptable()) { // 10.若选择键的IO事件是“连接就绪”事件,就获取客户端连接 SocketChannel socketChannel = serverSocketChannel.accept(); // 11.将新连接切换为非阻塞模式 socketChannel.configureBlocking(false); // 12.将该新连接的通道的可读事件,注册到选择器上 socketChannel.register(selector, SelectionKey.OP_READ); } else if (selectedKey.isReadable()) { // 13.若选择键的IO事件是“可读”事件, 读取数据 SocketChannel socketChannel = (SocketChannel) selectedKey.channel(); // 14.读取数据,然后丢弃 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int length = 0; while ((length = socketChannel.read(byteBuffer)) > 0) { byteBuffer.flip();//翻转buffer System.out.println(new String(byteBuffer.array(), 0, length)); byteBuffer.clear(); } socketChannel.close(); } // 15.移除选择键 selectedKeys.remove(); } } // 16.关闭连接 serverSocketChannel.close(); }
客户端代码:
public static void startClient() throws Exception { InetSocketAddress address = new InetSocketAddress("127.0.0.1",5555); // 1.获取通道 SocketChannel socketChannel = SocketChannel.open(address); // 2.切换成非阻塞模式 socketChannel.configureBlocking(false); //不断地自旋、等待连接完成,或者做一些其他的事情 while (!socketChannel.finishConnect()) { } System.out.println("客户端连接成功!"); // 3.分配指定大小的缓冲区 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put("hello world Java NIO".getBytes()); byteBuffer.flip(); //发送到服务器 socketChannel.write(byteBuffer); socketChannel.shutdownOutput(); socketChannel.close(); }