JAVA的BIO,NIO,AIO

一:名词解释

NIO

nio 是 java New IO 的简称,在 jdk1.4 里提供的新 api 。 Sun 官方标榜的特性如下:
– 为所有的原始类型提供 (Buffer) 缓存支持。
– 字符集编码解码解决方案。
– Channel :一个新的原始 I/O 抽象。
– 支持锁和内存映射文件的文件访问接口。
– 提供多路 (non-bloking) 非阻塞式的高伸缩性网络 I/O

来自:http://blog.csdn.net/kobejayandy/article/details/11543891

示例一:

package com.vin.nio;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class CopyFile
{
	public static void main(String[] args) throws Exception
	{
		File source=new File("E:/test/source.txt");
		File destination=new File("E:/test/destination.txt");
		if( !destination.exists() ) destination.createNewFile();
		//获取源文件和目标文件的输入输出流
		FileInputStream fileInputStream=new FileInputStream(source);
		FileOutputStream fileOutputStream=new FileOutputStream(destination);
		//获取输入输出通道
		FileChannel filein=fileInputStream.getChannel();
		FileChannel fileout=fileOutputStream.getChannel();
		//创建缓冲区
		ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
		//判断是否是到了输出文件的末尾
		while( filein.read(byteBuffer)!=-1 )
		{
			//flip方法是让缓冲区数据写入另一个通道
			byteBuffer.flip();
			fileout.write(byteBuffer);
			System.out.println(byteBuffer);
			System.out.println(byteBuffer.position());
			//清除缓冲区,使它可以接受读入的数据
			byteBuffer.clear();
		}
		filein.close();
		fileout.close();
		fileInputStream.close();
		fileOutputStream.close();
	}
}

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来处理,one connection one thread。无论连接是否有真正数据请求,都需要独占一个thread。

可以通过设立Socket池来一定程度上解决问题,但是使用池需要注意的问题是:1. 竞争等待比较多。 2. 需要控制好超时时间。

TCP/IP+NIO
使用Channel(SocketChannel和ServerSocketChannel)和Selector。

Server端通常由一个thread来监听connect事件,另外多个thread来监听读写事件。这样做的好处是这些连接只有在真是请求的时候才会创建thread来处理,one request one thread。这种方式在server端需要支持大量连接但这些连接同时发送请求的峰值不会很多的时候十分有效。

UDP/IP+BIO
DatagramSocket和DatagramPacket。DatagramSocket负责监听端口以及读写数据,DatagramPacket作为数据流对象进行传输。

UDP/IP是无连接的,无法进行双向通信,除非双方都成为UDP Server。

UDP/IP+NIO
通过DatagramChannel和ByteBuffer实现。DatagramChannel负责端口监听及读写。ByteBuffer负责数据流传输。

如果要将消息发送到多台机器,如果为每个目标机器都建立一个连接的话,会有很大的网络流量压力。这时候可以使用基于UDP/IP的Multicast协议传输,Java中可以通过MulticastSocket和DatagramPacket来实现。

Multicast一般多用于多台机器的状态同步,比如JGroups。SRM, URGCP都是Multicast的实现方式。eBay就采用SRM来实现将数据从主数据库同步到各个搜索节点机器。
Java aio(异步网络IO)初探

按照《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。

Java nio 2.0的主要改进就是引入了异步IO(包括文件和网络),这里主要介绍下异步网络IO API的使用以及框架的设计,以TCP服务端为例。首先看下为了支持AIO引入的新的类和接口:

java.nio.channels.AsynchronousChannel
标记一个channel支持异步IO操作。

java.nio.channels.AsynchronousServerSocketChannel
ServerSocket的aio版本,创建TCP服务端,绑定地址,监听端口等。

java.nio.channels.AsynchronousSocketChannel
面向流的异步socket channel,表示一个连接。

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允许绑定不同的线程池,通过三个静态方法来创建:

1public static AsynchronousChannelGroup withFixedThreadPool(int nThreads,  
2                                                              ThreadFactory threadFactory)  
3       throws IOException  
4  
5public static AsynchronousChannelGroup withCachedThreadPool(ExecutorService executor,  
6                                                               int initialSize)  
7  
8public static AsynchronousChannelGroup withThreadPool(ExecutorService executor)  
9       throws IOException  
10 

