本文转载自微信公众号《Java极客技术》,作者鸭血范。转载本文请联系Java极客技术公众号。阿芬第一次接触到io相关知识,是在网上看面经的时候。平时她只会写业务代码,但面对bio、nio、多路复用器的概念,她却不知所措。当阿芬试图独自学习这些名词时,他发现很难学习和理解。如果有文章说说他们的关系,可能对初学者有帮助,所以就放下面这篇文章。BIOBIO是阻塞IO的意思。通常我们在讲BIO的时候,都会结合服务器模型来谈。在实际应用中会更好理解。看下面的代码,估计大家刚开始学java网络编程的时候用过这种模式:publicstaticvoidmain(String[]args)throwsException{//创建一个socket,socket是客户端和服务端之间的桥梁ServerSocketserver=newServerSocket(9090,20);//通过死循环不断接收客户端请求while(true){//线程会阻塞这一行的accept方法Socketclient=server.accept();//新建线程处理新客户端的逻辑newThread(()->{//客户端的读写逻辑}).start();}}只要没有客户端连接到服务器,accept方法就永远不会返回,这是阻塞的;相应的读写操作也是一样的,认为读取数据必须等到数据到达才返回,这是阻塞的。我们也可以在阻塞的基础上考虑。为什么服务端模型要设计成客户端来了就创建一个新的线程呢?答案其实很简单。当客户端过来创建连接时,如果客户端不分配新的一个线程执行服务器逻辑,服务器就很难与第二个客户端建立连接。即使将客户端连接保存在集合中,也不可能通过单线程遍历集合来执行服务器端逻辑。因为如果一个客户端连接因为读写操作被阻塞了,其他的客户端就不会执行了。如果NIO说服务器只有少数人使用,那么上面的bio代码其实还不错,但问题是互联网正在蓬勃发展,随着服务器访问量的增加,这样的服务器模型会成为瓶颈。我们用C10K的思维看上面的服务器代码。如果我们客户端的连接数增加10K倍,就意味着需要创建10K个线程。单独创建线程是一个很大的开销。另外,线程需要来回切换,单机根本应付不了。如此大量的连接。由于瓶颈在线程上,我们考虑是否可以将服务器模型改为单线程模型。思路其实和我之前说的差不多。使用一个集合来保存每个连接的客户端,并使用一个while循环来处理每个连接。操作。我们之前说过,这样的操作的瓶颈是接受客户端时会阻塞,执行读写操作时会阻塞,导致单线程执行效率低下。为了突破这个瓶颈,操作系统开发了nio,这里的nio指的是非阻塞io。也就是说当accept客户端连接的时候,不需要阻塞。如果没有客户端连接,它将返回-1(java-NULL)。读写操作时,不会阻塞。有数据就读取,没有数据就直接返回,解决了单线程服务器的瓶颈问题。示例代码如下:publicstaticvoidmain(String[]args)throwsException{//CollectionforstoringclientsLinkedListclients=newLinkedList<>();//nio中的概念改为channelServerSocketChannelss=ServerSocketChannel.open();ss.bind(newInetSocketAddress(9090));//设置为非阻塞ss.configureBlocking(false);while(true){//下面的accept方法不会阻塞SocketChannelclient=ss.accept();if(client==null){System.out.println("null....");}else{//设置客户端操作为非阻塞client.configureBlocking(false);clients.add(client);}ByteBufferbuffer=ByteBuffer。allocateDirect(4096);//遍历连接的客户端是否可以读写数据for(SocketChannelc:clients){intnum=c.read(buffer);if(num>0){//其他操作}}}}Multiplexers虽然上面的单线程NIO服务器模型比BIO的好很多,还有一个大问题。客户端与服务器建立连接后,随后会进行一系列的读写操作。虽然这些读写操作是非阻塞的,但是每次读写操作都需要在操作系统层面进行一次用户态和内核态的切换,这也是一个巨大的开销(read和write等系统调用都是执行在内核态完成)。上面代码中,每次循环遍历都会进行读写操作。以读操作为例:大部分读操作都是在数据还没有准备好时进行的,相当于执行了空操作。我们得想办法避免这种无效的读操作,避免在内核态和用户态之间频繁切换。补充:客户端和服务端的两端都是通过socket连接的,socket在linux操作系统中有对应的文件描述符,我们的读写操作都是以文件描述符为单位进行的。为了避免上述的无效读写,我们得想办法知道当前文件描述符是否可读写。如果逐个询问文件描述符,效率和直接读写操作差不多。我们希望有一种方法可以一次性知道哪些文件描述符是可读的,哪些文件描述符是可写的。这就是后来的操作系统Multiplexer发展出来的。也就是说,多路复用器的核心作用就是告诉我们哪些文件描述符是可读的,哪些文件描述符是可写的。多路复用器也分为几种类型,它们也经历了一个进化过程。原来的multiplexer是select模型,它的模式是这样的:每次终端发送文件描述符集合给select系统调用,select遍历完每个文件描述符后返回那些可操作的文件描述符,然后程序读写可操作的文件描述符。它的缺点是一次传输的文件描述符集合是有限的,只能给出1024个文件描述符。poll在此基础上进行了改进,对文件描述符的个数没有限制。不过select和poll在性能方面还是可以优化的。它们共同的缺点是需要在内核中遍历所有传入的文件描述符,这也是一个耗时的操作(是否有优化空间还有待观察。研究)每次都需要将文件描述符从用户态内存到内核态内存,遍历完成后再搬回来,这种来回拷贝也是一个耗时的操作。后来操作系统加入了epoll多路复用器,彻底解决了这个问题:epoll多路复用器的模型是这样的:为了在发起系统调用时不遍历所有的文件描述符,epoll的优化在于:当数据到达时在网卡上,将触发中断。一般情况下,CPU会将相应的数据复制到内存中,并绑定到相关的文件描述符上。epoll在此基础上做了扩展。首先,epoll在内核中维护了一颗红黑树和一些链表结构。当数据到达网卡并复制到内存中时,它会将相应的文件描述符从红黑树中复制到链表中,链表存储的是已经到达的文件描述符,这样当程序调用epoll_wait,它可以直接将可读的文件描述符返回给应用程序。除了epoll_wait,epoll还有两个系统调用,分别是epoll_create和epoll_ctl,分别用来初始化epoll和向红黑树添加文件描述符。以上就是multiplexer和commonio模型的关系。网上经常有文章将多路复用器描述为nio的一部分。我觉得是有道理的,因为在具体的编程中,这两个概念往往是结合在一起的。作为一个。后续其实Java已经为我们封装了多路复用器和Selector类,我们完全可以基于Selector开发NIO服务器。但是我们自己写nioserver可能不够严谨。Java中有一个优秀的nio框架,叫做Netty。我们将把这部分留到下一次。