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照樣可以使用,不存在衝突。 



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