快速掌握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 testContect() 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(下)

一、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);這樣即便沒有讀到數據,「有數據讀入了。。」也能打印出來。

二、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();
		}
	}
}
}複製代碼
相關文章
相關標籤/搜索