Netty权威指南(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-13 06:06:40

点击下载

作者:李林锋

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Netty权威指南

Netty权威指南试读:

前言

大约在2008年的时候,我参与设计和开发的一个电信系统在月初出帐期,总是发生大量的连接超时和读写超时异常,业务的失败率相比于平时高了很多,报表中的很多指标都差强人意。后来经过排查,发现问题的主要原因出现在下游网元的处理性能上,月初的时候BSS出帐,在出帐期间BSS系统运行缓慢,由于双方采用了同步阻塞式的HTTP+XML进行通信,导致任何一方处理缓慢都会影响对方的处理性能。按照故障隔离的设计原则,对方处理速度慢或者不回应答,不应该影响系统的其他功能模块或者协议栈,但是在同步阻塞I/O通信模型下,这种故障传播和相互影响是不可避免的,很难通过业务层面解决。

受限于当时Tomcat和Servlet的同步阻塞I/O模型,以及在Java领域异步HTTP协议栈的技术积累不足,当时我们并没有办法完全解决这个问题,只能通过调整线程池策略和HTTP超时时间来从业务层面做规避。

2009年,由于对技术的热爱,我作为业务骨干被领导派去参加一个重点业务平台的研发工作,与两位资深的架构师(其中一位工作20年,做华为交换机出身)共同参与。这是我第一次全面接触异步I/O编程和高性能电信级协议栈的开发,眼界大开——异步高性能内部协议栈、异步HTTP、异步SOAP、异步SMPP……所有的协议栈都是异步非阻塞。后来的性能测试表明:基于Reactor模型统一调度的长连接和短连接协议栈,无论是性能、可靠性还是可维护性,都可以“秒杀”传统基于BIO开发的应用服务器和各种协议栈,这种差异本质上是一种代差。

在我从事异步NIO编程的2009年,业界还没有成熟的NIO框架,那个时候Mina刚刚开始起步,功能和性能都达不到商用标准。最困难的是,国内Java领域的异步通信还没有流行,整个业界的积累都非常少。那个时候资料匮乏,能够交流和探讨的圈内人很少,一旦踩住“地雷”,就需要夜以继日地维护。在随后2年多的时间里,经历了10多次的在通宵、凌晨被一线的运维人员电话吵醒等种种磨难之后,我们自研的NIO框架才逐渐稳定和成熟。期间,解决的BUG总计20~30个。

从2004年JDK1.4首次提供NIO 1.0类库到现在,已经过去了整整10年。JSR 51的设计初衷就是让Java能够提供非阻塞、具有弹性伸缩能力的异步I/O类库,从而结束Java在高性能服务器领域的不利地位。然而,在相当长的一段时间里,Java的NIO编程并没有流行起来,究其原因如下。

1.大多数高性能服务器,被C和C++语言盘踞,由于它们可以直接使用操作系统的异步I/O能力,所以对JDK的NIO并不关心;

2.移动互联网尚未兴起,基于Java的大规模分布式系统极少,很多中小型应用服务对于异步I/O的诉求不是很强烈;

3.高性能、高可靠性领域,例如银行、证券、电信等依然以C++为主导,Java充当打杂的角色,NIO暂时没有用武之地;

4.当时主流的J2EE服务器,几乎全部基于同步阻塞I/O构建,例如Servlet、Tomcat等,由于它们应用广泛,如果这些容器不支持NIO,用户很难具备独立构建异步协议栈的能力;

5.异步NIO编程门槛比较高,开发和维护一款基于NIO的协议栈对很多中小型公司来说像是一场噩梦;

6.业界NIO框架不成熟,很难商用;

7.国内研发界对NIO的陌生和认识不足,没有充分重视。

基于上述几种原因,NIO编程的推广和发展长期滞后。值得欣慰的是,随着大规模分布式系统、大数据和流式计算框架的兴起,基于Java来构建这些系统已经成为主流,NIO编程和NIO框架在此期间得到了大规模的商用。在互联网领域,阿里的分布式服务框架Dubbo、RocketMQ,大数据的基础序列化和通信框架Avro,以及很多开源的软件都已经开始使用Netty来构建高性能、分布式通信能力,Netty社区的活跃度也名列前茅。根据目前的信息,Netty已经在如下几个领域得到了大规模的商业应用。

1.互联网领域;

2.电信领域;

3.大数据领域;

4.银行、证券等金融领域;

5.游戏行业;

6.电力等企业市场。

2014年春节前,我分享了一篇博文《Netty5.0架构剖析和源码解读》,短短1个月下载量达到了4000多。很多网友向我咨询NIO编程技术、NIO框架如何选择等问题,也有一些圈内朋友和出版社邀请我写一本关于Netty的技术书籍。作为最流行、表现最优异的NIO框架,Netty深受大家喜爱,但是长期以来除了UserGuide之外,国内鲜有Netty相关的技术书籍供广大NIO编程爱好者学习和参考。由于Netty源码的复杂性和NIO编程本身的技术门槛限制,对于大多数读者而言,通过自己阅读和分析源码来深入掌握Netty的设计原理和实现细节是件困难的事情。从2011年开始我系统性的分析和应用了Netty和Mina,转瞬间已经过去了3年多。在这3年的时间里,我们的系统经受了无数严苛的考验,在这个过程中,对Netty和Mina有了更深刻的体验,也积累了丰富的运维和实战经验。我们都是开源框架Netty的受益者,为了让更多的朋友和同行能够了解NIO编程,深入学习和掌握Netty这个NIO利器,我打算将我的经验和大家分享,同时也结束国内尚无Netty学习教材的尴尬境地。联系方式

尽管我也有技术洁癖,希望诸事完美,但是由于Netty代码的庞杂和涉及到的知识点太多,一本书籍很难涵盖所有的功能点。如有遗漏或者错误,恳请大家能够及时批评和指正,如果你有好的建议或者想法,也可以联系我。我的联系方式如下。

邮箱:neu_lilinfeng@sina.com。

新浪微博:Nettying。

微信:Nettying。致谢

如果说个人能够改变自己命运的话,对于程序员来说,唯有通过不断的学习和实践,努力提升自己的技能,才有可能找到更好的机会,充分发挥和体现自己的价值。我希望本书能够为你的成功助一臂之力。

感谢博文视点的策划编辑丁一琼和幕后的美编,正是你们的辛苦工作才保证了本书能够顺利出版;感谢华为Netty爱好者和关注本书的领导同事们的支持,你们的理解和鼓励为我提供了足够的勇气。感谢我的家人和老婆的支持,写书占用了我几乎所有的业余时间,没有你们的理解和支持,我很难安心写作。

最后感谢Netty中国社区的朋友,我的微博粉丝和所有喜欢Netty的朋友们,你们对技术的热情是鼓励我写书的最重要动力,没有你们,就没有本书。希望大家一如既往的喜欢NIO编程,喜欢Netty,以及相互交流和分享,共同推动整个国内异步高性能通信领域的技术发展。李林锋5月11日于南京紫轩阁基础篇走进Java NIO第1章 Java的I/O演进之路

Java是由Sun Microsystems公司在1995年首先发布的编程语言和计算平台。这项基础技术支持最新的程序,包括实用程序、游戏和业务应用程序。Java在世界各地的8.5亿多台个人计算机和数十亿套设备上运行着,其中包括移动设备和电视设备。

Java之所以能够得到如此广泛的应用,除了摆脱硬件平台的依赖具有“一次编写、到处运行”的平台无关性特性之外,另一个重要原因是:其丰富而强大的类库以及众多第三方开源类库使得基于Java语言的开发更加简单和便捷。

但是,对于一些经验丰富的程序员来说,Java的一些类库在早期设计中功能并不完善或者存在一些缺陷,其中最令人恼火的就是基于同步I/O的Socket通信类库,直到2002年2月13日JDK1.4 Merlin的发布,Java才第一次支持非阻塞I/O,这个类库的提供为JDK的通信模型带来了翻天覆地的变化。

在开始学习Netty之前,我们首先对UNIX系统常用的I/O模型进行介绍,然后对Java的I/O历史演进进行简单说明。通过本章节的学习,希望读者对同步和异步I/O以及Java的I/O类库发展有个直观的了解,方便后续章节的学习。如果你已经熟练NIO编程或者从事过UNIX网络编程,希望直接学习Java的NIO和Netty,那就可以直接跳到第2章进行学习。

本章主要内容包括:● I/O基础入门● Java的I/O演进1.1 I/O基础入门

Java1.4之前的早期版本,Java对I/O的支持并不完善,开发人员在开发高性能I/O程序的时候,会面临一些巨大的挑战和困难,主要问题如下。● 没有数据缓冲区,I/O性能存在问题;● 没有C或者C++中的Channel概念,只有输入和输出流;● 同步阻塞式I/O通信(BIO),通常会导致通信线程被长时间阻塞;● 支持的字符集有限,硬件可移植性不好。

在Java支持异步I/O之前的很长一段时间里,高性能服务端开发领域一直被C++和C长期占据,Java的同步阻塞I/O被大家所诟病。1.1.1 Linux网络I/O模型简介

Linux的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符),描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性)。

