筆記: Java NIO套接字通訊

這篇文章是一個月前看一本Java7的書寫的筆記,以爲蠻重要,就把他加在博客裏面。多廢話一句,國產技術書害人,看的時候這部分書徹底沒說清楚,最後仍是查API文檔和本身寫Demo搞了一夜才基本理解過去了,筆記裏面添加了不少本身的理解,算原創吧。java

一 套接字通道服務器

1. 阻塞式套接字通道異步

    與Socket和ServerSocket對應,NIO提供了SocketChannel和ServerSocketChannel對應,這兩種通道同時支持通常的阻塞模式和更高效的非阻塞模式。工具

    客戶端經過SocketChannel.open()方法打開一個Socket通道,若是此時提供了SocketAddress參數,則會自動開始鏈接,不然須要主動調用connect()方法鏈接,建立鏈接後,能夠像通常的Channel同樣的用Buffer進行讀寫,這都是阻塞模式的。測試

    服務器端經過ServerSocketChannel.open()建立,並使用bind()方法綁定到一個監聽地址上,最後調用accept()方法阻塞等待客戶端鏈接。當客戶端鏈接後會返回一個SocketChannel以實現與客戶端的讀寫交互。線程

    總的來講,阻塞模式便是net包I/O的翻版,只是採用Channel和Buffer實現而已。rest

2.多路複用套接字通道(Selector實現的非阻塞式IO)code

    套接字通道多路複用的思想是建立一個Selector,將多個通道對它進行註冊,當套接字有關注的事件發生時,能夠選出這個通道進行操做。server

    服務器端的代碼以下,相關說明就帶在註釋裏了:對象

// 建立一個選擇器,可用close()關閉,isOpen()表示是否處於打開狀態,他不隸屬於當前線程
Selector selector = Selector.open();
// 建立ServerSocketChannel,並把它綁定到指定端口上
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress("127.0.0.1", 7777));
// 設置爲非阻塞模式, 這個很是重要
server.configureBlocking(false);
// 在選擇器裏面註冊關注這個服務器套接字通道的accept事件
// ServerSocketChannel只有OP_ACCEPT可用,OP_CONNECT,OP_READ,OP_WRITE用於SocketChannel
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
	// 測試等待事件發生,分爲直接返回的selectNow()和阻塞等待的select(),另外也可加一個參數表示阻塞超時
	// 中止阻塞的方法有兩種: 中斷線程和selector.wakeup(),有事件發生時,會自動的wakeup()
	// 方法返回爲select出的事件數(參見後面的註釋有說明這個值爲何可能爲0).
	// 另外務必注意一個問題是,當selector被select()阻塞時,其餘的線程調用同一個selector的register也會被阻塞到select返回爲止
	// select操做會把發生關注事件的Key加入到selectionKeys中(只管加無論減)
	if (selector.select() == 0) { //
		continue;
	}

	// 獲取發生了關注時間的Key集合,每一個SelectionKey對應了註冊的一個通道
	Set<SelectionKey> keys = selector.selectedKeys();
	// 多說一句selector.keys()返回全部的SelectionKey(包括沒有發生事件的)
	for (SelectionKey key : keys) {
		// OP_ACCEPT 這個只有ServerSocketChannel纔有可能觸發
		if (key.isAcceptable()) {
			// 獲得與客戶端的套接字通道
			SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();
			// 一樣設置爲非阻塞模式
			channel.configureBlocking(false);
			// 一樣將於客戶端的通道在selector上註冊,OP_READ對應可讀事件(對方有寫入數據),能夠經過key獲取關聯的選擇器
			channel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(1024));
		}
		// OP_READ 有數據可讀
		if (key.isReadable()) {
			SocketChannel channel = (SocketChannel) key.channel();
			// 獲得附件,就是上面SocketChannel進行register的時候的第三個參數,可爲隨意Object
			ByteBuffer buffer = (ByteBuffer) key.attachment();
			// 讀數據 這裏就簡單寫一下,實際上應該仍是循環讀取到讀不出來爲止的
			channel.read(buffer);
			// 改變自身關注事件,能夠用位或操做|組合時間
			key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
		}
		// OP_WRITE 可寫狀態 這個狀態一般老是觸發的,因此只在須要寫操做時才進行關注
		if (key.isWritable()) {
			// 寫數據掠過,能夠自建buffer,也可用附件對象(看狀況),注意buffer寫入後須要flip
			// ......
			// 寫完就吧寫狀態關注去掉,不然會一直觸發寫事件
			key.interestOps(SelectionKey.OP_READ);
		}

		// 因爲select操做只管對selectedKeys進行添加,因此key處理後咱們須要從裏面把key去掉
		keys.remove(key);
	}
}

