1. 程式人生 > >Java NIO學習指南

Java NIO學習指南

        摘要:讀完本章,您將瞭解什麼是NIO,NIO的基本原理,NIO的基本用法。

        概念:直貼百度百科上的描述

            

        NIO主要涉及Channel、Buffer、Selector。Channel譯為通道,類似流,主要負責資料的傳輸;Buffer譯為資料緩衝區,Channel上的資料只能通過Channel提供的read()或者write()方法讀到Buffer或者將Buffer的資料寫到Channel上,Buffer為所有原始型別(boolean型別除外)提供了子類(但我覺得除了ByteBuffer外,其他用處很小,因為Channel讀或寫的時候只能接受ByteBuffer型別,不像IO流有字元流可以很好的進行讀寫,這也是我對NIO的困惑之一,希望哪位高人指點一下);Selector譯為選擇器,正是因為有了Selector,NIO才有了它的強大之處(非阻塞式的高伸縮性網路),一個Selector可以管理多個非阻塞Channel,只有非阻塞Channel才可以註冊到Selector上,換言之Channel註冊之前需要設定為非阻塞(open之後預設是阻塞的),而像FileChannel是阻塞的不能註冊到selector。

        Channel介紹:

        Java NIO 中主要的通道實現有:

        a、FileChannel 從檔案讀寫資料

        b、DatagramChannel 通過UDP讀寫網路資料

        c、SocketChannel 通過TCP讀寫網路上的資料,主要有兩種方式建立SocketChannel,方式一為開啟一個SocketChannel並連線到網際網路上的某個伺服器,方式二為一個新連線到達ServerSocketChannel時會建立一個SocketChannel。

      d、ServerSocketChannel 可以監聽新進來的TCP連線,像WEB伺服器那樣。為每一個新進來的連線都會建立一個SocketChannel。

        針對每個Channel行駛的功能不一樣,方法也略有不同,這裡不再貼API。

        Buffer介紹:

        Buffer為什麼叫資料緩衝區,我覺得可以這樣解釋,無論是解析通道(channel)的資料還是將我們想要傳輸的資料寫到通道(channel)上,都需要經過Buffer,Buffer作為資料傳輸解析中間過度的存在者,所以叫做資料緩衝區。換言之,如果想要讀通道上的資料,需要先讀通道上的資料寫到buffer上,再從buffer裡獲取內容資料;如果我們想將資料寫到通道上,我們需要先把資料寫到Buffer上,再呼叫通道上的write方法寫到通道上,由此可見,buffer很好的啟到了資料緩衝的作用。

        Buffer主要有position、limit、capactity屬性,position記錄的是當前可操作(讀或寫)的下一個位置,limit表示的是這個buffer總共可操作的數量,capactity表示的是這個buffer的容量。比如新申請一個容量為1024的ByteBuffer,初始化之後的buffer是寫模式,即position=0,limit=capactity=1024;當往buffer寫了兩個位元組之後,position=2,limit還是等於capactity等於1024,這時候呼叫Buffer的flip()方法表示將當前buffer從寫模式切換為讀模式,limit=position=2表示可以讀的個數(往裡寫了多少個就可以讀多少個),將position重新置為0從第一位開始讀,capacticy還是等於1024;將buffer的2個位元組資料都寫到channel之後,position=2,limit也是等於上一步flip()方法之後的值也為2,capacticy還是等於1024,將buffer的資料全部寫到通道之後我們一般會呼叫buffer的clear()方法,重新將position置為0,limit=capactity,相當於重新初始化buffer,將其切換為寫模式。

        舉個簡單的SocketChannel/ServerSocketChannel例子,先不結合Selector使用:

        NIO TCP客戶端

