美文网首页
NIO学习笔记

NIO学习笔记

作者: 江江的大猪 | 来源:发表于2017-11-10 18:49 被阅读62次

    NIO

    操作系统背景知识

    unix提供了5中io模型,其中java的底层实现依赖的是操作系统的io复用模型。linux提供select/poll,进程通过将一个或多个fd(文件描述符)传递给select或者poll,阻塞在select上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。select/poll顺序扫描fd是否就绪,并且支持的fd数量有限。linux还提供了一个epoll系统调用,使用事件驱动的方式代替顺序扫描fd,当有fd就绪时,执行回调函数rollback,因此性能更高。


    epoll相比于select的改进:

    1. 一个进程打开的socket描述符(fd)不受限制,仅受限于操作系统的最大文件句柄数
      select单个线程打开的fd有限,由FD_SETSIZE设置,默认1024,可以修改这个宏重新编译内核,但越大,select的效率越低(遍历fd越来越慢)。epoll支持的fd上限是操作系统的最大文件句柄数,受内存影响,可以cat /proc/sys/fs/file-max查看。
    2. io效率不会随着fd的数目线性下降
      select/poll会遍历fd。内核实现中epoll根据每个fd的callback函数实现了只对活跃的socket进行操作,从这一点上,epoll实现了一个伪aio。如果所有的socket都处于活跃态,例如告诉lan环境,epoll并不比select/pollx效率高太多,如果过多使用epoll_ctl,效率还会下降;但是一旦使用wan环境,epoll效率远高于select/poll。
    3. 使用mmap加速内核和用户空间的信息传递
      无论是select/poll还是epoll都需要进行内核空间和用户空间的消息传递,如何避免不必要的内存复制就显得非常重要,epoll是通过内核和用户空间mmap同一块内存实现。

    nio基础知识

    NIO是NEW IO的简称,不同于传统基于流的io,是一套新的io标准,jdk4出现的nio对文件系统的处理能力不足,jdk7对nio进行了升级,被称nio2.0,提供了aio功能,支持基于文件的异步io和针对网络套接字的异步操作,但是因为nio和aio在操作系统上都是通过epoll实现的,所以实际效率差别不大,netty在提供了几个版本aio的实现后,也不继续支持了。
    1、基于块(block),以块为基本单位处理数据
    2、为所有原始类型提供buffer支持
    3、增加channel对象,作为新的原始io的抽象
    4、支持锁和内存映射文件的文件访问接口
    5、提供了基于selector的异步网络io,因为jdk使用epoll()代替传统的select()实现,所以没有最大连接句柄的限制,一个Selector可以解除成千上万的客户端

    Channel:Channel有四个重要的实现类
    • FileChannel

    从文件中读写数据

    • DatagramChannel

    能通过UDP读写网络中的数据

    • SocketChannel

    能通过TCP读写网络中的数据

    • ServerSocketChannel

    可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel

    • tips

    jdk4后引入nio的同时也对旧io重写了,旧io类库中的三个类被修改了,用以产生FileChannel。FileInputStream、FileOutputStream、RandomAccessFile。Reader和Writer这种字符模式类不能用来产生通道,字节流和底层nio的性质一样所以可以产生通道,但是channel提供了方法用以在通道中产生Reader、Writer

    Buffer
    • Buffer中的三个重要参数
    参数 写模式 读模式
    位置(position) 当前缓冲区的位置,将从position的下一个位置写入数据 当前缓冲区的位置,将从position的下一个位置读取数据
    容量(capacity) 缓存区的总容量上线 缓存区的总容量上线
    上限(limit) 缓存区的实际上限,总是小于等于容量,通常情况和容量相等 代表可读取的总容量,和上次写入的数据量相等
    Buffer常用方法
    • flip

    新建buffer时position为0,limit和capacity都是buffer的总容量上限。
    读写buffer时,position移动,limit和capacity不变。
    flipj将buffer从写模式转换为读模式,将limit设为之前position的位置,然后将position重置为0。

        public final Buffer flip() {
            limit = position;
            position = 0;
            mark = -1;
            return this;
        }
    
    • rewind

    position清零,清除mark标志位,用于重新读取buffer

        public final Buffer rewind() {
            position = 0;
            mark = -1;
            return this;
        }
    
    • clear

    position清零,清除mark标志位,将limit设置为capacity的大小,用于再次写入

        public final Buffer clear() {
            position = 0;
            limit = capacity;
            mark = -1;
            return this;
        }
    
    • compact

    compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity

    实际应用

    复制文件的三种方式,亲测使用channel的transfer方法效率更高,内部通过内存映射文件实现
    FileChannel readChannel = new FileInputStream("压缩文件.zip").getChannel();
    FileChannel writeChannel = new FileOutputStream("压缩文件备份.zip").getChannel();
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    while (readChannel.read(buffer) != -1) {
        buffer.flip();
        writeChannel.write(buffer);
        buffer.clear();
    }//1
    readChannel.transferTo(0, readChannel.size(), writeChannel);//2
    //FileChannel的size()返回关联文件的实际大小
    writeChannel.transferFrom(readChannel, 0, readChannel.size());//3
    writeChannel.close();
    readChannel.close();
    
    通过Selector使用异步io

    与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。实现Channel接口的抽象类SelectableChannel,继承SelectableChannel的Channel才可以使用Selector,因为SelectableChannel才有register()方法

    SelectableChannel

    SelectionKey register(Selector sel, int ops, Object att)

    第二个参数代表要监听的事件,监听多个事件通过|连接:
    SelectionKey.OP_CONNECT
    SelectionKey.OP_ACCEPT
    SelectionKey.OP_READ
    SelectionKey.OP_WRITE(很少注册该事件,该事件仅表示缓冲区是否可用,所以注册后会一直满足条件)
    第三个参数为和Channel绑定的对象,可以将Channel使用的Buffer传入,只传前两个参数也可以,有重载方法

    int validOps()

    判断该种Channel支持的监听事件

    Selector:

    Selector对象维护了3个SelectionKey的set,一个注册的,一个是就绪的,最后一个是cancel过但是未删除的。最后这个set我们没有方法直接获取到,通过SelectionKey的cancel方法将SelectionKey加入这个set,下次调用select方法就会清空这个set。

    int select()

    阻塞到至少有一个通道在你注册的事件上就绪了

    int select(long timeout)

    和select()一样,除了最长会阻塞timeout毫秒(参数)

    int selectNow()

    不会阻塞,不管什么通道就绪都立刻返回,此方法执行非阻塞的选择操作。如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零。

    tips

    select()方法返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。如果调用select()方法,因为有一个通道变成就绪状态从而返回了1,但是没有任何处理,再次调用select()方法,如果另一个通道就绪了,它会返回1而不是2
    如果第一次调用select()之前就已经有通道就绪了,select()会返回0,但是执行selectedKeys返回的set不为空。select()的返回值是自上次调用select()方法后有多少通道变成就绪状态,这一点很重要!

    Selector open()

    静态方法,工厂方法,返回Selector实例对象。

    Set<SelectionKey> keys()

    返回注册到该Selector上的所有通道的SelectionKey

    Set<SelectionKey> selectedKeys()

    一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectedKeys()方法,访问就绪通道

    Selector wakeUp()

    某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。只要让其它线程在调用select()方法的那个Selector对象上执行wakeup()方法即可。阻塞在select()方法上的线程会立马返回。

    close()

    用完Selector后调用其close()方法会关闭该Selector,且使注册到该Selector上的所有SelectionKey实例无效,通道本身并不会关闭。

    SelectionKey:Channel和Selector之间的关联对象

    int interestOps()

    返回代表侦听事件的int,不同的bit代表对应的事件

    int readyOps()

    返回代表被侦听的就绪事件的int,不同的bit代表对应的事件

    boolean isAcceptable()

    源码为(readyOps() & OP_ACCEPT) != 0,类似的还有isConnectable()、isReadable()、isWritable()

    SelectableChannel channel()

    返回被侦听的Channel

    Selector selector()

    返回注册到的Selector

    Object attach(Object ob)

    绑定对象,并返回之前的绑定对象

    Object attachment()

    返回register()时和Channel一起绑定的或使用attach绑定到Selector的对象

    cancel()

    并不直接生效,将该key放到cancelled-key set中,到Selector下次select()时将该key从所有set中删除,但SelectionKey的isValid()会立即回复false

    Selector selector = Selector.open();
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    ServerSocket serverSocket = serverSocketChannel.socket( );
    serverSocket.bind (new InetSocketAddress(8888));
    serverSocketChannel.configureBlocking(false);
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
        if (selector.select() == 0) {
            continue;
        }
        for (Iterator<SelectionKey> it = selector.selectedKeys().iterator(); it.hasNext(); it.remove()) {
            SelectionKey key = it.next();
            if (key.isValid() && key.isAcceptable()) {
                //TODO
                ServerSocketChannel server = (ServerSocketChannel) key.channel();
                SocketChannel channel = server.accept( );
                if (channel == null) {
                    continue;
                }
                channel.configureBlocking (false);
                channel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
    
            }
            if (key.isValid() && key.isReadable()) {
                SocketChannel channel = (SocketChannel) key.channel();
                ByteBuffer buffer = (ByteBuffer)key.attachment();
                if (channel.read(buffer) > 0) {
    
                } else {
                    key.cancel();
                }
            }
        }
    }
    selector.close();
    
    总结
    1. SelectionKey维护两个set集合,interestOps和readyOps。Selector维护三个set集合,registeredKeys、selectedKeys、cancelledKeys。每次select()方法调用时,先把cancelledKeys数据同步到registerKeys和selectedKeys,做减法以完成反注册。因为对这几个集合的操作不是线程安全的,所以一般使用Selector的select()只用单线程,而对于select得到的channel和对应的IO操作,可以新开线程或者使用线程池来处理。这也正是IO复用的意义所在。
    2. 直接使用jdk的原生nio很麻烦,而且还会碰到bug,多数开发会直接使用netty,性能高,用起来方便,netty5被雪藏了,所以现在还是使用netty4。很多开源工具的通信底层都是netty做的。
    肥肥小浣熊

    相关文章

      网友评论

          本文标题:NIO学习笔记

          本文链接:https://www.haomeiwen.com/subject/pbhimxtx.html