根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型,分别如下。(1)阻塞I/O模型:最常用的I/O模型就是阻塞I/O模型,缺省情形下,所有文件操作都是阻塞的。我们以套接字接口为例来讲解此模型:在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间一直会等待,进程在从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞I/O模型,如图1-1所示。图1-1 阻塞I/O模型(2)非阻塞I/O模型:recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。如图1-2所示。图1-2 非阻塞I/O模型(3)I/O复用模型:Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。Linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数rollback。如图1-3所示。图1-3 I/O复用模型(4)信号驱动I/O模型:首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。如图1-4所示。图1-4 信号驱动I/O模型(5)异步I/O:告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时可以开始一个I/O操作;异步I/O模型由内核通知我们I/O操作何时已经完成。如图1-5所示。图1-5 异步I/O模型

如果想要了解更多的UNIX系统网络编程知识,可以阅读《UNIX网络编程》,里面有非常详细的原理和API介绍。对于大多数Java程序员来说,不需要了解网络编程的底层细节,大家只需要有个概念,知道对于操作系统而言,底层是支持异步I/O通信的,只不过在很长一段时间Java并没有提供异步I/O通信的类库,导致很多原生的Java程序员对这块儿比较陌生。当你了解了网络编程的基础知识后,理解Java的NIO类库就会更加容易一些。

