NIO学习笔记

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做的。
肥肥小浣熊
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341