需要根据具体应用相应调整,从框架角度出发,需要暴露这样的配置选项给用户。

在介绍完了aio引入的TCP的主要接口和类之后,我们来设想下一个aio框架应该怎么设计。参考非阻塞nio框架的设计,一般都是采用Reactor模式,Reacot负责事件的注册、select、事件的派发;相应地,异步IO有个Proactor模式,Proactor负责 CompletionHandler的派发,查看一个典型的IO写操作的流程来看两者的区别:

Reactor: send(msg) -> 消息队列是否为空,如果为空 -> 向Reactor注册OP_WRITE,然后返回 -> Reactor select -> 触发Writable,通知用户线程去处理 ->先注销Writable(很多人遇到的cpu 100%的问题就在于没有注销),处理Writeable,如果没有完全写入,继续注册OP_WRITE。注意到,写入的工作还是用户线程在处理。
Proactor: send(msg) -> 消息队列是否为空,如果为空,发起read异步调用,并注册CompletionHandler,然后返回。 -> 操作系统负责将你的消息写入,并返回结果(写入的字节数)给Proactor -> Proactor派发CompletionHandler。可见,写入的工作是操作系统在处理,无需用户线程参与。事实上在aio的API 中,AsynchronousChannelGroup就扮演了Proactor的角色。

CompletionHandler有三个方法,分别对应于处理成功、失败、被取消(通过返回的Future)情况下的回调处理:

1public interface CompletionHandler<V,A> {  
2  
3     void completed(V result, A attachment);  
4  
5    void failed(Throwable exc, A attachment);  
6  
7     
8    void cancelled(A attachment);  
9}  

其中的泛型参数V表示IO调用的结果,而A是发起调用时传入的attchment。

在初步介绍完aio引入的类和接口后,我们看看一个典型的tcp服务端是怎么启动的,怎么接受连接并处理读和写,这里引用的代码都是yanf4j 的aio分支中的代码,可以从svn checkout,svn地址: http://yanf4j.googlecode.com/svn/branches/yanf4j-aio

第一步,创建一个AsynchronousServerSocketChannel,创建之前先创建一个 AsynchronousChannelGroup,上文提到AsynchronousServerSocketChannel可以绑定一个 AsynchronousChannelGroup,那么通过这个AsynchronousServerSocketChannel建立的连接都将同属于一个AsynchronousChannelGroup并共享资源:

1this.asynchronousChannelGroup = AsynchronousChannelGroup  
2                    .withCachedThreadPool(Executors.newCachedThreadPool(),  
3                            this.threadPoolSize);  

然后初始化一个AsynchronousServerSocketChannel,通过open方法:

1this.serverSocketChannel = AsynchronousServerSocketChannel  
2                .open(this.asynchronousChannelGroup);  
3 

通过nio 2.0引入的SocketOption类设置一些TCP选项:

1this.serverSocketChannel  
2                    .setOption(  
3                            StandardSocketOption.SO_REUSEADDR,true);  
4this.serverSocketChannel  
5                    .setOption(  
6                            StandardSocketOption.SO_RCVBUF,16*1024);  
7 

绑定本地地址:

1this.serverSocketChannel  
2                    .bind(new InetSocketAddress("localhost",8080), 100);  

其中的100用于指定等待连接的队列大小(backlog)。完了吗?还没有,最重要的监听工作还没开始,监听端口是为了等待连接上来以便accept产生一个AsynchronousSocketChannel来表示一个新建立的连接,因此需要发起一个accept调用,调用是异步的,操作系统将在连接建立后,将最后的结果——AsynchronousSocketChannel返回给你:

1public void pendingAccept() {  
2        if (this.started && this.serverSocketChannel.isOpen()) {  
3            this.acceptFuture = this.serverSocketChannel.accept(null,  
4                    new AcceptCompletionHandler());  
5  
6        } else {  
7            throw new IllegalStateException("Controller has been closed");  
8        }  
9    }  
10 

