NIO笔记(七)之读事件问题

读事件独立线程处理的问题

  1. 当把读事件放到单独的线程中执行,要注意,读事件的的延迟问题,即如果没有及时处理(将数据从Channel中读取处理)又没有将其注销,那么下一次迭代的时候,依然会触发读事件,这可能会产生一些问题

    public class ReadEvent {
        private ServerSocketChannel serverSocketChannel;
        private Selector selector;
    
    
        public ReadEvent() throws IOException {
            init();
        }
    
        public void init() throws IOException {
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().setReuseAddress(true);
            serverSocketChannel.bind(new InetSocketAddress(8888));
            System.out.println("启动服务并绑定端口");
    
        }
    
        private void handler() throws IOException {
            SocketChannel channel = serverSocketChannel.accept();
            channel.configureBlocking(false);
            System.out.println("接收一个新连接");
    
            selector = Selector.open();
            channel.register(selector, SelectionKey.OP_READ);
            while (true) {
                int n = selector.select();
                System.out.println(n);
                if (n == 0) {
                    continue;
                }
                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey selectionKey = keyIterator.next();
                    System.out.println(selectionKey);
                    keyIterator.remove();
    
                    if (selectionKey.isReadable()) {
                        System.out.println("处理读事件");
                        try {
                            /**
                             * 防止起了太多的线程,因为ReadTask执行有2s的延迟,所以此处如果
                             * 不休眠会导致ReadTask不断的创建,即主要为了防止启动太多的线程
                             */
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        /**
                         * 因为是多线程,可能读事件还没处理(将数据从 channel 中读取处理),主线程就结束了
                         * 那么下一次迭代的时候, 仍然会触发读事件, 这样一个 channel 中的数据就会被多个线程进行处理
                         */
                        new ReadTask(selectionKey).start();
                    }
                }
            }
        }
    
        public static void main(String[] args) throws IOException {
            new ReadEvent().handler();
        }
    
    }
    
    class ReadTask extends Thread {
        private SelectionKey key;
        private String name;
        private Charset charset = Charset.forName("UTF-8");
        private static final AtomicInteger COUNTER = new AtomicInteger(1);
    
        public ReadTask(SelectionKey key) {
            this.key = key;
            this.name = "ReadTask-" + COUNTER.getAndAdd(1);
        }
    
        @Override
        public void run() {
            //模拟读延迟
            mockReadDelay(2000);
    
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(64);
            try {
                int size = channel.read(buffer);
                buffer.flip();
                System.err.println(String.format("%s读取%s bytes,消息内容:%s", name, size, this.decode(buffer)));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public String decode(ByteBuffer buffer) {
            CharBuffer charBuffer = charset.decode(buffer);
            return charBuffer.toString();
        }
    
        public void mockReadDelay(long millis) {
            try {
                Thread.sleep(millis);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
    
  2. 输出结果如下,可以看出因为读事件线程的延迟处理(从Channel中读取数据)导致读事件被触发了三次,三个不同的线程来处理。

    启动服务并绑定端口
    接收一个新连接
    1
    sun.nio.ch.SelectionKeyImpl@4617c264
    处理读事件
    1
    sun.nio.ch.SelectionKeyImpl@4617c264
    处理读事件
    1
    sun.nio.ch.SelectionKeyImpl@4617c264
    处理读事件
    ReadTask-1读取5 bytes,消息内容:Aaaaa
    ReadTask-2读取0 bytes,消息内容:
    ReadTask-3读取0 bytes,消息内容:
    
    
  3. 怎么解决这个问题?通常的做法是服务器收到OP_READ事件之后,在下一次进行select()操作之前会将OP_READ先注销掉,以防止一个连接的读操作被分到多个线程中去.(这里只是实验性质来说明会有这个问题,所以不考虑粘包和拆包)

    public class ReadEvent {
        private ServerSocketChannel serverSocketChannel;
        private Selector selector;
    
    
        public ReadEvent() throws IOException {
            init();
        }
    
        public void init() throws IOException {
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().setReuseAddress(true);
            serverSocketChannel.bind(new InetSocketAddress(8888));
            System.out.println("启动服务并绑定端口");
    
        }
    
        private void handler() throws IOException {
            SocketChannel channel = serverSocketChannel.accept();
            channel.configureBlocking(false);
            System.out.println("接收一个新连接");
    
            selector = Selector.open();
            channel.register(selector, SelectionKey.OP_READ);
            while (true) {
                int n = selector.select();
                System.out.println(n);
                if (n == 0) {
                    continue;
                }
                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                while (keyIterator.hasNext()) {
                    SelectionKey selectionKey = keyIterator.next();
                    System.out.println(selectionKey);
                    keyIterator.remove();
    
                    //增加注销操作
                    int readyOps = selectionKey.readyOps();
                    //&~xx 代表取消事件
                    selectionKey.interestOps(selectionKey.interestOps() & ~readyOps);
    
    
                    if (selectionKey.isReadable()) {
                        System.out.println("处理读事件");
                        try {
                            /**
                             * 防止起了太多的线程,因为ReadTask执行有2s的延迟,所以此处如果
                             * 不休眠会导致ReadTask不断的创建,即主要为了防止启动太多的线程
                             */
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        /**
                         * 因为是多线程,可能读事件还没处理(将数据从 channel 中读取处理),主线程就结束了
                         * 那么下一次迭代的时候, 仍然会触发读事件, 这样一个 channel 中的数据就会被多个线程进行处理
                         */
                        new ReadTask(selectionKey).start();
                    }
                }
            }
        }
    
        public static void main(String[] args) throws IOException {
            new ReadEvent().handler();
        }
    
    }
    
    class ReadTask extends Thread {
        private SelectionKey key;
        private String name;
        private Charset charset = Charset.forName("UTF-8");
        private static final AtomicInteger COUNTER = new AtomicInteger(1);
    
        public ReadTask(SelectionKey key) {
            this.key = key;
            this.name = "ReadTask-" + COUNTER.getAndAdd(1);
        }
    
        @Override
        public void run() {
            //模拟读延迟
            mockReadDelay(2000);
    
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(64);
            try {
                int size = channel.read(buffer);
                buffer.flip();
                System.err.println(String.format("%s读取%s bytes,消息内容:%s", name, size, this.decode(buffer)));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public String decode(ByteBuffer buffer) {
            CharBuffer charBuffer = charset.decode(buffer);
            return charBuffer.toString();
        }
    
        public void mockReadDelay(long millis) {
            try {
                Thread.sleep(millis);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }  
    
  4. 输出

    启动服务并绑定端口
    接收一个新连接
    1
    sun.nio.ch.SelectionKeyImpl@4617c264
    处理读事件
    ReadTask-1读取5 bytes,消息内容:Aaaaa
    
    

解决方案

tomcat解决方案

  1. tomcat8.0.30
  2. 每次都将读取到的数据放到一个接收队列中,并且注销OP_READ事件,在处理完请求之后再重新注册OP_READ,等待后续操作。NioEndpoint.Poller#register()
public void register(final NioChannel socket) {
      socket.setPoller(this);
      KeyAttachment ka = new KeyAttachment(socket);
      ka.setPoller(this);
      ka.setTimeout(getSocketProperties().getSoTimeout());
      ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
      ka.setSecure(isSSLEnabled());
      PollerEvent r = eventCache.pop();
      ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
      if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);
      else r.reset(socket,ka,OP_REGISTER);
      //添加到事件队列
      addEvent(r);
}  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值