下一个小结我们重点讲下I/O多路复用技术,因为Java NIO的核心类库多路复用器Selector就是基于epoll的多路复用技术实现。1.1.2 I/O多路复用技术

在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源,I/O多路复用的主要应用场景如下。● 服务器需要同时处理多个处于监听状态或者多个连接状态的套接

字;● 服务器需要同时处理多种网络协议的套接字。

目前支持I/O多路复用的系统调用有select、pselect、poll、epoll,在Linux网络编程过程中,很长一段时间都使用select做轮询和网络事件通知,然而select的一些固有缺陷导致了它的应用受到了很大的限制,最终Linux不得不在新的内核版本中寻找select的替代方案,最终选择了epoll。epoll与select的原理比较类似,为了克服select的缺点,epoll作了很多重大改进,现总结如下。

1.支持一个进程打开的socket描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数)。

select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。对于那些需要支持上万个TCP连接的大型服务器来说显然太少了。可以选择修改这个宏然后重新编译内核,不过这会带来网络效率的下降。我们也可以通过选择多进程的方案(传统的Apache方案)解决这个问题,不过虽然在Linux上创建进程的代价比较小,但仍旧是不可忽视的,另外,进程间的数据交换非常麻烦,对于Java由于没有共享内存,需要通过Socket通信或者其他方式进行数据同步,这带来了额外的性能损耗,增加了程序复杂度,所以也不是一种完美的解决方案。值得庆幸的是,epoll并没有这个限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于1024。例如,在1GB内存的机器上大约是10万个句柄左右,具体的值可以通过cat /proc/sys/fs/file- max察看,通常情况下这个值跟系统的内存关系比较大。

2.I/O效率不会随着FD数目的增加而线性下降。

传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,由于网络延时或者链路空闲,任一时刻只有少部分的socket是“活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。epoll不存在这个问题,它只会对“活跃”的socket进行操作-这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的,那么,只有“活跃”的socket才会主动的去调用callback函数,其他idle状态socket则不会。在这点上,epoll实现了一个伪AIO。针对epoll和select性能对比的benchmark测试表明:如果所有的socket都处于活跃态-例如一个高速LAN环境,epoll并不比select/poll效率高太多;相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

3.使用mmap加速内核与用户空间的消息传递。

无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存复制就显得非常重要,epoll是通过内核和用户空间mmap同一块内存实现。

4.epoll的API更加简单。