注意,重复的accept调用将会抛出PendingAcceptException,后文提到的read和write也是如此。accept方法的第一个参数是你想传给CompletionHandler的attchment,第二个参数就是注册的用于回调的CompletionHandler,最后返回结果Future。你可以对future做处理,这里采用更推荐的方式就是注册一个CompletionHandler。那么accept的CompletionHandler中做些什么工作呢?显然一个赤裸裸的 AsynchronousSocketChannel是不够的,我们需要将它封装成session,一个session表示一个连接(mina里就叫 IoSession了),里面带了一个缓冲的消息队列以及一些其他资源等。在连接建立后,除非你的服务器只准备接受一个连接,不然你需要在后面继续调用pendingAccept来发起另一个accept请求:

1private final class AcceptCompletionHandler implements  
2            CompletionHandler<AsynchronousSocketChannel, Object> {  
3  
4        @Override  
5        public void cancelled(Object attachment) {  
6            logger.warn("Accept operation was canceled");  
7        }  
8  
9        @Override  
10        public void completed(AsynchronousSocketChannel socketChannel,  
11                Object attachment) {  
12            try {  
13                logger.debug("Accept connection from "  
14                        + socketChannel.getRemoteAddress());  
15                configureChannel(socketChannel);  
16                AioSessionConfig sessionConfig = buildSessionConfig(socketChannel);  
17                Session session = new AioTCPSession(sessionConfig,  
18                        AioTCPController.this.configuration  
19                                .getSessionReadBufferSize(),  
20                        AioTCPController.this.sessionTimeout);  
21                session.start();  
22                registerSession(session);  
23            } catch (Exception e) {  
24                e.printStackTrace();  
25                logger.error("Accept error", e);  
26                notifyException(e);  
27            } finally {  
28                <strong>pendingAccept</strong>();  
29            }  
30        }  
31  
32        @Override  
33        public void failed(Throwable exc, Object attachment) {  
34            logger.error("Accept error", exc);  
35            try {  
36                notifyException(exc);  
37            } finally {  
38                <strong>pendingAccept</strong>();  
39            }  
40        }  
41    }  
42 

注意到了吧,我们在failed和completed方法中在最后都调用了pendingAccept来继续发起accept调用,等待新的连接上来。有的同学可能要说了,这样搞是不是递归调用,会不会堆栈溢出?实际上不会,因为发起accept调用的线程与CompletionHandler回调的线程并非同一个,不是一个上下文中,两者之间没有耦合关系。要注意到,CompletionHandler的回调共用的是 AsynchronousChannelGroup绑定的线程池,因此千万别在CompletionHandler回调方法中调用阻塞或者长时间的操作,例如sleep,回调方法最好能支持超时,防止线程池耗尽。

连接建立后,怎么读和写呢?回忆下在nonblocking nio框架中,连接建立后的第一件事是干什么?注册OP_READ事件等待socket可读。异步IO也同样如此,连接建立后马上发起一个异步read调用,等待socket可读,这个是Session.start方法中所做的事情:

1public class AioTCPSession {  
2    protected void start0() {  
3        pendingRead();  
4    }  
5  
6    protected final void pendingRead() {  
7        if (!isClosed() && this.asynchronousSocketChannel.isOpen()) {  
8            if (!this.readBuffer.hasRemaining()) {  
9                this.readBuffer = ByteBufferUtils  
10                        .increaseBufferCapatity(this.readBuffer);  
11            }  
12            this.readFuture = this.asynchronousSocketChannel.read(  
13                    this.readBuffer, this, this.readCompletionHandler);  
14        } else {  
15            throw new IllegalStateException(  
16                    "Session Or Channel has been closed");  
17        }  
18    }  
19     
20}  
21 

AsynchronousSocketChannel的read调用与AsynchronousServerSocketChannel的accept调用类似,同样是非阻塞的,返回结果也是一个Future,但是写的结果是整数,表示写入了多少字节,因此read调用返回的是 Future,方法的第一个参数是读的缓冲区,操作系统将IO读到数据拷贝到这个缓冲区,第二个参数是传递给 CompletionHandler的attchment,第三个参数就是注册的用于回调的CompletionHandler。这里保存了read的结果Future,这是为了在关闭连接的时候能够主动取消调用,accept也是如此。现在可以看看read的CompletionHandler的实现:

