JAVA的BIO,NIO,AIO
2015-10-24 16:45
656 查看
一:名词解释
NIO
nio是javaNewIO的简称,在jdk1.4里提供的新api。Sun官方标榜的特性如下:–为所有的原始类型提供(Buffer)缓存支持。
–字符集编码解码解决方案。
–Channel:一个新的原始I/O抽象。
–支持锁和内存映射文件的文件访问接口。
–提供多路(non-bloking)非阻塞式的高伸缩性网络I/O
来自:http://blog.csdn.net/kobejayandy/article/details/11543891
示例一:
NIO通常采用Reactor模式,AIO通常采用Proactor模式。AIO简化了程序的编写,stream的读取和写入都有OS来完成,不需要像NIO那样子遍历Selector。Windows基于IOCP实现AIO,Linux只有eppoll模拟实现了AIO。
Java7之前的JDK只支持NIO和BIO,从7开始支持AIO。
4种通信方式:TCP/IP+BIO,TCP/IP+NIO,UDP/IP+BIO,UDP/IP+NIO。
TCP/IP+BIO、
Socket和ServerSocket实现,ServerSocket实现Server端端口监听,Socket用于建立网络IO连接。
不适用于处理多个请求1.生成Socket会消耗过多的本地资源。2.Socket连接的建立一般比较慢。
BIO情况下,能支持的连接数有限,一般都采取accept获取Socket以后采用一个thread来处理,oneconnectiononethread。无论连接是否有真正数据请求,都需要独占一个thread。
可以通过设立Socket池来一定程度上解决问题,但是使用池需要注意的问题是:1.竞争等待比较多。2.需要控制好超时时间。
TCP/IP+NIO
使用Channel(SocketChannel和ServerSocketChannel)和Selector。
Server端通常由一个thread来监听connect事件,另外多个thread来监听读写事件。这样做的好处是这些连接只有在真是请求的时候才会创建thread来处理,onerequestonethread。这种方式在server端需要支持大量连接但这些连接同时发送请求的峰值不会很多的时候十分有效。
UDP/IP+BIO
DatagramSocket和DatagramPacket。DatagramSocket负责监听端口以及读写数据,DatagramPacket作为数据流对象进行传输。
UDP/IP是无连接的,无法进行双向通信,除非双方都成为UDPServer。
UDP/IP+NIO
通过DatagramChannel和ByteBuffer实现。DatagramChannel负责端口监听及读写。ByteBuffer负责数据流传输。
如果要将消息发送到多台机器,如果为每个目标机器都建立一个连接的话,会有很大的网络流量压力。这时候可以使用基于UDP/IP的Multicast协议传输,Java中可以通过MulticastSocket和DatagramPacket来实现。
Multicast一般多用于多台机器的状态同步,比如JGroups。SRM,URGCP都是Multicast的实现方式。eBay就采用SRM来实现将数据从主数据库同步到各个搜索节点机器。
按照《Unix网络编程》的划分,IO模型可以分为:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO,按照POSIX标准来划分只分为两类:同步IO和异步IO。如何区分呢?首先一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作,同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO服用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
Javanio2.0的主要改进就是引入了异步IO(包括文件和网络),这里主要介绍下异步网络IOAPI的使用以及框架的设计,以TCP服务端为例。首先看下为了支持AIO引入的新的类和接口:
java.nio.channels.AsynchronousChannel
标记一个channel支持异步IO操作。
java.nio.channels.AsynchronousServerSocketChannel
ServerSocket的aio版本,创建TCP服务端,绑定地址,监听端口等。
java.nio.channels.AsynchronousSocketChannel
面向流的异步socketchannel,表示一个连接。
java.nio.channels.AsynchronousChannelGroup
异步channel的分组管理,目的是为了资源共享。一个AsynchronousChannelGroup绑定一个线程池,这个线程池执行两个任务:处理IO事件和派发CompletionHandler。AsynchronousServerSocketChannel创建的时候可以传入一个AsynchronousChannelGroup,那么通过AsynchronousServerSocketChannel创建的AsynchronousSocketChannel将同属于一个组,共享资源。
java.nio.channels.CompletionHandler
异步IO操作结果的回调接口,用于定义在IO操作完成后所作的回调工作。AIO的API允许两种方式来处理异步操作的结果:返回的Future模式或者注册CompletionHandler,我更推荐用CompletionHandler的方式,这些handler的调用是由AsynchronousChannelGroup的线程池派发的。显然,线程池的大小是性能的关键因素。AsynchronousChannelGroup允许绑定不同的线程池,通过三个静态方法来创建:
1publicstaticAsynchronousChannelGroupwithFixedThreadPool(intnThreads, 2ThreadFactorythreadFactory) 3throwsIOException 4 5publicstaticAsynchronousChannelGroupwithCachedThreadPool(ExecutorServiceexecutor, 6intinitialSize) 7 8publicstaticAsynchronousChannelGroupwithThreadPool(ExecutorServiceexecutor) 9throwsIOException 10
需要根据具体应用相应调整,从框架角度出发,需要暴露这样的配置选项给用户。
在介绍完了aio引入的TCP的主要接口和类之后,我们来设想下一个aio框架应该怎么设计。参考非阻塞nio框架的设计,一般都是采用Reactor模式,Reacot负责事件的注册、select、事件的派发;相应地,异步IO有个Proactor模式,Proactor负责CompletionHandler的派发,查看一个典型的IO写操作的流程来看两者的区别:
Reactor:send(msg)->消息队列是否为空,如果为空->向Reactor注册OP_WRITE,然后返回->Reactorselect->触发Writable,通知用户线程去处理->先注销Writable(很多人遇到的cpu100%的问题就在于没有注销),处理Writeable,如果没有完全写入,继续注册OP_WRITE。注意到,写入的工作还是用户线程在处理。
Proactor:send(msg)->消息队列是否为空,如果为空,发起read异步调用,并注册CompletionHandler,然后返回。->操作系统负责将你的消息写入,并返回结果(写入的字节数)给Proactor->Proactor派发CompletionHandler。可见,写入的工作是操作系统在处理,无需用户线程参与。事实上在aio的API中,AsynchronousChannelGroup就扮演了Proactor的角色。
CompletionHandler有三个方法,分别对应于处理成功、失败、被取消(通过返回的Future)情况下的回调处理:
1publicinterfaceCompletionHandler<V,A>{ 2 3voidcompleted(Vresult,Aattachment); 4 5voidfailed(Throwableexc,Aattachment); 6 7 8voidcancelled(Aattachment); 9}
其中的泛型参数V表示IO调用的结果,而A是发起调用时传入的attchment。
在初步介绍完aio引入的类和接口后,我们看看一个典型的tcp服务端是怎么启动的,怎么接受连接并处理读和写,这里引用的代码都是yanf4j的aio分支中的代码,可以从svncheckout,svn地址:
第一步,创建一个AsynchronousServerSocketChannel,创建之前先创建一个AsynchronousChannelGroup,上文提到AsynchronousServerSocketChannel可以绑定一个AsynchronousChannelGroup,那么通过这个AsynchronousServerSocketChannel建立的连接都将同属于一个AsynchronousChannelGroup并共享资源:
1this.asynchronousChannelGroup=AsynchronousChannelGroup 2.withCachedThreadPool(Executors.newCachedThreadPool(), 3this.threadPoolSize);
然后初始化一个AsynchronousServerSocketChannel,通过open方法:
1this.serverSocketChannel=AsynchronousServerSocketChannel 2.open(this.asynchronousChannelGroup); 3
通过nio2.0引入的SocketOption类设置一些TCP选项:
1this.serverSocketChannel 2.setOption( 3StandardSocketOption.SO_REUSEADDR,true); 4this.serverSocketChannel 5.setOption( 6StandardSocketOption.SO_RCVBUF,16*1024); 7
绑定本地地址:
1this.serverSocketChannel 2.bind(newInetSocketAddress("localhost",8080),100);
其中的100用于指定等待连接的队列大小(backlog)。完了吗?还没有,最重要的监听工作还没开始,监听端口是为了等待连接上来以便accept产生一个AsynchronousSocketChannel来表示一个新建立的连接,因此需要发起一个accept调用,调用是异步的,操作系统将在连接建立后,将最后的结果——AsynchronousSocketChannel返回给你:
1publicvoidpendingAccept(){ 2if(this.started&&this.serverSocketChannel.isOpen()){ 3this.acceptFuture=this.serverSocketChannel.accept(null, 4newAcceptCompletionHandler()); 5 6}else{ 7thrownewIllegalStateException("Controllerhasbeenclosed"); 8} 9} 10
注意,重复的accept调用将会抛出PendingAcceptException,后文提到的read和write也是如此。accept方法的第一个参数是你想传给CompletionHandler的attchment,第二个参数就是注册的用于回调的CompletionHandler,最后返回结果Future。你可以对future做处理,这里采用更推荐的方式就是注册一个CompletionHandler。那么accept的CompletionHandler中做些什么工作呢?显然一个赤裸裸的AsynchronousSocketChannel是不够的,我们需要将它封装成session,一个session表示一个连接(mina里就叫
IoSession了),里面带了一个缓冲的消息队列以及一些其他资源等。在连接建立后,除非你的服务器只准备接受一个连接,不然你需要在后面继续调用pendingAccept来发起另一个accept请求:
1privatefinalclassAcceptCompletionHandlerimplements 2CompletionHandler<AsynchronousSocketChannel,Object>{ 3 4@Override 5publicvoidcancelled(Objectattachment){ 6logger.warn("Acceptoperationwascanceled"); 7} 8 9@Override 10publicvoidcompleted(AsynchronousSocketChannelsocketChannel, 11Objectattachment){ 12try{ 13logger.debug("Acceptconnectionfrom" 14+socketChannel.getRemoteAddress()); 15configureChannel(socketChannel); 16AioSessionConfigsessionConfig=buildSessionConfig(socketChannel); 17Sessionsession=newAioTCPSession(sessionConfig, 18AioTCPController.this.configuration 19.getSessionReadBufferSize(), 20AioTCPController.this.sessionTimeout); 21session.start(); 22registerSession(session); 23}catch(Exceptione){ 24e.printStackTrace(); 25logger.error("Accepterror",e); 26notifyException(e); 27}finally{ 28<strong>pendingAccept</strong>(); 29} 30} 31 32@Override 33publicvoidfailed(Throwableexc,Objectattachment){ 34logger.error("Accepterror",exc); 35try{ 36notifyException(exc); 37}finally{ 38<strong>pendingAccept</strong>(); 39} 40} 41} 42
注意到了吧,我们在failed和completed方法中在最后都调用了pendingAccept来继续发起accept调用,等待新的连接上来。有的同学可能要说了,这样搞是不是递归调用,会不会堆栈溢出?实际上不会,因为发起accept调用的线程与CompletionHandler回调的线程并非同一个,不是一个上下文中,两者之间没有耦合关系。要注意到,CompletionHandler的回调共用的是AsynchronousChannelGroup绑定的线程池,因此千万别在CompletionHandler回调方法中调用阻塞或者长时间的操作,例如sleep,回调方法最好能支持超时,防止线程池耗尽。
连接建立后,怎么读和写呢?回忆下在nonblockingnio框架中,连接建立后的第一件事是干什么?注册OP_READ事件等待socket可读。异步IO也同样如此,连接建立后马上发起一个异步read调用,等待socket可读,这个是Session.start方法中所做的事情:
1publicclassAioTCPSession{ 2protectedvoidstart0(){ 3pendingRead(); 4} 5 6protectedfinalvoidpendingRead(){ 7if(!isClosed()&&this.asynchronousSocketChannel.isOpen()){ 8if(!this.readBuffer.hasRemaining()){ 9this.readBuffer=ByteBufferUtils 10.increaseBufferCapatity(this.readBuffer); 11} 12this.readFuture=this.asynchronousSocketChannel.read( 13this.readBuffer,this,this.readCompletionHandler); 14}else{ 15thrownewIllegalStateException( 16"SessionOrChannelhasbeenclosed"); 17} 18} 19 20} 21
AsynchronousSocketChannel的read调用与AsynchronousServerSocketChannel的accept调用类似,同样是非阻塞的,返回结果也是一个Future,但是写的结果是整数,表示写入了多少字节,因此read调用返回的是Future,方法的第一个参数是读的缓冲区,操作系统将IO读到数据拷贝到这个缓冲区,第二个参数是传递给CompletionHandler的attchment,第三个参数就是注册的用于回调的CompletionHandler。这里保存了read的结果Future,这是为了在关闭连接的时候能够主动取消调用,accept也是如此。现在可以看看read的CompletionHandler的实现:
1publicfinalclassReadCompletionHandlerimplements 2CompletionHandler<Integer,AbstractAioSession>{ 3 4privatestaticfinalLoggerlog=LoggerFactory 5.getLogger(ReadCompletionHandler.class); 6protectedfinalAioTCPControllercontroller; 7 8publicReadCompletionHandler(AioTCPControllercontroller){ 9this.controller=controller; 10} 11 12@Override 13publicvoidcancelled(AbstractAioSessionsession){ 14log.warn("Session("+session.getRemoteSocketAddress() 15+")readoperationwascanceled"); 16} 17 18@Override 19publicvoidcompleted(Integerresult,AbstractAioSessionsession){ 20if(log.isDebugEnabled()) 21log.debug("Session("+session.getRemoteSocketAddress() 22+")read+"+result+"bytes"); 23if(result<0){ 24session.close(); 25return; 26} 27try{ 28if(result>0){ 29session.updateTimeStamp(); 30session.getReadBuffer().flip(); 31session.decode(); 32session.getReadBuffer().compact(); 33} 34}finally{ 35try{ 36session.pendingRead(); 37}catch(IOExceptione){ 38session.onException(e); 39session.close(); 40} 41} 42controller.checkSessionTimeout(); 43} 44 45@Override 46publicvoidfailed(Throwableexc,AbstractAioSessionsession){ 47log.error("Sessionreaderror",exc); 48session.onException(exc); 49session.close(); 50} 51 52} 53
如果IO读失败,会返回失败产生的异常,这种情况下我们就主动关闭连接,通过session.close()方法,这个方法干了两件事情:关闭channel和取消read调用:
1if(null!=this.readFuture){ 2this.readFuture.cancel(true); 3} 4this.asynchronousSocketChannel.close(); 5
在读成功的情况下,我们还需要判断结果result是否小于0,如果小于0就表示对端关闭了,这种情况下我们也主动关闭连接并返回。如果读到一定字节,也就是result大于0的情况下,我们就尝试从读缓冲区中decode出消息,并派发给业务处理器的回调方法,最终通过pendingRead继续发起read调用等待socket的下一次可读。可见,我们并不需要自己去调用channel来进行IO读,而是操作系统帮你直接读到了缓冲区,然后给你一个结果表示读入了多少字节,你处理这个结果即可。而nonblockingIO框架中,是reactor通知用户线程socket可读了,然后用户线程自己去调用read进行实际读操作。这里还有个需要注意的地方,就是decode出来的消息的派发给业务处理器工作最好交给一个线程池来处理,避免阻塞group绑定的线程池。
IO写的操作与此类似,不过通常写的话我们会在session中关联一个缓冲队列来处理,没有完全写入或者等待写入的消息都存放在队列中,队列为空的情况下发起write调用:
1protectedvoidwrite0(WriteMessagemessage){ 2booleanneedWrite=false; 3synchronized(this.writeQueue){ 4needWrite=this.writeQueue.isEmpty(); 5this.writeQueue.offer(message); 6} 7if(needWrite){ 8pendingWrite(message); 9} 10} 11 12protectedfinalvoidpendingWrite(WriteMessagemessage){ 13message=preprocessWriteMessage(message); 14if(!isClosed()&&this.asynchronousSocketChannel.isOpen()){ 15this.asynchronousSocketChannel.write(message.getWriteBuffer(), 16this,this.writeCompletionHandler); 17}else{ 18thrownewIllegalStateException( 19"SessionOrChannelhasbeenclosed"); 20} 21} 22
write调用返回的结果与read一样是一个Future,而write的CompletionHandler处理的核心逻辑大概是这样:
1@Override 2publicvoidcompleted(Integerresult,AbstractAioSessionsession){ 3if(log.isDebugEnabled()) 4log.debug("Session("+session.getRemoteSocketAddress() 5+")writen"+result+"bytes"); 6 7WriteMessagewriteMessage; 8Queue<WriteMessage>writeQueue=session.getWriteQueue(); 9synchronized(writeQueue){ 10writeMessage=writeQueue.peek(); 11if(writeMessage.getWriteBuffer()==null 12||!writeMessage.getWriteBuffer().hasRemaining()){ 13writeQueue.remove(); 14if(writeMessage.getWriteFuture()!=null){ 15writeMessage.getWriteFuture().setResult(Boolean.TRUE); 16} 17try{ 18session.getHandler().onMessageSent(session, 19writeMessage.getMessage()); 20}catch(Exceptione){ 21session.onException(e); 22} 23writeMessage=writeQueue.peek(); 24} 25} 26if(writeMessage!=null){ 27try{ 28session.pendingWrite(writeMessage); 29}catch(IOExceptione){ 30session.onException(e); 31session.close(); 32} 33} 34} 35
compete方法中的result就是实际写入的字节数,然后我们判断消息的缓冲区是否还有剩余,如果没有就将消息从队列中移除,如果队列中还有消息,那么继续发起write调用。
重复一下,这里引用的代码都是yanf4jaio分支中的源码,感兴趣的朋友可以直接checkout出来看看:
在引入了aio之后,java对于网络层的支持已经非常完善,该有的都有了,java也已经成为服务器开发的首选语言之一。java的弱项在于对内存的管理上,由于这一切都交给了GC,因此在高性能的网络服务器上还是Cpp的天下。java这种单一堆模型比之erlang的进程内堆模型还是有差距,很难做到高效的垃圾回收和细粒度的内存管理。
这里仅仅是介绍了aio开发的核心流程,对于一个网络框架来说,还需要考虑超时的处理、缓冲buffer的处理、业务层和网络层的切分、可扩展性、性能的可调性以及一定的通用性要求。
tomcat的配置
Tomcat是一个小型的轻量级Connector(Tomcat
Connector的运行模式来提高Tomcat的运行性能呢?
下面,我们先大致了解TomcatConnector的三种运行模式。
bio
bio(blockingI/O),顾名思义,即阻塞式I/O操作,表示Tomcat使用的是传统的JavaI/O操作(即java.io包及其子包)。Tomcat在默认情况下,就是以bio模式运行的。遗憾的是,就一般而言,bio模式是三种运行模式中性能最低的一种。我们可以通过TomcatManager来查看服务器的当前状态。【点击这里可以查看Tomcat
Manager用户配置的相关信息】
nio
nio(newI/O),是JavaSE1.4及后续版本提供的一种新的I/O操作方式(即包及其子包)。Javajava.nio
nio是一个基于
API,因此nio也被看成是
non-blockingI/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。要让Tomcat以nio模式来运行也比较简单,我们只需要在
Tomcat文件中将如下配置:安装目录/conf/server.xml
<Connectorport="8080"protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443"/>
中的protocol属性值改为
org.apache.coyote.http11.Http11NioProtocol即可:
<Connectorport="8080"protocol="org.apache.coyote.http11.Http11NioProtocol" connectionTimeout="20000" redirectPort="8443"/>
此时,我们就可以在TomcatManager中看到当前服务器状态页面的HTTP协议的Connector运行模式已经从
http-bio-8080变成了
http-nio-8080。
apr
apr(ApachePortableRuntime/Apache可移植运行时),是ApacheHTTP服务器的支持库。你可以简单地理解为,Tomcat将以JNI的形式调用ApacheHTTP服务器的核心apr也是在Tomcat上运行高并发应用的首选模式。如果我们的Tomcat不是在apr模式下运行,在启动Tomcat的时候,我们可以在日志信息中看到类似如下信息:
2013-8-616:17:49org.apache.catalina.core.AprLifecycleListenerinit 信息:TheAPRbasedApacheTomcatNativelibrarywhichallowsoptimalperformanceinproductionenvironmentswasnotfoundonthejava.library.path:xxx/xxx(这里是路径信息)
Tomcatapr运行模式的配置是三种运行模式之中相对比较麻烦的一种。据官方文档所述,Tomcatapr需要以下三个
APRlibrary[APR库]
JNIwrappersforAPRusedbyTomcat(libtcnative)[简单地说,如果是在Windows操作系统上,就是一个名为tcnative-1.dll的
OpenSSLlibraries[OpenSSL库]
此外,与配置nio运行模式一样,也需要将对应的
Connector节点的
protocol属性值改为
org.apache.coyote.http11.Http11AprProtocol。不过,上述繁琐的操作都是Tomcat7.0.30之前的版本才需要这样配置,从Tomcat7.0.30版本开始,Tomcat已经自带了
tcnative-1.dll等文件,并且默认就是在Tomcatapr模式下运行,因此我们只需要下载最新版本的Tomcat直接使用即可。
此外,即使不使用TomcatManager,我们也可以区分出Tomcat当前的运行模式。如果以不同的Connector模式启动,在Tomcat的启动日志信息中一般会包含类似如下的不同内容,我们只需要根据这些信息即可判断出当前Tomcat的运行模式:
bio信息:StartingProtocolHandler["http-bio-8080"]2013-8-616:17:50org.apache.coyote.AbstractProtocolstartnio信息:StartingProtocolHandler["http-nio-8080"]2013-8-616:59:53org.apache.coyote.AbstractProtocolstartapr信息:StartingProtocolHandler["http-apr-8080"]2013-8-617:03:07org.apache.coyote.AbstractProtocolstart
Tomcat6.x版本从6.0.32开始就默认支持apr。 Tomcat7.x版本从7.0.30开始就默认支持apr。 因此,如果读者使用的Tomcat版本比较陈旧的话,强烈建议升级到最新的稳定版本。
tomcat配置说明-文章2
tomcat的运行模式有3种.修改他们的运行模式.3种模式的运行是否成功,可以看他的启动控制台,或者启动日志.或者登录他们的默认页面http://localhost:8080/查看其中的服务器状态。1)bio
默认的模式,性能非常低下,没有经过任何优化处理和支持.
2)nio
利用java的异步io护理技术,noblockingIO技术.
想运行在该模式下,直接修改server.xml里的Connector节点,修改protocol为
<Connectorport="80"protocol="org.apache.coyote.http11.Http11NioProtocol" connectionTimeout="20000" URIEncoding="UTF-8" useBodyEncodingForURI="true" enableLookups="false" redirectPort="8443"/>
启动后,就可以生效。
3)apr
安装起来最困难,但是从操作系统级别来解决异步的IO问题,大幅度的提高性能.
必须要安装apr和native,直接启动就支持apr。下面的修改纯属多余,仅供大家扩充知识,但仍然需要安装apr和native
如nio修改模式,修改protocol为org.apache.coyote.http11.Http11AprProtocol
Tomcat6.X实现了JCP的Servlet2.5和JSP2.1的规范,并且包括其它很多有用的功能,使它成为开发
和部署web应用和web服务的坚实平台。
NIO(No-blockingI/O)从JDK1.4起,NIOAPI作为一个基于缓冲区,并能提供非阻塞I/O操作的API
被引入。
作为开源web服务器的java实现,tomcat几乎就是web开发者开发、测试的首选,有很多其他商业服务
器的开发者也会优先选择tomcat作为开发时候使用,而在部署的时候,把应用发布在商业服务器上。也有
许多商业应用部署在tomcat上,tomcat承载着其核心的应用。但是很多开发者很迷惑,为什么在自己的应
用里使用tomcat作为平台的时候,而并发用户超过一定数量,服务器就变的非常繁忙,而且很快就出现了
connectionrefuse的错误。但是很多商业应用部署在tomcat上运行却安然无恙。
其中有个很大的原因就是,配置良好的tomcat都会使用APR(ApachePortableRuntime),APR是
ApacheHTTPServer2.x的核心,它是高度可移植的本地库,它使用高性能的UXINI/O操作,低性能的
javaio操作,但是APR对很多Java开发者而言可能稍稍有点难度,在很多OS平台上,你可能需要重新编
译APR。但是从Tomcat6.0以后,Java开发者很容易就可以是用NIO的技术来提升tomcat的并发处理能力。
但是为什么NIO可以提升tomcat的并发处理能力呢,我们先来看一下java传统io与javaNIO的差别。
Java传统的IO操作都是阻塞式的(blockingI/O),如果有socket的编程基础,你会接触过堵塞socket和
非堵塞socket,堵塞socket就是在accept、read、write等IO操作的的时候,如果没有可用符合条件的资
源,不马上返回,一直等待直到有资源为止。而非堵塞socket则是在执行select的时候,当没有资源的时
候堵塞,当有符合资源的时候,返回一个信号,然后程序就可以执行accept、read、write等操作,一般来
说,如果使用堵塞socket,通常我们通常开一个线程acceptsocket,当读完这次socket请求的时候,开一
个单独的线程处理这个socket请求;如果使用非堵塞socket,通常是只有一个线程,一开始是select状,
当有信号的时候可以通过可以通过多路复用(Multiplexing)技术传递给一个指定的线程池来处理请求,然
后原来的线程继续select状态。最简单的多路复用技术可以通过java管道(Pipe)来实现。换句话说,如果
客户端的并发请求很大的时候,我们可以使用少于客户端并发请求的线程数来处理这些请求,而这些来不
及立即处理的请求会被阻塞在java管道或者队列里面,等待线程池的处理。请求听起来很复杂,在这个架
构当道的java世界里,现在已经有很多优秀的NIO的架构方便开发者使用,比如Grizzly,ApacheMina等
等,如果你对如何编写高性能的网络服务器有兴趣,你可以研读这些源代码。
简单说一下,在web服务器上阻塞IO(BIO)与NIO一个比较重要的不同是,我们使用BIO的时候往往会
为每一个web请求引入多线程,每个web请求一个单独的线程,所以并发量一旦上去了,线程数就上去
了,CPU就忙着线程切换,所以BIO不合适高吞吐量、高可伸缩的web服务器;而NIO则是使用单线程(单
个CPU)或者只使用少量的多线程(多CPU)来接受Socket,而由线程池来处理堵塞在pipe或者队列里的请
求.这样的话,只要OS可以接受TCP的连接,web服务器就可以处理该请求。大大提高了web服务器的可
伸缩性。
我们来看一下配置,你只需要在server.xml里把HTTPConnector做如下更改,
<Connectorport="8080"protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"/>
改为
<Connectorport="8080"protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443"/>
然后启动服务器,你会看到org.apache.coyote.http11.Http11NioProtocolstart的信息,表示NIO已经启动。其他的配置请参考官方配置文档。
Enjoyit.
最后贴上官方文档上对tomcat的三种Connector的方式做一个简单比较,
JavaBlockingConnectorJavaNioBlockingConnectorAPRConnector ClassnameHttp11ProtocolHttp11NioProtocolHttp11AprProtocol TomcatVersion3.x4.x5.x6.x6.x5.5.x6.x SupportPollingNOYESYES PollingSizeN/AUnlimited-RestrictedbymemUnlimited ReadHTTPRequestBlockingBlockingBlocking ReadHTTPBodyBlockingBlockingBlocking WriteHTTPResponseBlockingBlockingBlocking SSLSupportJavaSSLJavaSSLOpenSSL SSLHandshakeBlockingNonblockingBlocking MaxConnectionsmaxThreadsSeepollingsizeSeepollingsize
如果读者有socket的编程基础,应该会接触过堵塞socket和非堵塞socket,堵塞socket就是在accept、read、write等IO操作的的时候,如果没有可用符合条件的资源,不马上返回,一直等待直到有资源为止。而非堵塞socket则是在执行select的时候,当没有资源的时候堵塞,当有符合资源的时候,返回一个信号,然后程序就可以执行accept、read、write等操作,这个时候,这些操作是马上完成,并且马上返回。而windows的winsock则有所不同,可以绑定到一个EventHandle里,也可以绑定到一个HWND里,当有资源到达时,发出事件,这时执行的io操作也是马上完成、马上返回的。一般来说,如果使用堵塞socket,通常我们时开一个线程acceptsocket,当有socket链接的时候,开一个单独的线程处理这个socket;如果使用非堵塞socket,通常是只有一个线程,一开始是select状态,当有信号的时候马上处理,然后继续select状态。
按照大多数人的说法,堵塞socket比非堵塞socket的性能要好。不过也有小部分人并不是这样认为的,例如Indy项目(Delphi一个比较出色的网络包),它就是使用多线程+堵塞socket模式的。另外,堵塞socket比非堵塞socket容易理解,符合一般人的思维,编程相对比较容易。
nio其实也是类似上面的情况。在JDK1.4,sun公司大范围提升Java的性能,其中NIO就是其中一项。Java的IO操作集中在java.io这个包中,是基于流的阻塞API(即BIO,BlockIO)。对于大多数应用来说,这样的API使用很方便,然而,一些对性能要求较高的应用,尤其是服务端应用,往往需要一个更为有效的方式来处理IO。从JDK1.4起,NIOAPI作为一个基于缓冲区,并能提供非阻塞O操作的API(即NIO,non-blockingIO)被引入。
BIO与NIO一个比较重要的不同,是我们使用BIO的时候往往会引入多线程,每个连接一个单独的线程;而NIO则是使用单线程或者只使用少量的多线程,每个连接共用一个线程。
这个时候,问题就出来了:我们非常多的java应用是使用ThreadLocal的,例如JSF的FaceContext、Hibernate的session管理、Struts2的Context的管理等等,几乎所有框架都或多或少地应用ThreadLocal。如果存在冲突,那岂不惊天动地?
后来终于在Tomcat6的文档(http://tomcat.apache.org/tomcat-6.0-doc/aio.html)找到答案。根据上面说明,应该Tomcat6应用nio只是用在处理发送、接收信息的时候用到,也就是说,tomcat6还是传统的多线程Servlet,我画了下面两个图来列出区别:
tomcat5:客户端连接到达->传统的SeverSocket.accept接收连接->从线程池取出一个线程->在该线程读取文本并且解析HTTP协议->在该线程生成ServletRequest、ServletResponse,取出请求的Servlet->在该线程执行这个Servlet->在该线程把ServletResponse的内容发送到客户端连接->关闭连接。
我以前理解的使用nio后的tomcat6:客户端连接到达->nio接收连接->nio使用轮询方式读取文本并且解析HTTP协议(单线程)->生成ServletRequest、ServletResponse,取出请求的Servlet->直接在本线程执行这个Servlet->把ServletResponse的内容发送到客户端连接->关闭连接。
实际的tomcat6:客户端连接到达->nio接收连接->nio使用轮询方式读取文本并且解析HTTP协议(单线程)->生成ServletRequest、ServletResponse,取出请求的Servlet->从线程池取出线程,并在该线程执行这个Servlet->把ServletResponse的内容发送到客户端连接->关闭连接。
从上图可以看出,BIO与NIO的不同,也导致进入客户端处理线程的时刻有所不同:tomcat5在接受连接后马上进入客户端线程,在客户端线程里解析HTTP协议,而tomcat6则是解析完HTTP协议后才进入多线程,另外,tomcat6也比5早脱离客户端线程的环境。
实际的tomcat6与我之前猜想的差别主要集中在如何处理servlet的问题上。实际上即使抛开ThreadLocal的问题,我之前理解tomcat6只使用一个线程处理的想法其实是行不同的。大家都有经验:servlet是基于BIO的,执行期间会存在堵塞的,例如读取文件、数据库操作等等。tomcat6使用了nio,但不可能要求servlet里面要使用nio,而一旦存在堵塞,效率自然会锐降。
所以,最终的结论当然是tomcat6的servlet里面,ThreadLocal照样可以使用,不存在冲突。
相关文章推荐
- myeclipse中间classpath
- struts 中default-action-ref的问题
- J2EE项目从Myeclipse中转换到eclipse中
- 自己习惯的Eclipse中设置注释模板
- Java String.startsWith()
- java在acm中的一些应用
- 记一次eclipse无法启动的排查过程
- Fail to create the java Virtual Machine
- Eclipse下运行Hadoop程序(以WordCount为例,使用Maven)
- AndroidStudio导入Eclipse生成的adt项目报unsatisfiedlinkerror错误
- Struts2接收参数的几种方式
- Java基础学习4(进制之间的转换)
- spring ioc原理
- Java字串加密
- spring mvc拦截器和<mvc:annotation-driven />的详解
- 【Java】qatools.properties
- (转载)Java基础知识总结
- dubbo+zookeeper+springmvc整合,小入门
- Java 内部类
- Java一步一脚—ArrayList使用