BIO、NIO




1. BIO和NIO

我們平常使用的IO是BIO(Blocking-IO),即阻塞IO、而NIO(No-blocking-IO)則是非阻塞IO,二者有什么區別呢?


預先知識准備

  • 同步:發起調用后,調用者一直處理任務至結束后才返回結果,期間不能執行其他任務
  • 異步:發起調用后,調用者立即返回結果的標記(當結果出來后用回調等機制通知),期間可以執行其他任務
  • 阻塞:發起請求后,發起者一直等待結果返回,期間不能執行其他任務
  • 非阻塞:發起請求后,發起者不用一直等待結果,期間可以執行其他任務
  • IO模式有五種(BIO、NIO、信號驅動IO、多路復用IO、異步IO)這里介紹同步阻塞和同步非阻塞IO,而剩下的后面回來填坑

NIO主要體現在網絡IO中,所以下面就圍繞網絡IO來說明,這里會涉及到傳統的BIO、網絡編程、反應器設計模式,如果不了解的童鞋這里有各自的傳送門 BIO ,[未完善]


二者區別

BIO NIO
類型 同步阻塞 同步非阻塞
面向 面向流 面向緩沖區
組件 選擇器

若沒有了解過NIO,那么列出的區別只需有個印象即可,后面會逐步說明



2.BIO


2.1 傳統BIO

傳統的IO其讀寫操作都阻塞在同一個線程之中,即在讀寫期間不能再接收其他請求


那么我們就來看看傳統BIO是怎么實現的,后面都以網絡編程的Socket為例,因其與后面的NIO有關

public class BIO {
	
	public static void main(String[] args) throws IOException {
		
		// 開個線程運行服務器端套接字
		new Thread( () -> {
			try {
                // 建立服務器端套接字
                ServerSocket serverSocket = new ServerSocket(8080);
                // 該方法阻塞至有請求過來
                Socket socket = serverSocket.accept();

                // 獲取輸入流
                int length = 0;
                byte[] bytes = new byte[1024];
                InputStream in = socket.getInputStream();

                // 客戶端關閉輸出流這里才會讀取到-1,shutdownOutput或者close
                while( (length = in.read(bytes)) != -1){
                    System.out.println(new String(bytes,0,length));
                }

                System.out.println("這里服務器端處理任務花費了10秒");
                Thread.sleep(10000);

                // 獲取輸出流
                OutputStream out = socket.getOutputStream();
                out.write( ("這里是服務器端發送給客戶端的消息: " + new Date()).getBytes() );

                // 關閉資源
                in.close();
                out.close();
                socket.close();
                serverSocket.close();
			} catch (Exception e) {
			}
		}).start();
		
		
		
		
		// 開個線程運行客戶端套接字
		new Thread( () -> {
			try {
				// 建立客戶端套接字
				Socket socket = new Socket("127.0.0.1",8080);
				
				// 獲取輸出流
				OutputStream out = socket.getOutputStream();
				out.write( ("這里是客戶端發送給服務器端的消息:" + new Date()).getBytes() );
				// 關閉輸出流,讓服務器知道數據已經發送完畢,剩下接收數據了
				socket.shutdownOutput();
				
				// 獲取輸入流
				int length = 0;
				byte[] bytes = new byte[1024];
				InputStream in = socket.getInputStream();
				while( (length = in.read(bytes)) != -1){
					System.out.println(new String(bytes,0,length));
				}
				
				// 關閉資源,若沒有關閉則會保持連接至超時,單線程服務器端就不能接收后來的連接請求
				out.close();
				in.close();
				socket.close();
			} catch (Exception e) {
			}
		}).start();

	}
}
這里是客戶端發送給服務器端的消息:Sat Feb 08 15:14:55 GMT+08:00 2020
這里服務器端處理任務花費了10秒
這里是服務器端發送給客戶端的消息: Sat Feb 08 15:15:05 GMT+08:00 2020
  • 從輸出可以看出,客戶端會一直等待阻塞直至服務器端返回內容

  • 服務器端的accept()方法會阻塞當前線程,直至有請求發送過來才會繼續accept()方法下面的代碼

  • 服務器端接收到一個請求后且該請求還沒處理完,后又再有一個請求過來,則后來的請求會被阻塞需排隊等待

  • 客戶端打開輸出流若沒關閉,則服務器端是不知道客戶端數據已經發送完,會一直等待至超時 ,關閉方法:

    • 客戶端socket.close(),整個連接也關閉了
    • 客戶端socket.shutdownOutput(),單方面關閉輸出流,不關閉連接
    • 客戶端的outputStream.close(),會造成socket被關閉