1public final class ReadCompletionHandler implements  
2        CompletionHandler<Integer, AbstractAioSession> {  
3  
4    private static final Logger log = LoggerFactory  
5            .getLogger(ReadCompletionHandler.class);  
6    protected final AioTCPController controller;  
7  
8    public ReadCompletionHandler(AioTCPController controller) {  
9        this.controller = controller;  
10    }  
11  
12    @Override  
13    public void cancelled(AbstractAioSession session) {  
14        log.warn("Session(" + session.getRemoteSocketAddress()  
15                + ") read operation was canceled");  
16    }  
17  
18    @Override  
19    public void completed(Integer result, AbstractAioSession session) {  
20        if (log.isDebugEnabled())  
21            log.debug("Session(" + session.getRemoteSocketAddress()  
22                    + ") read +" + result + " bytes");  
23        if (result < 0) {  
24            session.close();  
25            return;  
26        }  
27        try {  
28            if (result > 0) {  
29                session.updateTimeStamp();  
30                session.getReadBuffer().flip();  
31                session.decode();  
32                session.getReadBuffer().compact();  
33            }  
34        } finally {  
35            try {  
36                session.pendingRead();  
37            } catch (IOException e) {  
38                session.onException(e);  
39                session.close();  
40            }  
41        }  
42        controller.checkSessionTimeout();  
43    }  
44  
45    @Override  
46    public void failed(Throwable exc, AbstractAioSession session) {  
47        log.error("Session read error", exc);  
48        session.onException(exc);  
49        session.close();  
50    }  
51  
52}  
53 

如果IO读失败,会返回失败产生的异常,这种情况下我们就主动关闭连接,通过session.close()方法,这个方法干了两件事情:关闭channel和取消read调用:

1if (null != this.readFuture) {  
2            this.readFuture.cancel(true);  
3        }  
4this.asynchronousSocketChannel.close();  
5 

在读成功的情况下,我们还需要判断结果result是否小于0,如果小于0就表示对端关闭了,这种情况下我们也主动关闭连接并返回。如果读到一定字节,也就是result大于0的情况下,我们就尝试从读缓冲区中decode出消息,并派发给业务处理器的回调方法,最终通过pendingRead继续发起read调用等待socket的下一次可读。可见,我们并不需要自己去调用channel来进行IO读,而是操作系统帮你直接读到了缓冲区,然后给你一个结果表示读入了多少字节,你处理这个结果即可。而nonblocking IO框架中,是reactor通知用户线程socket可读了,然后用户线程自己去调用read进行实际读操作。这里还有个需要注意的地方,就是decode出来的消息的派发给业务处理器工作最好交给一个线程池来处理,避免阻塞group绑定的线程池。

IO写的操作与此类似,不过通常写的话我们会在session中关联一个缓冲队列来处理,没有完全写入或者等待写入的消息都存放在队列中,队列为空的情况下发起write调用:

1protected void write0(WriteMessage message) {  
2      boolean needWrite = false;  
3      synchronized (this.writeQueue) {  
4          needWrite = this.writeQueue.isEmpty();  
5          this.writeQueue.offer(message);  
6      }  
7      if (needWrite) {  
8          pendingWrite(message);  
9      }  
10  }  
11  
12  protected final void pendingWrite(WriteMessage message) {  
13      message = preprocessWriteMessage(message);  
14      if (!isClosed() && this.asynchronousSocketChannel.isOpen()) {  
15          this.asynchronousSocketChannel.write(message.getWriteBuffer(),  
16                  this, this.writeCompletionHandler);  
17      } else {  
18          throw new IllegalStateException(  
19                  "Session Or Channel has been closed");  
20      }  
21  }  
22 

write调用返回的结果与read一样是一个Future,而write的CompletionHandler处理的核心逻辑大概是这样:

1@Override  
2    public void completed(Integer result, AbstractAioSession session) {  
3        if (log.isDebugEnabled())  
4            log.debug("Session(" + session.getRemoteSocketAddress()  
5                    + ") writen " + result + " bytes");  
6                  
7        WriteMessage writeMessage;  
8        Queue<WriteMessage> writeQueue = session.getWriteQueue();  
9        synchronized (writeQueue) {  
10            writeMessage = writeQueue.peek();  
11            if (writeMessage.getWriteBuffer() == null  
12                    || !writeMessage.getWriteBuffer().hasRemaining()) {  
13                writeQueue.remove();  
14                if (writeMessage.getWriteFuture() != null) {  
15                    writeMessage.getWriteFuture().setResult(Boolean.TRUE);  
16                }  
17                try {  
18                    session.getHandler().onMessageSent(session,  
19                            writeMessage.getMessage());  
20                } catch (Exception e) {  
21                    session.onException(e);  
22                }  
23                writeMessage = writeQueue.peek();  
24            }  
25        }  
26        if (writeMessage != null) {  
27            try {  
28                session.pendingWrite(writeMessage);  
29            } catch (IOException e) {  
30                session.onException(e);  
31                session.close();  
32            }  
33        }  
34    }  
35 