包括创建一个epoll描述符、添加监听事件、阻塞等待所监听的事件发生,关闭epoll描述符等。

值得说明的是,用来克服select/poll缺点的方法不只有epoll,epoll只是一种Linux的实现方案。在freeBSD下有kqueue,而dev/poll是最古老的Solaris的方案,使用难度依次递增。kqueue是freebsd的宠儿,它实际上是一个功能相当丰富的kernel事件队列,它不仅仅是select/poll的升级,而且可以处理signal、目录结构变化、进程等多种事件,kqueue是边缘触发的。/dev/poll是Solaris的产物,是这一系列高性能API中最早出现的。Kernel提供一个特殊的设备文件/dev/poll,应用程序打开这个文件得到操作fd_set的句柄,通过写入pollfd来修改它,一个特殊的ioctl调用用来替换select,不过由于出现的年代比较早,所以/dev/poll的接口实现比较原始。

到这里,I/O的基础知识已经介绍完毕,从1.2章节开始介绍Java的I/O演进历史,从BIO到NIO是Java通信类库迈出的一小步,但却对Java在高性能通信领域的发展起到了关键性的推动作用。随着基于NIO的各类NIO框架的发展,以及基于NIO的Web服务器的发展,Java在很多领域取代了C和C++,成为企业服务端应用开发的首选语言。1.2 Java的I/O演进

在JDK 1.4推出Java NIO之前,基于Java的所有Socket通信都采用了同步阻塞模式(BIO),这种一请求一应答的通信模型简化了上层的应用开发,但是在性能和可靠性方面却存在着巨大的瓶颈。因此,在很长一段时间里,大型的应用服务器都采用C或者C++语言开发,因为它们可以直接使用操作系统提供的异步I/O或者AIO能力。当并发访问量增大、响应时间延迟增大之后,采用Java BIO开发的服务端软件只有通过硬件的不断扩容来满足高并发和低时延,它极大地增加了企业的成本,并且随着集群规模的不断膨胀,系统的可维护性也面临巨大的挑战,只能通过采购性能更高的硬件服务器来解决问题,这会导致恶性循环。

正是由于Java传统BIO的拙劣表现,才使得Java支持非阻塞I/O的呼声日渐高涨,最终,JDK1.4版本提供了新的NIO类库,Java终于也可以支持非阻塞I/O了。

Java的I/O发展简史

从JDK1.0到JDK1.3,Java的I/O类库都非常原始,很多UNIX网络编程中的概念或者接口在I/O类库中都没有体现,例如Pipe、Channel、Buffer和Selector等。2002年发布JDK1.4时,NIO以JSR-51的身份正式随JDK发布。它新增了个java.nio包,提供了很多进行异步I/O开发的API和类库,主要的类和接口如下。● 进行异步I/O操作的缓冲区ByteBuffer等;● 进行异步I/O操作的管道Pipe;● 进行各种I/O操作(异步或者同步)的Channel,包括

ServerSocketChannel和SocketChannel;● 多种字符集的编码能力和解码能力;● 实现非阻塞I/O操作的多路复用器selector;● 基于流行的Perl实现的正则表达式类库;● 文件通道FileChannel。

新的NIO类库的提供,极大地促进了基于Java的异步非阻塞编程的发展和应用,但是,它依然有不完善的地方,特别是对文件系统的处理能力仍显不足,主要问题如下。● 没有统一的文件属性(例如读写权限);● API能力比较弱,例如目录的级联创建和递归遍历,往往需要自

己实现;● 底层存储系统的一些高级API无法使用;● 所有的文件操作都是同步阻塞调用,不支持异步文件读写操作。

2011年7月28日,JDK1.7正式发布。它的一个比较大的亮点就是将原来的NIO类库进行了升级,被称为NIO2.0。NIO2.0由JSR-203演进而来,它主要提供了如下三个方面的改进。● 提供能够批量获取文件属性的API,这些API具有平台无关性,

不与特性的文件系统相耦合,另外它还提供了标准文件系统的

SPI,供各个服务提供商扩展实现;● 提供AIO功能,支持基于文件的异步I/O操作和针对网络套接字的

异步操作;● 完成JSR-51定义的通道功能,包括对配置和多播数据报的支持

等。1.3 总结