2.2 偽異步BIO

傳統的BIO是單線程的,一次只能處理一個請求,而我們可以改進為多線程,即服務器端每接收到一個請求就為該請求單獨創建一個線程,而主線程還是繼續監聽是否有請求過來,偽異步是因為accept方法到底還是同步的


public class SocketTest {
	
	// 定義線程接口
	class MyRunnable implements Runnable{
		
		@Override
		public void run(){
			try {
				Socket socket = new Socket("127.0.0.1",8080);
				
				// 獲取輸出流
				OutputStream out = socket.getOutputStream();
				out.write( ("這里是客戶端發送給服務器端的消息:" + new Date()).getBytes() );
				// 關閉輸出流,讓服務器知道數據已經發送完畢,剩下接收數據了
				socket.shutdownOutput();
				
				// 獲取輸入流
				int length = 0;
				byte[] bytes = new byte[1024];
				InputStream in = socket.getInputStream();
				while( (length = in.read(bytes)) != -1){
					System.out.println(new String(bytes,0,length));
				}
				
				// 關閉資源
				out.close();
				in.close();
				socket.close();
			} catch (Exception e) {
			}
		}
	}
	
	
	
	public static void main(String[] args) throws IOException, InterruptedException {
		
		// 開個線程運行服務器端套接字
		new Thread( () -> {
			try {
				// 建立服務器端套接字
				ServerSocket serverSocket = new ServerSocket(8080);
				
				// 循環接收請求
				while(true){
					Socket socket = serverSocket.accept();
					
					// 為每個請求單獨開線程,這里就不那么復雜使用線程池了
					new Thread( () -> {
						try {
							// 獲取輸入流
							int length = 0;
							byte[] bytes = new byte[1024];
							InputStream in = socket.getInputStream();
							while( (length = in.read(bytes)) != -1){
								System.out.println(new String(bytes,0,length));
							}
							
							// 獲取輸出流
							OutputStream out = socket.getOutputStream();
							out.write( ("這里是服務器端發送給客戶端的消息: " + new Date()).getBytes() );
							
							// 關閉資源
							in.close();
							out.close();
							socket.close();
						} catch (Exception e) {
							// TODO: handle exception
						}
					}).start();
				}
			} catch (Exception e) {
			}
		}).start();
		
        
		// 創建多線程,調用類中接口(為了偷懶寫成這樣了。。。)
        // 關注點在於偽異步,像線程計數器,,線程池,Lambda表達式也盡量少用,使代碼易懂
		SocketTest socketTest = new SocketTest();
		MyRunnable myRunnable = socketTest.new MyRunnable();
		new Thread(myRunnable).start();
		new Thread(myRunnable).start();
		new Thread(myRunnable).start();

	}
}
這里是客戶端發送給服務器端的消息:Sat Feb 08 15:52:00 GMT+08:00 2020
這里是服務器端發送給客戶端的消息: Sat Feb 08 15:52:00 GMT+08:00 2020
這里是客戶端發送給服務器端的消息:Sat Feb 08 15:52:00 GMT+08:00 2020
這里是客戶端發送給服務器端的消息:Sat Feb 08 15:52:00 GMT+08:00 2020
這里是服務器端發送給客戶端的消息: Sat Feb 08 15:52:00 GMT+08:00 2020
這里是服務器端發送給客戶端的消息: Sat Feb 08 15:52:00 GMT+08:00 2020
  • 服務器端每來一個請求就為之單獨創建線程來處理任務,使主線程可以繼續循環接收請求
  • 客戶端的請求之間就互不干擾了,不用等待上一個請求處理完才處理下一個
  • 其本質還是同步,使用了多線程才實現異步功能
  • 使用多線程,若在多高並發情況下,會大量創建線程而導致內存溢出(可以使用線程池優化,但有界限池終究不是辦法)




3. NIO

  • 看了上面那么多鋪墊,終於到我們的正題了。NIO主要使用在網絡IO中,當然文件IO也有使用,NIO在高並發的網絡IO中有極大的優勢,其在JDK1.4中引入,以我們傳統再傳統的開發環境--1.7中可以使用了
  • 在單線程中,NIO在寫讀數據的時候可以同時執行其他任務,不必等數據完全讀寫而導致阻塞(后面有地方說明)