compete方法中的result就是实际写入的字节数,然后我们判断消息的缓冲区是否还有剩余,如果没有就将消息从队列中移除,如果队列中还有消息,那么继续发起write调用。

重复一下,这里引用的代码都是yanf4j aio分支中的源码,感兴趣的朋友可以直接check out出来看看: http://yanf4j.googlecode.com/svn/branches/yanf4j-aio。
在引入了aio之后,java对于网络层的支持已经非常完善,该有的都有了,java也已经成为服务器开发的首选语言之一。java的弱项在于对内存的管理上,由于这一切都交给了GC,因此在高性能的网络服务器上还是Cpp的天下。java这种单一堆模型比之erlang的进程内堆模型还是有差距,很难做到高效的垃圾回收和细粒度的内存管理。

这里仅仅是介绍了aio开发的核心流程,对于一个网络框架来说,还需要考虑超时的处理、缓冲buffer的处理、业务层和网络层的切分、可扩展性、性能的可调性以及一定的通用性要求。

tomcat的配置

Tomcat是一个小型的轻量级应用服务器,也是JavaEE开发人员最常用的服务器之一。不过,许多开发人员不知道的是,Tomcat Connector(Tomcat连接器)有bionioapr三种运行模式,那么这三种运行模式有什么区别呢,我们又如何修改Tomcat Connector的运行模式来提高Tomcat的运行性能呢?

下面,我们先大致了解Tomcat Connector的三种运行模式。

bio

bio(blocking I/O),顾名思义,即阻塞式I/O操作,表示Tomcat使用的是传统的Java I/O操作(即java.io包及其子包)。Tomcat在默认情况下,就是以bio模式运行的。遗憾的是,就一般而言,bio模式是三种运行模式中性能最低的一种。我们可以通过Tomcat Manager来查看服务器的当前状态。【点击这里可以查看Tomcat Manager用户配置的相关信息】

tomcat-status-bio.jpg

nio

nio(new I/O),是Java SE 1.4及后续版本提供的一种新的I/O操作方式(即java.nio包及其子包)。Java nio是一个基于缓冲区、并能提供非阻塞I/O操作的Java API,因此nio也被看成是non-blocking I/O的缩写。它拥有比传统I/O操作(bio)更好的并发运行性能。要让Tomcat以nio模式来运行也比较简单,我们只需要在Tomcat安装目录/conf/server.xml文件中将如下配置:

<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />

中的protocol属性值改为org.apache.coyote.http11.Http11NioProtocol即可:

<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443" />

此时,我们就可以在Tomcat Manager中看到当前服务器状态页面的HTTP协议的Connector运行模式已经从http-bio-8080变成了http-nio-8080

tomcat-status-nio.jpg

apr

apr(Apache Portable Runtime/Apache可移植运行时),是Apache HTTP服务器的支持库。你可以简单地理解为,Tomcat将以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作,从而大大地提高Tomcat对静态文件的处理性能。Tomcat apr也是在Tomcat上运行高并发应用的首选模式。如果我们的Tomcat不是在apr模式下运行,在启动Tomcat的时候,我们可以在日志信息中看到类似如下信息:

2013-8-6 16:17:49 org.apache.catalina.core.AprLifecycleListener init
信息: The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: xxx/xxx(这里是路径信息)

Tomcat apr运行模式的配置是三种运行模式之中相对比较麻烦的一种。据官方文档所述,Tomcat apr需要以下三个组件的支持:

  • APR library[APR库]
  • JNI wrappers for APR used by Tomcat (libtcnative)[简单地说,如果是在Windows操作系统上,就是一个名为tcnative-1.dll的动态链接库文件]
  • OpenSSL libraries[OpenSSL库]