// open 一個socketChannel
		SocketChannel socketChannel = SocketChannel.open();
		SocketAddress address = new InetSocketAddress("127.0.0.1", 8091);
		// 連線到伺服器
		socketChannel.connect(address);
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		// 設定為非阻塞模式
		socketChannel.configureBlocking(false);
		// 如果未連線,等待連線完成再做
		while (!socketChannel.finishConnect()) {

		}
		String msg = "Hello I'm coming!";
		// 將msg寫到buffer中
		buffer.put(msg.getBytes());
		// 將buffer的寫模式切換為讀模式
		buffer.flip();
		// 每次往channel寫的資料不確定,只要buffer還有未寫的資料就繼續寫
		while (buffer.hasRemaining()) {
			socketChannel.write(buffer);
		}
		// 清空buffer
		buffer.clear();
		// 關閉socketChannel 輸出通道,對應的服務端才能讀到檔案末尾
		socketChannel.shutdownOutput();
		int i = 0;
		StringBuffer sb = new StringBuffer();
		// 讀取socketChannel上的資料,將其寫到buffer中
		while ((i = socketChannel.read(buffer)) != -1) {
			// socketChannel read()方法返回值表示讀到了多少個位元組,如果返回-1表示讀到了檔案末尾
			if (i != 0) {
				byte[] array = buffer.array();
				sb.append(new String(array, 0, i));
				// 每次寫完一次,清空buffer
				buffer.clear();
			}
		}
		System.out.println(sb);
		socketChannel.close();

        NIO TCP服務端(CHannel + Buffer實現)

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
		SocketAddress address = new InetSocketAddress("127.0.0.1", 8091);
		// 繫結埠號地址
		serverSocketChannel.bind(address);
		// 設定為非阻塞
		serverSocketChannel.configureBlocking(false);
		// 迴圈監聽多個連線,方法是阻塞的,每個時刻只能處理一個連線
		while (true) {
			// 每個新進來的連線都建立一個SocketChannel
			SocketChannel socketChannel = serverSocketChannel.accept();
			ByteBuffer buffer = ByteBuffer.allocate(1024);
			// 如果ServerSocketChannel是非阻塞的,accept可能返回空
			if (socketChannel != null) {
				StringBuffer sb = new StringBuffer();
				int i = 0;
				while ((i = socketChannel.read(buffer)) > 0) {
					byte[] array = buffer.array();
					sb.append(new String(array, 0, i));
					// 每次讀完需要清空
					buffer.clear();
				}
				System.out.println(sb);
				String msg = "Welcome to NIO!";
				// 清空,相當於重新初始化
				buffer.clear();
				buffer.put(msg.getBytes());
				// 將buffer的寫模式切換為讀模式
				buffer.flip();
				// 一次寫不能保證將buffer的內容都寫到channel中,所以需要判斷只要還有未寫位元組就接著寫
				while (buffer.hasRemaining()) {
					socketChannel.write(buffer);
				}
				socketChannel.shutdownOutput();
				socketChannel.close();
			}
		}

        Selector介紹:

        Selector(選擇器)是Java NIO中能夠檢測一到多個NIO通道,並能夠知曉通道是否為諸如讀寫事件做好準備的元件。這樣,一個單獨的執行緒可以管理多個Channel,從而管理多個網路連線。僅用單個執行緒來處理多個Channels的好處是,只需要更少的執行緒來處理通道。事實上,可以只用一個執行緒處理所有的通道。對於作業系統來說,執行緒之間上下文切換的開銷很大,而且每個執行緒都要佔用系統的一些資源(如記憶體)。因此,儘量保證使用的執行緒最少,效能最優。

        我認為NIO的難點在於如何使用Selector,如何管理Selector以及讓對應的Channel註冊到Selector上。

        下面用一個完整的例子說明如何建立Selector,如何將通道註冊到Selector,將上面服務端改造一下,用上Selector。

        Java NIO 服務端 Selector + Channel + Buffer實現:

// 建立選擇器
		Selector selector = Selector.open();
		// 建立ServerSocketChannel
		ServerSocketChannel serverSocket = ServerSocketChannel.open();
		SocketAddress address = new InetSocketAddress("127.0.0.1", 8091);
		// 繫結地址
		serverSocket.bind(address);
		// 必須設定為非阻塞模式
		serverSocket.configureBlocking(false);
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		// 將channel註冊到selector,因為是服務端的serverSocketChannel,所以監聽的事件是接受就緒事件
		// 一個server socket channel準備好接收新進入的連線稱為“接收就緒”
		serverSocket.register(selector, SelectionKey.OP_ACCEPT, buffer);
		// 輪詢
		while (true) {
			// 如果沒有連線通道就緒,輪詢selector,監聽準備好的通道
			while (selector.select() < 0) {

			}
			// 獲取準備好的SelectedKeys
			Set<SelectionKey> selectedKeys = selector.selectedKeys();
			Iterator<SelectionKey> iterator = selectedKeys.iterator();
			while (iterator.hasNext()) {
				SelectionKey selectionKey = iterator.next();
				// 一個server socket channel準備好接收新進入的連線稱為“接收就緒”
				if (selectionKey.isAcceptable()) {
					ServerSocketChannel serverChannel = (ServerSocketChannel) selectionKey.channel();
					// 如果是連線就緒,新進來的連線將會建立一個SocketChannel
					SocketChannel accept = serverChannel.accept();
					if (accept != null) {
						// 必須設定為非阻塞,不然不能註冊到selector
						accept.configureBlocking(false);
						// 監聽讀和寫事件
						accept.register(selectionKey.selector(), SelectionKey.OP_READ | SelectionKey.OP_WRITE);
					}
				}
				// 寫就緒,將資料寫到channel中
				if (selectionKey.isWritable()) {
					SocketChannel channel = (SocketChannel) selectionKey.channel();
					ByteBuffer tempBuffer = ByteBuffer.allocate(1024);
					tempBuffer.put("Server Msg !!!".getBytes());
					tempBuffer.flip();
					try {
						while (tempBuffer.hasRemaining()) {
							channel.write(tempBuffer);
						}
					} catch (Exception e) {

					}
					channel.shutdownOutput();
				}
				// 讀就緒,讀取通道上的資料將其寫到buffer
				if (selectionKey.isReadable()) {
					SocketChannel channel = (SocketChannel) selectionKey.channel();
					ByteBuffer tempBuffer = ByteBuffer.allocate(1024);
					StringBuffer sb = new StringBuffer();
					int i = 0;
					while ((i = channel.read(tempBuffer)) != -1) {
						if (i != 0) {
							byte[] array = tempBuffer.array();
							sb.append(new String(array, 0, i));
							tempBuffer.clear();
						}
					}
					if (sb.length() > 0) {
						System.out.println(sb);
					}
				}
				if (selectionKey.isConnectable()) {
					System.out.println("selectionKey.isConnectable()");
				}
				iterator.remove();
			}
		}

        上面的客戶端也可進行改造,但是我感覺沒多大必要,場景過於簡單沒必要去輪詢Selector,要是單純的一直輪詢Selector,容易造成cpu使用率100%導致電腦卡頓,為了學習,這裡稍加升級一下,供大家學習參考。