NIO的組成

  • Buffer(緩沖區)
  • Channel(通道)
  • Selector(選擇器)

那么我們就來看看NIO的三個組成把



3.1 Buffer

NIO是面向緩沖區的,一次處理一個區的數據,在NIO中我們都是使用緩沖區來處理數據,即數據的讀入或寫出都要經過緩沖區


緩沖區的類型有:

  • ByteBuffer、

  • ShortBuffer、

  • IntBuffer、

  • LongBuffer、

  • FloatBuffer、

  • DoubleBuffer、

  • CharBuffer

最常用是ByteBuffer,記住后面要用到,可使用靜態方法獲取緩沖區:ByteBuffer.allocate(1024)


Buffer類中主要的方法:

返回類型 函數 解釋
XXXBuffer allocate(int capacity) 返回指定容量的緩沖區
ByteBuffer put(byte[] src) 向緩沖區添加字節數組
ByteBuffer get(byte[] dst) 向緩沖區獲取字節數組
XXXBuffer flip() 切換成讀模式
XXXBuffer clear() 清除此緩沖區

其內部維護了幾個變量

// Invariants: mark <= position <= limit <= capacity
private int mark = -1;	// 標記這里不講解
private int position = 0;	//位置
private int limit;		// 限制
private int capacity;	// 容量大小

變量的變化:

初始化時:position為0,limit和capacity為容量大小,且capacity不變化,后面省略

put數據時:position為put進去數據大小(如放進5字節數據,則position=5),其余不變,正常默認為寫模式

切換讀模式:limit賦值為position的當前值,而position賦值為0

get數據時:讀取多少個數據,position就前進幾個位置

清空:調用clear(),變量變為初始化狀態,即position為0,limit為容量大小



3.2 Channel

通道主要是傳輸數據的,不進行數據操作,並且與流不同可以前后移動,而且通道是雙向的讀寫的,最重要的是Channel只能與Buffer交互,所以要使用NIO就要用Channel和Buffer來配合


其類型包括:

  • FileChannel
  • DatagramChannel
  • SocketChannel:
  • ServerSocketChannel

可以看出NIO主要支持網絡IO及文件IO,可通過靜態方法獲取:ServerSocketChannel.open(),然后通過ServerSocketChannel.socket()獲取對應的套接字,套接字的獲取通道方法前提是已經綁定了通道才行,不然空指針


通道的主要方法:

類型 函數名 解釋
ServerSocketChannel open 返回對應的通道
int read(ByteBuffer dst) 從該通道讀取到給定緩沖區的字節序列
int write(ByteBuffer src) 從給定的緩沖區向該通道寫入一個字節序列
ServerSocketChannel bind(SocketAddress local) 將通道的套接字綁定到本地,設為監聽連接
SelectableChannel configureBlocking(Boolean bool) 設置通道的阻塞模式
SelectionKey register(Selector sel, int ops) 將通道注冊到選擇器

配合Channel和Buffer來簡單實現數據流通

int length = 0;
while( (length = inChannel.read()) != -1 ){
    buffer.flip();	//切換讀模式
    outChannel.write(buffer);	// 數據寫入通道
    buffer.clear();		// 清空緩沖區,實現可再寫入
}


3.3 Selector

NIO特有的組件(選擇器容器),注意只有在網絡IO中才具有非阻塞性,網絡IO中的套接字的通道才有非阻塞的配置。使用單線程通過Selector來輪詢監聽多個Channel,在IO事件還沒到達時不會陷入阻塞態等待。划重點:傳統BIO在事件還沒到達時該線程會被阻塞而等待,一次只能處理一個請求(可以使用多線程來提高處理能力)。而NIO在事件還沒到達是非阻塞輪詢監聽的,一次可以處理多個事件。使用一個線程來處理多個事件明顯比一個線程處理一個事件更優秀,獲取選擇器:Selector.open()



選擇器主要方法:

類型 方法名 解釋
void close 關閉此選擇器
Selector open 打開選擇器
int select 選擇一組准備好的IO鍵
Set selectedKeys 返回選擇器的鍵集

這里補充一下注冊通道時返回的鍵的方法

XXXChannel channel 返回鍵對應的通道,類似於句柄
boolean isAcceptable 鍵對應的通道是否准備好了
boolean isReadable 鍵對應的通道是否可讀
boolean isWritable 鍵對應的通道是否可寫