此外,与配置nio运行模式一样,也需要将对应的Connector节点的protocol属性值改为org.apache.coyote.http11.Http11AprProtocol。不过,上述繁琐的操作都是Tomcat 7.0.30之前的版本才需要这样配置,从Tomcat 7.0.30版本开始,Tomcat已经自带了tcnative-1.dll等文件,并且默认就是在Tomcat apr模式下运行,因此我们只需要下载最新版本的Tomcat直接使用即可。

tomcat-apr-status

此外,即使不使用Tomcat Manager,我们也可以区分出Tomcat当前的运行模式。如果以不同的Connector模式启动,在Tomcat的启动日志信息中一般会包含类似如下的不同内容,我们只需要根据这些信息即可判断出当前Tomcat的运行模式:

bio
信息: Starting ProtocolHandler ["http-bio-8080"]2013-8-6 16:17:50 org.apache.coyote.AbstractProtocol start
nio
信息: Starting ProtocolHandler ["http-nio-8080"]2013-8-6 16:59:53 org.apache.coyote.AbstractProtocol start
apr
信息: Starting ProtocolHandler ["http-apr-8080"]2013-8-6 17:03:07 org.apache.coyote.AbstractProtocol start
Tomcat 6.x版本从6.0.32开始就默认支持apr。
Tomcat 7.x版本从7.0.30开始就默认支持apr。
因此,如果读者使用的Tomcat版本比较陈旧的话,强烈建议升级到最新的稳定版本。

tomcat 配置说明 - 文章2

tomcat的运行模式有3种.修改他们的运行模式.3种模式的运行是否成功,可以看他的启动控制台,或者启动日志.或者登录他们的默认页面http://localhost:8080/查看其中的服务器状态。

1)bio

默认的模式,性能非常低下,没有经过任何优化处理和支持.

2)nio

利用java的异步io护理技术,no blocking IO技术.

想运行在该模式下,直接修改server.xml里的Connector节点,修改protocol为

 <Connector port="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

 

Tomcat 6.X实现了JCP的Servlet 2.5和JSP2.1的规范,并且包括其它很多有用的功能,使它成为开发
和部署web应用和web服务的坚实平台。
       NIO (No-blocking I/O)从JDK 1.4起,NIO API作为一个基于缓冲区,并能提供非阻塞I/O操作的API
被引入。


       作为开源web服务器的java实现,tomcat几乎就是web开发者开发、测试的首选,有很多其他商业服务
器的开发者也会优先选择tomcat作为开发时候使用,而在部署的时候,把应用发布在商业服务器上。也有
许多商业应用部署在tomcat上,tomcat承载着其核心的应用。但是很多开发者很迷惑,为什么在自己的应
用里使用tomcat作为平台的时候,而并发用户超过一定数量,服务器就变的非常繁忙,而且很快就出现了
connection refuse的错误。但是很多商业应用部署在tomcat上运行却安然无恙。

      其中有个很大的原因就是,配置良好的tomcat都会使用APR(Apache Portable Runtime),APR是
Apache HTTP Server2.x的核心,它是高度可移植的本地库,它使用高性能的UXIN I/O操作,低性能的
java io操作,但是APR对很多Java开发者而言可能稍稍有点难度,在很多OS平台上,你可能需要重新编
译APR。但是从Tomcat6.0以后, Java开发者很容易就可以是用NIO的技术来提升tomcat的并发处理能力。
但是为什么NIO可以提升tomcat的并发处理能力呢,我们先来看一下java 传统io与 java NIO的差别。
    
Java 传统的IO操作都是阻塞式的(blocking I/O), 如果有socket的编程基础,你会接触过堵塞socket和
非堵塞socket,堵塞socket就是在accept、read、write等IO操作的的时候,如果没有可用符合条件的资
源,不马上返回,一直等待直到有资源为止。而非堵塞socket则是在执行select的时候,当没有资源的时
候堵塞,当有符合资源的时候,返回一个信号,然后程序就可以执行accept、read、write等操作,一般来
说,如果使用堵塞socket,通常我们通常开一个线程accept socket,当读完这次socket请求的时候,开一
个单独的线程处理这个socket请求;如果使用非堵塞socket,通常是只有一个线程,一开始是select状,
当有信号的时候可以通过 可以通过多路复用(Multiplexing)技术传递给一个指定的线程池来处理请求,然
后原来的线程继续select状态。 最简单的多路复用技术可以通过java管道(Pipe)来实现。换句话说,如果
客户端的并发请求很大的时候,我们可以使用少于客户端并发请求的线程数来处理这些请求,而这些来不
及立即处理的请求会被阻塞在java管道或者队列里面,等待线程池的处理。请求 听起来很复杂,在这个架
构当道的java 世界里,现在已经有很多优秀的NIO的架构方便开发者使用,比如Grizzly,Apache Mina等
等,如果你对如何编写高性能的网络服务器有兴趣,你可以研读这些源代码。

      简单说一下,在web服务器上阻塞IO(BIO)与NIO一个比较重要的不同是,我们使用BIO的时候往往会
