1. 程式人生 > 程式設計 >快速掌握NIO和BIO的區別

快速掌握NIO和BIO的區別

NIO和BIO對比

NIO(non blocking I/O)非阻塞I/O,jdk1.4引入的新I/O,平時接觸的檔案的I/O操作是BIO,即阻塞I/O

BIO API使用

具體流程:

A.測試accept()方法的阻塞
public void testAccept() throws IOException{
	ServerSocket ss = new ServerSocket();
	ss.bind(new InetSocketAddress(9999));
	Socket sk = ss.accept();
	System.out.println("有連線連入");
}複製程式碼
JUnit測試,“有連線接入”沒有輸出,說明accept()方法產生阻塞了。 B.然後新增connect()方法測試的程式碼:
public void test
Contect() throws Exception{ Socket sk = new Socket(); sk.connect(new InetSocketAddress( "127.0.0.1",9999)); System.out.println("連線成功"); }複製程式碼
先執行伺服器端方法(testAccept()),再執行客戶端方法,發現accept()方法阻塞釋放了。另外“連線成功”正確輸出。如果不先啟動伺服器端方法,而直接執行客戶端方法,發現先是阻塞了一下,然後JUnit測試丟擲異常。 總結:connect()方法會產生阻塞,指定連線成功,阻塞才釋放。 accept()方法產生的阻塞,直到伺服器獲得到連線後,阻塞才釋放。 C.測試read()方法的阻塞性 C1. 再次修改testAccept()方法
InputStream  in
= sk.getInputStream(); byte bts[] = new byte[1024]; in.read(bts); System.out.println("讀取到了資料:"+new String(bts));複製程式碼
C2.為了不讓連線中斷,需要修改testConnect()
whiletrue);複製程式碼

總結:read()方法會產生阻塞,直到讀取到內容後,阻塞才被釋放。 D.測試write()方法的阻塞性 D1.修改testAccept()方法
for(int i =1;i<100000;i++){
	out.write("HelloWorld".getBytes());
	System.out.println(i);
}
System.out.println("資料寫完了。。。"
); }複製程式碼
先執行伺服器端方法,再執行客戶端方法;發現i輸出值為65513,阻塞了。
        for(int i =1;i<200000;i++){
		out.write("Hello".getBytes());
		System.out.println(i);
	}複製程式碼
微調程式碼,輸出到131026阻塞了。 總結:write()方法也會產生阻塞,write()一直往出寫資料,但是沒有任何一方讀取資料,直到寫出到一定量(我的是655130B,不同電腦可能不同)的時候,產生阻塞。向網路卡裝置緩衝區中寫資料。

NIO 相關API

Channel檢視API ServerSocketChannel,SocketChannel基於NIO的(基於tcp實現的,安全的基於握手機制) DatagramChannel基於UDP協議,不安全

NIO-Channel API(上)

accept和connect使用

/**ServerSocketChannel.open()建立伺服器端物件
 * nio提供兩種模式:阻塞模式和非阻塞模式
 * 預設情況下是阻塞模式。
 * 通過ssc.configureBlocking(false)設定為非阻塞模式
 * @throws Exception
 */
@Test
public void testAccept() throws Exception{
	//建立伺服器端的服務通道
	ServerSocketChannel ssc = 
			ServerSocketChannel.open();
	//繫結埠號
	ssc.bind(new InetSocketAddress(8888));
	//設定非阻塞模式
	ssc.configureBlocking(false);
	//呼叫accpet方法獲取使用者請求的連線通到
	SocketChannel sc = ssc.accept();
	System.out.println("有連線連入");
}複製程式碼
執行發現,並沒有輸出“有連線接入”,通道提供阻塞和非阻塞兩種模式,預設為阻塞模式。可以在bind port之前新增ssc.configureBlocking(false);設定通道的非阻塞模式。再次執行“有連線接入”便輸出了。
public void testConnect() throws Exception{
	SocketChannel sc = SocketChannel.open();
	sc.configureBlocking(false);
	sc.connect(new InetSocketAddress("127.0.0.1",8888));
	System.out.println("連線成功");
}複製程式碼
為加sc.configureBlocking(false);之前,執行該方法丟擲異常,並沒有輸出“連線成功”,通道的connect()方法也是阻塞的;使用方法sc.configureBlocking(false);可以將客戶端連線通道設定為非阻塞模式。

read()、write()方法測試(過度)

sc.read(ByteBuffer dst)
sc.write(ByteBuffer src)複製程式碼
由於這兩個方法都需要ByteBuffer物件作為引數,所以我們需要先講ByteBuffer緩衝區。

