《Netty权威指南第2版.docx》由会员分享,可在线阅读,更多相关《Netty权威指南第2版.docx(611页珍藏版)》请在课桌文档上搜索。
1、应用进程系统调用内核(recvfrom无数据报,EWoULDBLOCK准备好系统调用k无数据报35Krecvfrom1EWOULDBLoCK准备好州洛就第上系统调用,进程反复调用)recvfrom等待返回成功(轮询)recvfrom数据报准备好复制.据报7数据从内核复制到用户空间【处理数据报一返回成功更制完成J图1-2非阻塞I/O模型I/O复用模型:LinUX提供SeIeCt/poll,进程通过将一个或多个fd传递给SeIeCt或Pc)II系统调用,阻塞在SeleCt操作上,这样SeIeCt/poll可以帮我们侦测多个fd是否处于就绪状态。SeIeCuPOII是顺序扫描fd是否就绪,而且支持的
2、fd数量有限,因此它的使用受到了一些制约。LinUX还提供了一个epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数rollback,如图13所示。(4)信号驱动1/。模型:首先开启套接口信号驱动I/O功能,并通过系统调用SigaCtiOn执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGlo信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据,如图1-4所示。应用进程系统调用 select报核据好内数备无准进程受阻于SeleCt 调用,等待1
3、个或 多个套接字变为 可读.返回可读条件数据报准备好等待数据准备就绪If系统调用后的L中出recvfrom复制割据报、数据复制到应用J 缓冲区期间进程、 阻塞数据从内核复制到FiCJ用户空间I处理数据报囚”伙J复制完成Jrecvfrom系统调用,复制数据报)数据复制到应用J 缓冲区期间进程、 阻塞I处理数据报返回成功数据从内 卜核复制到用户空间复制完成图13I/O复用模型进程继续执行Yr詈少普2C系统调用内核、建乂SlGloeUccMcc)信号处理程序(9等待数据准备就绪7信号处理.递交SIGIo数据报准备好图L4信号驱动I/O模型(5)异步I/O:告知内核启动某个操作,并让内核在整个操作完成
4、后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动1/。由内核通知我们何时可以开始一个I/O操作;异步I/O模型由内核通知我们I/O操作何时已经完成,如图L5所示。如果想要了解更多的UNIX系统网络编程知识,可以阅读UNIX网络编程,里面有非常详细的原理和APl介绍。对于大多数JaVa程序员来说,不需要了解网络编程的底层细节,大家只需要有个概念,知道对于操作系统而言,底层是支持异步I/O通信的。只不过在很长一段时间JaVa并没有提供异步I/O通信的类库,导致很多原生的JaVa程序员对这块儿比较陌生。当你了解了网络编程的基础知识后,理解JaVa的N
5、IO类库就会更加容易一些。Z置:系统调用无数据报、Fi准备好等待数据I准备就绪信号处理一递父SlGIo数据报准备好J进程继续执行复制割据报数据从内A核复制.递交在aio_read中用广,士间信号处理程序复制完成J图L5异步I/O模型下一个小结我们重点讲下1/0多路复用技术,因为JaVaNlo的核心类库多路复用器SeleCIor就是基于epoll的多路复用技术实现。1.1.2I/O多路复用技术在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。1/。多路复用技术通过把多个1/。的阻塞复用到同一个SeIeCt的阻塞上,从而使得系统在单线程的情况下可
6、以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源,I/O多路复用的主要应用场景如下。服务器需要同时处理多个处于监听状态或者多个连接状态的套接字;服务器需要同时处理多种网络协议的套接字。目前支持I/O多路复用的系统调用有SeIeCt、PSeleCt、pollsepoll,在LinUX网络编程过程中,很长一段时间都使用SeleCt做轮询和网络事件通知,然而SeleCt的一些固有缺陷导致了它的应用受到了很大的限制,最终LinUX不得不在新的内核版本
7、中寻找Selea的替代方案,最终选择了epoll。epoll与SeleCt的原理比较类似,为了克服SeIeCt的缺点,epoll作了很多重大改进,现总结如下。1.支持一个进程打开的SoCket描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数)Seleet最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。对于那些需要支持上万个TCP连接的大型服务器来说显然太少了。可以选择修改这个宏然后重新编译内核,不过这会带来网络效率的下降。我们也可以通过选择多进程的方案(传统的APaChe方案)解决这个问题,不过虽然在LinUX上创建进程的代价比较小,但
8、仍旧是不可忽视的。另外,进程间的数据交换非常麻烦,对于JaVa来说,由于没有共享内存,需要通过Socket通信或者其他方式进行数据同步,这带来了额外的性能损耗,增加了程序复杂度,所以也不是一种完美的解决方案。值得庆幸的是,epoll并没有这个限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于1024。例如,在IGB内存的机器上大约是10万个句柄左右,具体的值可以通过cat/proc/sys/fs/file-max察看,通常情况下这个值跟系统的内存关系比较大。1.1 /0效率不会随着FD数目的增加而线性下降。传统SelewPOH的另一个致命弱点,就是当你拥有一个很大的SoCke
9、t集合时,由于网络延时或者链路空闲,任一时刻只有少部分的SoCket是“活跃”的,但是SeIewPOII每次调用都会线性扫描全部的集合,导致效率呈现线性下降。epoll不存在这个问题,它只会对“活跃”的SoCket进行操作这是因为在内核实现中,epoll是根据每个fd上面的CallbaCk函数实现的。那么,只有“活跃”的SoCket才会去主动调用CallbaCk函数,其他idle状态的SOCket则不会。在这点上,epol实现了一个伪AI0。针对epoll和SeleCt性能对比的benchmark测试表明:如果所有的SoCket都处于活跃态例如一个高速LAN环境,epoll并不比SeIeet/
10、poll效率高太多;相反,如果过多使用RpolLctL效率相比还有稍微地降低。但是一旦使用idleCOnneCtionS模拟WAN环境,叩On的效率就远在SeIeCt/poll之上了。3 .使用mmap加速内核与用户空间的消息传递。无论是SeleCt、PolI还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存复制就显得非常重要,epoll是通过内核和用户空间mm叩同一块内存束实现的。4 .epoll的API更加简单。包括创建一个epoll描述符、添加监听事件、阻塞等待所监听的事件发生、关闭epoll描述符等。值得说明的是,用来克服SeleCt/poll缺点的方法不只有epo
11、ll,epoll只是一种LinlIX的实现方案。在freeBSD下有kqueue,而dev/poll是最古老的SoEriS的方案,使用难度依次递增。kqueue是freebsd的宠儿,它实际上是一个功能相当丰富的kernel事件队列,它不仅仅是SeleWPon的升级,而且可以处理SignaI、目录结构变止、进程等多种事件。kqueue是边缘触发的。devpoll是S。IariS的产物,是这一系列高性能APl中最早出现的。Kemel提供了一个特殊的设备文件devpoll,应用程序打开这个文件得到操作fd_set的句柄,通过写入POnfd来修改它,一个特殊的iol调用用来替换SeleCt。不过由于
12、出现的年代比较早,所以devpoll的接口实现比较原始。到这里,I/O的基础知识已经介绍完毕。从L2节开始介绍JaVa的I/O演进历史,从BlO到NIO是JaVa通信类库迈出的一小步,但却对JaVa在高性能通信领域的发展起到了关键性的推动作用。随着基于NlO的各类NIo框架的发展,以及基于Nlo的Web服务器的发展,Java在很多领域取代了C和C+,成为企业服务端应用开发的首选语言。1.2 JaVa的I/O演进在JDK1.4推出JaVaNlO之前,基于JaVa的所有SOCket通信都采用了同步阻塞模式(BIO),这种一请求一应答的通信模型简化了上层的应用开发,但是在性能和可靠性方面却存在着巨大
13、的瓶颈。因此,在很长一段时间里,大型的应用服务器都采用C或者C+语言开发,因为它们可以直接使用操作系统提供的异步I/O或者AIO能力。当并发访问量增大、响应时间延迟增大之后,采用JaVaBIO开发的服务端软件只有通过硬件的不断扩容来满足高并发和低时延,它极大地增加了企业的成本,并且随着集群规模的不断膨胀,系统的可维护性也面临巨大的挑战,只能通过采购性能更高的硬件服务器来解决问题,这会导致恶性循环。正是由于JaVa传统BIO的拙劣表现,才使得JaVa支持非阻塞I/O的呼声日渐高涨,最终,JDKL4版本提供了新的NIo类库,JaVa终于也可以支持非阻塞I/O了oJaVa的I/O发展简史从JDKLO
14、到JDK1.3,JaVa的I/O类库都非常原始,很多UNIX网络编程中的概念或者接口在I/O类库中都没有体现,例如Pipe、Channel.BUffer和SeIeCtOr等。2002年发布JDK1.4时,NlO以JSR51的身份正式随JDK发布。它新增了个java.nio包,提供了很多进行异步I/O开发的APl和类库,主要的类和接口如下。 进行异步I/O操作的缓冲区ByteBUffer等; 进行异步I/O操作的管道PiPe; 进行各种I/O操作(异步或者同步)的Channel,包括ServerSocketChannel和SocketChannel; 多种字符集的编码能力和解码能力; 实现非阻塞
15、I/O操作的多路复用器SeIeaor; 基于流行的PerI实现的正则表达式类库; 文件通道Fileehanne1。新的NIO类库的提供,极大地促进了基于JaVa的异步非阻塞编程的发展和应用,但是,它依然有不完善的地方,特别是对文件系统的处理能力仍显不足,主要问题如下。 没有统一的文件属性(例如读写权限) API能力比较弱,例如目录的级联创建和递归遍历,往往需要自己实现; 底层存储系统的一些高级APl无法使用; 所有的文件操作都是同步阻塞调用,不支持异步文件读写操作。2011年7月28日,JDKL7正式发布。它的一个比较大的亮点就是将原来的NIO类库进行了升级,被称为NIO2.0。NlO2.0由
16、JSR-203演进而来,它主要提供了如下三个方面的改进。提供能够批量获取文件属性的APL这些APl具有平台无关性,不与特性的文件系统相耦合。另外它还提供了标准文件系统的SPL供各个服务提供商扩展实现; 提供AlO功能,支持基于文件的异步I/O操作和针对网络套接字的异步操作; 完成JSR-51定义的通道功能,包括对配置和多播数据报的支持等。1.3 总结通过本章的学习,我们了解了UNlX网络编程的5种I/O模型,学习了I/O多路复用技术的基础知识。通过对JaVal/O演进历史的总结和介绍,相信大家对JaVa的I/O演进有了一个更加直观的认识。后面的第2章节会对阻塞I/O和非阻塞I/O进行详细讲解,
17、同时给出代码示例。相信学完第2章之后,大家就能够对传统的阻塞I/O的弊端和非阻塞I/O的优点有更加深刻的体会。好,稍微休息片刻,我们继续畅游在NlO编程的快乐海洋中!第2章NIO入门在本章中,我们会分别对JDK的BIO、NlO和JDKL7最新提供的NIO2.0的使用进行详细说明,通过流程图和代码讲解,让大家体会到:随着JaVal/0类库的不断发展和改进,基于JaVa的网络编程会变得越来越简单;随着异步I/O功能的增强,基于JaVaNIO开发的网络服务器甚至不逊色于采用C+开发的网络程序。本章主要内容包括: 传统的同步阻塞式I/O编程 基于NIo的非阻塞编程 基于NlO2.0的异步非阻塞(AIo
18、)编程 为什么要使用Nlo编程 为什么选择Netty2.1 传统的BIO编程网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。在基于传统同步阻塞模型开发中,SerVerSOCk&负责绑定IP地址,启动监听端口;SOCket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。下面,我们就以经典的时间服务器(TimeSerVer)为例,通过代码分析来回顾和
19、熟悉Ble)编程。2.1.1 BlO通信模型图首先,我们通过图21所示的通信模型图来熟悉BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的ACCePtOr线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答通信模型。图21同步阻塞I/O服务端通信模型(一客户端一线程)该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是JaVa虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧
20、下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。下面的两个小节,我们会分别对服务端和客户端进行源码分析,寻找同步阻塞I/O的弊上山2.1.2 同步阻塞式I/O创建的TimeSeNer源码分析(备注:以下代码行号均对应源代码中实际行号)1. ty.bio;2. importjava.io.IOException;3. .ServerSocket;4. .Socket;5. *6. *GauthorIilinfeng7. .*date2014年2月14日8. *version1.09*/11.12./*13.*Qparam
21、args14.*QthrowsIOException15.*/16.publicstaticvoidmain(Stringargs)throwsIOException17.intport=8080;18.if(args!=null&args.length0)19.20.try(21.port=Integer.valuef(args0);22.catch(NumberFormatExceptione)23./采用默认值24.)25.26.)27.ServerSocketserver=null;28.try(29.server=newServerSocket(port);30.System,out
22、.printIn(,Thetimeserverisstartinport:,31.Socketsocket=null;32.while(true)33.socket=server.accept();34.newThread(newTimeServerHandler(socket).start();35.36.finally10.publicclassTimeServerport);代码清单2-1同步阻塞I/O的TimeSerVer37. if(server!=null)38. System.out.println(,Thetimeserverclose*);39. server.close()
23、;40. server=null;41. 42. 43. 44. TimeSerVer根据传入的参数设置监听端口,如果没有入参,使用默认值8080。第29行通过构造函数创建SerVerSe)Cket,如果端口合法且没有被占用,服务端监听成功。第3235行通过一个无限循环来监听客户端的连接,如果没有客户端接入,则主线程阻塞在SerVerSoCket的accept操作上。启动TimeSerVer,通过JViSUalVM打印线程堆栈,我们可以发现主程序确实阻塞在acc叩t操作上,如图2-2所示。mainPriO=6tid=0x00879800nid=0xblcrunnableOxOOQafOOOja
24、va.lang.Thread.State:RUNNABLE.TwoStacksPlainSocketlmpl.SocketAccept(KativeMethod).AbstractpiainSocketImpl.accept(KbstractPlainSocketlmpl.java:398).PlainSocketIpl.acceptfflainSocketI11pl.java:198)-locked3x23021358(.SocksSocketlmpl).ServerSocket.implAccept(ServerSocket.java:530).ServerSocket.accept(Se
25、rverSocket.java:498)ty.bio.TimeServer.mainCTimeServer.java:50)1.ockedOWnablesynchronizers:-None图22主程序线程堆栈当有新的客户端接入的时候,执行代码第34行,以SOCket为参数构造TimeSerVerHandIer对象,TimeSerVerHandler是一个RUnnabIe,使用它为构造函数的参数创建一个新的客户端线程处理这条SOCket链路。下面我们继续分析TimeSerVerHandIer的代码。13.14.publicclassTimeServerHandlerimplementsRunn
26、able15.16.17.18.privateSocketsocket;publicTimeServerHandler(Socketsocket)this.socket=socket;代码清单22同步阻塞I/O的TimeSerVeiHandler19. )20.21. *22. *(non-Javadoc)23. *24. *seejava.lang.Runnabletrun()25. */26. eOverride27. publicvoidrun()28. BufferedReaderin=null;29. PrintWriterout=null;30. try(31. in=newBuf
27、feredReader(newInputStreamReader(32. this.socket.getInputStream();33. out=newPrintWriter(this.socket.getutputstream(),true);34. StringCurrentTime=null;35. Stringbody=null;36. while(true)37. .body=in.readLine();38. if(body=null)39. break;40. System,out.printin(,Thetimeserverreceiveorder:+body);41. Cu
28、rrentTime=QUERYTIMEORDERh.equalsIgnoreCase(body)?newjava.util.Date(42. .System.CurrentTimeMillis().toString():BADORDER,;43. out.printin(CurrentTime);44. 45.46. catch(Exceptione)47. if(in!=null)48. try49. in.closeO;50. catch(IOExceptionel)51. el.printStackTrace();52. )53. 154. if(out!=null)55. out.cl
29、oseO;56. out=null;57. 58. if(this.socket!=null)第37行通过BUfferedReader读取一行,如果已经读到了输入流的尾部,则返回值为null,退出循环。如果读到了非空值,则对内容进行判断,如果请求消息为查询时间的指令”QUERYTIMEORDER,则获取当前最新的系统时间,通过PrintWriter的PrintIn函数发送给客户端,最后退出循环。代码第4764行释放输入流、输出流和SOC套接字句柄资源,最后线程自动销毁并被虚拟机回收。在下一个小结,我们将介绍同步阻塞I/O的客户端代码,然后分别运行服务端和客户端,查看下程序的运行结果。2.1.3
30、同步阻塞式I/O创建的TimeCIient源码分析客户端通过SoCket创建,发送查询时间服务器的QUERYTIMEORDER”指令,然后读取服务端的响应并将结果打印出来,随后关闭连接,释放资源,程序退出执行。13.publicclassTimeClient14.15.16.17.18.19.20.21.22.23.24.25./*paramargs*/publicstaticvoidmain(Stringargs)intport=8080;if(args!=null&args.length0)try(port=Integer.valuef(args0);catch(NumberFormatE
31、xceptione)/采用默认值)代码清单2-3同步阻塞I/O的TimeQient26.27.Socketsocket=null;28.BufferedReaderin=null;29.PrintWriterout=null;30.try31.socket=newSocket(,127.0.0.l,rport);32.in=newBufferedReader(newInputStreamReader(33.socket.getlnputStream();34.out=newPrintWriter(socket.getOutputStream(),35.out.println(,QUERYTIM
32、EORDER1*);36.System.out.printin(HSendorder2serversucceed.37.Stringresp=in.readLine();38.System.out.printin(Nowis:,+resp);39.catch(Exceptione)(40.不需要处理41.finally42.if(out!=null)43.out.close();44.out=null;45.)46.47.if(in!=null)48.try(49.in.close();50.catch(IOExceptione)51.e.PrintStackTrace();52.53.in=
33、null;54.55.if(socket!=null)56.try57.socket.close();58.catch(IOExceptione)59.e.PrintStackTrace();60.61.socket=null;62.63.)64.)65.true);第35行客户端通过PrintWriler向服务端发送QUERYTIMEORDER指令,然后通过BUfferedReader的readLine读取响应并打印。分别执行服务端和客户端,执行结果如下。服务端执行结果如图23所示。3-ProbltasJvdocDcl*rtton,SrchSConsol汉TiProCr - X“口一TiBe
34、ServerJtvApplication:FrogrfFilsJvjdki7.045binjv0)22. try23. port=Integer.valuef(args0);24. catch(NumberFormatExceptione)25. /采用默认值26. 27. 28. ServerSocketserver=null;29. try30. server=newServerSocket(port);31. System.out.printIn(*,Thetimeserverisstartinport:,+port);32. Socketsocket=null;33. Timeserv
35、erHandlerExecutePoolsingleExecutor=newTimeserverHandlerExecutePool(34.35.50,1000。“创建工/。任务线程池while(true)36. socket三server.accept();37. singleEecutor.execute(newTimeServerHandler(socket);38. 39. finally 40.if (server != null) 41. System.out.printin(Thetimeserverclose*);42. server.close();43. server=nu
36、ll;44. 45. 46. 47. 伪异步I/O的主函数代码发生了变化,我们首先创建一个时间服务器处理类的线程池,当接收到新的客户端连接时,将请求SoCket封装成一个Task,然后调用线程池的execute方法执行,从而避免了每个请求接入都创建一个新的线程。12. publicclassTimeserverHandlerExecutePool13.14. privateExecutorServiceexecutor;15.16. publicTimeserverHandlerExecutePool(intmaxPoolSize,intqueuesize)代码清单25伪异步I/O的TimeS
37、erVerHandIerEXeCUtePoOl17. .executor=newThreadpoolExecutor(Runtime.getRuntime()18. .availableProcessorsO,maxPoolSize,120L,TimeUnit.SECONDS,19. newArrayBlockingQueue(queuesize);20. )21. publicvoidexecute(java.lang.Runnabletask)(22. executor.execute(task);23. 24. 由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致
38、线程个数过于膨胀或者内存溢出,相比于传统的一连接一线程模型,是一种改良。由于客户端代码并没有改变,因此,我们直接运行服务端和客户端,执行结果如下。服务端运行结果如图2-6所示。.ProblwsaJvdoc0Dcltrtion。Starch曰Congl,汉vProrssX-X砧虎I餐-TiSrvr(1)JtvApplicationI:ProcrFilsJavajdkl.7,0_45bnjvawxC014年2月15日下午92Thetimeserverisstartinport:8080Thetimeserverreceiveorder:QUERYTIMEORDER图2-6伪异步I/O时间服务器服务
39、端运行结果客户端运行结果如图2-7所示。ProblemsJarrdOCDeclarationSearChConsolejProgress)C*It砧|TimtClinJtvtApplicationE:ProcrtmFilsJvjdkl.7045binjvw.xt014年2月15日下午9:29:5,Sendorder2serversucceed.Nowis:SatFeb1521:29:59CST2014图2-7伪异步I/O时间服务器客户端运行结果伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从
40、根本上解决问题。下个小节我们对伪异步I/O进行深入分析,找到它的弊端,然后看看NIO是如何从根本上解决这个问题的。223伪异步I/O弊端分析要对伪异步I/O的弊端进行深入分析,首先我们看两个JaVa同步I/O的API说明,随后结合代码进行详细分析。* Readssomenumberofbytesfromtheinputstreamandstorestheminto*thebufferarrayb.Thenumberofbytesactuallyreadis*returnedasaninteger.Thismethodblocksuntilinputdatais*available,endoffileisdetected,oranexceptionisthrown.* Ifthelengthofbcod