关于 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事件主要是在发送缓冲区空间满的情况下使用的。如:

1
2
3
4
5
6
7
8
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读/写的当前线程发生中断时。

参考

  • 圣思园netty课程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/329930.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

转:Centos防火墙设置与端口开放的方法

转自&#xff1a; Centos防火墙设置与端口开放的方法_tianxin的专栏-CSDN博客Centos升级到7之后&#xff0c;内置的防火墙已经从iptables变成了firewalld。所以&#xff0c;端口的开启还是要从两种情况来说明的&#xff0c;即iptables和firewalld。更多关于CentOs防火墙的最新…

583. 两个字符串的删除操作用时6ms的另类解法

开门见山 看见这道题&#xff0c;我的第一反应不是去找出符合这道题的动态规划递推公式&#xff0c;我反而认为可以借用一下1143. 最长公共子序列的题解 class Solution {public int longestCommonSubsequence(String text1, String text2) {int[][] dp new int[text1.lengt…

Java 非阻塞 IO 和异步 IO

转载自 Java 非阻塞 IO 和异步 IO上一篇文章介绍了 Java NIO 中 Buffer、Channel 和 Selector 的基本操作&#xff0c;主要是一些接口操作&#xff0c;比较简单。 本文将介绍非阻塞 IO 和异步 IO&#xff0c;也就是大家耳熟能详的 NIO 和 AIO。很多初学者可能分不清楚异步和非阻…

element js 包含字符_selenium3.x(10)js弹框处理

web应用中&#xff0c;经常会遇到弹框。不处理弹框&#xff0c;页面其他元素都是不能操作的。js弹框有3种&#xff1a;alert警告框、confirm确认窗口、prompt信息输入窗口。webdriver提供了处理这3种弹框的方法。首先通过switch_to定位到弹框&#xff0c;然后针对弹框的不同&am…

转:centos8开启防火墙端口

转自&#xff1a; Centos8开放防火墙端口_Programmer-Awei的博客-CSDN博客查看防火墙某个端口是否开放firewall-cmd --query-port3306/tcp开放防火墙端口3306firewall-cmd --zonepublic --add-port3306/tcp --permanent查看防火墙状态systemctl status firewalld关闭防火墙sys…

AQS的细节--自用,非正常教程

AQS的概念 AQS叫抽象队列同步器&#xff0c;是一个框架&#xff0c;我们可以在JUC很多包看见AQS的具体实现&#xff0c;比如锁和读写锁&#xff0c;condition等&#xff0c;具有可扩展性&#xff0c;可以根据此自定义同步工具类&#xff0c;优点是系统开销低&#xff0c;实现锁…

mininet编程实现交换机规则的插入、删除与修改。_可编程网卡芯片在滴滴云网络的应用实践...

桔妹导读&#xff1a;随着云规模不断扩大以及业务层面对延迟、带宽的要求越来越高&#xff0c;采用DPDK 加速网络报文处理的方式在横向纵向扩展都出现了局限性。可编程芯片成为业界热点。本文主要讲述了可编程网卡芯片在滴滴云网络中的应用实践&#xff0c;遇到的问题、带来的收…

centos8上docker tomcat容器访问报404解决方法

目录 【README】 【1】docker安装tomcat 【2】启动多个tomcat容器 【README】 1.本文记录了 访问docker tomcat容器报404的解决方法&#xff1b; 2.附带安装tomcat步骤&#xff1b; 3.centos8 安装docker&#xff0c;refers2 centos8安装docker_PacosonSWJTU的博客-CSDN博…

ConcurrentHashMap--自用,非教学

结论先行&#xff0c;细节在下面 jdk1.7是如何解决并发问题的以及完整流程 一.首先new一个concurrentHashMap 调用默认构造方法 二.初始化 初始化initialCapacity&#xff08;默认是16&#xff0c;指一个segment内Entry的数量&#xff09;&#xff0c;loadFactor&#xff…

Java开发必须掌握的线上问题排查命令