NIO-ByteBuffer緩衝區API

public class DemoByteBuffer {
	/**ByteBuffer緩衝區類,有三個重要的屬性
	 * capacity	10:容量,該緩衝區可以最多儲存10個位元組
	 * position	0:表示位置
	 * limit 10:限制位(用在獲取元素時限制獲取的邊界)	
	 */
	@Test
	public void testByteBuffer(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		System.out.println();
	}
	/**put(byte bt)向快取區中新增一個位元組
	 *   每呼叫一次該方法position的值會加一。
	 */
	@Test
	public void testPut(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		byte b1 = 1;
		byte b2 = 2;
		buf.put(b1);
		buf.put(b2);
		buf.putInt(3);
		System.out.println();
	}
	/**get()獲取position指定位置的一個位元組內容。
	 * 每呼叫一次該方法,position++;
	 * 如果在呼叫get()時,position>=limit,
	 * 則丟擲異常BufferUnderflowException
	 * 
	 * position(int pt):設定position的值為pt
	 * position():獲取當前緩衝區的position屬性的值
	 * limit(int):設定限制為的值
	 * limit():獲取當前緩衝區的limit屬性的值。
	 */
	@Test
	public void testGet(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		byte b1 = 1;
		byte b2 = 2;
		buf.put(b1);//1
		buf.put(b2);//2
		//設定position的值為0
		buf.position(0);
		//設定限制位(不想讓使用者獲取無用的資訊)
		buf.limit(2);
		System.out.println(buf.get());//
		System.out.println(buf.get());
		System.out.println(buf.get());
	}
	/**flip()方法:反轉快取區,一般用在新增完資料後。
	 * limit = position;將limit的值設定為當前position的值
       position = 0;再將position的值設定為0
	 */
	@Test
	public void testFlip(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		byte b1 = 1;
		byte b2 = 2;
		buf.put(b1);//1
		buf.put(b2);//2
		/*buf.limit(buf.position());
		buf.position(0);*/
		buf.flip();
	}
	/**clear():"清除快取區"
	 * 底層原始碼:
	 *  position = 0;
        limit = capacity;
       	通過資料覆蓋的方式達到清除的目的。
	 */
	@Test
	public void testClear(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		byte b1 = 1;
		byte b2 = 2;
		buf.put(b1);//1
		buf.put(b2);//2
		buf.clear();
		byte b3=33;
		buf.put(b3);
		buf.flip();
		for(int i = 0;i<buf.limit();i++){
			System.out.println(buf.get());
		}
	}
	/**hasRemaining()判斷緩衝區中是否還有有效的資料,有返回
	 * true,沒有返回false
	 * public final boolean hasRemaining() {
	        return position < limit;
	   }
	 */
	@Test
	public void testClear12(){
		ByteBuffer buf = ByteBuffer.allocate(10);
		byte b1 = 1;
		byte b2 = 2;
		buf.put(b1);//1
		buf.put(b2);//2
		buf.clear();
		byte b3=33;
		buf.put(b3);
		buf.flip();
		/*for(int i = 0;i<buf.limit();i++){
			System.out.println(buf.get());
		}*/
		/*int i =0;
		while(i<buf.limit()){
			System.out.println(buf.get());
			i++;
		}*/
		while(buf.hasRemaining()){
			System.out.println(buf.get());
		}
	}
}複製程式碼

NIO-Channel API(下)

1、read()方法 修改ChanelDemo類的testAccept方法:
ByteBuffer buf = ByteBuffer.allocate(10);
sc.read(buf);
System.out.println("有資料讀入:"+buf.toString());複製程式碼
testConnect()方法不做任何修改,先執行testAccept()方法,發現在sc.read(buf)行丟擲了空指標異常。buf物件不可能為null,所以sc為null. 非阻塞程式設計最大的問題:不知道是否真正的有客戶端接入,所以容易產生空指標;所以需要人為設定阻塞。 將SocketChannel sc = ssc.accept();改為:
while(sc==null){
	sc = ssc.accept();
}複製程式碼
再次執行testAccept()方法,空指標的問題解決了;然後再執行testConnect()方法,發現連線能夠正常建立,但是“有資料讀入了。。”並沒有輸出,說明即使ssc服務通道設定了非阻塞,也沒有改變得到的通道sc預設為阻塞模式,所以sc.read(buf)阻塞了。要不想讓read()方法阻塞,需要在呼叫read()之前加sc.configureBlocking(false);這樣即使沒有讀到資料,“有資料讀入了。。”也能打印出來。
2、write()方法 修改testContect()方法,追加以下程式碼:
ByteBuffer buf = ByteBuffer.wrap("HelloWorld".getBytes());
sc.write(buf);複製程式碼
測試bug,先不執行伺服器端方法,直接執行客戶端方法testConnect(),輸出“連線成功”,但是sc.write(buf)行丟擲NotYetConnectException異常。sc為何丟擲該異常?非阻塞模式很坑的地方在於不知道連線是否真正的建立。修改testConnect():
ByteBuffer buf = ByteBuffer.wrap("HelloWorld".getBytes());
while(!sc.isConnected()){
	sc.finishConnect();
}
sc.write(buf);複製程式碼
再次執行testConnect(),之前的異常解決了,但是有出現了新的異常:
java.net.ConnectException: Connection refused: no further information
	at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)複製程式碼