這裏須要着重說明一下select操做作了什麼(根據現象推的,具體好像沒有找到這個的文檔說明),他每次檢查keys裏面每一個Key對應的通道的狀態,若是有關注狀態時,就決定返回,這時會同時將Key對象加入到selectedKeys中,並返回selectedKeys本次變化的對象數(本來就在selectedKeys中的對象是不計的),因爲一個Key對應一個通道(可能同時處於多個狀態,因此注意上面的if語句我都沒有寫else),因此select返回0也是有可能的。另外OP_WRITE和OP_CONNET這兩個狀態是不能長期關注的,只在有須要的時候監聽,處理完必須立刻去掉。若是沒有發現有任何關注狀態,select會一直阻塞到有狀態變化或者超時什麼的。

SelectionKey的其餘幾個方法,attach(Object)爲key設置附件,並返回以前的附件;interestOps()和readyOps()返回關注狀態和當前狀態;cancel()爲取消註冊;isValid()表示key是否有效(在key取消註冊,通道關閉,選擇器關閉這三個事情發生以前,key均爲有效的,但不包括對方關閉通道,因此讀寫應注意異常)。

    還有一個狀態上面沒有使用,OP_CONNECT這個主要是用於客戶端,對應的key的方法是isConnectable()表示已經建立好了鏈接。

非阻塞實現的客戶端以下:

Selector selector = Selector.open();
// 建立一個套接字通道,注意這裏必須使用無參形式
SocketChannel channel = SocketChannel.open();
// 設置爲非阻塞模式,這個方法必須在實際鏈接以前調用(因此open的時候不能提供服務器地址,不然會自動鏈接)
channel.configureBlocking(false);
// 鏈接服務器,因爲是非阻塞模式,這個方法會發起鏈接請求,並直接返回false(阻塞模式是一直等到連接成功並返回是否成功)
channel.connect(new InetSocketAddress("127.0.0.1", 7777));
// 註冊關聯連接狀態
channel.register(selector, SelectionKey.OP_CONNECT);
while (true) {
	// 前略 和服務器端的相似
	// ...
	// 獲取發生了關注時間的Key集合,每一個SelectionKey對應了註冊的一個通道
	Set<SelectionKey> keys = selector.selectedKeys();
	for (SelectionKey key : keys) {
		// OP_CONNECT 兩種狀況,連接成功或失敗這個方法都會返回true
		if (key.isConnectable()) {
			// 因爲非阻塞模式,connect只管發起鏈接請求,finishConnect()方法會阻塞到連接結束並返回是否成功
			// 另外還有一個isConnectionPending()返回的是是否處於正在鏈接狀態(還在三次握手中)
			if (channel.finishConnect()) {
				// 連接成功了能夠作一些本身的處理,略
				// ...
				// 處理完後必須吧OP_CONNECT關注去掉,改成關注OP_READ
				key.interestOps(SelectionKey.OP_READ);
			}
		}
		// 後略 和服務器端的相似
		// ...
	}
}

    雖然例子是這樣的,不過服務器和客戶端能夠本身單方面選擇是否採用非阻塞模式,用阻塞模式的客戶端鏈接非阻塞模式的服務器端是OK的。

二 NIO2的異步IO通道

如下API是由Java7提供。老版本沒法使用。

    異步IO通道的實現有兩種實現方式,一是在阻塞模式的原方法(主要指的是read和write,具體能夠查看API文檔)上傳於一個CompletionHandler實例以實現回調,另外也能夠令其返回一個Future實例(Java5新增同步工具包java.util.concurrent中的API),而後再適當的時候經過其get方法來獲取返回的結果。異步文件I/O通道爲AsynchronousFileChannel,而異步套接字通道爲AsynchronousServerSocketChannel,分別對應其各自的原始通道。

    異步I/O須要與一個AsynchronousChannelGroup對象關聯,他實質上就是一個用於I/O的線程池。AsynchronousChannelGroup對象能夠經過其自身靜態方法的withThreadPool(),withCachedThreadPool(),withFixedThreadPool()提供一個線程池來建立(線程池也是Java5新增同步工具包java.util.concurrent中的API)。在異步通道建立open()時,可將這個對象傳入進行關聯。若是沒有提供這個對象的話,就默認使用系統分組。可是須要注意的是系統分組的線程池是個守護線程池,JVM是可能在沒有讀寫完成前正常結束的。AsynchronousChannelGroup在使用完後須要shutdowm(),這方面和線程池的關閉是相似的。

相關文章
相關標籤/搜索