本文揭示了Java NIO底层的诸多细节与使用和理解上的陷阱,对于NIO的学习非常有帮助。

本文是笔者在学习NIO过程中发现的一些比较容易让人忽略的知识的一个总结,而这些让人忽略的小细节恰恰是NIO网络编程中必不可少。虽然现在我们不会直接编写NIO来完成我们的网络层通讯,而是使用成熟的基于NIO的网络框架来实现我们的网络层。如,netty、mina。但对NIO网络编程过程的了解,非常有助于我们更深入的理解netty、mina等网络框架,以至于能更好的使用它们。因此,本文并不对NIO的一些底层知识做过多的介绍,主要侧重于NIO编程中细节的讲解。

NIO vs IO

  • 标准的IO基于字节流和字符流进行操作的;而NIO是基于通道(Channel)进行操作的。

  • 通道是双向的,既可以写数据到通道,又可以从通道中读取数据;而流的读写通常是单向的,要么是输入流,要么是输出流,不能既是输入流又是输出流。

  • NIO能够实现非阻塞的网络通信,而IO只能实现阻塞式的网络通信。

Buffer

Java NIO中的Buffer用于和NIO通道进行交互。数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中。
Buffer是一个特定的原生类型数据容器。
Buffer是一种特定的原生类型的线程的、有限的元素序列。除了它的内容之外,一个Buffer一个重要的本质属性是它的capacity、limit、和position;

  • capacity:一个buffer的capacity指的就是它所包含的元素的个数。buffer的capacity永远不会是负数,且永远不会变化。

  • limit:一个buffer的limit指的是不应该被读或写的第一个元素的索引( position <= limit )。一个buffer的limit永远不会是负数的,并且永远不会超过它的capacity。

  • position:一个buffer的position指的是下一个将要被读或写的元素的索引。一个buffer的position永远不会是负数的,并且永远不会超过它的limit( 这里也说明,position最多等于limit,当position==limit时,这个时候是不能够在从buffer中读取到数据了 )。

数据操作:

Buffer的每一个子类都定义了两类get和put操作。

  • 相对操作:读或写 一个或多个元素 从当前position位置开始并且会根据转换元素数量增加position的值。如果要求的转换超过了limit,那么一个相关的get操作会抛出BufferUnderflowException,一个相关的put操作会抛出一个BufferOverflowException,无论是这两个哪种情况发生,都不会有数据被传递。

  • 绝对操作:会接受一个显示元素的索引并且不会影响position。如果索引参数超过了limit,那么绝对的get和put操作会抛出一个IndexOutOfBoundsException异常。

不变性:

0 <= mark <= position <= limit <= capacity

线程安全性:

buffer在多线程并发下并不是安全的。如果一个buffer会在多个线程使用,那么需要使用恰当的同步操作来访问buffer。也就是buffer本身并不是线程安全的。

Java NIO 内存分配
  • Heap buffer :堆栈的内存分配。堆栈就是Java内存模型当中内存的区域,位于堆上,堆是我们生成对象的区域。

  • Direct buffer :堆外内存分配。这个内存本身不是由JVM进行控制的,它是由操作系统进行统一的处理的。通过这种直接的缓冲就能实现zero-copy(零拷贝)的动作。[ 关于堆外内存可详见:堆外内存 之 DirectByteBuffer 详解 ]

方法
  • flip()
    flip方法将Buffer从写模式切换到读模式。

  • rewind()
    rewind()方法将position设回0,limit保持不变,所以你可以重读Buffer中的所有数据。可见在调用rewind()之前Buffer已经是处于读模式了

  • clear()
    让Buffer重新准备好重头开始再次被写入。该方法会将position、limit重置。如果此时还没有读取的数据,则就无法读取到了。虽然clear()不会清楚数据,但是position、limit标志位被重置了,所以无法找到哪些未读取数据的位置了。

  • compact()
    compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。clear() VS compact()
    clear只是对position、limit、mark进行重置,而compact在对position进行设置,以及limit、mark进行重置的同时,还涉及到数据在内存中拷贝。所以compact比clear更耗性能。但compact能保存你未读取的数据,将新数据追加到为读取的数据之后;而clear则不行,若你调用了clear,则未读取的数据就无法再读取到了。

  • Slice Buffer与原有buffer共享相同的底层数据
    ByteBuffer.slice(start, end) —————— [start, end),即包含start,不包含end
    slice返回的ByteBuffer底层数据和源ByteBuffer是共享的,所以无论对那个buffer进行修改,都会影响到另一buffer。

  • buffer.asReadOnlyBuffer()
    只读buffer适用于方法传递时,你只希望你的调用端去读取你所提供的buffer。即,将一个只读buffer当做参数传递给某个方法。

  • ByteBuffer.wrap(byte[] array)
    该方法生成的ByteBuffer底层就是你传进来的这个array数组,并没有进行数组拷贝,所以是和你传进来的array共享内容的。这也导致如果你修改了传进来的array数组的内容,是会反映到ByteBuffer的。

  • 关于Buffer的Scattering与Gathering
    Scattering:允许read的时候传递一个buffer[]数组。将一个Channel中的数据给读到了多个buffer当中,它是按照顺序依次读入buffer当中的,而且总是当当前buffer已经写满了才会写下一个buffer。
    Gathering:允许write的时候传递一个buffer[]数组。将多个buffer的数据写到一个Channel中。它会将第一个buffer中可读的数据都写入channel后,再将下一个buffer中的数据写入到channel中,以此依次将buffer中可读取的数据写到channel中。
    Scattering与Gathering适用于网络操作中的自定义协议。比如,一个请求中带有两个请求头以及一个body,第一个请求头的数据长度固定是10个byte,第二个请求头的数据长度固定是5个byte,而body的长度是不确定的。那么我们就可以用3个buffer组成的数组来接这样的请求。bytebuffer[]数组中,第一个bytebuffer元素的容量为10,用于接受第一个请求头的信息;第二个bytebuffer元素的容量为5,用于接受第二个请求头的信息;第三个定义一个大容量的bytebuffer用于接受body的信息。这样就天然的实现了一种数据的分门别类。