Selector selector = Selector.open();
		SocketChannel socketChannel = SocketChannel.open();
		SocketAddress address = new InetSocketAddress("127.0.0.1", 8091);
		socketChannel.connect(address);
		// 設定為非阻塞模式
		socketChannel.configureBlocking(false);
		// 註冊多個事件,事件之間用|連線
		socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE, ByteBuffer.allocate(1024));
		int p = 0;
		int q = 0;
		while (true) {
			// 如果沒有連線通道就緒,將會一直輪詢
			while (selector.select() < 0) {

			}
			Set<SelectionKey> selectedKeys = selector.selectedKeys();
			Iterator<SelectionKey> iterator = selectedKeys.iterator();
			while (iterator.hasNext()) {
				SelectionKey selectionKey = iterator.next();
				// 某個channel成功連線到另一個伺服器稱為“連線就緒”。
				// 不知道在什麼時間點會進來,一直沒執行
				if (selectionKey.isConnectable()) {
					System.out.println("selectionKey.isConnectable()");
				}
				if (selectionKey.isWritable()) {
					doWrite(selectionKey);
					p++;
				}
				if (selectionKey.isReadable()) {
					doRead(selectionKey);
					q++;
				}
				iterator.remove();
			}
			// 這裡讀寫只操作一次,一直輪詢會消耗cpu,造成cpu使用率100%
			if (p > 0 && q > 0) {
				break;
			}
		}
	}

	public static void doWrite(SelectionKey selectionKey) throws Exception {
		SocketChannel channel = (SocketChannel) selectionKey.channel();
		ByteBuffer tempBuffer = ByteBuffer.allocate(1024);
		tempBuffer.put("Client Msg ,do it better!".getBytes());
		tempBuffer.flip();
		try {
			while (tempBuffer.hasRemaining()) {
				channel.write(tempBuffer);
			}
		} catch (Exception e) {

		}
		channel.shutdownOutput();
	}

	public static void doRead(SelectionKey selectionKey) throws Exception {
		SocketChannel channel = (SocketChannel) selectionKey.channel();
		ByteBuffer tempBuffer = ByteBuffer.allocate(1024);
		StringBuffer sb = new StringBuffer();
		int i = 0;
		while ((i = channel.read(tempBuffer)) != -1) {
			if (i != 0) {
				byte[] array = tempBuffer.array();
				sb.append(new String(array, 0, i));
				tempBuffer.clear();
			}
		}
		if (sb.length() > 0) {
			System.out.println(sb);
		}
	}

        分析一下上面的步驟,主要包括Selector的建立;Channel的建立;將Channel註冊到對應的Selector中,監聽感興趣的事件;輪詢Selector,找到就緒的事件,拿到通道進行對應的操作比如讀寫操作。       

        到這裡Java NIO的基本方法使用演示完畢,您應該知道如何正確使用Selector/Buffer/Channel。當然這只是你使用NIO的第一步,實際的開發應用場景遠比這複雜,但始終都脫離不了這些基礎。

        相比於普通IO,NIO的效能要好的多,其中一個原因就是NIO是非阻塞的,利用Selector一個執行緒可以管理多個通道可以提高CPU的使用率。還有就是,ByteBuffer.allocateDirector()分配的記憶體使用的是本機記憶體而不是Java堆上的記憶體,每一次分配記憶體時會呼叫作業系統的os::malloc()函式,直接ByteBuffer產生的資料如果和網路或者磁碟互動都在作業系統的核心空間中發生,不需要將資料複製到Java記憶體中,很顯然執行這種IO操作要比一般的從作業系統的核心空間到Java堆上的切換操作快得多,因為它們可以避免在Java堆與本機堆之間複製資料。