通过本章的学习,我们了解了UNIX网络编程的5种I/O模型,学习了I/O多路复用技术的基础知识。通过对Java I/O演进历史的总结和介绍,相信大家对Java的I/O演进有了一个更加直观的认识。后面的第2章节会对阻塞I/O和非阻塞I/O进行详细讲解,同时给出代码示例。相信学完第2章之后,大家就能够对传统的阻塞I/O的弊端和非阻塞I/O的优点有更加深刻的体会。好,稍微休息片刻,我们继续畅游在NIO编程的快乐海洋中!第2章 NIO入门

在本章中,我们会分别对JDK的BIO、NIO和JDK1.7最新提供的NIO2.0的使用进行详细说明,通过流程图和代码讲解,让大家体会到随着Java I/O类库的不断发展和改进,基于Java的网络编程会变得越来越简单,随着异步I/O功能的增强,基于Java NIO开发的网络服务器甚至不逊色于采用C++开发的网络程序。

本章主要内容包括:● 传统的同步阻塞式I/O编程● 基于NIO的非阻塞编程● 基于NIO2.0的异步非阻塞(AIO)编程● 为什么要使用NIO编程● 为什么选择Netty2.1 传统的BIO编程

网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。

在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功之后,双方通过输入和输出流进行同步阻塞式通信。

下面,我们就以经典的时间服务器(TimeServer)为例,通过代码分析来回顾和熟悉下BIO编程。2.1.1 BIO通信模型图

首先,我们通过图2-1所示的通信模型图来熟悉下BIO的服务端通信模型:采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答通信模型。图2-1 同步阻塞I/O服务端通信模型(一客户端一线程)

该模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是Java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能将急剧下降,随着并发访问量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。

下面的两个小节,我们会分别对服务端和客户端进行源码分析,寻找同步阻塞I/O的弊端。2.1.2 同步阻塞式I/O创建的TimeServer源码分析

