天天看點

Java NIO學習指南

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

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

Java 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堆與本機堆之間複制資料。