别挠头了!我教你什么是BIO,NIO,AIO
什么是网络IO模型?
网络IO模型指的是程序在进行网络通信时所采用的IO(InputOutput)方式。目前比较常见的有如下几种方式:
1。BIO:BlockingIO即同步阻塞式IO
2。NIO:NoBlockingIO即同步非阻塞式IO
3。AIO:AsynchronousIO即异步非阻塞IO(常见但是开发的时候一般不用)
什么是BIO?
先看一段代码:BIO网络IO模型服务端代码publicclassServer{publicstaticvoidmain(String〔〕args)throwsIOException{创建ServerSocket,并绑定端口号ServerSocketserverSocketnewServerSocket(8080);while(true){阻塞等待客户端连接SocketsocketserverSocket。accept();读取客户端发送的数据,如果没有数据可读或者读数据的过程中会一直阻塞InputStreamissocket。getInputStream();byte〔〕buffernewbyte〔1024〕;intlenis。read(buffer);StringmsgnewString(buffer,0,len);System。out。println(Receivedmessagefromclient:msg);发送响应给客户端,如果没有数据可读或者读数据的过程中会一直阻塞OutputStreamossocket。getOutputStream();os。write(Hello,Client!。getBytes());关闭socket连接socket。close();}}}
BIO网络IO模型客户端代码publicclassClient{publicstaticvoidmain(String〔〕args)throwsIOException{创建Socket,并连接服务端SocketsocketnewSocket(localhost,8080);向服务端发送数据OutputStreamossocket。getOutputStream();os。write(Hello,Server!。getBytes());读取服务端响应的数据InputStreamissocket。getInputStream();byte〔〕buffernewbyte〔1024〕;intlenis。read(buffer);StringmsgnewString(buffer,0,len);System。out。println(Receivedmessagefromserver:msg);关闭socket连接socket。close();}}
这就是经典的BIO模型下的客户端和服务端网络连接。从服务端的代码中可以看到主要有两个地方存在阻塞:
1。服务端等待客户端连接的时候阻塞
2。进行读写的时候,数据没有准备好就会阻塞(比如客户端一直不发数据)
对于第一种情况的阻塞,只会让accept()所在的线程做不了其他事情,但不会影响客户端的连接。
但是如果读写时候发生阻塞,那么其他的客户端要连接服务端,就会出现无法连接的情况。因为在一个while循环中阻塞住就不会进入下一次循环。
针对读写阻塞,无法连接多个客户端的情况,一种比较容易想到的方案是把读写放到其他线程。
这样读写就不会阻塞while循环了,也就不会影响其他客户端连接服务器了。
代码如下:publicclassBIOServer{privatestaticfinalintPORT8080;publicstaticvoidmain(String〔〕args)throwsIOException{ServerSocketservernewServerSocket(PORT);System。out。println(ServerstartedonportPORT);while(true){Socketclientserver。accept();System。out。println(Acceptedconnectionfromclient。getRemoteSocketAddress());多线程方式处理读写操作newThread(newClientHandler(client))。start();}}}classClientHandlerimplementsRunnable{privateSocketclient;publicClientHandler(Socketclient){this。clientclient;}Overridepublicvoidrun(){try{byte〔〕buffernewbyte〔1024〕;intlen;while((lenclient。getInputStream()。read(buffer))0){StringrequestnewString(buffer,0,len);System。out。println(Receivedrequest:request);StringresponseHellofromserver;client。getOutputStream()。write(response。getBytes());client。getOutputStream()。flush();}client。close();System。out。println(Connectionclosedbyclient);}catch(IOExceptione){处理异常}}}
每次一个链接过来都会把要读写的操作单独开一个线程,这样while就不会被读写阻塞,可以允许多个客户端链接。
但是要注意,这里读写虽然不阻塞while所在的线程,依旧会阻塞新开辟的线程。
比如A客户端链接到了服务器,于是服务器给它开辟了一个线程A。
可是该客户端一直不发数据,那么线程A则一直会被阻塞在哪里(此时线程什么也没干,还占着资源)。
这个模型被称为BIO的原因就是accept(),read(),write()会发生阻塞。
另外因为读写阻塞的存在,新创建的线程就会被阻塞而无法释放。如果并发量比较大的情况下就会有大量的线程被创建。
默认情况下Java中一个线程需要分配1M的内存空间。这对服务器的资源造成了很大的浪费。
大佬们意识到阻塞问题的严重性,于是捣鼓出了NIO模型,专注于解决阻塞问题。
那BIO用线程池行吗?
线程池是可以限制创建线程的多少。但是只限制达不到目的,因为并发量比较大的情况下,一旦客户端连接的数量超过了限制的最大值,就会导致客户端连接不上服务器。
什么是NIO?
先上代码为敬:publicclassNIOServer{publicstaticvoidmain(String〔〕args)throwsIOException{创建一个SelectorSelectorselectorSelector。open();创建一个ServerSocketChannel,并将其绑定到本地地址8888上ServerSocketChannelserverSocketChannelServerSocketChannel。open();serverSocketChannel。socket()。bind(newInetSocketAddress(8888));serverSocketChannel。configureBlocking(false);将ServerSocketChannel注册到Selector中,并指定需要监听OPACCEPT事件serverSocketChannel。register(selector,SelectionKey。OPACCEPT);while(true){阻塞直到有事件发生selector。select();获取所有SelectionKeyIteratorSelectionKeyiteratorselector。selectedKeys()。iterator();处理每一个SelectionKeywhile(iterator。hasNext()){SelectionKeykeyiterator。next();iterator。remove();如果SelectionKey是OPACCEPT,则处理新的连接if(key。isAcceptable()){ServerSocketChannelserver(ServerSocketChannel)key。channel();非阻塞SocketChannelclientserver。accept();System。out。println(Acceptedconnectionfromclient);client。configureBlocking(false);client。register(selector,SelectionKey。OPREAD);如果SelectionKey是OPREAD,则读取数据}elseif(key。isReadable()){SocketChannelclient(SocketChannel)key。channel();ByteBufferbufferByteBuffer。allocate(1024);client。read(buffer);buffer。flip();byte〔〕bytesnewbyte〔buffer。remaining()〕;buffer。get(bytes);System。out。println(Receivedmessage:newString(bytes));返回响应给客户端ByteBufferresponseBufferByteBuffer。wrap(Hello,client!。getBytes());client。write(responseBuffer);}}}}}
代码虽长,却非常简单。
NIO只需要关注三个东西:
1。Channel:可以理解为BIO中的Socket通道,只不过BIO的Socket是单向的,这个是双向的,可以同时读写。
2。Selector:选择器。把那些活跃的Channel(想要链接服务端的Channel,需要读写数据的Channel)挑出来,供后续的处理。
3。SelectionKey:标记Channel想要进行什么操作,比如连接操作,读写操作。处理Channel的时候会根据Key来进行相应的操作。
代码的大致原理如下图:
比如客户端连接服务器,可能会经过如下步骤:
1。客户端A要连接服务器,服务器接收到之后就会打开一个ServerSocketChannel
2。然后就会把这个ServerSocketChannel(可以理解为BIO中的ServerSocket)注册到Selector(也就是交给Selector管理这个Channel),并绑定一个OPACCEPT(代表Channel需要进行连接)事件
3。然后会有一个循环,不断的从Selector获取活跃的Key并进行处理
4。一旦发现了OPACCEPT,就会创建一个SocketChannel(相当于BIO中的Socket),此时一个连接通道就建立完成了
5。然后会把这个SocketChannel注册到Selector中,并绑定一个OPREAD事件
6。如果客户端A发送了一个数据,那么Selector就会监控到这个动作,下一次循环的时候就会从Selector中取出活跃的Channel,并根据对应的OPREAD事件进行处理。
这就是NIO处理连接的大体流程。
可能有人会有疑问。这哪里变成非阻塞了?
对照BIO。NIO的非阻塞就是上面说的那两个:
1。服务端不会阻塞的等待客户端链接,即server。accept()不会阻塞。
2。数据没有准备好的时候,读写不会阻塞。即NIO不会存在像BIO那样有线程什么也不干,白白楞在那。
之所以不会阻塞,是因为在NIO中一个线程会处理多个连接。并且处理的都是Selector过滤之后的活跃连接(有accept或者读写操作的连接),所以不存在线程啥也不干,空等的阻塞现象。
这里的阻塞很多人其实没有理解明白,网上很多人其实自己都不理解,写出的文章更是难以自圆其说。
为了行文流畅,这里先不花篇幅解释这个概念。下文会集中讲解。
什么是Netty?
因为NIO的API封装的并不好,业务端开发的时候会写很多的模板代码。所以Netty对NIO进行了再次的封装,并在NIO的基础上进行了一些优化。
Netty就不做过多介绍了,只要理解了上面的NIO模型,Netty很容易理解。
什么是AIO?
该模型是异步非阻塞。你可以理解为在NIO中如果读一个数据,是自己的应用程序在处理这些数据。
但是如果是AIO,就相当于在自己的程序中定义了一段逻辑,但是执行的时候是由操作系统直接执行的,和应用没有关系。
也就是说如果发现IO读写是自己的程序进行读写,那就是同步。如果不是那就是异步。
一个故事说明BIO,NIO,AIO
比如一个餐馆。每个顾客就是一个客户端,每个服务员就是一个线程。
在BIO模型下餐馆会给每个顾客配一个服务员,如果该顾客有需求,那么服务员就处理顾客的需求。如果顾客没有要求,那服务员就在旁边傻站着发呆。
在NIO模型下餐馆会让一个服务员负责多个顾客。哪个顾客有需求,服务员就去服务那个顾客。如果收到一个需求就会一直处理,直到处理好返回给顾客。并且会经常问顾客们:还需不需要什么服务?。
在AIO模型下一个服务员同样负责多个顾客。但是如果有多个顾客点了菜,那么服务员会写一个纸条(回调函数),纸条上写好客户的需求以及如何处理,然后交给一个神秘大佬(操作系统)。神秘大佬操作完成之后会通知服务员。相当于服务员把自己的活外包了。
可以仔细体会下,NIO和AIO的区别就是IO的操作由谁来完成。如果由应用程序自己来完成,那就是传说中的同步IO,否则就是传说中的异步IO。
究竟什么是多路复用?
多路复用就是一个线程管理多个连接。路可以理解为一个个的Channel,多个客户端就有多个Channel。
但是并不是每个Channel都是活跃的,所以经过Selector之后,会把活跃的Channel挑出来,并对这些活跃的Channel进行处理。
这个一个线程过滤多个连接并处理多个连接的动作就是多路复用,即多个连接复用一个线程。
select,poll,epoll和网络IO模型究竟是什么关系?
select,poll,epoll是多路复用的Linux操作系统层面的实现(Selector的底层实现)。网上资料比较多,没有什么歧义,此处不展开。
什么是阻塞?
其实网络IO模型中的阻塞指的是线程的空等现象。
就比如BIO中开启的线程,可能什么也不干,等着客户端发数据,这种情况就是阻塞。
再比如BIO中服务端accept()空等客户端的连接,这种情况也是阻塞。
那NIO中accept(),read(),write()有人说也是阻塞,因为这些操作执行过程中需要时间,看起来也阻塞了线程。
但是注意这里线程是真正的在干活,在工作,所以这种情况不叫阻塞,而是叫同步。
什么是异步和同步?
就像上面所说,NIO中的accept(),read(),write()操作需要应用程序亲自进行数据的操作就叫同步。
相反,如果在应用程序中遇到这种操作直接往下走,而把这些操作交给操作系统执行,执行完成后通知应用程序,那么这种就叫异步。
总结一下,就是说应用程序自己花时间读写数据就叫同步,如果应用程序自己不花时间,而是把这种操作交给其他人(比如操作系统),那么就是异步。
那你思考一个问题,如果一个线程遇到了读数据的操作,然后开了一个子线程处理这种操作。这种情况属于同步还是异步?
这种情况属于业务线程模型是异步,但是IO模型是同步。其实你可以这么理解,把业务线程和IO当作两个层面的东西。
在一个业务线程中如果遇到读写自己不操作,而交给另一个线程操作,这叫业务线程的异步。
那么同样在IO模型层面,如果应用自己不操作,而交给操作系统,那么这就叫做IO异步。
更简单的理解方式就是你就看read(),write(),accept()等方法在自己程序中调用的时候会不会直接返回,如果是,那就是异步,如果不是那就是同步。
多路复用一定要非阻塞吗?
是,多路复用是一个线程负责处理多个连接。如果是阻塞的,只要有一个连接把线程阻塞了。那这个线程就废了,其他连接也处理不了。
BIO一定不能用吗?
技术都有它的应用场景。如果并发量比较低,是可以用BIO的。
在连接数比较小的情况下BIO模型因为没有多路复用遍历活跃连接的过程,并且每个连接独享线程。性能不一定比NIO差。
Redis用的什么IO模型?
Redis底层也是多路复用。经常听到的别人口中的Redis是单线程,但是还是非常快的原因就是Redis是用epoll实现的多路复用。
正因为是多路复用,如果一个命令耗时太长就可能占用线程的时间过长。影响其他命令的执行。
所以用Redis的时候一定要关注执行的命令时间复杂度如何。比如keys一般情况下公司是禁止执行的。
小思考题:你能用一句话说明白什么才算是高性能吗?快发到评论区吧〔看〕