代码清单2-1 同步阻塞I/O的TimeServer(备注:以下代码行号均对应源代码中实际行号。) 1. package com.phei.netty.bio; 2. import java.io.IOException; 3. import java.net.ServerSocket; 4. import java.net.Socket; 5. /** 6. * @author lilinfeng 7. * @date 2014年2月14日 8. * @version 1.0 9. */ 10. public class TimeServer { 11. 12. /** 13. * @param args 14. * @throws IOException 15. */ 16. public static void main(String[] args) throws IOException { 17. int port = 8080; 18. if (args != null && args.length > 0) { 19. 20. try { 21. port = Integer.valueOf(args[0]); 22. } catch (NumberFormatException e) { 23. // 采用默认值 24. } 25. 26. } 27. ServerSocket server = null; 28. try { 29. server = new ServerSocket(port); 30. System.out.println("The time server is start in port : " + port); 31. Socket socket = null; 32. while (true) { 33. socket = server.accept(); 34. new Thread(new TimeServerHandler(socket)).start(); 35. } 36. } finally { 37. if (server != null) { 38. System.out.println("The time server close"); 39. server.close(); 40. server = null; 41. } 42. } 43. } 44. }

TimeServer根据传入的参数设置监听端口,如果没有入参,使用默认值8080,29行通过构造函数创建ServerSocket,如果端口合法且没有被占用,服务端监听成功。32~35行通过一个无限循环来监听客户端的连接,如果没有客户端接入,则主线程阻塞在ServerSocket的accept操作上。启动TimeServer,通过JvisualVM打印线程堆栈,我们可以发现主程序确实阻塞在accept操作上,如图2-2所示。图2-2 主程序线程堆栈

当有新的客户端接入的时候,执行代码34行,以Socket为参数构造TimeServerHandler对象,TimeServerHandler是一个Runnable,使用它为构造函数的参数创建一个新的客户端线程处理这条Socket链路。下面我们继续分析TimeServerHandler的代码。

代码清单2-2 同步阻塞I/O的TimeServerHandler 13. public class TimeServerHandler implements Runnable { 14. 15. private Socket socket; 16. 17. public TimeServerHandler(Socket socket) { 18. this.socket = socket; 19. } 20. 21. /* 22. * (non-Javadoc) 23. * 24. * @see java.lang.Runnable#run() 25. */ 26. @Override 27. public void run() { 28. BufferedReader in = null; 29. PrintWriter out = null; 30. try { 31. in = new BufferedReader(new InputStreamReader( 32. this.socket.getInputStream())); 33. out = new PrintWriter(this.socket.getOutputStream(), true); 34. String currentTime = null; 35. String body = null; 36. while (true) { 37. body = in.readLine(); 38. if (body == null) 39. break; 40. System.out.println("The time server receive order : " + body); 41. currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date( 42. System.currentTimeMillis()).toString() : "BAD ORDER"; 43. out.println(currentTime); 44. } 45. 46. } catch (Exception e) { 47. if (in != null) { 48. try { 49. in.close(); 50. } catch (IOException e1) { 51. e1.printStackTrace(); 52. } 53. } 54. if (out != null) { 55. out.close(); 56. out = null; 57. } 58. if (this.socket != null) { 59. try { 60. this.socket.close(); 61. } catch (IOException e1) { 62. e1.printStackTrace(); 63. } 64. this.socket = null; 65. } 66. } 67. } 68. }

37行通过BufferedReader读取一行,如果已经读到了输入流的尾部,则返回值为null,退出循环。如果读到了非空值,则对内容进行判断,如果请求消息为查询时间的指令"QUERY TIME ORDER"则获取当前最新的系统时间,通过PrintWriter的println函数发送给客户端,最后退出循环。代码47~64行释放输入流、输出流、和Socket套接字句柄资源,最后线程自动销毁并被虚拟机回收。

在下一个小结,我们将介绍同步阻塞I/O的客户端代码,然后分别运行服务端和客户端,查看下程序的运行结果。2.1.3 同步阻塞式I/O创建的TimeClient源码分析

客户端通过Socket创建,发送查询时间服务器的"QUERY TIME ORDER"指令,然后读取服务端的响应并将结果打印出来,随后关闭连接,释放资源,程序退出执行。

代码清单2-3 同步阻塞I/O的TimeClient 13. public class TimeClient { 14. 15. /** 16. * @param args 17. */ 18. public static void main(String[] args) { 19. int port = 8080; 20. if (args != null && args.length > 0) { 21. try { 22. port = Integer.valueOf(args[0]); 23. } catch (NumberFormatException e) { 24. // 采用默认值 25. } 26. } 27. Socket socket = null; 28. BufferedReader in = null; 29. PrintWriter out = null; 30. try { 31. socket = new Socket("127.0.0.1", port); 32. in = new BufferedReader(new InputStreamReader( 33. socket.getInputStream())); 34. out = new PrintWriter(socket.getOutputStream(), true); 35. out.println("QUERY TIME ORDER"); 36. System.out.println("Send order 2 server succeed."); 37. String resp = in.readLine(); 38. System.out.println("Now is : " + resp); 39. } catch (Exception e) { 40. //不需要处理 41. } finally { 42. if (out != null) { 43. out.close(); 44. out = null; 45. } 46. 47. if (in != null) { 48. try { 49. in.close(); 50. } catch (IOException e) { 51. e.printStackTrace(); 52. } 53. in = null; 54. } 55. if (socket != null) { 56. try { 57. socket.close(); 58. } catch (IOException e) { 59. e.printStackTrace(); 60. } 61. socket = null; 62. } 63. } 64. } 65. }

第35行客户端通过PrintWriter向服务端发送"QUERY TIME ORDER"指令,然后通过BufferedReader的readLine读取响应并打印。

分别执行服务端和客户端,执行结果如下。

服务端执行结果如图2-3所示。图2-3 同步阻塞I/O时间服务器服务端运行结果

客户端执行结果如图2-4所示。图2-4 同步阻塞IO时间服务器客户端运行结果

到此为止,同步阻塞式I/O开发的时间服务器程序已经讲解完毕,我们发现,BIO主要的问题在于每当有一个新的客户端请求接入时,服务端必须创建一个新的线程处理新接入的客户端链路,一个线程只能处理一个客户端连接。在高性能服务器应用领域,往往需要面向成千上万个客户端的并发连接,这种模型显然无法满足高性能、高并发接入的场景。

为了改进一线程一连接模型,后来又演进出了一种通过线程池或者消息队列实现1个或者多个线程处理N个客户端的模型,由于它的底层通信机制依然使用同步阻塞I/O,所以被称为“伪异步”,下面章节我们就对伪异步代码进行分析,看看伪异步是否能够满足我们对高性能、高并发接入的诉求。2.2 伪异步I/O编程

为了解决同步阻塞I/O面临的一个链路需要一个线程处理的问题,后来有人对它的线程模型进行了优化,后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

下面,我们结合连接模型图和源码,对伪异步I/O进行分析,看它是否能够解决同步阻塞I/O面临的问题。2.2.1 伪异步I/O模型图

采用线程池和任务队列可以实现一种叫做伪异步的I/O通信框架,它的模型图如图2-5所示。图2-5 伪异步I/O服务端通信模型(M:N)

当有新的客户端接入的时候,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

下面的小节,我们依然采用时间服务器程序,将其改造成伪异步I/O时间服务器,然后通过对代码进行分析,找出其弊端。2.2.2 伪异步式I/O创建的TimeServer源码分析

我们对服务端代码进行一些改造,代码如下。

代码清单2-4 伪异步I/O的TimeServer 13. public class TimeServer { 14. 15. /** 16. * @param args 17. * @throws IOException 18. */ 19. public static void main(String[] args) throws IOException { 20. int port = 8080; 21. if (args != null && args.length > 0) { 22. try { 23. port = Integer.valueOf(args[0]); 24. } catch (NumberFormatException e) { 25. // 采用默认值 26. } 27. } 28. ServerSocket server = null; 29. try { 30. server = new ServerSocket(port); 31. System.out.println("The time server is start in port : " + port); 32. Socket socket = null; 33. TimeServerHandlerExecutePool singleExecutor = new TimeServerHandlerExecutePool( 34. 50, 10000);// 创建I/O任务线程池 35. while (true) { 36. socket = server.accept(); 37. singleExecutor.execute(new TimeServerHandler(socket)); 38. } 39. } finally { 40. if (server != null) { 41. System.out.println("The time server close"); 42. server.close(); 43. server = null; 44. } 45. } 46. } 47. }

伪异步I/O的主函数代码发生了变化,我们首先创建一个时间服务器处理类的线程池,当接收到新的客户端连接的时候,将请求Socket封装成一个Task,然后调用线程池的execute方法执行,从而避免了每个请求接入都创建一个新的线程。

代码清单2-5 伪异步I/O的TimeServerHandlerExecutePool 12. public class TimeServerHandlerExecutePool { 13. 14. private ExecutorService executor; 15. 16. public TimeServerHandlerExecutePool(int maxPoolSize, int queueSize) { 17. executor = new ThreadPoolExecutor(Runtime.getRuntime() 18. .availableProcessors(), maxPoolSize, 120L, TimeUnit.SECONDS, 19. new ArrayBlockingQueue(queueSize)); 20. } 21. public void execute(java.lang.Runnable task) { 22. executor.execute(task); 23. } 24. }

由于线程池和消息队列都是有界的,因此,无论客户端并发连接数多大,它都不会导致线程个数过于膨胀或者内存溢出,相比于传统的一连接一线程模型,是一种改良。

由于客户端代码并没有改变,因此,我们直接运行服务端和客户端,执行结果如下。

服务端运行结果如图2-6所示。图2-6 伪异步I/O时间服务器服务端运行结果

客户端运行结果如图2-7所示。图2-7 伪异步I/O时间服务器客户端运行结果

伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。下个小节我们对伪异步I/O进行深入分析,找到它的弊端,然后看看NIO是如何从根本上解决这个问题的。2.2.3 伪异步I/O弊端分析

要对伪异步I/O的弊端进行深入分析,首先我们看两个Java同步I/O的API说明,随后我们结合代码进行详细分析。

代码清单2-6 Java输入流InputStream /** * Reads some number of bytes from the input stream and stores them into * the buffer array b. The number of bytes actually read is * returned as an integer. This method blocks until input data is * available, end of file is detected, or an exception is thrown. * *

If the length of b is zero, then no bytes are read and * 0 is returned; otherwise, there is an attempt to read at * least one byte. If no byte is available because the stream is at the * end of the file, the value -1 is returned; otherwise, at * least one byte is read and stored into b. * *

The first byte read is stored into element b[0], the * next one into b[1], and so on. The number of bytes read is, * at most, equal to the length of b. Let k be the * number of bytes actually read; these bytes will be stored in elements * b[0] through b[k-1], * leaving elements b[k] through * b[b.length-1] unaffected. * * @param b the buffer into which the data is read. * @return the total number of bytes read into the buffer, or * -1 if there is no more data because the end of * the stream has been reached. * @exception IOException If the first byte cannot be read for any reason * other than the end of the file, if the input stream has been closed, or * if some other I/O error occurs. * @exception NullPointerException if b is null . */ public int read(byte b[]) throws IOException { return read(b, 0, b.length); }

请注意加粗斜体字部分的API说明,当对Socket的输入流进行读取操作的时候,它会一直阻塞下去,直到发生如下三种事件。● 有数据可读;● 可用数据已经读取完毕;● 发生空指针或者I/O异常。

这意味着当对方发送请求或者应答消息比较缓慢、或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方要60s才能够将数据发送完成,读取一方的I/O线程也将会被同步阻塞60s,在此期间,其他接入消息只能在消息队列中排队。

下面我们接着对输出流进行分析,还是看JDK I/O类库输出流的API文档,然后结合文档说明进行故障分析。

代码清单2-7 Java输入流OutputStream public void write(byte b[]) throws IOException *Writes an array of bytes. This method will block until the bytes are *actually written. Parameters: b - the data to be written Throws: IOException If an I/O error has occurred.

当调用OutputStream的write方法写输出流的时候,它将会被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。学习过TCP/IP相关知识的人都知道,当消息的接收方处理缓慢的时候,将不能及时地从TCP缓冲区读取数据,这将会导致发送方的TCP window size不断减小,直到为0,双方处于Keep-Alive状态,消息发送方将不能再向TCP缓冲区写入消息,这时如果采用的是同步阻塞I/O,write操作将会被无限期阻塞,直到TCP window size大于0或者发生I/O异常。

通过对输入和输出流的API文档进行分析,我们了解到读和写操作都是同步阻塞的,阻塞的时间取决于对方I/O线程的处理速度和网络I/O的传输速度。本质上来讲,我们无法保证生产环境的网络状况和对端的应用程序能足够快,如果我们的应用程序依赖对方的处理速度,它的可靠性就非常差。也许在实验室进行的性能测试结果令人满意,但是一旦上线运行,面对恶劣的网络环境和良莠不齐的第三方系统,问题就会如火山一样喷发。

伪异步I/O实际上仅仅只是对之前I/O线程模型的一个简单优化,它无法从根本上解决同步I/O导致的通信线程阻塞问题。下面我们就简单分析下如果通信对方返回应答时间过长,会引起的级联故障。(1)服务端处理缓慢,返回应答消息耗费60s,平时只需要10ms。(2)采用伪异步I/O的线程正在读取故障服务节点的响应,由于读取输入流是阻塞的,因此,它将会被同步阻塞60s。(3)假如所有的可用线程都被故障服务器阻塞,那后续所有的I/O消息都将在队列中排队。(4)由于线程池采用阻塞队列实现,当队列积满之后,后续入队列的操作将被阻塞。(5)由于前端只有一个Accptor线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,客户端会发生大量的连接超时。(6)由于几乎所有的连接都超时,调用者会认为系统已经崩溃,无法接收新的请求消息。

如何破解这个难题?下节的NIO将给出答案。2.3 NIO编程

在介绍NIO编程之前,我们首先需要澄清一个概念:NIO到底是什么的简称?有人称之为New I/O,因为它相对于之前的I/O类库是新增的,所以被称为New I/O,这是它的官方叫法。但是,由于之前老的I/O类库是阻塞I/O,New I/O类库的目标就是要让Java支持非阻塞I/O,所以,更多的人喜欢称之为非阻塞I/O(Non-block I/O),由于非阻塞I/O更能够体现NIO的特点,所以本书使用的NIO都指的是非阻塞I/O。

与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式则正好相反。开发人员一般可以根据自己的需要来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻塞I/O以降低编程复杂度,但是对于高负载、高并发的网络应用,需要使用NIO的非阻塞模式进行开发。

下面的小节首先介绍NIO编程中的一些基本概念,然后通过NIO服务端的序列图和源码讲解,让大家快速地熟悉NIO编程的关键步骤和API的使用。如果你已经熟悉了NIO编程,可以跳过2.3节直接学习后面的章节。2.3.1 NIO类库简介

新的输入/输出(NIO)库是在JDK 1.4中引入的。NIO弥补了原来

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载