文章原始出處:http://navigating.blogbus.com/logs/18024423.html
在Java1.4以前,Java的網路編程是只有阻塞方式的,在Java1.4以及之後,Java提供了非阻塞的網路編程API.從Java的發展來看,由於Java的快速發展,JVM效能的提升,涉足到服務端應用程式開發也越來越多,要求高效能的網路應用越來越多,這是Java推出非阻塞網路編程的最主要原因吧。
對我而言,以前的大部分服務端應用主要是搭建在應用伺服器之上,所以通訊這部分工作都是有應用伺服器來實現和管理的。這次由於通訊和協議,我們必須自己實現一個能處理大量並發用戶端的高效能平行處理的Java服務端程式。因此,選擇非阻塞的處理方式也是必然的。我們首先來看看阻塞的處理方式:
在阻塞的網路編程方式中,針對於每一個單獨的網路連接,都必須有一個線程對應的綁定該網路連接,進行網路位元組流的處理。下面是一段代碼:
public static void main(String[] args) {
try {
ServerSocket ssc = new ServerSocket(23456);
while (true) {
System.out.println("Enter Accept:");
Socket s = ssc.accept();
try {
(new Thread(new Worker(s))).start();
} catch (Exception e) {
// TODO
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static class Worker implements Runnable {
private Socket s;
private boolean running = true;;
public Worker(Socket s) {
this.s = s;
}
public void run() {
try {
InputStream is = s.getInputStream();
OutputStream os = s.getOutputStream();
while (running) {
byte[] b = this.readByLength(is, 1024);
this.process(b);
}
} catch (Throwable t) {
// TODO
t.printStackTrace();
}
}
private byte[] readByLength(InputStream is, int contLen) throws IOException {
byte[] b = new byte[contLen];
int off = 0;
int length = 0;
while ((length = is.read(b, off, contLen - off)) >= 0) {
off = +length;
if (off >= contLen) {
break;
}
}
return b;
}
private void process(byte[] b) {
}
}
在這段代碼中,我們看到有兩個阻塞的方法,是ServerSocket的accept()方法;和InputStream的read()方式。因此我們需要兩類型的線程分別進行處理。而且每一個阻塞方法所綁定的線程的生命週期和網路連接的生命週期是一致的。基於以上的原因,NIO應運而生,一方面,為每一個網路連接建立一個線程對應,同時每一個線程有大量的線程處於讀寫以外的空閑狀態,因此希望降低線程的數量,降低每個空閑狀態,提高單個線程的運行執行效率,實際上是在更加充分運用CPU的計算、運行能力(因為,如果有大量的鏈路存在,就存在大量的線程,而大量的線程都阻塞在read()或者write()方法,同時CPU又需要來回頻繁的在這些線程中間調度和切換,必然帶來大量的系統調用和資源競爭.);另外一方面希望提高網路IO和硬碟IO操作的效能。在NIO主要出現了三個新特性:
1.資料緩衝處理(ByteBuffer):由於作業系統和應用程式資料通訊的原始類型是byte,也是IO資料操作的基本單元,在NIO中,每一個基本的原生類型(boolean除外)都有Buffer的實現:CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、FloatBuffer和ByteBuffer,資料緩衝使得在IO操作中能夠連續的處理資料流。當前有兩種ByteBuffer,一種是Direct ByteBuffer,另外一種是NonDirect ByteBuffer;ByteBuffer是普通的Java對象,遵循Java堆中對象存在的規則;而Direct ByteBuffer是native代碼,它記憶體的分配不在Java的堆棧中,不受Java記憶體回收的影響,每一個Direct ByteBuffer都是直接分配的一塊連續的記憶體空間,也是NIO提高效能的重要辦法之一。另外資料緩衝有一個很重要的特點是,基於一個資料緩衝可以建立一個或者多個邏輯的視圖緩衝(View Buffer).比方說,通過View Buffer,可以將一個Byte類型的Buffer換作Int類型的緩衝;或者一個大的緩衝轉作很多小的Buffer。之所以稱為View Buffer是因為這個轉換僅僅是邏輯上,在物理上並沒有建立新的Buffer。這為我們操作Buffer帶來諸多方便。
2.非同步通道(Channel):Channel是一個與作業系統緊密結合的本地代碼較多的對象。通過Channel來實現網路編程的非阻塞操作,同時也是其與ByteBuffer、Socket有效結合充分利用非阻塞、ByteBuffer的特性的。在後面我們會看到具體的SocketChannel的用法。
3.有條件的選擇(Readiness Selection):大多數作業系統都有支援有條件選擇準備就緒IO通道的API,即能夠保證一個線程同時有效管理多個IO通道。在NIO中,由Selector(維護註冊進來的Channel和這些Channel的狀態)、SelectableChannel(能被Selector管理的Channel)和SelectionKey(SelectionKey標識Selector和SelectableChannel之間的映射關係,一旦一個Channel註冊到Selector中,就會返回一個SelectionKey對象。SelectionKey儲存了兩類狀態:對應的Channel註冊了哪些操作;對應的Channel的那些操作已經準備好了,可以進行相應的資料操作了)結合來實現這個功能的。
NIO的包中主要包含了這樣幾種抽象資料類型:
- Buffer:包含資料且用於讀寫的線形表結構。其中還提供了一個特殊類用於記憶體對應檔的I/O操作。
- Charset:它提供Unicode字串影射到位元組序列以及逆映射的操作。
- Channels:包含socket,file和pipe三種管道,都是全雙工系統的通道。
- Selector:多個非同步I/O操作集中到一個或多個線程中(可以被看成是Unix中select()函數的物件導向版本)。
Technorati 標籤: Java,NIO,TCP
NIO非阻塞的典型編程模型如下:
private Selector selector = null;
private static final int BUF_LENGTH = 1024;
public void start() throws IOException {
if (selector != null) {
selector = Selector.open();
}
ServerSocketChannel ssc = ServerSocketChannel.open();
ServerSocket serverSocket = ssc.socket();
serverSocket.bind(new InetSocketAddress(80));
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
try {
while (true) {
int nKeys = UnblockServer.this.selector.select();
if (nKeys > 0) {
Iterator it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel channel = server.accept();
if (channel == null) {
continue;
}
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
readDataFromSocket(key);
}
it.remove();
}
}
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
/**
* @param key
* @throws IOException
*/
private void readDataFromSocket(SelectionKey key) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(BUF_LENGTH);
SocketChannel sc = (SocketChannel) key.channel();
int readBytes = 0;
int ret;
try {
while ((ret = sc.read(buf.buf())) > 0) {
readBytes += ret;
}
} finally {
buf.flip();
}
// process buffer
// buf.clear();
}
從這段程式,我們基本可以瞭解到NIO網路編程的一些特點,建立一個SocketServer的方式已經發生了變化,需要指定非阻塞模式,需要建立一個Channel然後註冊到Selector中去,同樣,建立一個網路連接過程也是一樣的模式,然後就是有條件的選擇(Readiness Selection).這樣,我們的每一個線程只需要處理一類型的網路選擇。在代碼上,我們發現處理的方式和阻塞完全不一樣了,我們需要完全重新考慮如何編寫網路通訊的模組了:
1.持久串連的逾時問題(Timeout),因為API沒有直接的支援timeout的參數設定功能,因此需要我們自己實現一個這樣功能。
2.如何使用Selector,由於每一個Selector的處理能力是有限的,因此在大量連結和訊息處理過程中,需要考慮如何使用多個Selector.
3.在非阻塞情況下,read和write都不在是阻塞的,因此需要考慮如何完整的讀取到確定的訊息;如何在確保在網路環境不是很好的情況下,一定將資料寫進IO中。
4.如何應用ByteBuffer,本身大量建立ByteBuffer就是很耗資源的;如何有效使用ByteBuffer?同時ByteBuffer的操作需要仔細考慮,因為有position()、mark()、limit()、capacity等方法。
5.由於每一個線程在處理網路連接的時候,面對的都是一系列的網路連接,需要考慮如何更好的使用、調度多線程。在對訊息的處理上,也需要保證一定的順序,比方說,登入訊息最先到達,只有登入訊息處理之後,才有可能去處理同一個鏈路上的其他類型的訊息。
6.在網路編程中可能出現的記憶體流失問題。
在NIO的接入處理架構上,大約有兩種並發線程:
1.Selector線程,每一個Selector單獨佔用一個線程,由於每一個Selector的處理能力是有限的,因此需要多個Selector並行工作。
2.對於每一條處於Ready狀態的鏈路,需要線程對於相應的訊息進行處理;對於這一類型的訊息,需要並發線程共同工作進行處理。在這個過程中,不斷可能需要訊息的完整性;還要涉及到,每個鏈路上的訊息可能有時序,因此在處理上,也可能要求相應的時序性。
當前社區的開源NIO架構實現有MINA、Grizzly、NIO framework、QuickServer、xSocket等,其中MINA和Grizzly最為活躍,而且代碼的品質也很高。他們倆在實現的方法上也完全大不一樣。(大部分Java的開原始伺服器都已經用NIO重寫了網路部分。 )
不管是我們自己實現NIO的網路編程架構,還是基於MINA、Grizzly等這樣的開源架構進行開發,都需要理解確定的瞭解NIO帶來的益處和NIO編程需要解決的眾多類型的問題。充足、有效單元測試,是我們寫好NIO代碼的好助手:)
Resource:
http://www.cis.temple.edu/~ingargio/cis307/readings/unix4.html#states
《Java NIO》
《GlassFish--開源的Java EE應用伺服器》