NIO有三宝:Buffer,Channel,Selector少不了。本文将会介绍NIO三件套中的最后一套Selector,并在理解Selector的基础上,协助小师妹发一张好人卡。咱们开始吧。java
小师妹:F师兄,最近个人桃花有点旺,好几个师兄莫名其妙的跟我打招呼,但是我一心向着工做,不想谈论这些事情。毕竟先有事业才有家嘛。我又很差直接拒绝,有没有什么比较隐晦的方法来让他们放弃这个想法?程序员
更多内容请访问 www.flydean.com
这个问题,我沉思了大约0.001秒,因而给出了答案:给他们发张好人卡吧,应该就不会再来纠缠你了。spring
小师妹:F师兄,若是给他们发无缺人卡尚未用呢?服务器
那就只能切断跟他们的联系了,来个一刀两断。哈哈。socket
这样吧,小师妹你最近不是在学NIO吗?恰好咱们能够用Selector来模拟一下发好人卡的过程。ide
假如你的志伟师兄和子丹师兄想跟你创建联系,每一个人都想跟你创建一个沟统统道,那么你就须要建立两个channel。spring-boot
两个channel其实还好,若是有多我的都想同时跟你创建联系通道,那么要维持这些通道就须要保持链接,从而浪费了资源。区块链
可是创建的这些链接并非时时刻刻都有消息在传输,因此其实大多数时间这些创建联系的通道实际上是浪费的。spa
若是使用Selector就能够只启用一个线程来监听通道的消息变更,这就是Selector。.net
从上面的图能够看出,Selector监听三个不一样的channel,而后交给一个processor来处理,从而节约了资源。
先看下selector的定义:
public abstract class Selector implements Closeable
Selector是一个abstract类,而且实现了Closeable,表示Selector是能够被关闭的。
虽然Selector是一个abstract类,可是能够经过open来简单的建立:
Selector selector = Selector.open();
若是细看open的实现能够发现一个颇有趣的现象:
public static Selector open() throws IOException { return SelectorProvider.provider().openSelector(); }
open方法调用的是SelectorProvider中的openSelector方法。
再看下provider的实现:
public SelectorProvider run() { if (loadProviderFromProperty()) return provider; if (loadProviderAsService()) return provider; provider = sun.nio.ch.DefaultSelectorProvider.create(); return provider; } });
有三种状况能够加载一个SelectorProvider,若是系统属性指定了java.nio.channels.spi.SelectorProvider,那么从指定的属性加载。
若是没有直接指定属性,则从ServiceLoader来加载。
最后若是都找不到的状况下,使用默认的DefaultSelectorProvider。
关于ServiceLoader的用法,咱们后面会有专门的文章来说述。这里先不作多的解释。
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress("localhost", 9527)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
若是是在服务器端,咱们须要先建立一个ServerSocketChannel,绑定Server的地址和端口,而后将Blocking设置为false。由于咱们使用了Selector,它其实是一个非阻塞的IO。
注意FileChannels是不能使用Selector的,由于它是一个阻塞型IO。
小师妹:F师兄,为啥FileChannel是阻塞型的呀?作成非阻塞型的不是更快?
小师妹,咱们使用FileChannel的目的是什么?就是为了读文件呀,读取文件确定是一直读一直读,没有可能读一会这个channel再读另一个channel吧,由于对于每一个channel本身来说,在文件没读取完以前,都是繁忙状态,没有必要在channel中切换。
最后咱们将建立好的Selector注册到channel中去。
SelectionKey表示的是咱们但愿监听到的事件。
总的来讲,有4种Event:
public static final int OP_READ = 1 << 0; public static final int OP_WRITE = 1 << 2; public static final int OP_CONNECT = 1 << 3; public static final int OP_ACCEPT = 1 << 4;
咱们能够看到上面的4个Event是用位运算来定义的,若是将这个四个event使用或运算合并起来,就获得了SelectionKey中的interestOps。
和interestOps相似,SelectionKey还有一个readyOps。
一个表示感兴趣的操做,一个表示ready的操做。
最后,SelectionKey在注册的时候,还能够attach一个Object,好比咱们能够在这个对象中保存这个channel的id:
SelectionKey key = channel.register( selector, SelectionKey.OP_ACCEPT, object); key.attach(Object); Object object = key.attachment();
object能够在register的时候传入,也能够调用attach方法。
最后,咱们能够经过key的attachment方法,得到该对象。
咱们经过selector.select()这个一个blocking操做,来获取一个ready的channel。
而后咱们经过调用selector.selectedKeys()来获取到SelectionKey对象。
在SelectionKey对象中,咱们经过判断ready的event来处理相应的消息。
接下来,咱们把以前将的串联起来,先创建一个小师妹的ChatServer:
public class ChatServer { private static String BYE_BYE="再见"; public static void main(String[] args) throws IOException, InterruptedException { Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress("localhost", 9527)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); ByteBuffer byteBuffer = ByteBuffer.allocate(512); while (true) { selector.select(); Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectedKeys.iterator(); while (iter.hasNext()) { SelectionKey selectionKey = iter.next(); if (selectionKey.isAcceptable()) { register(selector, serverSocketChannel); } if (selectionKey.isReadable()) { serverResonse(byteBuffer, selectionKey); } iter.remove(); } Thread.sleep(1000); } } private static void serverResonse(ByteBuffer byteBuffer, SelectionKey selectionKey) throws IOException { SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); socketChannel.read(byteBuffer); byteBuffer.flip(); byte[] bytes= new byte[byteBuffer.limit()]; byteBuffer.get(bytes); log.info(new String(bytes).trim()); if(new String(bytes).trim().equals(BYE_BYE)){ log.info("说再见不如不见!"); socketChannel.write(ByteBuffer.wrap("再见".getBytes())); socketChannel.close(); }else { socketChannel.write(ByteBuffer.wrap("你是个好人".getBytes())); } byteBuffer.clear(); } private static void register(Selector selector, ServerSocketChannel serverSocketChannel) throws IOException { SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } }
上面例子有两点须要注意,咱们在循环遍历中,当selectionKey.isAcceptable时,表示服务器收到了一个新的客户端链接,这个时候咱们须要调用register方法,再注册一个OP_READ事件到这个新的SocketChannel中,而后继续遍历。
第二,咱们定义了一个stop word,当收到这个stop word的时候,会直接关闭这个client channel。
再看看客户端的代码:
public class ChatClient { private static SocketChannel socketChannel; private static ByteBuffer byteBuffer; public static void main(String[] args) throws IOException { ChatClient chatClient = new ChatClient(); String response = chatClient.sendMessage("hello 小师妹!"); log.info("response is {}", response); response = chatClient.sendMessage("能不能?"); log.info("response is {}", response); chatClient.stop(); } public void stop() throws IOException { socketChannel.close(); byteBuffer = null; } public ChatClient() throws IOException { socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9527)); byteBuffer = ByteBuffer.allocate(512); } public String sendMessage(String msg) throws IOException { byteBuffer = ByteBuffer.wrap(msg.getBytes()); String response = null; socketChannel.write(byteBuffer); byteBuffer.clear(); socketChannel.read(byteBuffer); byteBuffer.flip(); byte[] bytes= new byte[byteBuffer.limit()]; byteBuffer.get(bytes); response =new String(bytes).trim(); byteBuffer.clear(); return response; } }
客户端代码没什么特别的,须要注意的是Buffer的读取。
最后输出结果:
server收到: INFO com.flydean.ChatServer - hello 小师妹! client收到: INFO com.flydean.ChatClient - response is 你是个好人 server收到: INFO com.flydean.ChatServer - 能不能? client收到: INFO com.flydean.ChatClient - response is 再见
解释一下整个流程:志伟跟小师妹创建了一个链接,志伟向小师妹打了一个招呼,小师妹给志伟发了一张好人卡。志伟不死心,想继续纠缠,小师妹回复再见,而后本身关闭了通道。
本文介绍了Selector和channel在发好人卡的过程当中的做用。
本文做者:flydean程序那些事本文连接:http://www.flydean.com/java-io-nio-selector/
本文来源:flydean的博客
欢迎关注个人公众号:程序那些事,更多精彩等着您!