为每一个web请求引入多线程,每个web请求一个单独的线程,所以并发量一旦上去了,线程数就上去
了,CPU就忙着线程切换,所以BIO不合适高吞吐量、高可伸缩的web服务器;而NIO则是使用单线程(单
个CPU)或者只使用少量的多线程(多CPU)来接受Socket,而由线程池来处理堵塞在pipe或者队列里的请
求.这样的话,只要OS可以接受TCP的连接,web服务器就可以处理该请求。大大提高了web服务器的可
伸缩性。

    我们来看一下配置,你只需要在server.xml里把 HTTP Connector做如下更改,

    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    改为
    <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
               connectionTimeout="20000"
               redirectPort="8443" />

然后启动服务器,你会看到org.apache.coyote.http11.Http11NioProtocol start的信息,表示NIO已经启动。其他的配置请参考官方配置文档。

Enjoy it.

最后贴上官方文档上对tomcat的三种Connector的方式做一个简单比较,
   

Java Blocking Connector       Java Nio Blocking Connector       APR Connector

Classname         Http11Protocol                  Http11NioProtocol         Http11AprProtocol

Tomcat Version   3.x 4.x 5.x 6.x                       6.x                     5.5.x 6.x

Support Polling         NO                             YES                        YES

Polling Size           N/A                   Unlimited - Restricted by mem        Unlimited

Read HTTP Request     Blocking                     Blocking                       Blocking

Read HTTP Body        Blocking                     Blocking                       Blocking

Write HTTP Response   Blocking                     Blocking                       Blocking

SSL Support           Java SSL                     Java SSL                       OpenSSL

SSL Handshake         Blocking                     Non blocking                   Blocking

Max Connections       maxThreads                   See polling size               See polling size
 
 
如果读者有socket的编程基础,应该会接触过堵塞socket和非堵塞socket,堵塞socket就是在accept、read、write等IO操作的的时候,如果没有可用符合条件的资源,不马上返回,一直等待直到有资源为止。而非堵塞socket则是在执行select的时候,当没有资源的时候堵塞,当有符合资源的时候,返回一个信号,然后程序就可以执行accept、read、write等操作,这个时候,这些操作是马上完成,并且马上返回。而windows的winsock则有所不同,可以绑定到一个EventHandle里,也可以绑定到一个HWND里,当有资源到达时,发出事件,这时执行的io操作也是马上完成、马上返回的。一般来说,如果使用堵塞socket,通常我们时开一个线程accept socket,当有socket链接的时候,开一个单独的线程处理这个socket;如果使用非堵塞socket,通常是只有一个线程,一开始是select状态,当有信号的时候马上处理,然后继续select状态。 
   
  按照大多数人的说法,堵塞socket比非堵塞socket的性能要好。不过也有小部分人并不是这样认为的,例如Indy项目(Delphi一个比较出色的网络包),它就是使用多线程+堵塞socket模式的。另外,堵塞socket比非堵塞socket容易理解,符合一般人的思维,编程相对比较容易。 
   
  nio其实也是类似上面的情况。在JDK1.4,sun公司大范围提升Java的性能,其中NIO就是其中一项。Java的IO操作集中在java.io这个包中,是基于流的阻塞API(即BIO,Block IO)。对于大多数应用来说,这样的API使用很方便,然而,一些对性能要求较高的应用,尤其是服务端应用,往往需要一个更为有效的方式来处理IO。从JDK 1.4起,NIO API作为一个基于缓冲区,并能提供非阻塞O操作的API(即NIO,non-blocking IO)被引入。 
   
  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照样可以使用,不存在冲突。 



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章