参考:http://ifeve.com/selectors/
参考:https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html
netty的NioEventLoop类的实现也是类似
这些都是在实践中踩过雷的,今天某应用再次踩雷,把遇到的几个雷都收集一下,给后来者参考。
1.即使是accept事件,没有真正的read和write,Channel也要关闭,否则unix domain socket会被泄漏(WINDOWS更可怕),因为NIO的每个
Channel上都有两个FD用来监听事件(接收和发送走不同的FD)。
2.cancel事件导致CPU占用100%,http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6403933
其原因就是调用key.cancel()时底层在下一次seelect前并没有真正的取消。导致等待select事件返回却又没有返回我们注册的key.这个事件不断地
循环触发,CPU一直处理返回 key为0的select()调用。解决方法有两种,一是在key.cancel()后立即selectNow();但是如果是多线程并发操作,有
可能这两行语句中间线程被切换,使得key.cancel()后没有立即执行 selectNow().这在多Selector情况下是可能的。另一种就是jetty处理方式,如果
select()返回0且连续几次出现这样的情况(有事件触发返回,却不是返回我们注册的KEY),就将有效的key重新注册到一个新的selector上。其实
glassfish在处理多次次次次write返回为0的情况时也是这种策略。
示例代码:(真实的项目中)
int selectTimeout = connectionConfig.getSelectTimeout(); int allProcessMaxTime = connectionConfig.getAllProcessMaxTime(); //selector在实现时有bug,epool底层可能会发送一个错误的信号导致select方法提前返回,但没有 //返回注册的事件,而且不断循环造成CPU100% int slelectZeroCount = 0; int maxZeroCount = 20; int fixed = 0; while (selector.isOpen() && selector.keys().size() != 0 && allProcessMaxTime > 0) { long start = System.currentTimeMillis(); // 查询看是否有已经准备好的通道,指定超时时间 int count = selector.select(selectTimeout); if (count == 0) { slelectZeroCount++; } else { slelectZeroCount = 0; //保证是连续的count==0时才将slelectZeroCount++,如果其中有一次返回注册事件测已经正常 } if (slelectZeroCount > maxZeroCount && fixed == 0) { //没有尝试修复动作,则先进行修复干预 for (SelectionKey key : selector.keys()) { if (key.isValid() && key.interestOps() == 0) { key.cancel(); } } fixed = 1; } else if (slelectZeroCount > maxZeroCount && fixed == 1) { //如果已经干预过仍然连续返回0,注意如果不返回0的话slelectZeroCount就被置0. //重新获取一个selector,将当前事件重新注册到新的selector上。并销毁当前selector Selector newSelector = this.getSelector(); this.changeSelector(selector, newSelector); selector = newSelector; } //对channel进行正常处理
重新注册的代码:
private synchronized void changeSelector(Selector oldSelector, Selector newSelector) { for (SelectionKey key : oldSelector.keys()) { if (!key.isValid() || key.interestOps() == 0) { continue; } Object att = key.attachment(); try { if (att == null) { key.channel().register(newSelector, key.interestOps()); } else { key.channel().register(newSelector, key.interestOps(), att); } } catch (ClosedChannelException e) { SocketChannel sc = (SocketChannel) key.channel(); sc.close(); } } try { oldSelector.close(); } catch (IOException e) { logger.error(e.getMessage()); } }
同样对于网络状态不好时,连续写操作返回0的处理:
private void flushData(Selector selector, SocketChannel socketChannel, ByteBuffer byteBuffer) throws IOException { int count = 0; int maxCount = 20; while (byteBuffer.hasRemaining()) { int len = socketChannel.write(byteBuffer); if (len < 0) { throw new EOFException("write channel is closed."); } // 如果不对len==0(即当前网络不可用)的情况处理,则while(byteBuffer.hasRemaining())可能一直 // 循环下去而消耗大量的CPU. if (len == 0) { count++; } else { count = 0; } if (count > maxCount) { throw new IOException("can't connect to target."); } } }
我自己写的代码:
package com.eshore.ismp.hbinterface.crm; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.util.Iterator; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.eshore.ismp.hbinterface.service.BizCommonService; /** * @author mercy *接收CRM优惠工单信息 */ public class CrmServerTest { private static final Logger logger = LoggerFactory.getLogger(CrmServerTest.class); private Selector selector=null; private ServerSocketChannel serverSocketChannel=null; private int port=10003; //private Charset charset=Charset.forName("GBK");//返回一个字符类型对象 public CrmServerTest() throws IOException{ serverSocketChannel=ServerSocketChannel.open(); serverSocketChannel.socket().setReuseAddress(true); serverSocketChannel.configureBlocking(false);//设置无阻塞模式 serverSocketChannel.socket().bind(new InetSocketAddress(port)); logger.info("服务已启动..."); } public void start(BizCommonService bizCommonService) throws IOException{ buildSelector(); this.service(bizCommonService); } public void buildSelector()throws IOException{ logger.info("构建selector"); selector=Selector.open(); //创建Selector对象 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); } //重建selector public void rebuildSelector(){ logger.info("重建selector"); Selector oldSelector=selector; if (oldSelector == null) { return; } try { selector=Selector.open(); } catch (IOException e1) { e1.printStackTrace(); } int nChannels = 0; for (SelectionKey key: oldSelector.keys()) { //返回key的附加对象 Object a = key.attachment(); try{ if (!key.isValid() || key.channel().keyFor(selector) != null) { continue; } int interestOps = key.interestOps(); key.cancel(); key.channel().register(selector, interestOps, a); //就不再更新通道的selectKey nChannels ++; } catch (Exception e) { logger.warn("Failed to re-register a Channel to the new Selector.", e); } } try { // time to close the old selector as everything else is registered to the new one oldSelector.close(); } catch (Throwable t) { if (logger.isWarnEnabled()) { logger.warn("Failed to close the old Selector.", t); } } logger.info("Migrated " + nChannels + " channel(s) to the new Selector."); } public void service(BizCommonService bizCommonService) throws IOException{ int start=0; while(true){ if(selector.select()==0){ start++; logger.info("continue...."); continue; } if(start==5000){ start=0; logger.info("重建selector"); rebuildSelector(); } Set readyKeys=selector.selectedKeys(); Iterator it=readyKeys.iterator(); while(it.hasNext()){ SelectionKey key=null; try{ key=(SelectionKey)it.next(); it.remove();//删除集合中的key if(key.isAcceptable()){//是否可以接收客户端的socket连接 ServerSocketChannel ssc=(ServerSocketChannel)key.channel(); SocketChannel socketChannel=ssc.accept(); //logger.info("接收到的客户端连接,来自:"+socketChannel.socket().getInetAddress()+":"+socketChannel.socket().getPort()); socketChannel.configureBlocking(false);//设置无阻塞模式 ByteBuffer buffer=ByteBuffer.allocate(6000);//创建一个ByteBuffer对象用于存放数据(数据存放缓冲区) socketChannel.register(selector,SelectionKey.OP_READ|SelectionKey.OP_WRITE,buffer);//注册事件,Selector会监控事件是否发生 } if(key.isReadable()){//key的channel是否可读 receive(key); } if(key.isWritable()){//key的channel是否可写 send(key,bizCommonService); } }catch(IOException e){ logger.info(" crm exception...."); e.printStackTrace(); try{ if(key!=null){ key.cancel(); key.channel().close(); selector.selectNow(); } }catch(Exception ex){ ex.printStackTrace(); } } } } } /** * @param key * @throws IOException * @author mercy * 根据读取的数据处理完返回给客户端 */ public void send(SelectionKey key,BizCommonService bizCommonService) throws IOException{ ByteBuffer buffer=(ByteBuffer) key.attachment(); SocketChannel socketChannel=(SocketChannel) key.channel(); buffer.flip(); String data=decode(buffer);//解码客户端发过来的数据 if(data.length()==0){ return ; } String outputData=data;//.substring(0, data.indexOf("\n")+1); //logger.info("客户端发送的数据:"+outputData+",length:"+outputData.length()); //String reply="FFFF02141433570200012400050301IBSS01662 001023CS0214143357*0189086510002001100301420170214143500004003099005007success"; //ByteBuffer outputBuffer=encode("echo:"+reply);//返回给客户端的数据 boolean result = bizCommonService.sendOperToCacheAysn(String.valueOf(outputData)); //创建响应报文 String res = bizCommonService.createResponseStr(String.valueOf(outputData),result); ByteBuffer outputBuffer=encode(res);//返回给客户端的数据 while(outputBuffer.hasRemaining()){ //System.out.println("=="+decode(outputBuffer)); socketChannel.write(outputBuffer); } ByteBuffer temp=encode(outputData); buffer.position(temp.limit()); buffer.compact(); if(outputData.length()==0){ key.cancel(); socketChannel.close(); logger.info("关闭与某客户端的连接"); } } /** * @param key * @throws IOException * @author mercy * 读取客户端发来的数据 */ public void receive(SelectionKey key) throws IOException{ ByteBuffer buffer=(ByteBuffer) key.attachment(); SocketChannel socketChannel=(SocketChannel) key.channel(); ByteBuffer readBuffer=ByteBuffer.allocate(6000);//创建自定义内存的buffer(存放读到的数据) socketChannel.read(readBuffer); readBuffer.flip(); buffer.limit(buffer.capacity());//设置buffer的极限为buffer的容量 buffer.put(readBuffer);//复制到缓存区 } public String decode(ByteBuffer buffer){//解码 CharBuffer charBuffer=Charset.forName("GBK").decode(buffer); return charBuffer.toString(); } public ByteBuffer encode(String str){//编码 return Charset.forName("GBK").encode(str); } public static void main(String[] args) throws IOException { final Logger log = LoggerFactory.getLogger(CrmServerTest.class); try{ ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( new String[] { "applicationContext.xml" }); context.start(); BizCommonService bizCommonService = (BizCommonService) context.getBean("bizCommonService"); new CrmServerTest().start(bizCommonService); }catch(Exception e){ log.error("start agent interface server error:",e); System.exit(-1); } } }
一般来说
while(true){ if(selector.select()==0){ start++; logger.info("continue...."); continue; }
没有
selector.select()>0
好,因为万一 selector.select() 不阻塞了返回0用while(true)会反复执行 if(selector.select()==0) 这一段