后端深入理解Java三种IO模式和Epoll模型
IO模型
IO模型就是说用什么样的通道进行数据的发送和接收,Java共支持3种网络编程IO模式:BIO,NIO,AIO。BIO(BlockingIO)
同步阻塞模型,一个客户端连接对应一个处理线程。
BIO代码示例:BIO服务端代码importjava。net。ServerSocket;importjava。net。Socket;importjava。util。logging。Handler;publicclassSocketServer{publicstaticvoidmain(String〔〕args)throwsException{ServerSocketserverSocketnewServerSocket(9000);while(true){System。out。println(等待连接);阻塞连接SocketclientSocketserverSocket。accept();System。out。println(有客户端连接);创建新线程执行handle方法newThread(newRunnable(){Overridepublicvoidrun(){try{handle(clientSocket);}catch(Exceptione){e。printStackTrace();}}})。start();}}publicstaticvoidhandle(SocketclientSocket)throwsException{byte〔〕bytesnewbyte〔1024〕;System。out。println(准备read);接收客户端的数据,没有数据可读时就阻塞intreadclientSocket。getInputStream()。read(bytes);System。out。println(read完毕。);if(read!1){System。out。println(接收到客户端数据:newString(bytes,0,read));}clientSocket。getOutputStream()。write(helloClient。getBytes());clientSocket。getOutputStream()。flush();}}BIO客户端代码importjava。io。IOException;importjava。net。Socket;publicclassSocketClient{publicstaticvoidmain(String〔〕args)throwsIOException{SocketsocketnewSocket(localhost,9000);向服务端发送数据socket。getOutputStream()。write(HelloServer。getBytes());socket。getOutputStream()。flush();System。out。println(向服务端发送数据结束);byte〔〕bytesnewbyte〔1024〕;接收服务端回传的数据socket。getInputStream()。read(bytes);System。out。println(接收到服务端的数据:newString(bytes));socket。close();}}
缺点:
从上面的代码我们可以看出来,BIO代码中连接事件和读写数据事件都是阻塞的,所以这种模式的缺点非常的明显。
1、如果我们连接完成以后,不做读写数据操作会导致线程阻塞,浪费资源。
2、如果没来一个连接我们都需要启动一个线程处理,那么会导致服务器线程太多,压力太大,比如C10K。
应用场景:
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,但是程序比较简单。NIO(NonBlockingIO)
同步非阻塞模型,服务器实现模式为一个线程可以处理多个请求连接,客户端发送的连接请求都会注册到多路复用器(selector)上,多路复用器轮询到连接有IO请求就进行处理,JDK1。4开始引入。NIO服务端代码(没有引入多路复用器的代码)importjava。net。InetSocketAddress;importjava。nio。ByteBuffer;importjava。nio。channels。ServerSocketChannel;importjava。nio。channels。SocketChannel;importjava。util。ArrayList;importjava。util。Iterator;importjava。util。List;publicclassNioServer{staticListSocketChannelchannelListnewArrayList();publicstaticvoidmain(String〔〕args)throwsException{创建NIOServerSocketChannelserverSocketServerSocketChannel。open();serverSocket。socket()。bind(newInetSocketAddress(9000));设置非阻塞serverSocket。configureBlocking(false);System。out。println(服务启动);while(true){非阻塞模式accept方法不会阻塞,否则会阻塞NIO的非阻塞模式是由操作系统内部实现,底层调用了Linux内核的accept函数SocketChannelsocketChannelserverSocket。accept();if(socketChannel!null){System。out。println(连接成功);设置socketchannel为非阻塞socketChannel。configureBlocking(false);保存客户端连接到listchannelList。add(socketChannel);}遍历连接读数据IteratorSocketChanneliteratorchannelList。iterator();while(iterator。hasNext()){SocketChannelsciterator。next();ByteBufferbyteBufferByteBuffer。allocate(128);非阻塞模式read方式不会阻塞,否则会阻塞intlensc。read(byteBuffer);if(len0){System。out。println(接收到消息:newString(byteBuffer。array()));}elseif(len1){如果客户端断开,把socket从集合中去掉iterator。remove();System。out。println(客户端断开连接);}}}}}
缺点:
如果连接数太多的话,会有大量的无效遍历,假如有10000个连接,其中只有1000个连接有写数据,但是由于其他9000个连接并没有断开看我们还是每次轮询遍历一万次,其中有十分之一的遍历都是无效的,这显然是一个非常浪费资源的做法。NIO服务端代码(引入多路复用器的代码)importjava。net。InetSocketAddress;importjava。nio。ByteBuffer;importjava。nio。channels。SelectionKey;importjava。nio。channels。Selector;importjava。nio。channels。ServerSocketChannel;importjava。nio。channels。SocketChannel;importjava。security。Key;importjava。util。Iterator;importjava。util。Set;publicclassNioSelectorServer{publicstaticvoidmain(String〔〕args)throwsException{创建ServerSocketChannleServerSocketChannelserverSocketServerSocketChannel。open();serverSocket。bind(newInetSocketAddress(9000));设置ServerSocketChannel为非阻塞serverSocket。configureBlocking(false);打开Selector处理channel,即创建epollSelectorselectorSelector。open();把ServerSocketChannel注册selector上,并且select对客户端accept连接操作感兴趣serverSocket。register(selector,SelectionKey。OPACCEPT);System。out。println(服务启动);while(true){阻塞等待需要处理的事件发生selector。select();获取selector中注册的全部事件的SelectionKey实例SetSelectionKeyselectionKeysselector。selectedKeys();IteratorSelectionKeyiteratorselectionKeys。iterator();遍历selectionKeys对事件进行处理while(iterator。hasNext()){SelectionKeykeyiterator。next();如果是accept事件,则进行连接获取和事件注册if(key。isAcceptable()){ServerSocketChannelserver(ServerSocketChannel)key。channel();SocketChannelsocketChannelserver。accept();socketChannel。configureBlocking(false);socketChannel。register(selector,SelectionKey。OPREAD);System。out。println(客户端连接成功);}elseif(key。isReadable()){进行数据读取SocketChannelsocketChannel(SocketChannel)key。channel();ByteBufferbyteBufferByteBuffer。allocate(128);intlensocketChannel。read(byteBuffer);如果有数据,把数据打印出来if(len0){System。out。println(接收到消息:newString(byteBuffer。array()));}elseif(len1){如果客户端断开连接,关闭SocketSystem。out。println(客户端断开连接);socketChannel。close();}}从事件集合里删除本次处理的key,防止下次select重复处理iterator。remove();}}}}
上面代码是利用NIO一个线程处理所有请求,这种单个线程处理的方式肯定是存在问题的,例如现在有10w个请求中,有1w个连接进行读写数据,那么SelectionKey就会有1w个请求,所以我们需要循环这1w个事件进行处理,比较费时间,如果这个时候再有连接进来,只能阻塞。
NIO有三大核心组件:Channel(通道),Buffer(缓冲区)Selector(多路复用器)
1、channel类似流,每个channel对应一个buffer缓冲区,buffer底层是个数组。
2、channel会注册到selector上,由selector根据channel的读写事件发生将其交由某个空闲的线程处理。
3、NIO的Buffer和channel都是既可以读又可以写的。
NIO底层在JDK1。4版本是用linux的内核函数select()或poll()来实现,跟上面的NioServer代码类似,selector每次都会轮询所有的socktchannel看下哪个channel有读写事件,有的话就处理,没有就继续遍历,JDK1。5开始引入了epoll基于事件响应机制来优化NIO。
举个例子:例如我们去酒吧喝酒,在吧台坐下了20个人,中间一个服务员,select()或者poll()模式就是,服务员每次都是询问这个20个人是否需要喝酒,而epoll模型则是,20个人谁需要喝酒谁就举手,服务员每次只处理举手的那几个人即可。
NioSelectorServer代码里如下几个方法非常重要,我们从Hotspot与Linux内核函数级别来理解下。Selector。open()创建多路复用器socketChannel。register(selector,SelectionKey。OPREAD)将channel注册到多路复用器上selector。select()阻塞等待需要处理的事件发生
总结:
NIO整个调用流程就是Java调用了操作系统的内核函数来创建Socket,获取Socket文件描述符,再创建一个Selector对象,对应操作系统的Epoll描述符,将获取到的Socket连接的文件描述符的事件绑定到Selector对应的文件描述符上,进行事件的异步通知,这样就实现了使用一条线程,并且不需要太多的无效遍历,将事件处理交给了操作系统内核(操作系统的终端程序),大大提高了效率。
Epoll函数详解intepollcreate(intsize);
创建一个epoll实例,并返回一个非负数作为文件描述符,用于对epoll接口的所有后续调用。参数size代表可能会容纳size个描述符,但size不是一个最大值,只是提示操作系统它的数量级,现在这个参数基本上已经弃用了。intepollctl(intepfd,intop,intfd,structepolleventevent);
使用文件描述符epfd引用epoll实例,对目标文件描述符fs执行op操作。
参数epfd表示epoll对应的文件描述符,参数fd表示socket对应的文件描述符。
参数op有以下几个值:EPOLLCTLADD:注册新的fd到epfd中,并关联事件event;EPOLLCTLMOD:修改已经注册的fd的监听事件;EPOLLCTLDEL:从epfd中移除fd,并且忽略掉绑定的event,这时event可以为null;
参数event是一个结构体structepollevent{uint32tevents;Epolleventsepolldatatdata;Userdatavariable};typedefunionepolldata{voidptr;intfd;uint32tu32;uint64tu64;}epolldatat;
events有很多可选值,这里只举例最常见的几个:EPOLLIN:表示对应的文件描述符是可读的;EPOLLOUT:表示对应的文件描述符是可写的;EPOLLERR:表示对应的文件描述符发生了错误;
成功则返回0,失败返回1。intepollwait(intepfd,structepolleventevents,intmaxevents,inttimeout);
等待文件描述符epfd上的事件。
epfd就是Epoll对应的文件描述符,events表示调用者所有可用事件的集合,maxevents表示最大等到多少个事件就返回,timeout是超时时间。
IO多路复用底层主要用Linux内核函数(select、poll、epoll)来实现。
AIO模型
异步非阻塞模型,由操作系统完成后回调通知服务端程序启动线程去处理,一般适用于连接数比较多且连接时间比较长的应用。
应用场景
AIO方式适用于连接数多且连接比较长(重操作)的架构,JDK1。7开始支持。AIO服务端代码importjava。io。IOException;importjava。net。InetSocketAddress;importjava。nio。ByteBuffer;importjava。nio。channels。AsynchronousChannel;importjava。nio。channels。AsynchronousServerSocketChannel;importjava。nio。channels。AsynchronousSocketChannel;importjava。nio。channels。CompletionHandler;publicclassAIOServer{publicstaticvoidmain(String〔〕args)throwsException{finalAsynchronousServerSocketChannelserverChannelAsynchronousServerSocketChannel。open()。bind(newInetSocketAddress(9000));serverChannel。accept(null,newCompletionHandler(){Overridepublicvoidcompleted(AsynchronousSocketChannelsocketChannel,Objectattachment){try{System。out。println(2Thread。currentThread()。getName());再此接收客户端连接,如果不写这行代码后面的客户端连接不上服务端serverChannel。accept(attachment,this);System。out。print(socketChannel。getRemoteAddress());ByteBufferbufferByteBuffer。allocate(1024);socketChannel。read(buffer,buffer,newCompletionHandlerInteger,ByteBuffer(){Overridepublicvoidcompleted(Integerresult,ByteBufferbuffer){System。out。println(3Thread。currentThread()。getName());buffer。flip();System。out。println(newString(buffer。array(),0,result));socketChannel。write(ByteBuffer。wrap(HelloClient。getBytes()));}Overridepublicvoidfailed(Throwableexc,ByteBufferbuffer){exc。printStackTrace();}});}catch(IOExceptione){e。printStackTrace();}}Overridepublicvoidfailed(Throwableexc,Objectattachment){}});System。out。println(1Thread。currentThread()。getName());Thread。sleep(Integer。MAXVALUE);}}AIO客户端代码importjava。net。InetSocketAddress;importjava。nio。ByteBuffer;importjava。nio。channels。AsynchronousSocketChannel;publicclassAIOClient{publicstaticvoidmain(String。。。args)throwsException{AsynchronousSocketChannelsocketChannelAsynchronousSocketChannel。open();socketChannel。connect(newInetSocketAddress(127。0。0。1,9000))。get();socketChannel。write(ByteBuffer。wrap(HelloServer。getBytes()));ByteBufferbufferByteBuffer。allocate(512);IntegerlensocketChannel。read(buffer)。get();if(len!1){System。out。println(客户端收到信息:newString(buffer。array(),0,len));}}}
为什么Netty使用NIO而不是AIO?
因为在Linux系统上,AIO的底层实现扔使用Epoll模型,没有很好的使用AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易再次进行深度优化,Linux上AIO还不够成熟。Netty是异步非阻塞框架,Netty在NIO上做了很多异步封装。
以巴风云(06)大卫王城,三教圣地,耶路撒冷之橄榄山橄榄山,耶路撒冷老城东部的一座遍植橄榄树的小山,是犹太教和基督教的圣山。犹太教认为这是弥赛亚(救世主)末日降临,新世界开始的地方。基督教认为这是耶稣最后一周传教,复活后升天之地……
印度深夜扣押小米48亿资产,为何吃相这么难看?大家好,我是无相君。前几年,美国一直搞华为,确实也搞出成绩了。华为被迫拆分手机业务,手机的销量也大不如前。不知道是不是受到启发,印度也开始对逐渐占领国内市场的……
儿童早餐食谱麻油莴笋丝清爽可口充足而均衡合理的营养是孩子生长发育的物质基础〔微风〕处于生长发育期的孩子只有摄取种类丰富的食物,才能保证营养素的全面均衡,促进生长发育和提高免疫力。〔微风〕合理调整……
OPPOPad跑分及核心配置曝光,FindX3让路新机跌至谷自2021年下半年以来,关于OPPO首款平板电脑的传闻就一直没有停止过。此前有消息称,该设备已通过3C认证,支持33W快充方案。现在,Geekbench4基准数据库已经公布了O……
韩服遭国人选手霸榜前五霸占四席伴随昨日跨年夜的结束,我们也正式来到了2022S12新赛季,也是在昨日晚间,韩服跨年排行中,前五位韩服王者选手,有四位均是国人。虽然仅仅是韩服Rank排位数据,但能在韩服这样顶……
法网郑钦文晋级第三轮来源:新华网5月26日,中国选手郑钦文在比赛后庆祝胜利。当日,在法国巴黎举行的2022法国网球公开赛女子单打第二轮比赛中,中国选手郑钦文以2比1战胜罗马尼亚选手哈勒普,晋……
那些神仙句子,建议收藏(魂归篇)1。死亡不是失去了生命,只是走出了时间。2。诠释自由,定义永恒。它给予我浓缩的生命,更贵重,且更沉淀的营养,浸润我枯败的所有。于是我借夜的意识,再度光临这人间。3。……
大S再次闪婚,她才是人间清醒45岁的大S官宣再婚了。老公是韩国歌手具俊晔,据说是20年前的初恋。具俊晔在社交媒体上发文称:我们结婚了,和20年前相爱的女人延续爱情。大S也通过工作室……
杨颖离婚后再出神图!花仙造型少女感满满,网友颜不崩就还能爱不久前黄晓明和Angelababy杨颖官宣离婚的消息,闹得是沸沸扬扬。这对曾经创造出世纪婚礼的夫妻,最终还是分道扬镳,让不少网友感叹:圈内恩爱难道都是假的?不过爱情这种事,如鱼……
花滑女神特鲁索娃晒近照!身材发福双下巴明显,恋爱前后变化好大近期,俄罗斯花滑女神特鲁索娃在社媒上晒出了自己在夏训中的照片。从照片中来看,特鲁索娃变化还是很大的,画面中的特鲁索娃穿着紧身的运动装,红色的头发垂到了腰间,非常的美丽迷人。不过……
药名一字之差,功效大不相同小编导读在丰富的中药宝库中,许多中药虽然名字只有一字之差,但功效却相差巨大。浙贝母与川贝母、青皮与陈皮、赤芍与白芍是三组常用的同名异姓中药,他们的药名只有一字之差,功效却……
请保持一份对美的自律忙忙碌碌的时光虽然匆匆,但不虚度昨晚健身回来,利用冲凉洗漱的空挡,在十点读书直播间听了听春季护肤的知识讲座,时间虽短,获益匪浅,也明白了一些护肤误区,比如,并非所有……