在IO執行有兩個階段很重要:
- 等待資料準備
- 將資料從核心複製到程式中
BIO(Blocking I/O)阻塞I/O模型
當使用者程式呼叫了recvfrom這個系統呼叫,核心就開始了io的第一個階段:等待資料準備。如果資料還沒準備好(比如還沒有收到一個完整的udp包),這時候核心要等待足夠的資料到來,而在使用者程式這邊,程式會被阻塞。當核心等到資料準備好,程式將資料從核心中拷貝到使用者空間,然後核心返回結果,使用者程式才解除block狀態,重新執行起來。
BIO在IO執行的兩個階段都被阻塞了。
示例程式碼:
public static void server(){
ServerSocket serverSocket = null;
InputStream in = null;
try
{
serverSocket = new ServerSocket(8080);
int recvMsgSize = 0;
byte[] recvBuf = new byte[1024];
while(true){
Socket clntSocket = serverSocket.accept();
SocketAddress clientAddress = clntSocket.getRemoteSocketAddress();
System.out.println("Handling client at "+clientAddress);
in = clntSocket.getInputStream();
while((recvMsgSize=in.read(recvBuf))!=-1){
byte[] temp = new byte[recvMsgSize];
System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
System.out.println(new String(temp));
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
finally{
try{
if(serverSocket!=null){
serverSocket.close();
}
if(in!=null){
in.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
複製程式碼
NIO(Non-Blocking I/O)非阻塞I/O模型
NIO是一種同步非阻塞的IO模型。同步指執行緒不斷輪詢IO事件是否就緒,非阻塞是指執行緒在等待IO的時候,可以做其他任務。同步的核心是Selector,Selector代替了執行緒本身輪詢IO事件,避免了阻塞同時減少了不必要的執行緒消耗;非阻塞的核心是通道和緩衝區,當IO事件就緒時,可以通過寫入緩衝區,保證IO的成功,無需執行緒阻塞式地等待。
當使用者程式呼叫recvfrom時,系統不會阻塞使用者程式,如果資料還沒準備好,系統立刻返回一個ewouldblock錯誤給程式,使用者程式知道資料還沒準備好,使用者程式就可以去做其他事了。程式輪詢核心檢視資料是否準備好。直到資料準備好了,並且收到使用者程式的system call,程式複製資料包,然後返回。
NIO有三大核心部分:Channel(通道),Buffer(緩衝區),Selector(多路複用器)
Channel
基本上,所有的IO在NIO中都從一個Channel開始。資料可以從Channel讀到Buffer中,也可以從Buffer寫到Channel。
Buffer
Buffer通過以下幾個變數來儲存這個資料的當前位置狀態:
capacity:緩衝區陣列的總長度
position:下一個要操作的資料元素的位置
limit:緩衝區陣列中不可操作的下一個元素的位置
mark:用於記錄當前position的前一個位置或者預設是-1
0 <= mark <= position <= limit <= capacity複製程式碼
開始時Buffer的position為0,limit為capacity,程式寫入資料到緩衝區,position往後移。當Buffer寫入資料結束之後,呼叫flip()方法之後(此時limit=position,position=0),Buffer為輸出資料做好準備;當Buffer輸出資料結束之後,呼叫clear()方法(此時limit=capacity,position=0),clear()方法不是清空Buffer的資料,它僅僅將position置為0,將limit置為capacity,為再次向Buffer裝入資料做準備;
Buffer的使用:
- 分配空間:ByteBuffer buf = ByteBuffer.allocate(1024);
- 寫入資料到Buffer:int bytesRead = fileChannel.read(buf);
- 呼叫flip()方法:buf.flip();
- 從Buffer中讀取資料:buf.get();
- 呼叫clear()方法或compact()方法
Buffer一些重要方法:
- flip():把limit設定為position,position設定為0,為輸出資料做好準備
- clear():把position設定為0,limit設定為capacity,為再次向Buffer裝入資料做準備
- compact():將所有未讀的資料拷貝到Buffer起始處,把position設定到最後一個未讀元素的正後面,limit設定為capacity
- mark():可以標記Buffer中的一個特定的position,之後可以通過reset()恢復到position的位置
- rewind():將position設回為0,limit保持不變,這樣可以重讀Buffer中的所有資料。
示例程式碼:
public static void client(){
ByteBuffer buffer = ByteBuffer.allocate(1024);
SocketChannel socketChannel = null;
try
{
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("10.10.195.115",8080));
if(socketChannel.finishConnect())
{
int i=0;
while(true)
{
TimeUnit.SECONDS.sleep(1);
String info = "I'm "+i+++"-th information from client";
buffer.clear();
buffer.put(info.getBytes());
buffer.flip();
while(buffer.hasRemaining()){
System.out.println(buffer);
socketChannel.write(buffer);
}
}
}
}
catch (IOException | InterruptedException e)
{
e.printStackTrace();
}
finally{
try{
if(socketChannel!=null){
socketChannel.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
複製程式碼
Selector
Channel和Selector配合使用,必須先將Channel註冊到Selector上,通過register()方法來實現。Channel.register()方法會返回一個SelectionKey物件。這個物件代表了註冊到該Selector的通道。
一旦向Selector註冊了一個或多個通道,就可以呼叫select()方法。
select()方法返回的int值表示有多少通道已經就緒。
參考資料:瘋狂Java講義