分散(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 == 0) continue;
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. }