[網路]NIO學習筆記

mapw1993發表於2014-12-10

      最近為了一個開源專案,重新學習了下NIO的知識。在此分享下我的學習心得。

一、為什麼引入NIO

       NIOnew IO的簡稱,從1.4版本後引入。傳統的套接字(Socket)對於小規模的系統可以很好的執行,但是如果要同時處理上千個客戶機時,伺服器就需要產生上千個執行緒來等待使用者的輸入,這樣就產生了嚴重的資源浪費,那麼如何解決這個問題呢?NIO的提出正是解決了這個問題。

     NIO採用輪詢的方式來查詢哪個客戶機需要服務,從而提供服務,這也正是NIO中的SelectorChannel抽象的關鍵點。一個Channel例項代表了一個“可輪詢的”I/O目標。NIO另外一個重要特性是Buffer類。

 

二、通道(Channel)與套接字(Socket)的不同點

      通道需要通過呼叫靜態工廠方法來獲得例項:

	SocketChannel sc = SocketChannel.open();
	ServerSocketChannel ssc = ServerSocketChannel.open();


 

      Channel使用的不是流,而是緩衝區來傳送和讀取資料。Buffer類或其任何子類的例項都可以看作是一個定長度的JAVA基本資料型別元素序列。與流不同,緩衝區具有固定的、有限的容量。還有一點需要注意,Buffer例項化是通過呼叫allocate()方法。

                              ByteBuffer buffer = ByteBuffer.allocate(256);//根據實際情況來定緩衝區大小

     或者通過包裝一個已有的陣列來建立:

                            ByteBuffer buffer = ByteBuffer.wrap(byteArray);

 

       NIO的強大功能部分來自於channel的非阻塞特性。Socket的某些操作可能會無限期的阻塞。例如,對accept()方法的呼叫可能會因為等待一個客戶端連線而阻塞;對read()方法的呼叫可能會因為沒有資料可讀而阻塞,直到連線的另一端傳來新的資料。NIOchannel抽象的一個特徵就是可以通過配置它的阻塞行為,以實現非阻塞的通道。

                                cc.configureBlocking(false);

      在非阻塞式通道上呼叫一個方法總是會立即返回。這種呼叫的返回值指示了所請求的操作完成的程度。例如,在一個非阻塞式ServerSockerChannel上呼叫accept()方法,如果有連線請求在等待,則返回客戶端SocketChannel,否則返回null

 

三、Selector介紹

      Selector類可用於避免使用非阻塞式客戶端中很浪費資源的“忙等”方法。例如,考慮一個即時訊息傳送器。可能有上千個客戶端同時連線到了伺服器,但在任何時刻都只有非常少量的訊息需要讀取和分發。這就需要一種方法阻塞等待,直到至少有一個通道可以進行I/O操作,並指出是哪個通道。NIOSelector就實現了這個功能。一個Selector例項可以同時檢查一組通道的I/O狀態。

     那麼如何使用Selector來監聽呢?首先需要建立一個Selector例項(使用靜態工廠方法open())並將其註冊(一個通道可以註冊多個Selector例項)到想要監聽的通道上。如下:

                  Selector selector = Selector.open();
		DatagramChannel channel = DatagramChannel.open();
		channel.configureBlocking(false);
		channel.socket().bind(new InetSocketAddress(servPort));//繫結埠
                     channel.register(selector, SelectionKey.OP_READ);//註冊

最後,呼叫選擇器上的select方法。

int num = selector.select();//獲取

獲取可進行I/O操作的通道數量。如果在一個單獨的執行緒中,通過呼叫sleect()方法就能檢查多個通道是否準備I/O操作。如果經過一段時間後任然沒有通道準備好,則返回0,並允許程式繼續執行其它任務。

那麼如何在通道上對“感興趣的”I/O操作進行監聽呢?SelectorChannel之間的關聯由一個SelectionKey例項表示。SelectionKey維護了一個通道上感興趣的操作型別資訊,並將這些資訊存放在一個int型的點陣圖(bitmap)中,該int型資料的每一位都有相應的含義。

