BIO、NIO、IO的三種機制

一、BIO-blocking IO:同步阻塞式IO

在執行accept、 connect、 read、 write這四步操作的過程中都會產生阻塞。
服務端:

public static void main(String[] args) throws Exception {
	//1.創建服務端
	ServerSocket ss = new ServerSocket();
	//2.綁定監聽指定端口
	ss.bind(new InetSocketAddress(12345));
	//3.等待客戶端連接 - ACCEPT產生阻塞
	ss.accept();
}

客戶端:

public static void main(String[] args) throws Exception {
		//1.創建客戶端
		Socket s = new Socket();
		//2.連接指定服務器指定端口
		s.connect(new InetSocketAddress("127.0.0.1", 12345));
		
		while(true){}
}

運行結果:
拋出異常,連接失敗,accept,connect操作產生阻塞

服務端:

public static void main(String[] args) throws Exception {
		ServerSocket ss = new ServerSocket();
		ss.bind(new InetSocketAddress(12345));
		Socket socket = ss.accept();
		InputStream in = socket.getInputStream();
		in.read();

客戶端:

public static void main(String[] args) throws Exception {
		Socket s = new Socket();
		s.connect(new InetSocketAddress("127.0.0.1",12345));
		while(true){}
}

運行結果:
異常,read阻塞。

服務端:

public static void main(String[] args) throws Exception {
		ServerSocket ss = new ServerSocket();
		ss.bind(new InetSocketAddress(12345));
		Socket socket = ss.accept();		
		while(true){}
	}

客戶端:

public static void main(String[] args) throws Exception {
		Socket s = new Socket();
		s.connect(new InetSocketAddress("127.0.0.1", 12345));		
		//向服務端輸出數據 
		OutputStream out = s.getOutputStream();
		int i = 0;
		while(true){
			out.write("a".getBytes());
			System.out.println(++i);
		}
	}

運行結果:
異常,write操作產生阻塞,一開始可以寫進一點數據,因爲緩存區可以放一些,但是一直沒有讀操作,一直寫就會阻塞。

**

二、BIO代碼理解

**
服務端:

class SRunable implements Runnable{
	private Socket s = null;
	public SRunable(Socket s) {
		this.s = s;
	}
	
	@Override
	public void run() {
		try {
			//5.從socket中讀取數據
			InputStream in = s.getInputStream();
			int len = in.available();
			byte data[] = new byte[len];
			in.read(data,0,len);  
			String str = new String(data);
			System.out.println(str);
			s.close();
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}	
}
public class ServerSocket1 {
	public static void main(String[] args) throws Exception {
		ServerSocket ss = new ServerSocket();
		ss.bind(new InetSocketAddress(12345));
		while(true){
			//4.循環等待客戶端連接,一旦有連接成功,開啓線程進行處理
			Socket s = ss.accept();
			new Thread(new SRunable(s)).start();
		}	
	}
}

服務端:

public class Socket1{
	public static void main(String[] args) throws Exception {
		Socket s = new Socket();
		s.connect(new InetSocketAddress("127.0.0.1",12345));
		OutputStream out = s.getOutputStream();
		out.write("hello".getBytes());
		out.flush();
		
		//4.關閉連接
		out.close();
	}
}

開線程的圖示如下:
在這裏插入圖片描述

造成線程資源的浪費:
應該:連接之後不立即開線程,而是有個中轉中心,誰真的使用,中心轉發交給線程。達到少量線程處理多請求。
在這裏插入圖片描述

但是這種模型BIO不適用。所以引入NIO。

三、NIO(NonBlockingIO)

` 相比於傳統的BIO最主要的特點是:在執行accept、connect、read、write操作時是非阻塞的。
` 在服務器開發中, 用少量的線程來處理多個客戶端請求, 由於以上四種操作都是非阻塞的, 可以隨時讓線程切換所處理的客戶端 ,從而可以實現高併發服務器的開發。
兩者的區別:
BIO:同步阻塞式IO —面向流—操作字節或字符—單向傳輸數據,
NIO:同步非阻塞式IO—面向通道—操作緩衝區—雙向傳輸數據。

四、緩衝區Buffer

1、概述:
是一段連續的內存空間,用來臨時存放大量指定類型的數據
java.nio.Buffer—abstract
子類有7個,對應基本數據類型,沒有布爾類:
ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer

2、重要定義:
capacity - 容量,在創建Buffer時就需要指定好,後續不可修改
position - 當前位置,初始值爲0,指定Buffer進行讀寫操作時操作位置,每當操作過後position自動+1指向下一個位置
limit - 限制位,初始值等於capacity,position永遠小於等於limit

3、創建緩衝區
沒有構造方法,有靜態方法。
方法一:直接創建指定大小的空緩衝區

ByteBuffer.allocate(capacity); 

方法二:通過已經有的字節數組創建緩衝區

byte [] data = “hello”.getBytes();
ByteBuffer buf = ByteBuffer.wrap(data);

4、向緩衝區寫入數據
position指向寫入數據數據的位置,每當寫入一個數據,position自動+1指向下一個位置,position不可大於limit,如果一直寫入,達到limit大小,再寫入會拋出異常
通過.putXXX()寫入數據,可以是不同類型的數據
(1)順序寫入數據

buffer.put("a".getBytes());

(2)手動指定position,

buffer.position(2);
buffer.put("f".getBytes());

(3)不修改position,直接覆蓋,修改指定位置的值

buffer.put(2, "f".getBytes()[0]);

5、從buffer中獲取數據
position指向讀取數據的位置,每當讀到一個數據,position自動+1指向下一個位置,position不可大於limit,如果一直讀取,達到limit大小,再讀取會拋出異常。
讀取數據通過.getXXX()。

byte [] data = new byte[1];
buffer.get(data);
System.out.println(new String(data));

手動控制limit和position實現讀取,並防止越界

		buffer.limit(buffer.position());
		buffer.position(0);
		while(buffer.position()<buffer.limit()){
			byte [] data = new byte[1];
			buffer.get(data);
			System.out.println(new String(data));
}

這樣做的便捷方法:

6、反轉緩衝區

buffer.flip();

反轉緩衝區本質上等價於先把limit放到positon,在把position歸0,這兩步操作。實現讀取而且不越界。

7、判斷邊界

.remaining()和.hasRemaining()

前者可以返回 limit - position的值,通常用來獲取讀寫時是距離邊界的距離
後者可以返回 limit-position>0 的值,通常用來判斷度寫時是否到達了邊界
後者buffer.hasRemaining()等價於前者buffer.remaining()>0

8、重繞緩衝區
也就是重新從頭讀取數據
通過.rewind(); 等價於.position(0);

9、設置和重置標記的方法

.mark();

//回到標記的位置

.reset();

此處注意mark的位置:
例子:

public void test(){
		ByteBuffer buffer = ByteBuffer.allocate(5);
		buffer.put("a".getBytes());
		buffer.put("b".getBytes());
		buffer.put("c".getBytes());
		buffer.put("d".getBytes());
		buffer.put("e".getBytes());
		buffer.flip();
		byte [] data = new byte[1];
		buffer.get(data);
		System.out.println(new String(data));
		data = new byte[1];
		buffer.get(data);
		System.out.println(new String(data));
		//--打標記
		buffer.mark();
		data = new byte[1];
		buffer.get(data);
		System.out.println(new String(data));
		data = new byte[1];
		buffer.get(data);
		System.out.println(new String(data));	
		//--回到標記,buffer.position(mark的位置)
		buffer.reset();
		data = new byte[1];
		buffer.get(data);
		System.out.println(new String(data));
	}

得到的是:c。

10、清空緩衝

.clear();

等價於position=0.limit=capacity=n,mark取消。
· 清空緩衝之後,再執行.position(n);//還能得到n上的值,說明數據不會清除。雖然數據不會清除,但是後續正常的寫入讀取的操作不會受影響,因爲沒有機會讀到未刪除的垃圾數據。

五、通道Channel

1、概述:
通道注意是個接口,操作的是緩衝區,可以雙向傳輸數據。

2、ServerSocketChannel:
tcp通信中的服務器端
通過.open()創建,通過.socket()獲取底層的套接字對象,通過bind(InetSocketAddress(端口))綁定監聽端口(JDK7纔有,舊方法是:ssc.socket().bind();)。
直接運行的話,運行結果是阻塞,因爲ServerSocketChannel默認情況下是阻塞模式。設置爲非阻塞:

ssc.sonfigureBlocking(false);

.accept()是等待客戶端連接,在阻塞模式下會一直阻塞直到客戶端連接,返回一個代表鏈接的SockentChannel對象sc。在非阻塞模式下,此方法不會阻塞,直接執行下去,如果沒有得到一個新的連接,此方法返回null。
注意:
在非阻塞模式下,accept操作沒有阻塞,無論是否收到一個連接,都直接執行下去,此時即使accept方法執行成功,也無法確認連接完成.此時應該自己通過代碼來控制實現連接,或者,通過選擇器來實現選擇操作.

SocketChannel sc = null;
   while(sc == null){
   sc = ssc.accept();
}

能走下來說明連接成功繼續執行後續操作:從sc中讀取數據。

3、SocketChannel:
tcp通信中的客戶端
open().configureBlocking()和服務端一樣,還有connect(),finishConnect()
connect()命令客戶端連接指定服務器地址端口, 如果通道處於阻塞模式, 則此方法會一直阻塞, 直到 連接成功。 而如果通道處於非阻塞模式, 此方法將僅僅嘗試着去連接, 如果連接成功則返回true; 如果連接一時間沒有結束, 也不阻塞程序, 此方法返回false。 程序繼續執行, 此時需要在後續調用finishConnection方法來完成連接。finishConnection方法也是非阻塞的 調用結束並不意味着連接完成 所以如果此方法返回false, 應該繼續重複調用,直到返回true才表明連接完成。

while(!sc.isConnected()){
sc.finishConnect();
}

六、NIO案例

服務端:

package cn.nio.channel;
 
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
 
public class ServerSocketChannel1 {
public static void main(String[] args) throws Exception {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(12345));
ssc.configureBlocking(false);
SocketChannel sc = null;
while(sc == null){
sc = ssc.accept();
}
sc.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(5);  
while(buf.hasRemaining()){
sc.read(buf);
}
byte[] arr = buf.array();
String str = new String(arr);
System.out.println(str);
 sc.close();
ssc.close();
}
}

客戶端:

package cn.nio.channel;
 
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
 
public class SocketChannel1 {
public static void main(String[] args) throws Exception {
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
boolean isConn = sc.connect(new InetSocketAddress("127.0.0.1", 12345));
if(!isConn){
while(!sc.finishConnect()){
}
}
 ByteBuffer buf = ByteBuffer.wrap("hello".getBytes());
while(buf.hasRemaining()){
sc.write(buf);
}
sc.close();
}
}

七、選擇器Selector

概述:
目的是實現少量線程服務多個客戶端。一方面允許多個客戶端連接註冊到選擇器中關注對應的事件.另一方面提供了"選擇"操作 來在之前註冊的操作中選擇已經就緒的操作 交給線程來執行. 一頭連接了多個客戶端連接 另一頭連接了少量的線程,進行協調 。
創建唯一的選擇器通過.open()
註冊通道到選擇器通過.regist(選擇器,要關注的事件)
這裏的第二個參數是int類型,可供選擇的操作類型;

OP_ACCEPT 
          用於套接字接受操作的操作集位。
OP_CONNECT 
          用於套接字連接操作的操作集位。
OP_READ 
          用於讀取操作的操作集位。
OP_WRITE 
          用於寫入操作的操作集位。

regist方法返回的是一個SelectionKey對象。此對象是一個代表本次註冊事件的對象。從這個對象上可以得到對應的是哪個通道,註冊在哪個選擇器上,關注的是哪個事件。
選擇已經就緒的事件通過.select();如果沒有時間就緒,則進入阻塞狀態。返回類型是int。
獲取就緒的鍵通過.selectedKeys();//返回的是selectionKey組成的集合,集合中的selectionKey包含通道的引用和哪類事件的信息,類似於去銀行存錢的,銀行給的存摺,誰,什麼時候,存了多少錢。

八、利用Selector+channel+Buffer實現 少量線程處理多個客戶端請求

客戶端

public class SocketChannel1 {
    public static void main(String[] args) throws Exception {
	    Selector selc = Selector.open();
	    SocketChannel sc = SocketChannel.open();
	    sc.configureBlocking(false);
	    sc.connect(new InetSocketAddress("127.0.0.1", 12345));
	    sc.register(selc, SelectionKey.OP_CONNECT);
 
	    //通過選擇器實行選擇操作
	    while(true){
	    selc.select();//選擇器嘗試選擇就緒的鍵 選不到就阻塞 選擇到就返回就緒的鍵的數量
 
		    //得到並遍歷就緒的鍵們
		    Set<SelectionKey> keys = selc.selectedKeys();
		    Iterator<SelectionKey> it = keys.iterator();
		    while(it.hasNext()){
		    //得到每一個就緒的鍵
		    SelectionKey key = it.next();
		    //獲取就緒的鍵 對應的 操作 和 通道
		    if(key.isAcceptable()){
     
		    }else if(key.isConnectable()){
		    //--是通道的Connect操作
		    //--獲取通道
		    SocketChannel scx = (SocketChannel) key.channel();
		    if(!scx.isConnected()){
		    while(!scx.finishConnect()){};
    }
	    //--將通道再次註冊到selc中 關注write操作
	    scx.register(selc, SelectionKey.OP_WRITE);
	    }else if(key.isReadable()){
     
	    }else if(key.isWritable()){
	    //--發現是Write操作就緒
	    SocketChannel scx = (SocketChannel) key.channel();
	    ByteBuffer buf = ByteBuffer.wrap("hello ".getBytes());
	    while(buf.hasRemaining()){
	    scx.write(buf);
	    }
	    //取消掉當前通道 在選擇器中的註冊 防止重複寫出
	    key.cancel();
	    //scx.close();
	    }else{
	    throw new RuntimeException("未知的鍵");
	    }
	    //移除就緒鍵
	    it.remove();
   			 }
  	 	 }
  	 }
 }

服務端

public class ServerSocket1 {
 public static void main(String[] args) throws Exception {
    Selector selc = Selector.open();
    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.configureBlocking(false);
    ssc.bind(new InetSocketAddress(12345));
    //將ssc註冊到選擇器中關注ACCEPT操作
    ssc.register(selc, SelectionKey.OP_ACCEPT);
    //通過選擇器選擇就緒的鍵
    while(true){
    selc.select();//嘗試到註冊的鍵集中來尋找就緒的鍵 如果一個就緒的鍵都找不到 就進入阻塞 直到找到就緒的鍵 返回就緒的鍵的個數
 
    //獲取就緒的鍵的集合
    Set<SelectionKey> keys = selc.selectedKeys();
 
	//遍歷處理就緒的鍵 代表的操作
	 Iterator<SelectionKey> it = keys.iterator();
	  while(it.hasNext()){
	    //--獲取到就緒的鍵 根據鍵代表的操作的不同 來進行不同處理
	    SelectionKey key = it.next();
 
		    if(key.isAcceptable()){
		    //--發現了Accept操作 
		    ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
		    SocketChannel sc = sscx.accept();
		    sc.configureBlocking(false);
		    sc.register(selc, SelectionKey.OP_READ);
		    }else if(key.isConnectable()){
		     
		    }else if(key.isWritable()){
		     
		    }else if(key.isReadable()){
		    //--發現了Read操作
		    SocketChannel scx = (SocketChannel) key.channel();
		    ByteBuffer buf = ByteBuffer.allocate(5);
		    while(buf.hasRemaining()){
		    scx.read(buf);
		    }
		    String msg = new String(buf.array());
		    System.out.println("[收到來自客戶端的消息]:"+msg);
		    }else{
		    throw new RuntimeException("未知的鍵,");
		    }

		 it.remove();
		 }
	  }
   }
}

九、粘包問題

1、概述:
當通過socket發送多段數據時,底層的tcp協議會自動根據需要將數據拆分或合併 組成數據包後發送給接受者 ,接受者收到數據後 ,無法直接通過tcp協議本身判斷數據的邊界,這個問題就稱之爲粘包問題。
比如:三條數據111、22222、33經過傳輸之後,得到就是1112222233

2、問題本質:
粘包問題本質上是因爲tcp協議是傳輸層的協議 本身沒有對會話控制提供相應的能力 我們基於socket開發網絡程序時 相當於在自己實現 會話層 表示層 和應用層的功能 所以 需要自己來相辦法解決粘包問題。

3、粘包問題的解決方案
(1)只發送固定長度的數據
通信的雙發約定每次發送數據的長度,每次只發送固定長度的數據,接收數據方 每次都按照固定長度獲取數據
缺點:
不夠靈活,只適合每次傳輸的數據都有固定長度的場景
(2)約定分隔符
通信雙方約定一個特殊的分隔符用來表示數據的邊界,接收方收到數據時,不停讀取,以分隔符爲標誌,區分數據的邊界
缺點:
如果數據本身就包含分隔符字符,則需要對數據進行預處理將數據本身包含的分隔符進行轉義,相對來說比較麻煩
(3)使用協議—分頭和體傳輸數據
在頭信息中描述數據的格式和長度信息,在接收方接收數據時,先讀取頭信息,再根絕頭信息來決定獲取後續數據。
我們自己規定了規則,實現了通信,可以認爲是私有的協議,在小範圍內實現通信。

十 、通過自定義協議完成任意長度數據通信

服務端:

public class ServerSocketChannel1 {
	public static void main(String[] args) throws Exception {
		 Selector selc = Selector.open();
		 ServerSocketChannel ssc = ServerSocketChannel.open();
		 ssc.configureBlocking(false);
		 ssc.bind(new InetSocketAddress(12345));
		ssc.register(selc, SelectionKey.OP_ACCEPT);
 
	 while(true){
		 selc.select();
		 Set<SelectionKey> keys = selc.selectedKeys();
		 Iterator<SelectionKey> it = keys.iterator();
	   
	    while(it.hasNext()){
	    SelectionKey key = it.next();
 
	    if(key.isAcceptable()){
	    ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
	    SocketChannel sc = sscx.accept();
	    sc.configureBlocking(false);
	    sc.register(selc,SelectionKey.OP_READ);
	    }else if(key.isConnectable()){
 
	    }else if(key.isReadable()){
	    SocketChannel scx = (SocketChannel) key.channel();
	    ByteBuffer tmp = ByteBuffer.allocate(1);
	    String line = "";
	    while(!line.endsWith("\r\n")){
	    scx.read(tmp);
	    line += new String(tmp.array());
	    tmp.clear();
	    }
	    int len = Integer.parseInt(line.substring(0, line.length()-2));
	    ByteBuffer buf = ByteBuffer.allocate(len);
	    while(buf.hasRemaining()){
	    scx.read(buf);
	    }
    String msg = new String(buf.array());
    System.out.println("收到了來自客戶端的消息:["+msg+"]");

    scx.register(selc, SelectionKey.OP_WRITE);
 
	    }else if(key.isWritable()){
	    SocketChannel scx = (SocketChannel) key.channel();
	    String str = "來自服務器的相應消息:[你好,客戶端]";
	    String data = str.getBytes().length+"\r\n"+str;
	    ByteBuffer buf = ByteBuffer.wrap(data.getBytes());
	    while(buf.hasRemaining()){
	    scx.write(buf);
	    }
	    key.cancel();
	    }else{
	    throw new RuntimeException("未知的鍵");
	    }
	    it.remove();
		  }
	   }
	 }
 }

客戶端:

public class SocketChannel1 {
	public static void main(String[] args) throws Exception {
		Selector selc = Selector.open();
	    SocketChannel sc = SocketChannel.open();
	    sc.configureBlocking(false);
	    sc.connect(new InetSocketAddress("127.0.0.1", 12345));
	    sc.register(selc, SelectionKey.OP_CONNECT);

	    while(true){
	    selc.select();

		Set<SelectionKey> keys = selc.selectedKeys();	
	    Iterator<SelectionKey> it = keys.iterator();
		while(it.hasNext()){
		SelectionKey key = it.next();
 
	    if(key.isAcceptable()){
 
	    }else if(key.isConnectable()){
	    SocketChannel scx = (SocketChannel) key.channel();
	    if(!scx.isConnected()){
	    while(!scx.finishConnect()){}
	    }
	    scx.register(selc, SelectionKey.OP_WRITE);
	    }else if(key.isReadable()){
	    SocketChannel scx = (SocketChannel) key.channel();
	    ByteBuffer tmp = ByteBuffer.allocate(1);
	    String line = "";
	    while(!line.endsWith("\r\n")){
	    scx.read(tmp);
	    line += new String(tmp.array());
	    tmp.clear();
	    }
	    int len = Integer.parseInt(line.substring(0, line.length()-2));
	    ByteBuffer buf = ByteBuffer.allocate(len);
	    while(buf.hasRemaining()){
	    scx.read(buf);
	    }
	    String msg = new String(buf.array());
	    System.out.println("收到了來自服務器的響應:["+msg+"]");
	     
	    }else if(key.isWritable()){
	    SocketChannel scx = (SocketChannel) key.channel();
	    String str = "hello java hello nio hello China~";
	    String data = str.getBytes().length+"\r\n"+str;
	    ByteBuffer buf = ByteBuffer.wrap(data.getBytes());
	    while(buf.hasRemaining()){
	    scx.write(buf);
	    }
	     
	    scx.register(selc, SelectionKey.OP_READ);
	    }else{
	    throw new RuntimeException("未知的鍵");
	    }
	    it.remove();
			}
		 }
	 }
}

十一、三種IO機制的區別

阻塞/非阻塞:
考慮的是線程的角度,當執行某些操作不能立即完成時,線程是否被掛起,失去cpu爭奪權,無法繼續執行,直到阻塞結束或被喚醒。
同步/異步:
考慮的是參與通信雙方的工作機制,是否需要互相等待對方的執行.
同步指的是通信過程中, 一方在處理通信 ,另一方要等待對方執行不能去做其他無關的事。
異步指的是通信過程中 ,一方在處理通信, 另一方可以不用等待對方可以去做其他無關的事, 直到對方處理通信完成, 再在適合的時候繼續處理通信過程。

BIO jdk1.0 同步阻塞式IO 面向流 操作字節或字符 單向傳輸數據
NIO jdk4.0 同步非阻塞式IO 面向通道 操作緩衝區 雙向傳輸數據
AIO jdk7.0 異步非阻塞式IO 大量使用回調函數 異步處理通信過程

補充:
NIO常見的框架:MINA Netty

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章