Selector

为什么使用Selector?

仅用单个线程来处理多个Channels的好处是,只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。因为对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源。因此,使用的线程越少越好。

selector的非阻塞模式

与Selector一起使用时,Channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。

方法

  • wakeUp()
    如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即"醒来(wake up)"。

  • close()
    用完Selector后调用其close()方法会关闭该Selector,即使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。
    linux下Selector底层是通过epoll来实现的,当创建好epoll句柄后,它就会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

关于selector的详细实现可见浅谈 Linux 中 Selector 的实现原理

SocketChannel

Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。

方法
  • connect()
    如果这个channel是非阻塞模式的,那么该方法的调用将启动一个非阻塞的操作。如果连接立即建立,当在连接一个本地地址时会发生,那么该方法会返回true。否则若连接还未建立该方法会返回一个false,并且连接操作最后必须通过调用finishConnect方法来完成。
    这个方法可能在任何时候被调用。如果在该方法调用时,对应的channel执行了read或write操作,那么read或write操作将先会被阻塞直到connect操作完成。如果连接尝试启动但是失败了,也就是说,如果connect方法的调用抛出了一个检查异常,那么该通道将被关闭。
    写了代码测试了下,无论是是本机,还是跨机器调用,都是返回false。

  • finishConnect()
    通过设置一个socket为非阻塞模式来开启一个非阻塞连接操作,然后调用该socket的connect方法。一旦连接建立,或者尝试连接失败,那么socket channel将变为可连接的并且该方法可能被调用已完成连接的后续事件。如果连接操作失败,则调用该方法将导致一个相关的IOException异常被抛出。
    如果这个channel已经连接了,那么调用该方法不会阻塞并会立即返回true。如果这个channel是非阻塞模式的,那么该方法将返回false如果连接操作还没完成。如果这个channel是阻塞模式的,那么该方法将会阻塞直到连接成功或失败,如果连接成功则返回true,否则将抛出一个检查异常以描述失败。
    这个方法可能在任何时候被调用。如果在该方法调用时,对应的channel执行了read或write操作,那么read或write操作将先会被阻塞直到connect操作完成。如果了解尝试启动但是失败了,也就是说,如果connect方法的调用抛出了一个检查异常,那么该通道将被关闭。

  • isConnectionPending()
    告知这个channel是否正在进行连接操作。
    仅当这个channel的连接操作已经启动,但是还没完成( 用通过调用finishConnect方法来完成 )。

示例:
无论如何在connect后finishConnect()sorry 方法都是需要被调用的。调用finishConnect()的三种返回:

① 如果你在connect()后直接调用了finishConnect()( 并非在CONNECT事件中调用 ),则若finishConnect()返回了true,则表示channel连接已经建立,而且CONNECT事件也不会被触发了。
② 如果finishConnect()方法返回false,则表示连接还未建立好。那么就可以通过CONNECT事件来监听连接的完成。当然也可以像上面的写法,无论如何都会给SocketChannel注册CONNECT事件,finishConnect()方法的调用放到CONNECT事件处理中调用。
③ 如果finishConnect()方法抛出了一个IOException异常,则表示连接操作失败。

支持的事件:SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE

ServerSocketChannel Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样。

支持的事件:SelectionKey.OP_ACCEPT