先啟動伺服器端(testAccept()),後啟動客戶端(testConnect())即可。 手寫NIO非阻塞模式難度較大,程式碼不是重點,重要在於引出設計思想。

Selector設計思想

問題的引入


使用BIO編寫程式碼模擬一下 (編寫一個伺服器端和客戶端程式,執行一次伺服器程式,執行四次客戶端程式模擬四個使用者執行緒)
public class BIOServer {
	public static void main(String[] args) throws Exception {
		ServerSocket ss = new ServerSocket();
		ss.bind(new InetSocketAddress(7777));
		while(true){
			Socket sk = ss.accept();
			new Thread(new ServiceRunner(sk)).start();
		}
	}
}
class ServiceRunner implements Runnable{
	private Socket sk;
	public ServiceRunner(Socket sk){
		this.sk = sk;
	}
	public void run(){
		System.out.println("提供服務的執行緒id:"+
				Thread.currentThread().getId());
		try {
			Thread.sleep(Integer.MAX_VALUE);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
public class BIOClient {
	public static void main(String[] args) throws Exception {
		Socket sk = new Socket();
		sk.connect(new InetSocketAddress("127.0.0.1",7777));
		while(true);
	}
}複製程式碼

伺服器啟動 負責為客戶端提供服務,當前執行緒的id:9 負責為客戶端提供服務,當前執行緒的id:10 負責為客戶端提供服務,當前執行緒的id:11 負責為客戶端提供服務,當前執行緒的id:12


分析該模式的缺點: 缺點1:每增加一個使用者請求,就會建立一個新的執行緒為之提供服務。當使用者請求量特別巨大,執行緒數量就會隨之增大,繼而記憶體的佔用增大,所有不適用於高併發、高訪問的場景。 缺點2:執行緒特別多,不僅佔用記憶體開銷,也會佔用大量的cpu開銷,因為cpu要做執行緒排程。 缺點3:如果一個使用者僅僅是連入操作,並且長時間不做其他操作,會產生大量閒置執行緒。會使cpu做無意義的空轉,降低整體效能。 缺點4:這個模型會導致真正需要被處理的執行緒(使用者請求)不能被及時處理。

解決方法

針對缺點3和缺點4,可以將閒置的執行緒設定為阻塞態,cpu是不會排程阻塞態的執行緒,避免了cpu的空轉。所以引入事件監聽機制實現。 Selector多路複用選擇器,起到事件監聽的作用。 監聽哪個使用者執行操作,就喚醒對應的執行緒執行。那麼都有哪些事件呢? 事件:1.accept事件、2.connect事件、3.read事件、4.write事件

針對缺點1和缺點2,可以利用非阻塞模型來實現,利用少量執行緒甚至一個執行緒來處理多使用者請求。但是注意,這個模型是有使用場景的,適用於大量短請求場景。(比如使用者訪問電商網站),不適合長請求場景(比如下載大檔案,這種場景,NIO不見得比BIO好)

擴充套件知識 驚群現象,隱患:cpu的負載會在短時間之內聚升,最嚴重的情況時出現短暫卡頓甚至宕機。第二個問題就是效能不高。

Selector服務通道API

accept事件

編寫伺服器端程式:
public class NIOServer {
	public static void main(String[] args) throws Exception {
		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.bind(new InetSocketAddress(6666));
		//設定為非阻塞
		ssc.configureBlocking(false);
		//定義多路複用選擇器
		Selector sel = Selector.open();
		//註冊accept事件
		ssc.register(sel,SelectionKey.OP_ACCEPT);
		while(true){
			//select()在沒有收到相關事件時產生阻塞,直到
			//有事件觸發,阻塞才會得以釋放
			sel.select();
			//獲取所有的請求的事件
			Set<SelectionKey> sks = sel.selectedKeys();
			Iterator<SelectionKey> iter = sks.iterator();
			while(iter.hasNext()){
				SelectionKey sk = iter.next();
				if(sk.isAcceptable()){
					ServerSocketChannel ssc1= 
						(ServerSocketChannel)sk.channel();
					SocketChannel sc = ssc1.accept();
					while(sc==null){
						sc = ssc1.accept();
					}
					sc.configureBlocking(false);
					//為sc註冊read和write事件
					//0000 0001  OP_READ
					//0000 0100  OP_WRITE
					//0000 0101  OP_READ和OP_WRITE
					sc.register(sel,SelectionKey.OP_WRITE|SelectionKey.OP_READ);
					System.out.println("提供服務的執行緒id:"+
						Thread.currentThread().getId());
				}
				if(sk.isWritable()){
				}
				if(sk.isReadable()){
				}
                                iter.remove();
			}
		}
	}
}

編寫客戶端程式碼:
public static void main(String[] args) throws Exception {
		SocketChannel sc = SocketChannel.open();
		sc.connect(new InetSocketAddress("127.0.0.1",6666));
		//sc.configureBlocking(false);
		System.out.println("客戶端有連線連入");
while(true);
	}
}複製程式碼
伺服器端啟動一次,客戶端啟動三次,伺服器端的控制檯輸出: 伺服器端啟動 有客戶端連入,負責處理該請求的執行緒id:1 有客戶端連入,負責處理該請求的執行緒id:1 有客戶端連入,負責處理該請求的執行緒id:1 處理多個請求使用同一個執行緒。 該設計架構只適用的高併發短請求的場景中。

read事件

修改Server類
if(sk.isReadable()){
	//獲取連線物件
	SocketChannel sc = (SocketChannel)sk.channel();
	ByteBuffer buf = ByteBuffer.allocate(10);
	sc.read(buf);
	System.out.println("伺服器端讀取到:"+new String(buf.array()));
	//0000 0101  sk.interestOps()獲取原事件
	//1111 1110   !OP_READ
	//0000 0100  OP_WRITE
	//sc.register(sel,SelectionKey.OP_WRITE);
	sc.register(sel,sk.interestOps()&~SelectionKey.OP_READ);
}複製程式碼

修改Client類
System.out.println("客戶端連入");
ByteBuffer buffer = ByteBuffer.wrap(
"helloworld".getBytes());
sc.write(buffer);
while(true);複製程式碼

write事件

修改Servet
if(sk.isWritable()){
	//獲取SocketChannel
	SocketChannel sc = (SocketChannel)sk.channel();
	ByteBuffer buf = ByteBuffer.wrap("get".getBytes());
	sc.write(buf);
	//去掉寫事件
	sc.register(sel,sk.interestOps()&~SelectionKey.OP_WRITE);
}複製程式碼
修改Client類
public class NIOClient {
	public static void main(String[] args) throws Exception {
		SocketChannel sc = SocketChannel.open();
		sc.configureBlocking(false);
		sc.connect(new InetSocketAddress("127.0.0.1",6666));
		while(!sc.isConnected()){
			sc.finishConnect();
		}
		System.out.println("客戶端有連線連入");
		ByteBuffer buf = ByteBuffer.wrap(
				"helloworld".getBytes());
		sc.write(buf);
		System.out.println("客戶端資訊已經寫出");
		ByteBuffer readBuf = ByteBuffer.allocate(3);
		sc.read(readBuf);
		System.out.println("客戶端讀到伺服器端傳遞過來的資訊:"
		      +new String(readBuf.array()));
		while(true);
	}
}複製程式碼
public class Client2 {
public static void main(String[] args) throws IOException {
	SocketChannel sc = SocketChannel.open();
	sc.configureBlocking(false);
	sc.connect(new InetSocketAddress("127.0.0.1",9999));
	//對於客戶端,最開始要註冊連線監聽
	Selector selector = Selector.open();
	sc.register(selector,SelectionKey.OP_CONNECT);
        while(true){
		selector.select();
		Set<SelectionKey> set = selector.selectedKeys();
		Iterator<SelectionKey> iter = set.iterator();
		while(iter.hasNext()){
			SelectionKey sk = iter.next();
			if(sk.isConnectable()){
			}
			if(sk.isWritable()){
			}
			if(sk.isReadable()){
			}
			iter.remove();
		}
	}
}
}複製程式碼