转载自 Java开发必须掌握的线上问题排查命令作为一个合格的开发人员&#xff0c;不仅要能写得一手还代码&#xff0c;还有一项很重要的技能就是排查问题。这里提到的排查问题不仅仅是在coding的过程中debug等&#xff0c;还包括的就是线上问题的排查。由于在生产环境中&#x…

centos8启动docker-mysql8容器

【README】 本文记录了 centos8 安装&#xff0c;启动mysql8的docker容器的步骤&#xff1b; 【1】安装mysql8 docker容器 步骤1&#xff0c; 查看mysql8 docker镜像版本 &#xff1b; 最简单的方式是上 Docker Hubhttps://hub.docker.com/直接搜索mysql&#xff0c;查看其 …

Java命令学习系列(一)——Jps

转载自 Java命令学习系列&#xff08;一&#xff09;——Jpsjps位于jdk的bin目录下&#xff0c;其作用是显示当前系统的java进程情况&#xff0c;及其id号。 jps相当于Solaris进程工具ps。不象"pgrep java"或"ps -ef grep java"&#xff0c;jps并不使用应用…

springboot2.5.5配置druid数据源1.2.8与jdbc

【README】 本文记录了 springboot配置 druid数据源的步骤&#xff1b; 【1】新建springboot项目并配置druid 步骤1&#xff0c;新建springbt项目 步骤2&#xff0c;选择spring web&#xff0c;jdbc&#xff0c;mysql驱动依赖&#xff1b; 步骤3&#xff0c;添加 druid数据源…

tsc244标签编辑软件_能打小票的标签机,M110智能标签打印机来了!

每张被贴上的标签背后&#xff0c;都是对待梦想的认真、对待生活的用心&#xff0c;M110智能标签打印机为你标记美好&#xff0c;实现品质与效率兼得的追求。01、 产品简介M110智能标签打印机采取热敏无墨打印技术&#xff0c;无需碳带&#xff0c;便捷经济&#xff0c;配套“标…

面试官:简述实现一个线程池的设计思路

前言 二面碰到这个问题人都麻了&#xff0c;我扯了好多没用的&#xff0c;面后赶紧来补一下&#xff0c;但是找到的基本都是一堆纯代码&#xff0c;不是讲思路的。下面的思路是我参考美团技术团队文章后总结的。 具体思路 一、总体设计 线程池在内部实际上构建了一个生产者…

Java命令学习系列(四)——jstat

转载自 Java命令学习系列&#xff08;四&#xff09;——jstatjstat(JVM Statistics Monitoring Tool)是用于监控虚拟机各种运行状态信息的命令行工具。他可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据&#xff0c;在没有GUI图形的服务器上&…

修改打印机ip工具_使用富士施乐一体机因动态IP导致不能打印与扫描的解决方法...

背景在使用富士施乐的一体机中(其他厂商的一体机 也类似)&#xff0c;很多人的网络环境是动态IP的&#xff0c;即打印的IP地址是不固定的&#xff0c;随着每次开机或关机会变化&#xff0c;从而经常有人反应打印机不能打印或者扫描了。。总体思路1. 检查当前的IP设置2. 把相应的…

springboot2.5.5配置mybatis

【README】 1.本文记录了 springboot2.5.5 配置 mybatis的步骤&#xff1b; 2.配置mybatis 分为注解和配置两种方式&#xff1b; 3.引入mybatis&#xff0c;包括了 创建springbt项目&#xff1b;druid数据源配置&#xff1b;数据库表与javabean&#xff1b;mybatis配置与sq…

Java命令学习系列(三)——Jmap

转载自 Java命令学习系列&#xff08;三&#xff09;——Jmapjmap是JDK自带的工具软件&#xff0c;主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。可以使用jmap生成Heap Dump。在Java命令学习系列&#xff08;零&#xff09;——常见命…

skimage直方图如何保存_LightGBM的参数详解以及如何调优

lightGBM可以用来解决大多数表格数据问题的算法。有很多很棒的功能&#xff0c;并且在kaggle这种该数据比赛中会经常使用。但我一直对了解哪些参数对性能的影响最大以及我应该如何调优lightGBM参数以最大限度地利用它很感兴趣。我想我应该做一些研究&#xff0c;了解更多关于li…