ServerSocketChannel & SocketChannel 关于selectedKey集合的处理 对于已经处理的SelectionKey需要充selectedKey集合中移除,如果不将已经处理的SelectionKey从selectedKey集合中移除,那么下次有新事件到来时,在遍历selectedKey集合时又会遍历到这个SelectionKey,这个时候就很可能出错了。比如,如果没有在处理完OP_ACCEPT事件后将对应SelectionKey从selectedKey集合移除,那么下次遍历selectedKey集合时,处理到到该SelectionKey,相应的ServerSocketChannel.accept()将返回一个空(null)的SocketChannel。

关于OP_WRITE事件:

OP_WRITE事件的就绪条件并不是发生在调用channel的write方法之后,而是在当底层缓冲区有空闲空间的情况下。因为写缓冲区在绝大部分时候都是有空闲空间的,所以如果你注册了写事件,这会使得写事件一直处于就就绪,选择处理现场就会一直占用着CPU资源。所以,只有当你确实有数据要写时再注册写操作,并在写完以后马上取消注册。

其实,在大部分情况下,我们直接调用channel的write方法写数据就好了,没必要都用OP_WRITE事件。那么OP_WRITE事件主要是在什么情况下使用的了?

其实OP_WRITE事件主要是在发送缓冲区空间满的情况下使用的。如:

while (buffer.hasRemaining()) {     int len = socketChannel.write(buffer);  if (len == 0) {          selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE);          selector.wakeup();          break;     }}

当buffer还有数据,但缓冲区已经满的情况下,socketChannel.write(buffer)会返回已经写出去的字节数,此时为0。那么这个时候我们就需要注册OP_WRITE事件,这样当缓冲区又有空闲空间的时候就会触发OP_WRITE事件,这是我们就可以继续将没写完的数据继续写出了。

而且在写完后,一定要记得将OP_WRITE事件注销:

selectionKey.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE);

注意,这里在修改了interest之后调用了wakeup();方法是为了唤醒被堵塞的selector方法,这样当while中判断selector返回的是0时,会再次调用selector.select()。而selectionKey的interest是在每次selector.select()操作的时候注册到系统进行监听的,所以在selector.select()调用之后修改的interest需要在下一次selector.select()调用才会生效。

关于远端关闭事件

SelectionKey并没有提供关闭事件,其实通过OP_READ是可以监听到远端的关闭操作的。
当OP_READ事件触发使,int readByteNum = channel.read(buffer)会返回从channel读取到的字节数。
① 当readByteNum > 0 时,表示从channel读取到了readByteNum个字节到buffer中。
② 当readByteNum == 0 时,表示channel中已经没有数据可以读取了,这个时候buffer的position==limit。
③ 当 readByteNum == -1 时,表示远端channel正常关闭了。这个时候我们就需要进行该通道的关闭和注销操作了。 netty源码中OP_READ事件也会根据读取到的字节数为-1时,进行channel的关闭操作。

这里closeOnRead(pipeline)方法最终会调用channel.close()方法来完成tcp套接字的关闭(这点下面会详细说明)

如何正确的关闭一个已经注册的SelectableChannel了?

需要调用channel.close()

最终调用的会使AbstractInterruptibleChannel的close方法

总归来说,调用channel.close()方法:

① 能够调动channel对应的SelectionKey的cancel()方法使该SelectionKey加到Selector的cancel selectionKey set集合中,这样在下一次selector的时候,就会将其从selector中相关的selectionKey集合中移除,并且不会监听该selectionKey所感兴趣的事件了。
② 会关闭底层的套接字连接。
这里注意:如果只是通过调用SelectionKey.cancel()来注销一个远端已经关闭了的channel,是一个不对的方法。因为selector.select()在处理cancel selectionKey set(注销的SelectionKey集合)的时候,会判断若该SelectionKey对应的channel已经没有注册到其他的selector,并且该channel open表示为false的情况下,才会去调用底层套接字的关闭操作。所以如果之调用SelectionKey.cancel()来注销一个远端已经关闭了的channel,会导致本段的TCP连接处于“CLOSE_WAIT”状态,一直在等待程序调用套接字的关闭。
补充:channel的open标志,只有在下面两种情况下才会将open置为false。
a) 调用了channel.close()方法;

b) 或者操作channel读/写的当前线程发生中断时。

