缓冲区(Buffer)
缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。
Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
在NIO中,Buffer是一个顶级父类,它是一个抽象类,类的层级关系如图:(每个buffer实现类中都有对应数据类型的数组用来存储数据(所以buffer实际上数据是存在该数组中的))
通过allocate()获取缓冲区
//创建一个IntBuffer,大小为5,可以存放5个int
IntBuffer intBuffer = IntBuffer.allocate( 5 );
Buffer中重要的四个属性:
Capacity:容量,即缓冲区可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
Limit:界限,表示缓冲区的当前终点(可操作数据的大小),不能对缓冲区超过极限的位置进行读写操作。且界限是可以修改的。
Position:位置,下一个要被读或写的元素的索引(正在操作数据的位置),每次读写缓冲区数据时都会改变改值,为下次读写作准备
Mark:标记,表示记录当前Position的位置,可以通过reset()方法恢复到mark的位置
0<=Mark<=Position<=Limit<=Capacity
buffer类相关方法:
方法名 | 描述 |
---|---|
rewind() | 可重复读,position重新回到0读取(也有可能有别的方法,可以回到指定位置重复读) |
clear() | 回到最初状态,position为0,limit=capacity,但其中的数据并没有清空 |
mark() | 标记当前Position位置,mark=Position |
reset() | 恢复到Position位置,此时Position=mark |
hasRemaining() | 判断缓冲区中是否还有可操作剩余数据 |
remaining() | 获取缓冲区中还可以操作的数量 |
public abstract class Buffer {
//JDK1.4时,引入的api
public final int capacity( )//返回此缓冲区的容量
public final int position( )//返回此缓冲区的位置
public final Buffer position (int newPositio)//设置此缓冲区的位置
public final int limit( )//返回此缓冲区的限制
public final Buffer limit (int newLimit)//设置此缓冲区的限制
public final Buffer mark( )//在此缓冲区的位置设置标记
public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置
public final Buffer clear( )//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
public final Buffer flip( )//反转此缓冲区
public final Buffer rewind( )//重绕此缓冲区
public final int remaining( )//返回当前位置与限制之间的元素数
public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素
public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区
//JDK1.6时引入的api
public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
public abstract Object array();//返回此缓冲区的底层实现数组
public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区
}
ByteBuffer
从前面可以看出对于 Java 中的基本数据类型(boolean除外),都有一个 Buffer 类型与之相对应,最常用的自然是ByteBuffer 类(二进制数据),该类的主要方法如下:
public abstract class ByteBuffer {
//缓冲区创建相关api
public static ByteBuffer allocateDirect(int capacity)//创建直接缓冲区
public static ByteBuffer allocate(int capacity)//设置缓冲区的初始容量
public static ByteBuffer wrap(byte[] array)//把一个数组放到缓冲区中使用
//构造初始化位置offset和上界length的缓冲区
public static ByteBuffer wrap(byte[] array,int offset, int length)
//缓存区存取相关API
public abstract byte get( );//从当前位置position上get,get之后,position会自动+1
public abstract byte get (int index);//从绝对位置get
public abstract ByteBuffer put (byte b);//从当前位置上添加,put之后,position会自动+1
public abstract ByteBuffer put (int index, byte b);//从绝对位置上put
}
通道channel
1)NIO的通道类似于流,但有些区别如下:
通道可以同时进行读写,而流只能读或者只能写
通道可以实现异步读写数据
通道可以从缓冲读数据,也可以写数据到缓冲:
2)BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel)是双向的,可以读操作,也可以写操作。
3)Channel在NIO中是一个接口public interface Channel extends Closeable{}
4)常用的 Channel 类有:FileChannel、DatagramChannel、ServerSocketChannel 和 SocketChannel。【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】
5)FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。
服务端会先创建ServerSocketChanne,客户端连接请求过来,服务端会通过ServerSocketChanne分配给客户端一个与之对应的SocketChannel
FileChannel
主要用来对本地文件进行IO操作,常见的方法有:
1.public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
2.public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
3.public long transferFrom(ReadableByteChannel src, long position, long count),从目标通道中复制数据到当前通道
4.public long transferTo(long position, long count, WritableByteChannel target),把数据从当前通道复制给目标通道
关于Buffer 和 Channel的注意事项和细节
1)ByteBuffer 支持类型化的put 和 get, put 放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有 BufferUnderflowException 异常。
2)可以将一个普通Buffer 转成只读Buffer
3)NIO 还提供了 MappedByteBuffer, 可以让文件直接在内存(堆外的内存)中进行修改, 而如何同步到文件由NIO 来完成。
4)前面我们讲的读写操作,都是通过一个Buffer 完成的,NIO 还支持 通过多个Buffer (即 Buffer 数组) 完成读写操作,即 Scattering 和 Gathering。
选择器selector
- Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)
- Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
- 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
- 避免了多线程之间的上下文切换导致的开销
特点再说明:
1)Netty 的 IO 线程 NioEventLoop 聚合了 Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
2)当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
3)线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。
4)由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
5)一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
selector的相关方法
流程:
channel先注册到selector上,selector监听channel上发生的事件
当发生事件时,selector会将发生事件的channel对应的selectionKey添加到其内部存储set集合中
处理时,selector会通过其内部set集合中取出selectionKey,通过selectionKey反向得到对应的channel
selector是一个抽象类,其常用方法:
public abstract class Selector implements Closeable { public static Selector open();//得到一个选择器对象public int select(long timeout);//(毫秒)监控所有注册的通道,当其中有 IO 操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间(阻塞的时间)select()方法,表示会一直监听,直到发生关注的事件,(是阻塞的)selectNow(),如果有事件发生会立刻返回,(非阻塞的)wakeup(),唤醒selector,使阻塞的selector立刻返回public Set selectedKeys();//从内部集合中得到所有的 SelectionKey }
NIO 非阻塞 网络编程相关的(Selector、SelectionKey、ServerScoketChannel和SocketChannel) 关系梳理图
处理流程:
- 当客户端连接时,会通过ServerSocketChannel 得到 SocketChannel(ServerSocketChannel 需要先绑定注册到服务器端selector,然后服务器端创建与之关联的SocketChannel)
- Selector 进行监听 select 方法, 返回有事件发生的通道的个数.
- 将socketChannel注册到Selector上, register(Selector sel, int ops), 一个selector上可以注册多个SocketChannel
- 注册后返回一个 SelectionKey, 会和该Selector 关联(集合)
- 进一步得到各个 SelectionKey (有事件发生)
- 再通过 SelectionKey 反向获取 SocketChannel , 方法 channel()
- 可以通过 得到的 channel , 完成业务处理
代码示例:
package com.ywb.javaroad.netty.nio;
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.util.Iterator;
import java.util.Set;
/**
* @author: yangwen-bo
*
* NIO 非阻塞 网络编程处理流程(服务端)
*
* 处理流程:
* 当客户端连接时,会通过ServerSocketChannel 得到 SocketChannel
* Selector 进行监听 select 方法, 返回有事件发生的通道的个数.
* 将socketChannel注册到Selector上, register(Selector sel, int ops), 一个selector上可以注册多个SocketChannel
* 注册后返回一个 SelectionKey, 会和该Selector 关联(集合)
* 进一步得到各个 SelectionKey (有事件发生)
* 在通过 SelectionKey 反向获取 SocketChannel , 方法 channel()
* 可以通过 得到的 channel , 完成业务处理
*/
public class NioServer {
public static void main(String[] args) throws IOException {
//创建ServerSocketChannel-当客户端连接发生关注监听事件后会产生得到对应的SocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//selector
Selector selector = Selector.open();
//绑定端口,让服务器端监听(为ServerSocketChannel分配一个端口)
serverSocketChannel.socket().bind(