1、Java NIO 简介
Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的 IO 有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
传统IO | NIO |
---|---|
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
(无) | 选择器(Selectors) |
2、通道与缓冲区
Java NIO系统的核心在于:通道 (Channel ) 和缓冲区 (Buffer) 。通道表示打开到IO 设备(例如:文件、套接字)的连接。若需要使用NIO 系统,需要获取用于连接IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
简而言之,Channel 负责传输,Buffer负责存储(跟上面的图不太一致要体谅一下)
1、缓冲区
1、概述
缓冲区(Buffer):一个用于特定基本数据类型的容器。由java.nio 包定义的,所有缓冲区都是Buffer 抽象类的子类
Java NIO 中的Buffer 主要用于与NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
Buffer 就像一个数组,可以保存多个相同类型的数据。根据数据类型不同(boolean 除外) ,有以下Buffer 常用子类:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
上述Buffer 类他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。
都是通过如下方法获取一个Buffer对象:
//创建一个容量为capacity 的XxxBuffer 对象
static XxxBuffer allocate(int capacity)
2、缓冲区的基本属性
1、Buffer 中的重要概念:
- 容量(capacity) :表示Buffer 最大数据容量,缓冲区容量不能为负,并且创
建后不能更改。 - 限制(limit):第一个不应该读取或写入的数据的索引,即位于limit 后的数据
不可读写。缓冲区的限制不能为负,并且不能大于其容量。 - 位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为
负,并且不能大于其限制 - 标记(mark)与重置(reset):标记是一个索引,通过Buffer 中的mark() 方法
指定Buffer 中一个特定的position,之后可以通过调用reset() 方法恢复到这
个position.
源码
public abstract class Buffer {
/**
* The characteristics of Spliterators that traverse and split elements
* maintained in Buffers.
*/
static final int SPLITERATOR_CHARACTERISTICS =
Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
}
2、标记、位置、限制、容量遵守以下不变式:
0 <= mark <= position <= limit <= capacity
3、图示:
3、Buffer的常用方法
4、缓冲区的数据操作
Buffer 所有子类提供了两个用于数据操作的方法: get() 与 put() 方法
1、获取Buffer 中的数据:
- get() :读取单个字节
- get(byte[] dst):批量读取多个字节到dst 中
- get(int index):读取指定索引位置的字节(不会移动position)
2、放入数据到Buffer 中
- put(byte b):将给定单个字节写入缓冲区的当前位置
- put(byte[] src):将src 中的字节写入缓冲区的当前位置
- put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动position)
5、测试代码
@Test
public void test1(){
String str = "abcde";
//1. 分配一个指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("-----------------allocate()----------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//2. 利用 put() 存入数据到缓冲区中
buf.put(str.getBytes());
System.out.println("-----------------put()----------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//3. 切换读取数据模式
buf.flip();
System.out.println("-----------------flip()----------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//4. 利用 get() 读取缓冲区中的数据
byte[] dst = new byte[buf.limit()];
buf.get(dst);
System.out.println(new String(dst, 0, dst.length));
System.out.println("-----------------get()----------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//5. rewind() : 可重复读
buf.rewind();
System.out.println("-----------------rewind()----------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//6. clear() : 清空缓冲区. 但是缓冲区中的数据依然存在,但是处于“被遗忘”状态
buf.clear();
System.out.println("-----------------clear()----------------");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
System.out.println((char)buf.get());
}
@Test
public void test2(){
String str = "abcde";
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(str.getBytes());
buf.flip();
byte[] dst = new byte[buf.limit()];
buf.get(dst, 0, 2);
System.out.println(new String(dst, 0, 2));
System.out.println(buf.position());
//mark() : 标记
buf.mark();
buf.get(dst, 2, 2);
System.out.println(new String(dst, 2, 2));
System.out.println(buf.position());
//reset() : 恢复到 mark 的位置
buf.reset();
System.out.println(buf.position());
//判断缓冲区中是否还有剩余数据
if(buf.hasRemaining()){
//获取缓冲区中可以操作的数量
System.out.println(buf.remaining());
}
}
6、直接与非直接缓冲区
- 字节缓冲区要么是直接的,要么是非直接的。如果为直接字节缓冲区,则Java 虚拟机会尽最大努力直接在
此缓冲区上执行本机I/O 操作。也就是说,在每次调用基础操作系统的一个本机I/O 操作之前(或之后),
虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。 - 直接字节缓冲区可以通过调用此类的
allocateDirect()
工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的
本机I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好
处时分配它们。 - 直接字节缓冲区还可以通过
FileChannel 的map()
方法将文件区域直接映射到内存中来创建。该方法返回
MappedByteBuffer
。Java 平台的实现有助于通过JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在
访问期间或稍后的某个时间导致抛出不确定的异常。 - 字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其
isDirect()
方法来确定。提供此方法是为了能够在性能关键型代码中执行显式缓冲区管理
/**
* 四、直接缓冲区与非直接缓冲区:
* 非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存中
* 直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中
* 可以提高效率
*/
@Test
public void test3(){
//分配直接缓冲区
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
//判断是否是直接缓冲区
System.out.println(buf.isDirect());
}
2、通道
1、概述
通道(Channel):由java.nio.channels 包定义的。Channel 表示IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过Channel本身不能直接访问数据,Channel 只能与Buffer 进行交互。
2、主要实现类
Java 为Channel 接口提供的最主要实现类如下:
- FileChannel:用于读取、写入、映射和操作文件的通道。
- DatagramChannel:通过UDP 读写网络中的数据通道。
- SocketChannel:通过TCP 读写网络中的数据。
- ServerSocketChannel:可以监听新进来的TCP 连接,对每一个新进来的连接都会创建一个SocketChannel。
3、获取通道
获取通道的一种方式是对支持通道的对象调用getChannel()
方法。
支持通道的类如下:
- FileInputStream
- FileOutputStream
- RandomAccessFile
- DatagramSocket
- Socket
ServerSocket
在 JDK 1.7 中的 NIO.2 针对各个通道提供了静态方法 open()
- 在 JDK 1.7 中的 NIO.2 的 Files 工具类的 newByteChannel()
4、通道数据传输 测试代码
//1、将 Buffer 中的数据写入到 Channel 中
//写入到同道中就代表着写入到了文件中
int bytesWritten = inChannel.write(buf);
//2、从 Channel 读取数据到 Buffer
int bytesRead = inChannel.read(buf);
1、使用非直接缓冲区
@Test
//利用通道完成文件的复制(非直接缓冲区)
public void fun1() throws Exception {
FileInputStream inputStream = new FileInputStream("e:/1.mp4");
FileOutputStream outputStream = new FileOutputStream("e:/海贼王.mp4");
//1、获取通道
FileChannel inChannel = inputStream.getChannel();
FileChannel outChannel = outputStream.getChannel();
//2、因为通道只能跟Buffer进行交互
// 分配指定大小的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//3、使用 buffer 从同道中获取数据
while (inChannel.read(byteBuffer) >= 0) {
//已经将数据写入到 buffer中了,我们开启读模式
byteBuffer.flip();
//通过 buffer 将数据写入到通道中
outChannel.write(byteBuffer);
//将各个位置恢复原样以便下次读取
byteBuffer.clear();
}
}
2、使用直接缓冲区完成文件的复制(内存映射文件)
//使用直接缓冲区完成文件的复制(内存映射文件)
@Test
public void test2() throws IOException{//2127-1902-1777
long start = System.currentTimeMillis();
//这里使用了jdk7里面的工具类 Paths
FileChannel inChannel = FileChannel
.open(Paths.get("d:/1.mkv"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel
.open(Paths.get("d:/2.mkv"),
StandardOpenOption.WRITE, StandardOpenOption.READ,
StandardOpenOption.CREATE);
//内存映射文件
MappedByteBuffer inMappedBuf = inChannel
.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedBuf = outChannel
.map(MapMode.READ_WRITE, 0, inChannel.size());
//直接对缓冲区进行数据的读写操作
byte[] dst = new byte[inMappedBuf.limit()];
inMappedBuf.get(dst);
outMappedBuf.put(dst);
inChannel.close();
outChannel.close();
long end = System.currentTimeMillis();
System.out.println("耗费时间为:" + (end - start));
}
3、通道之间的数据传输(直接缓冲区)
在Java NIO中如果一个channel是FileChannel类型的,那么他可以直接把数据传输到另一个channel。逐个特性得益于FileChannel包含的transferTo和transferFrom两个方法。
//通道之间的数据传输(直接缓冲区)
@Test
public void test3() throws IOException{
FileChannel inChannel = FileChannel
.open(Paths.get("d:/1.mkv"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.
open(Paths.get("d:/2.mkv"),
StandardOpenOption.WRITE, StandardOpenOption.READ,
StandardOpenOption.CREATE);
// inChannel.transferTo(0, inChannel.size(), outChannel);
outChannel.transferFrom(inChannel, 0, inChannel.size());
inChannel.close();
outChannel.close();
}
3、分散和聚集
1、分散读取(Scattering Reads)
指从Channel 中读取的数据“分散”到多个Buffer 中。
2、聚集写入(Gathering Writes)
指将多个Buffer 中的数据“聚集”到Channel
//分散和聚集
@Test
public void test4() throws IOException{
RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");
//1. 获取通道
FileChannel channel1 = raf1.getChannel();
//2. 分配指定大小的缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(100);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
//3. 分散读取
ByteBuffer[] bufs = {buf1, buf2};
channel1.read(bufs);
for (ByteBuffer byteBuffer : bufs) {
//开启读的模式
byteBuffer.flip();
}
System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
System.out.println("-----------------");
System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
//4. 聚集写入
RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
FileChannel channel2 = raf2.getChannel();
channel2.write(bufs);
}
4、字符集(Charset)
jdk 1.4 提供了Charset 来处理字节跟字符的转换
1、获取 Charset能支持的所有编码集
@Test
public void test5(){
Map<String, Charset> map = Charset.availableCharsets();
Set<Entry<String, Charset>> set = map.entrySet();
for (Entry<String, Charset> entry : set) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}
2、Charset Demo
//字符集
@Test
public void test6() throws IOException{
Charset cs1 = Charset.forName("GBK");
//获取编码器
CharsetEncoder ce = cs1.newEncoder();
//获取解码器
CharsetDecoder cd = cs1.newDecoder();
CharBuffer cBuf = CharBuffer.allocate(1024);
cBuf.put("测试一下Charset的编码解码功能!");
cBuf.flip();
//编码
ByteBuffer bBuf = ce.encode(cBuf);
for (int i = 0; i < 12; i++) {
System.out.println(bBuf.get());
}
//解码
bBuf.flip();
CharBuffer cBuf2 = cd.decode(bBuf);
System.out.println(cBuf2.toString());
System.out.println("-------------------------------------------------");
Charset cs2 = Charset.forName("GBK");
bBuf.flip();
CharBuffer cBuf3 = cs2.decode(bBuf);
System.out.println(cBuf3.toString());
}
3、NIO 的非阻塞式网络通信
1、阻塞与非阻塞
阻塞与非阻塞是相对于网络通信来说的
传统的IO 流都是阻塞式的。也就是说,当一个线程调用read() 或write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞IO 的空闲时间用于在其他通道上执行IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端
2、选择器(Selector)
1、概述
选择器(Selector)是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个Channel。Selector 是非阻塞IO 的核心
选择器提供选择执行已经就绪的任务的能力.从底层来看,Selector提供了询问通道是否已经准备好执行每个I/O操作的能力。Selector 允许单线程处理多个Channel。仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道,这样会大量的减少线程之间上下文切换的开销。
在开始之前,需要了解一下Selector、SelectableChannel 和 SelectionKey:
选择器(Selector)
Selector选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器是一起被注册的,并且使用选择器来更新通道的就绪状态
可选择通道(SelectableChannel)
SelectableChannel这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。因为FileChannel类没有继承SelectableChannel因此是不是可选通道,而所有socket通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。SelectableChannel可以被注册到Selector对象上,同时可以指定对那个选择器而言,那种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
选择键(SelectionKey)
选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register()返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。
2、Selector的使用
1、创建 Selector
Selector Selector=Selector.open();
2、将Channel注册到Selector
//将 SocketChannel 切换成非阻塞模式
channel.configureBlocking(false);
//将Selector 注册到 Channel
SelectionKey key= channel.register(selector,SelectionKey,OP_READ);
register()方法的第二个参数是“interest集合”,表示选择器所关心的通道操作,它实际上是一个表示选择器在检查通道就绪状态时需要关心的操作的比特掩码。比如一个选择器对通道的read和write操作感兴趣,那么选择器在检查该通道时,只会检查通道的read和write操作是否已经处在就绪状态。
它有以下四种操作类型:
- Connect 连接
- Accept 接受
- Read 读
- Write 写
需要注意并非所有的操作在所有的可选择通道上都能被支持,比如 ServerSocketChannel支持Accept,而SocketChannel 中不支持。我们可以通过通道上的validOps()方法来获取特定通道下所有支持的操作集合。
Java中定义了四个常量来表示这四种操作类型:
当通道触发了某个操作之后,表示该通道的某个操作已经就绪,可以被操作。
1、SelectionKey.OP_READ (1)
一个有数据可读的通道可以说是“读就绪”
2、SelectionKey.OP_WRITE (4)
等待写数据的通道可以说是“写就绪”
3、SelectionKey.OP_CONNECT (8)
某个SocketChannel成功连接到另一个服务器称为“连接就绪”
4、SelectionKey.OP_ACCEPT (16)
一个ServerSocketChannel准备好接收新进入的连接称为“接收就绪”
1、4、8、16 任意相加结果表示的都是唯一的
多个操作类型可以使用
int interestSet= SelectionKey.OP_READ|SelectionKey.OP_WRITE;
3、SelectionKey
1、概述
表示SelectableChannel 和Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。
2、常用方法
3、部分方法详解
interest集合是Selector感兴趣的集合,用于指示选择器对通道关心的操作,可通过SelectionKey对象的interestOps()获取。最初,该兴趣集合是通道被注册到Selector时传进来的值。该集合不会被选择器改变,但是可通过interestOps()改变。我们可以通过以下方法来判断Selector是否对Channel的某种事件感兴趣:
int interestSet=selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT)
== SelectionKey.OP_ACCEPT;
read集合是通道已经就绪的操作的集合,表示一个通道准备好要执行的操作了,可通过SelctionKey对象的readyOps()来获取相关通道已经就绪的操作。它是interest集合的子集,并且表示了interest集合中从上次调用select()以后已经就绪的那些操作。(比如选择器对通道的read,write操作感兴趣,而某时刻通道的read操作已经准备就绪可以被选择器获知了,前一种就是interest集合,后一种则是read集合。)。JAVA中定义以下几个方法用来检查这些操作是否就绪:
//int readSet=selectionKey.readOps();
//等价于selectionKey.readyOps()&SelectionKey.OP_ACCEPT
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
需要注意的是,通过相关的选择键的readyOps()方法返回的就绪状态指示只是一个提示,底层的通道在任何时候都会不断改变,而其他线程也可能在通道上执行操作并影响到它的就绪状态。另外,我们不能直接修改read集合。
4、代码
1、TCP
/*
* 一、使用 NIO 完成网络通信的三个核心:
*
* 1. 通道(Channel):负责连接
*
* java.nio.channels.Channel 接口:
* |--SelectableChannel
* |--SocketChannel
* |--ServerSocketChannel
* |--DatagramChannel
*
* |--Pipe.SinkChannel
* |--Pipe.SourceChannel
*
* 2. 缓冲区(Buffer):负责数据的存取
*
* 3. 选择器(Selector):是 SelectableChannel 的多路复用器。
* 用于监控 SelectableChannel 的 IO 状况
*
*/
public class TestNonBlockingNIO {
//客户端
@Test
public void client() throws IOException{
//1. 获取通道
SocketChannel sChannel = SocketChannel.
open(new InetSocketAddress("127.0.0.1", 9898));
//2. 切换非阻塞模式
sChannel.configureBlocking(false);
//3. 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4. 发送数据给服务端
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buf.put((new Date().toString() + "\n" + str).getBytes());
buf.flip();
sChannel.write(buf);
buf.clear();
}
//5. 关闭通道
sChannel.close();
}
//服务端
@Test
public void server() throws IOException{
//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
//6. 轮询式的获取选择器上已经“准备就绪”的事件
while(selector.select() > 0){
//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()){
//8. 获取准备“就绪”的是事件
SelectionKey sk = it.next();
//9. 判断具体是什么事件准备就绪
if(sk.isAcceptable()){
//10. 若“接收就绪”,获取客户端连接
SocketChannel sChannel = ssChannel.accept();
//11. 切换非阻塞模式
sChannel.configureBlocking(false);
//12. 将该通道注册到选择器上
sChannel.register(selector, SelectionKey.OP_READ);
}else if(sk.isReadable()){
//13. 获取当前选择器上“读就绪”状态的通道
SocketChannel sChannel = (SocketChannel) sk.channel();
//14. 读取数据
ByteBuffer buf = ByteBuffer.allocate(1024);
int len = 0;
while((len = sChannel.read(buf)) > 0 ){
buf.flip();
System.out.println(new String(buf.array(), 0, len));
buf.clear();
}
}
//15. 取消选择键 SelectionKey
it.remove();
}
}
}
}
2、UDP
public class TestNonBlockingNIO2 {
@Test
public void send() throws IOException{
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buf.put((new Date().toString() + ":\n" + str).getBytes());
buf.flip();
dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
buf.clear();
}
dc.close();
}
@Test
public void receive() throws IOException{
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
dc.bind(new InetSocketAddress(9898));
Selector selector = Selector.open();
dc.register(selector, SelectionKey.OP_READ);
while(selector.select() > 0){
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()){
SelectionKey sk = it.next();
if(sk.isReadable()){
ByteBuffer buf = ByteBuffer.allocate(1024);
dc.receive(buf);
buf.flip();
System.out.println(new String(buf.array(), 0, buf.limit()));
buf.clear();
}
}
it.remove();
}
}
}