无法分配更多的internet句柄怎么回事_一文精通Java NIO(内容较多,无耐心者勿点)...相关推荐

  1. 打开ftp服务器地址,提示无法分配更多的Internet句柄

    打开ftp服务器地址,提示无法分配更多的Internet句柄 杀掉explorer,运行重开,解决 posted on 2019-08-16 19:56 墨水熊 阅读(...) 评论(...) 编辑 ...

  2. java 句柄数 设置_技术文档(2)--Linux 句柄数设置情况,问题:java.io.IOException: Too many open files...

    打开的文件过多,一般来说是由于应用程序对资源使用不当造成,比如没有及时关闭Socket或数据库连接等.但也可能应用确实需要打开比较多的文件句柄,而系统本身的设置限制了这一数量. 第一个异常在错误影响到 ...

  3. Win11系统怎么给C盘分配更多的空间?

    当C盘的存储空间不足时,Win11如何给C盘分配更多的空间?别担心,下面小编就给大家分享具体的操作方法,一起来看看吧. 在Win11中为什么要给C盘更多的空间? 一般当您的磁盘空间不足时,您是无法更新 ...

  4. 独占设备的分配与回收_灵魂拷问:Java对象的内存分配过程是如何保证线程安全的?...

    点击上方"linkoffer", 选择关注公众号高薪职位第一时间送达 作者 l Hollis JVM内存结构,是很重要的知识,相信每一个静心准备过面试的程序员都可以清楚的把堆.栈. ...

  5. fi选项 电脑没有连接wi_您好,电脑Wi-Fi连接上但是无internet是怎么回事呢

    电脑连接上WiFi却无internet的原因有:1.笔记本WIFI连接是否异常,用手机或者其他具有WIFI的设备连接该WIFI,看看是不是出现同样的状来况,若无,说明笔记本WIFI有问题,若同样有问题 ...

  6. 允许其他网络用户通过此计算机的internet连接来连_「Azure云」什么是Azure虚拟网络?...

    Azure虚拟网络(VNet)是Azure中私有网络的基本构件.VNet支持许多类型的Azure资源,比如Azure虚拟机(VM),来安全地与internet和内部网络进行通信.VNet类似于您在自己 ...

  7. 项目部署时网关怎么回事_使用Kubernetes部署聊天网关(或技术按预期运行时)...

    项目部署时网关怎么回事 by Richard Li 理查德·李(Richard Li) 使用Kubernetes部署聊天网关(或技术按预期运行时) (Using Kubernetes to deplo ...

  8. 已阻止网站自动通话怎么回事_如何实现百度快速收录网站与快速排名

    如何实现百度快速收录网站与快速排名也是我们要重点掌握的,因为我们做网站优化最重要的就是让百度收录我们的网站并且把排名做上去,所以在做网站的时候一定要注意以下几点: 一.如何实现快速收录 1.网站内容要 ...

  9. emqx http not found 怎么回事_幽默笑话:行倒是行,但两个大男人这么说话算怎么回事...

    原标题:幽默笑话:行倒是行,但两个大男人这么说话算怎么回事 1. 今天陪女儿午睡的时候快递给我打电话.我怕吵醒宝宝就压低声音说:"你谁啊?"结果快递也压低声音说:"有你的 ...

最新文章

  1. kotlin + springboot启用elasticsearch搜索
  2. Zookeeper3.4.9分布式集群安装
  3. python好学吗一般要学多久-转行学Python开发难吗,月薪过万需要多久
  4. python使用scrapy_python使用scrapy发送post请求的坑
  5. 股市永不过时的黄金铁律:零下金叉疯狂买,零上死叉立即卖
  6. 关于might_sleep的一点说明---CONFIG_DEBUG_ATOMIC_SLEEP【转】
  7. python io_NumPy IO
  8. 什么是SSID/ESSID/BSSID
  9. python安装copy_python中copy和deepcopy 的区别
  10. 转换流指定编码读写文件
  11. python中的文件备份过程
  12. python读取两个csv文件后比较_python – 读取两个csv文件并比较每一行.如果行匹配打印两行,如果不相似则打印无效...
  13. mysql事务管理及spring声明式事务中主动异常抛出使数据库回滚
  14. 达摩院 2020 预测:模块化降低芯片设计门槛 | 问底中国 IT 技术演进
  15. HTML网页中table居中和表格内容居中
  16. cramer定理_线性代数部分重要定理总结
  17. python numpy库下载_python3.6下Numpy库下载与安装图文教程
  18. 使用Markdown编写手册
  19. 计算机的地址是指什么意思,什么是PC地址,什么是PLC地址
  20. Albert Einstein—一个真正值得敬仰的伟人(什么超级什么星什么一班都靠边站兼滚蛋!)...

热门文章

  1. earcharts tree 节点间隔_InnoDB是顺序查找B-Tree叶子节点的吗?
  2. 仿射密码介绍以及解题脚本
  3. Python基础教程:迭代器
  4. linux dmesg命令(显示开机信息)
  5. python 文件操作 os 如何检索文件夹内文件数量
  6. 除 Intel Realsense Dxxx 外 各市面深度摄像头对比(小觅智能 D1000-IR-120/Color、INDEMIND、领晰(LEADSENSE))(212)
  7. windows下的正向shell
  8. Python中的正则表达式(基础)
  9. python——初识python篇
  10. Ubuntu16.04安装PCL库的python实现python-pcl,并且通过pcl加载pcd文件验证python-pcl是否安装成功