NIO Buffer缓冲和Selector
一、Buffer
- 通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中
- 在 NIO 库中,所有数据都是用缓冲区处理的
1.1 基本用法
使用 Buffer 读写数据,四个步骤
(1)写入数据到 Buffer
(2)调用 flip()方法
(3)从 Buffer 中读取数据
(4)调用 clear()方法或者 compact()方法
读数据的完整例子
@Test
public void buffer01() throws Exception {
//FileChannel
RandomAccessFile aFile =
new RandomAccessFile("d://opencoder.txt","rw");
FileChannel channel = aFile.getChannel();
//创建buffer大小
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读
int bytesRead = channel.read(buffer);
while(bytesRead != -1) {
//read模式
buffer.flip();
while(buffer.hasRemaining()) {
System.out.println((char)buffer.get());
}
buffer.clear();
bytesRead = channel.read(buffer);
}
aFile.close();
}
写数据的完整例子
//创建buffer
IntBuffer buffer = IntBuffer.allocate(8);
//buffer放
for (int i = 0; i < buffer.capacity(); i++) {
int j = 2*(i+1);
buffer.put(j);
}
//重置缓冲区
buffer.flip();
//获取
while(buffer.hasRemaining()) {
int value = buffer.get();
System.out.println(value+" ");
}
1.2 三个重要属性
Buffer还有三个属性:Capacity、Position、limit
capacity 内存块固定大小值
一旦 Buffer 满了,需要将其清空,才能写入
position
写入数据的时候,初始值为0,慢慢会往下移动,最大值为-1表示满了
读入数据的时候, position=2 时表示已开始读入了 3 个 byte。ByteBuffer.flip()切换到读模式时 position 会被重置为 0limit
limit 表示可对 Buffer 最多写入或者读取多少个数据
1.3 核心方法的使用
分配字节数据
要想获得一个 Buffer 对象首先要进行分配。 每一个 Buffer 类都有一个 allocate 方法 比如:
ByteBuffer buf = ByteBuffer.allocate(48);
写数据的两种方式
(1)从 Channel 写到 Buffer。
(2)通过 Buffer 的 put()方法写到 Buffer 里。
int bytesRead = inChannel.read(buf); //read into buffer
buf.put(127);
读写模式转换 flip()
flip 方法将 Buffer 从写模式切换到读模式。调用 flip()方法会将 position 设回 0,并将 limit 设置成之前 position 的值。
从 Buffer 中读取数据
有两种方式:
(1)从 Buffer 读取数据到 Channel。
(2)使用 get()方法从 Buffer 中读取数据
//read from buffer into channel.
int bytesWritten = inChannel.write(buf);
byte aByte = buf.get();
1.4 其他方法
rewind()将 position 设回 0
clear()与 compact()都是清空数据,但有所区别:
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear()或 compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面
mark()与 reset()一个标记一个回到标记点
通过调用 Buffer.mark()方法,可以标记 Buffer 中的一个特定 position。之后可以通过调用 Buffer.reset()方法恢复到这个 position
1.5 缓冲区的分类
缓冲区可以分为四种类型分别为:缓冲区分片、只读缓冲区、直接缓冲区、内存映射文件 I/O
1.6 缓冲区分片
根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。调用 slice()方法可以创建一个子缓冲区.
完整演示代码如下:
//缓冲区分片
@Test
public void b01() {
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte)i);
}
//创建子缓冲区
buffer.position(3);
buffer.limit(7);
ByteBuffer slice = buffer.slice();
//改变子缓冲区内容
for (int i = 0; i <slice.capacity() ; i++) {
byte b = slice.get(i);
b *=10;
slice.put(i,b);
}
buffer.position(0);
buffer.limit(buffer.capacity());
while(buffer.remaining()>0) {
System.out.println(buffer.get());
}
}
1.7 只读缓冲区
可以读取它们,但是不能向它们写入数据。可以通过调用缓冲区的 asReadOnlyBuffer()方法与原缓冲区共享数据,只不过它是只读的。
如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化。
完整演示代码如下:
//只读缓冲区
@Test
public void b02() {
ByteBuffer buffer = ByteBuffer.allocate(10);
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte)i);
}
//创建只读缓冲区
ByteBuffer readonly = buffer.asReadOnlyBuffer();
for (int i = 0; i < buffer.capacity(); i++) {
byte b = buffer.get(i);
b *=10;
buffer.put(i,b);
}
readonly.position(0);
readonly.limit(buffer.capacity());
while (readonly.remaining()>0) {
System.out.println(readonly.get());
}
}
1.8 直接缓冲区
加快 I/O 速度要分配直接缓冲区,需要调用 allocateDirect()方法,而不是 allocate()方法,使用方式与普通缓冲区并无区别
完整演示代码如下:
//直接缓冲区
@Test
public void b03() throws Exception {
String infile = "d://opencoder1.txt";
FileInputStream fin = new FileInputStream(infile);
FileChannel finChannel = fin.getChannel();
String outfile = "d://opencoder12.txt";
FileOutputStream fout = new FileOutputStream(outfile);
FileChannel foutChannel = fout.getChannel();
//创建直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
buffer.clear();
int r = finChannel.read(buffer);
if(r == -1) {
break;
}
buffer.flip();
foutChannel.write(buffer);
}
}
1. 9 内存映射文件 I/O
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快的多
static private final int start = 0;
static private final int size = 1024;
//内存映射文件io
@Test
public void b04() throws Exception {
RandomAccessFile raf = new RandomAccessFile("d://opencoder1.txt", "rw");
FileChannel fc = raf.getChannel();
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, start, size);
mbb.put(0, (byte) 97);
mbb.put(1023, (byte) 122);
raf.close();
}
二、Selector、
- 通过一个Selector可以检查更多的通道
- 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销
可选择通道:
- 不是所有的 Channel 都可以被 Selector 复用的。判断他是否继承了一个抽象类 SelectableChannel。如果继承了SelectableChannel,则可以被复用,否则不能
- 一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。通道和选择器之间的关系,使用注册的方式完成。SelectableChannel 可以被注册到Selector 对象上,在注册的时候,需要指定通道的哪些操作,是 Selector 感兴趣的
Channel 注册到 Selector:
Channel.register(Selector sel,int ops)
一个通道注册到一个选择器时。
第一个参数,指定通道要注册的选择器。
第二个参数指定选择器需要查询的通道操作
供选择器查询的通道
- 可读 : SelectionKey.OP_READ
- 可写 : SelectionKey.OP_WRITE
- 连接 : SelectionKey.OP_CONNECT
- 接收 : SelectionKey.OP_ACCEPT
选择键:
- Channel 注册到后,并且一旦通道处于某种就绪的状态,就可以被选择器查询到。这个工作,使用选择器 Selector 的 select()方法完成。select 方法的作用,对感兴趣的通道操作,进行就绪状态的查询。
- Selector 可以不断的查询 Channel 中发生的操作的就绪状态。并且挑选感兴趣的操作就绪状态。一旦通道有操作的就绪状态达成,并且是 Selector 感兴趣的操作,就会被 Selector 选中,放入选择键集合中
2.1 核心方法
创建一个 Selector 对象:
// 1、获取 Selector 选择器 Selector selector = Selector.open();
注册 Channel 到 Selector
// 1、获取 Selector 选择器 Selector selector = Selector.open(); // 2、获取通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 3.设置为非阻塞 serverSocketChannel.configureBlocking(false); // 4、绑定连接 serverSocketChannel.bind(new InetSocketAddress(9999)); // 5、将通道注册到选择器上,并制定监听事件为:“接收”事件 serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
轮询查询就绪操作
(1)通过 Selector 的 select()方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,包存在一个元素是 SelectionKey 对象的 Set 集合中。
(2)下面是 Selector 几个重载的查询 select()方法:
- select():阻塞到至少有一个通道在你注册的事件上就绪了。
- select(long timeout):和 select()一样,但最长阻塞事件为 timeout 毫秒。
- selectNow():非阻塞,只要有通道就绪就立刻返回。
select()方法返回的 int 值,表示有多少通道已经就绪,更准确的说,是自前一次 select方法以来到这一次 select 方法之间的时间段上,有多少通道变成就绪状态。
如:首次调用 select()方法,如果有一个通道变成就绪状态,返回了 1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回 1。如果对第一个就绪的channel 没有做任何操作,现在就有两个就绪的通道,但在每次 select()方法调用之间,只有一个通道就绪了。一旦调用 select()方法,并且返回值不为 0 时,在 Selector 中有一个 selectedKeys()方法,用来访问已选择键集合,迭代集合的每一个选择键元素,根据就绪操作的类型,完成对应的操作。
Set selectedKeys = selector.selectedKeys(); Iterator keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); }
2.2 完成的一个客户端和服务器端的实战例子
服务端代码:
//服务端代码
@Test
public void serverDemo() throws Exception {
//1 获取服务端通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2 切换非阻塞模式
serverSocketChannel.configureBlocking(false);
//3 创建buffer
ByteBuffer serverByteBuffer = ByteBuffer.allocate(1024);
//4 绑定端口号
serverSocketChannel.bind(new InetSocketAddress(8080));
//5 获取selector选择器
Selector selector = Selector.open();
//6 通道注册到选择器,进行监听
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//7 选择器进行轮询,进行后续操作
while(selector.select()>0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历
Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
while(selectionKeyIterator.hasNext()) {
//获取就绪操作
SelectionKey next = selectionKeyIterator.next();
//判断什么操作
if(next.isAcceptable()) {
//获取连接
SocketChannel accept = serverSocketChannel.accept();
//切换非阻塞模式
accept.configureBlocking(false);
//注册
accept.register(selector,SelectionKey.OP_READ);
} else if(next.isReadable()) {
SocketChannel channel = (SocketChannel) next.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//读取数据
int length = 0;
while((length = channel.read(byteBuffer))>0) {
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(),0,length));
byteBuffer.clear();
}
}
selectionKeyIterator.remove();
}
}
}
客户端代码:
//客户端代码
@Test
public void clientDemo() throws Exception {
//1 获取通道,绑定主机和端口号
SocketChannel socketChannel =
SocketChannel.open(new InetSocketAddress("127.0.0.1",8080));
//2 切换到非阻塞模式
socketChannel.configureBlocking(false);
//3 创建buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//4 写入buffer数据
byteBuffer.put(new Date().toString().getBytes());
//5 模式切换
byteBuffer.flip();
//6 写入通道
socketChannel.write(byteBuffer);
//7 关闭
byteBuffer.clear();
}
public static void main(String[] args) throws IOException {
//1 获取通道,绑定主机和端口号
SocketChannel socketChannel =
SocketChannel.open(new InetSocketAddress("127.0.0.1",8080));
//2 切换到非阻塞模式
socketChannel.configureBlocking(false);
//3 创建buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()) {
String str = scanner.next();
//4 写入buffer数据
byteBuffer.put((new Date().toString()+"--->"+str).getBytes());
//5 模式切换
byteBuffer.flip();
//6 写入通道
socketChannel.write(byteBuffer);
//7 关闭
byteBuffer.clear();
}
}
}
总结
今天主要给大家介绍的是Buffer的基本使用这个也是NIO里面最总要的概率之一,里面的操作也是有一些复杂的同时也是需要大家必须要重点掌握的知识点,同时也介绍了一下Selector的用法下一篇文章我们将为大家介绍Pipe管道以及FileLock文件锁这也是NIO里面最后的一分部内容了。