3.4 使用事例

綜合上面BIO的 2.1和 2.2的代碼,客戶端基本不用改動,使用多線程來模擬多次請求,而重點改造在於服務器端


這里的服務器端用單線程來處理請求,即一對多使用了多路復用。若是BIO單線程則會阻塞,即一請求一應答

public class NIOTest {0.
	
	// 定義線程接口
		class MyRunnable implements Runnable{
			
			@Override
			public void run(){
				try {
					Socket socket = new Socket("127.0.0.1",8080);
					
					// 獲取輸出流
					OutputStream out = socket.getOutputStream();
					out.write( ("這里是客戶端發送給服務器端的消息:" + new Date()).getBytes() );
					// 關閉輸出流,讓服務器知道數據已經發送完畢,剩下接收數據了
					socket.shutdownOutput();
					
					// 獲取輸入流
					int length = 0;
					byte[] bytes = new byte[1024];
					InputStream in = socket.getInputStream();
					while( (length = in.read(bytes)) != -1){
						System.out.println(new String(bytes,0,length));
					}
					
					// 這里故意不關閉資源,保持連接
					// 如果是BIO單線程,沒有斷開連接,則會阻塞后面的請求
					// 而NIO則不會阻塞,因為是多路復用
					
				} catch (Exception e) {
				}
			}
		}
	
	

	public static void main(String[] args) throws UnknownHostException, IOException {
		
		// 開個線程運行服務器端套接字
		new Thread( () -> {
			
			try {
				// 靜態方法獲取選擇器
				// 開啟選擇器的線程會被選擇器阻塞,所以要另開一個線程執行
				Selector selector = Selector.open();
				
				// 獲取服務器端通道並配置非阻塞
				ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
				serverSocketChannel.configureBlocking(false);
				InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 8080);
				serverSocketChannel.bind(inetSocketAddress);
				
				// Selector管理Channel,則需將對應的channel注冊上去,且指定類型
				// 將服務器通道注冊到選擇器上,注冊為accept
				// 可頻道為:一看能看出來不解釋了
				/**
				 * SelectionKey.OP_CONNECT
				 * SelectionKey.OP_ACCEPT
				 * SelectionKey.OP_READ
				 * SelectionKey.OP_WRITE
				 */
				serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
				
				while(true){
					// 輪詢監聽是否有准備好的連接
					if(selector.select() > 0){
						// 獲取鍵集
						Set<SelectionKey> set = selector.selectedKeys();
						Iterator iterator = set.iterator();
						// 迭代器迭代
						while(iterator.hasNext()){
							SelectionKey selectionKey = (SelectionKey) iterator.next();
							
							// 接收連接事件
							if(selectionKey.isAcceptable()){
								SocketChannel socketChannel = serverSocketChannel.accept();
								
								// 設置客戶端通道為非阻塞,不然選擇器會被阻塞,其存在沒有意義了
								socketChannel.configureBlocking(false);
								
								// 將客戶端通道注冊到選擇器上,使選擇器可以統一管理
								socketChannel.register(selector, SelectionKey.OP_READ);
								
							// 處理可讀事件
							}else if(selectionKey.isReadable()){
								// 通過key來獲取通道
								SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
								
								// 配合緩沖區
								ByteBuffer bytebuffer = ByteBuffer.allocate(1024);
								
								int length = 0;
								byte[] bytes = new byte[1024];
								while( (length = socketChannel.read(bytebuffer)) != -1){
									bytebuffer.flip();
									
									// 將緩沖區數據放入字節數組,並輸出
									bytebuffer.get(bytes, 0, length);
									System.out.println(new String(bytes,0,length));
									
									bytebuffer.clear();
								}
							}
							// 取消選擇鍵,因為有些已經處理了
							iterator.remove();
						}
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}).start();
		
		
		// 調用類中接口,創建多線程(為了偷懶寫成這樣了。。。)
		NIOTest NIOTest = new NIOTest();
		MyRunnable myRunnable = NIOTest.new MyRunnable();
		new Thread(myRunnable).start();
		new Thread(myRunnable).start();
		new Thread(myRunnable).start();

	}
}
  • 上面客戶端故意不關閉連接,未超時情況下也能處理多請求,則說明NIO是非阻塞的,最大好處就在於這里


總結

挖坑:AIO異步的IO,基於網絡編程的Netty框架,越來越多的坑要填了 —_—!






免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM