zoukankan      html  css  js  c++  java
  • NIO学习笔记

    为什么用NIO

    NIO是一种IO多路复用机制。这里的复用是指复用的线程而不是连接,这对理解NIO非常重要。关于NIO的介绍的文章网上已经有很多了,那么NIO相对于BIO有哪些优点呢,NIO在哪些场景下会更具有优势呢?

    我们可以首先思考下,使用BIO的时候是怎样的工作流程。BIO中每次来一个请求ServerSocket就会返回一个Socket,这个Socket中有标识来自客户端的ip和port,客户端的ip和port可以唯一标识一个TCP连接。注意,即便是同一个主机的客户端,每次连接使用的port是不一样的,所以客户端的ip和port是可以唯一标识一个TCP连接的。accept方法是阻塞的,如果是使用单线程的话,只能处理了一个客户端的请求,然后关闭socket再接收下一个请求。为了避免这种情况,所以一般使用线程池去处理。这种对于短连接还好(例如,读取1k字节就关闭socket),线程会很快被回收,可以用来处理下一个请求;但是如果是长连接或者下载大文件这种就还是会遇到之前的问题。例如我们初始化了一个5个线程的线程池,那么最多同时处理5个连接,如果是下载大文件或者长连接,那么5个线程就会一直用来处理前5个下载任务,其他的请求都需要等待,这样服务端不得不开辟更多的线程来处理更多的请求。也就是说对于海量的长连接,BIO是很容易遇到瓶颈的。这里海量的长连接可以举例,如即时通信、游戏服务器。

    这里注意到没有,BIO会一直占用一个线程,除非socket关闭。因此我们会想,对于即时通信这种,能不能在没有消息的时候read直接返回不占用线程,然后把对应socket的引用保存起来管理,但是不关闭连接,这样就可以在不关闭socket的情况下结束线程,让线程先处理其他来的连接。或者对于大文件下载这种情况,我们让每个请求都下载一小部分,然后返回,类似于网络上的分组,这样每个请求都会得到及时响应(同样每个请求下载的速度会慢,因为分时复用了)。这就是NIO要做的事情,selector就是帮我们管理socket的管家,在NIO中一个socket连接就是一个channel,selector会知道哪个channel可读,哪个channel可写了等。

    NIO举例

    我们通过一个下载大文件的demo来看下NIO的使用和需要注意的地方(参考: http://suhuanzheng7784877.iteye.com/blog/1122131)。

    public class NIOServer {
        static int BLOCK = 500 * 1024;
    
        /**
         * 处理客户端的内部类,专门负责处理与用户的交互
         */
        public class HandleClient {
            protected FileChannel channel;
            protected ByteBuffer buffer;
            String filePath;
    
            /**
             * 构造函数,文件的管道初始化
             *
             * @param filePath
             * @throws IOException
             */
            public HandleClient(String filePath) throws IOException {
    
                //文件的管道
                this.channel = new FileInputStream(filePath).getChannel();
    
                //建立缓存
                this.buffer = ByteBuffer.allocate(BLOCK);
                this.filePath = filePath;
            }
    
            /**
             * 读取文件管道中数据到缓存中
             *
             * @return
             */
            public ByteBuffer readBlock() {
                try {
    
                    //清除缓冲区的内容,posistion设置为0,limit设置为缓冲的容量大小capacity
                    buffer.clear();
    
                    //读取
                    int count = channel.read(buffer);
    
                    //将缓存的中的posistion设置为0,将缓存中的limit设置在原始posistion位置上
                    buffer.flip();
                    if (count <= 0)
                        return null;
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return buffer;
            }
    
            /**
             * 关闭服务端的文件管道
             */
            public void close() {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
        protected Selector selector;
        protected String filename = "d:\film\60.rmvb"; // a big file
        protected ByteBuffer clientBuffer = ByteBuffer.allocate(BLOCK);
        protected CharsetDecoder decoder;
    
        //构造服务端口,服务管道等等
        public NIOServer(int port) throws IOException {
            selector = this.getSelector(port);
            Charset charset = Charset.forName("GB2312");
            decoder = charset.newDecoder();
        }
    
        // 获取Selector
        //构造服务端口,服务管道等等
        protected Selector getSelector(int port) throws IOException {
            ServerSocketChannel server = ServerSocketChannel.open();
            Selector sel = Selector.open();
            server.socket().bind(new InetSocketAddress(port));
            server.configureBlocking(false);
    
            //刚开始就注册链接事件
            server.register(sel, SelectionKey.OP_ACCEPT);
            return sel;
        }
    
        // 服务启动的开始入口
        public void listen() {
            try {
                for (; ; ) {
    
                    //?
                    selector.select();
                    Iterator<SelectionKey> iter = selector.selectedKeys()
                            .iterator();
                    while (iter.hasNext()) {//首先是最先感兴趣的连接事件
                        SelectionKey key = iter.next();
    
                        //不移除的后果是本次的就绪的key集合下次会再次返回,导致无限循环,CPU消耗100%
                        // https://stackoverflow.com/questions/7132057/why-the-key-should-be-removed-in-selector-selectedkeys-iterator-in-java-ni
                        iter.remove(); // 注意这里必须移除,原因如上
    
                        //处理事件
                        handleKey(key);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        // 处理事件
        protected void handleKey(SelectionKey key) throws IOException {
            if (key.isAcceptable()) { // 接收请求
    
                //允许网络连接事件
                ServerSocketChannel server = (ServerSocketChannel) key.channel();
    
                //一个channel对应一个tcp连接, 用ip:port来唯一标识
                SocketChannel channel = server.accept(); //新建一个channel 并注册让selector管理, 
                channel.configureBlocking(false);
    
                //网络管道准备处理读事件
                channel.register(selector, SelectionKey.OP_READ);
            } else if (key.isReadable()) { // 通道可以读
                SocketChannel channel = (SocketChannel) key.channel();
    //            channel.
    
                //从客户端读过来的数据块
                int count = channel.read(clientBuffer);
                if (count > 0) {
    
                    //读取过来的缓存进行有效分割,posistion设置为0,保证从缓存的有效位置开始读取,limit设置为原先的posistion上
                    //这样一来从posistion~limit这段缓存数据是有效,可利用的
                    clientBuffer.flip();
    
                    //对客户端缓存块进行编码
                    CharBuffer charBuffer = decoder.decode(clientBuffer);
                    System.out.println("Client >>download>>" + charBuffer.toString());
    
                    //对网络管道注册写事件
                    SelectionKey wKey = channel.register(selector,
                            SelectionKey.OP_WRITE);
    
                    //将网络管道附着上一个处理类HandleClient,用于处理客户端事件的类
                    wKey.attach(new HandleClient(charBuffer.toString()));
                } else { // 返回-1 说明对方已经主动关闭
                    //如客户端没有可读事件,关闭管道
                    channel.close();
                }
    
                clientBuffer.clear();
            } else if (key.isWritable()) { // 通道可以写
                SocketChannel channel = (SocketChannel) key.channel();
    
                //从管道中将附着处理类对象HandleClient取出来
                HandleClient handle = (HandleClient) key.attachment();
    
                //读取文件管道,返回数据缓存
                ByteBuffer block = handle.readBlock();
                if (block != null) {
                    //System.out.println("---"+new String(block.array()));
    
                    //写给客户端
                    channel.write(block);
                } else { // 写完毕了,server端主动关闭连接
                    handle.close();
                    channel.close();
                }
            }
        }
    
        public static void main(String[] args) {
            int port = 12345;
            try {
                NIOServer server = new NIOServer(port);
                System.out.println("Listernint on " + port);
                while (true) {
                    server.listen();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    客户端

    public class MyNIOClient {
        static CharsetEncoder encoder = Charset.forName("GB2312").newEncoder();
        public static void main(String[] args) throws Exception{
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
    
            Selector selector = Selector.open(); // 无参数open,不会阻塞
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
    
            socketChannel.connect(new InetSocketAddress("127.0.0.1", 12345)); // 非阻塞
            while (true) {
                selector.select();
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                while (iter.hasNext()) {
                    SelectionKey key = iter.next();
                    iter.remove();
                    if (key.isConnectable()) {
                        // 获得该事件中的管道对象
                        SocketChannel channel = (SocketChannel) key
                                .channel();
                        System.out.println("connect");
                        // 如果该管道对象已经连接好了
                        if (channel.isConnectionPending()) // 由于是非阻塞,socket链接可能处于一种中间态
                            channel.finishConnect(); // finishConnect方法可以用来检查在非阻塞套接字上试图进行的连接的状态,还可以在阻塞套接字建立连接的过程中阻塞等待,直到连接成功建立
    
                        // 往管道中写一些块信息
                        channel.write(encoder.encode(CharBuffer
                                .wrap("d://film//1.rmvb")));
                    } else if (key.isWritable()) {
                        SocketChannel client = (SocketChannel) key.channel();
                        System.out.println("writable");
                        client.write(encoder.encode(CharBuffer.wrap("hello")));
                    }
                }
            }
        }
    }
    
    

    总结

    • 一个channel就代表一个tcp连接, 用客户端的ip和port可以唯一标识,客户端每次连接都是随机一个port
    • 如果是长连接,那么客户端和服务端要保持心跳来判断对方是否存活,否则的话,服务端没有长时间没有接受消息,一直保持channel,但是客户端重新使用了新的端口进行连接,旧的channel就永远不会关闭,造成内存泄漏
    • NIO更适用海量的长连接。这种场景更能体现NIO的威力,线程被复用了,多个连接复用单个或几个线程,减少线程切换
    • IO多路复用,复用的是线程,而不是tcp连接
    • 对方主动关闭,read会返回-1,因此如果read返回-1,那么就可以关闭channel了

    参考

    1. http://suhuanzheng7784877.iteye.com/blog/1122131
    2. http://book.51cto.com/art/200902/109752.htm
    3. https://stackoverflow.com/questions/7132057/why-the-key-should-be-removed-in-selector-selectedkeys-iterator-in-java-ni
    4. http://www.importnew.com/1178.html
  • 相关阅读:
    解决ftp的pasv模式下iptables设置问题
    linux修改运行中的脚本
    shell脚本——列出质数
    转载:tomcat设置https的两种方式
    Centos缺少ifconfig命令
    转载:MySQL 数据库设计总结
    转载:HTML5视频推流方案
    转载:Linux五种方案快速恢复你的系统
    转载:HT可视化案例
    转载:21种JavaScript设计模式最新记录(含图和示例)
  • 原文地址:https://www.cnblogs.com/set-cookie/p/8661372.html
Copyright © 2011-2022 走看看