SelectionKey類中的常量定義了通道上可能感興趣的操作型別,每個這種常量都是隻有一位設定為1的位掩碼。在API文件中,我們查知:

OP_ACCEPT      16             10000

OP_CONNECT   8               01000

OP_WRITE          4               00100

OP_READ          1                00001

通過對OP_ACCEPT,OP_CONNECT,OP_READ以及OP_WRITE中適當的常量進行按位OR,我們可以構造一個位向量來指定一組操作。例如,一個包含了讀和寫的操作集合可由表示式(OP_READ|OP_WRITE)來指定。

通過Channel類中的validOps()方法,我們可以知道該通道可以監聽哪些I/O操作。如果定義了OP_READ|OP_WRITE,則validOps()方法的返回值為500101);定義了上述四種操作,則其值應該為2911101)。

下面筆者將實際使用興趣集中常見的錯誤進行下彙總(一般使用OP_WRITEOP_READ是不會發生錯誤的):

Error1:

Exception in thread "main" java.lang.IllegalArgumentException

at java.nio.channels.spi.AbstractSelectableChannel.register(Unknown Source)

at java.nio.channels.SelectableChannel.register(Unknown Source)

at UdpServer.main(UdpServer.java:20)

 

錯誤使用OP_CONNECT,其正確使用方法應該是先建立連線。正確的一個例子如下(參考自:http://blog.csdn.net/zhouhl_cn/article/details/6582420

TCPNIO實現網路上很多,筆者在網路發現很少有關於UDPNIO實現。下面給出筆者親測的NIO版本的UDP實現。

 

/**
 * 伺服器的實現
 */


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Iterator;

public class UdpServer {
	public static void main(String args[]) throws IOException {
		int servPort = 999;
		Selector selector = Selector.open();
		DatagramChannel channel = DatagramChannel.open();
		channel.configureBlocking(false);
		channel.socket().bind(new InetSocketAddress(servPort));
		channel.register(selector, SelectionKey.OP_READ);
		//channel.register(selector, 1);與上句子同效果
		while (true) {
			int num = selector.select();
			if (num == 0) {
				continue;
			}
			Iterator<SelectionKey> Keys = selector.selectedKeys().iterator();
			while (Keys.hasNext()) {
				SelectionKey k = Keys.next();
				
				if ((k.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
					DatagramChannel cc = (DatagramChannel) k.channel();
					// 非阻塞
					cc.configureBlocking(false);
					ByteBuffer buffer = ByteBuffer.allocateDirect(255);
					// 接收資料並讀到buffer中
					buffer.clear();
					channel.receive( buffer ) ;
					buffer.flip();
					byte b[] = new byte[buffer.remaining()];
					for (int i = 0; i < buffer.remaining(); i++) {
						b[i] = buffer.get(i);
					}
					Charset charset = Charset.forName("UTF-8");
					CharsetDecoder decoder = charset.newDecoder();
					CharBuffer charBuffer = decoder.decode(buffer);
					System.out.println("The imformation recevied:"+charBuffer.toString());
					Keys.remove(); //一定要remove
				}
			}

		}

	}

}


 

/**
 * 客戶端的實現
 */

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.Scanner;

public class UdpClient {
	@SuppressWarnings("resource")
	public static void main(String agrs[]) throws IOException {
		String s = null;
		while ((s =  new Scanner(System.in).nextLine()) != null) {
			DatagramChannel dc = null;
			dc = DatagramChannel.open();
			SocketAddress address = new InetSocketAddress("localhost", 999);
			ByteBuffer bb = ByteBuffer.allocate(255);
			byte[] b = new byte[130];
			b = s.getBytes();
			bb.clear();
			bb.put(b);
			bb.flip();
			dc.send(bb, address);
		}
	}
}


      如對NIO有興趣的朋友,可以參考下《JAVA TCP/IP Socket程式設計》(原書第二版),筆者也有部分地方是參考該本書的,歡迎各位同行的批評指正!

相關文章