NIO缓冲区(3)之分散(Scatter)/聚集(Gather)、通道之间的数据传输和选择器(Selector)

分散(Scatter)/聚集(Gather)

分散(scatter):从Channel中读操作时将读取的数据写入多个buffer中。即,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。 
聚集(gather):写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,即,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。 
scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。 
Scattering Reads 
Scattering Reads是指数据从一个channel读取到多个buffer中。如下图描述: 


代码示例如下: 

Java代码 

1.   ByteBuffer header = ByteBuffer.allocate(128);  

2.   ByteBuffer body   = ByteBuffer.allocate(1024);  

3.     

4.   ByteBuffer[] bufferArray = { header, body };  

5.     

6.   channel.read(bufferArray);  

read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。 
Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads才能正常工作。 
Gathering Writes 
Gathering Writes是指数据从多个buffer写入到同一个channel。如下图描述: 


代码示例如下: 

Java代码 

1.   ByteBuffer header = ByteBuffer.allocate(128);  

2.   ByteBuffer body   = ByteBuffer.allocate(1024);  

3.     

4.   //write data into buffers  

5.     

6.   ByteBuffer[] bufferArray = { header, body };  

7.     

8.   channel.write(bufferArray);  

write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的处理动态消息。 

通道之间的数据传输

transferFrom() 
FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中。

Java代码 

1.   RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt""rw");  

2.   FileChannel      fromChannel = fromFile.getChannel();  //源文件的Channel

3.     

4.   RandomAccessFile toFile = new RandomAccessFile("toFile.txt""rw");  

5.   FileChannel      toChannel = toFile.getChannel();  //目的文件的Channel

6.     

7.   long position = 0;  

8.   long count = fromChannel.size();  

9.     

10.  toChannel.transferFrom(position, count, fromChannel);  

方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。 
transferTo() 
transferTo()方法将数据从FileChannel传输到其他的channel中。下面是一个简单的例子: 

Java代码 

1.   RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt""rw");  

2.   FileChannel      fromChannel = fromFile.getChannel();  

3.     

4.   RandomAccessFile toFile = new RandomAccessFile("toFile.txt""rw");  

5.   FileChannel      toChannel = toFile.getChannel();  

6.     

7.   long position = 0;  

8.   long count = fromChannel.size();  

9.     

10.  fromChannel.transferTo(position, count, toChannel);  

选择器(Selector)

(1)  为什么使用Selector? 
仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。 
(2)  Selector的创建 
通过调用Selector.open()方法创建一个Selector,如下: 

Java代码 

1.   Selector selector = Selector.open();  

(3) 向Selector注册通道 
为了将Channel和Selector配合使用,必须将channel注册到selector上。通过SelectableChannel.register()方法来实现,如下: 

Java代码 

1.   channel.configureBlocking(false);  

2.   SelectionKey key = channel.register(selector,  

3.       Selectionkey.OP_READ);  

与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。 

注意register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件: 

·        Connect

·        Accept

·        Read

·        Write

通道触发了一个事件指的是该事件已经就绪。所以,某个channel成功连接到另一个服务器称为“连接就绪”。一个server socket channel准备好接收新进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。 

这四种事件用SelectionKey的四个常量来表示: 

·        SelectionKey.OP_CONNECT

·        SelectionKey.OP_ACCEPT

·        SelectionKey.OP_READ

·        SelectionKey.OP_WRITE

如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下: 

Java代码 

1.   int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;  

(4) SelectionKey 
在上一小节中,当向Selector注册Channel时,register()方法会返回一个SelectionKey对象。这个对象包含了一些你感兴趣的属性: 

·        interest集合

·        ready集合

·        Channel

·        Selector

·        附加的对象(可选)

下面我会描述这些属性。 
interest集合 

获取所有感兴趣的事件集合。可以通过SelectionKey读写interest集合,像这样: 

Java代码 

1.   int interestSet = selectionKey.interestOps();    

可以看到,用“位与”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中。 
ready集合 
ready 集合是通道已经准备就绪的读操作的集合。
int readySet = selectionKey.readyOps(); 

Java代码 

1.   selectionKey.isAcceptable();  

2.   selectionKey.isConnectable();  

3.   selectionKey.isReadable();  

4.   selectionKey.isWritable();  

Channel + Selector 
从SelectionKey访问Channel和Selector很简单。如下: 

Java代码 

1.   Channel  channel  = selectionKey.channel();  

2.   Selector selector = selectionKey.selector();  

(5)  通过Selector选择通道 

下面是select()方法: 

·        int select()

·        int select(long timeout)

·        int selectNow()

select():阻塞直到至少有一个通道就绪了。 
select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数)。 
selectNow()不会阻塞:不管什么通道就绪都立刻返回,没有通道变成可选择的,则此方法直接返回0 

select()方法返回的int值:表示有多少通道已经就绪。

selectedKeys() 
一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪通道。如下所示: 

Java代码 

1.   Set selectedKeys = selector.selectedKeys();  //获取所有的SelectedKey

当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法访问这些对象。 
可以遍历这个已选择的键集合来访问就绪的通道。如下: 

Java代码 

1.   Set selectedKeys = selector.selectedKeys();  

2.   Iterator keyIterator = selectedKeys.iterator();  

3.   while(keyIterator.hasNext()) {  

4.       SelectionKey key = keyIterator.next();  

5.       if(key.isAcceptable()) {  

6.           // a connection was accepted by a ServerSocketChannel.  

7.       } else if (key.isConnectable()) {  

8.           // a connection was established with a remote server.  

9.       } else if (key.isReadable()) {  

10.          // a channel is ready for reading  

11.      } else if (key.isWritable()) {  

12.          // a channel is ready for writing  

13.      }  

14.  }  

注意每次迭代末尾的keyIterator.remove()调用。Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。 
(6)  wakeUp() 
某个线程调用select()方法后阻塞了,即使没有通道已经就绪,调用Selector.wakeup()方法阻塞在select()方法上的线程会立马返回。 
如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”。 
(7)  close() 
用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。 
(8)  完整的示例 
这里有一个完整的示例,打开一个Selector,注册一个通道注册到这个Selector上(通道的初始化过程略去),然后持续监控这个Selector的四种事件(接受,连接,读,写)是否就绪。 

Java代码 

1.   Selector selector = Selector.open();  

2.   channel.configureBlocking(false);  

3.   SelectionKey key = channel.register(selector, SelectionKey.OP_READ);  

4.   while(true) {  

5.     int readyChannels = selector.select();  

6.     if(readyChannels == 0continue;  

7.     Set selectedKeys = selector.selectedKeys();  

8.     Iterator keyIterator = selectedKeys.iterator();  

9.     while(keyIterator.hasNext()) {  

10.      SelectionKey key = keyIterator.next();  

11.      if(key.isAcceptable()) {  

12.          // a connection was accepted by a ServerSocketChannel.  

13.      } else if (key.isConnectable()) {  

14.          // a connection was established with a remote server.  

15.      } else if (key.isReadable()) {  

16.          // a channel is ready for reading  

17.      } else if (key.isWritable()) {  

18.          // a channel is ready for writing  

19.      }  

20.  }  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值