目錄
1 同步異步 阻塞非阻塞
故事:老王燒開水。
出場人物:老張,水壺兩把(普通水壺,簡稱水壺;會響的水壺,簡稱響水壺)。
老王想了想,有好幾種等待方式
1.老王用水壺煮水,並且站在那裏,不管水開沒開,每隔一定時間看看水開了沒。-同步阻塞
老王想了想,這種方法不夠聰明。
2.老王還是用水壺煮水,不再傻傻的站在那裏看水開,跑去寢室上網,但是還是會每隔一段時間過來看看水開了沒有,水沒有開就走人。-同步非阻塞
老王想了想,現在的方法聰明瞭些,但是還是不夠好。
3.老王這次使用高大上的響水壺來煮水,站在那裏,但是不會再每隔一段時間去看水開,而是等水開了,水壺會自動的通知他。-異步阻塞
老王想了想,不會呀,既然水壺可以通知我,那我爲什麼還要傻傻的站在那裏等呢,嗯,得換個方法。
4.老王還是使用響水壺煮水,跑到客廳上網去,等着響水壺自己把水煮熟了以後通知他,老王豁然,這下感覺輕鬆了很多-異步非阻塞
-
同步和異步
同步就是燒開水,需要自己去輪詢(每隔一段時間去看看水開了沒),異步就是水開了,然後水壺會通知你水已經開了,你可以回來處理這些開水了。
同步和異步是相對於操作結果來說,會不會等待結果返回。 -
阻塞和非阻塞
阻塞就是說在煮水的過程中,你不可以去幹其他的事情,非阻塞就是在同樣的情況下,可以同時去幹其他的事情。阻塞和非阻塞是相對於線程是否被阻塞。
其實,這兩者存在本質的區別,它們的修飾對象是不同的。阻塞和非阻塞是指進程訪問的數據如果尚未就緒,進程是否需要等待,簡單說這相當於函數內部的實現區別,也就是未就緒時是直接返回還是等待就緒。
而同步和異步是指訪問數據的機制,同步一般指主動請求並等待I/O操作完畢的方式,當數據就緒後在讀寫的時候必須阻塞,異步則指主動請求數據後便可以繼續處理其它任務,隨後等待I/O,操作完畢的通知,這可以使進程在數據讀寫時也不阻塞。
2 Linux的網絡模型
同步阻塞I/O
最常用的一個模型是同步阻塞 I/O 模型。其行爲非常容易理解,其用法對於典型的應用程序來說都非常有效。在調用 read
系統調用時,應用程序會阻塞並對內核進行上下文切換。然後會觸發讀操作,當響應返回時(從我們正在從中讀取的設備中返回),數據就被移動到用戶空間的緩衝區中。然後應用程序就會解除阻塞(read
調用返回)。
圖 2. 同步阻塞 I/O 模型
同步非阻塞 I/O
同步阻塞 I/O 的一種效率稍低的變種是同步非阻塞 I/O。在這種模型中,設備是以非阻塞的形式打開的。這意味着 I/O 操作不會立即完成,read
操作可能會返回一個錯誤代碼,說明這個命令不能立即滿足(EAGAIN
或 EWOULDBLOCK
)
圖 3. 同步非阻塞 I/O 模型
當一個應用進程像這樣對一個非阻塞描述符循環調用recvfrom時,我們稱之爲輪詢(polling)。應用進程只需輪詢內核,以查看某個操作是否就緒。這麼做往往耗費大量CPU時間。
I/O複用模型
I/O 複用有時又被稱爲 事件驅動 I/O, 它的最大優勢在於,我們可以將感興趣的多個I/O事件(更精確的說,應該是 I/O 所對應的文件描述符)註冊到 select/poll/epoll/kqueue 之中某一個系統調用上(很多時候,這些系統調用又被稱爲多路複用器。假設此時我們選擇了 select() )。此後,調用進程會阻塞在 select() 系統調用之上(而不是阻塞在真正的 I/O 系統調用(如 read(), write() 等)上)。select() 會負責監視所有已註冊的 I/O 事件,一旦有任意一個事件的數據準備好,那麼 select() 會立即返回,此時我們的用戶進程便能夠進行數據的複製操作。
圖 4. I/O複用模型
總而言之,I/O 複用的優點就在於可以同時等待多個I/O事件;而缺點是會進行兩次系統調用(一次 select(), 一次 read() )。
信號驅動式I/O模型
在這種模型下,我們首先開啓套接字的信號驅動式I/O功能,並通過sigaction系統調用安裝一個信號處理函數。改系統調用將立即返回,我們的進程繼續工作,也就是說他沒有被阻塞。當數據報準備好讀取時,內核就爲該進程產生一個SIGIO信號。我們隨後就可以在信號處理函數中調用read讀取數據報,並通知主循環數據已經準備好待處理,也可以立即通知主循環,讓它讀取數據報。
圖 5. 信號驅動I/O模型
無論如何處理SIGIO信號,這種模型的優勢在於等待數據報到達期間進程不被阻塞。主循環可以繼續執行,只要等到來自信號處理函數的通知:既可以是數據已準備好被處理,也可以是數據報已準備好被讀取。
異步非阻塞 I/O
異步非阻塞 I/O 模型是一種處理與 I/O 重疊進行的模型。讀請求會立即返回,說明 read
請求已經成功發起了。在後臺完成讀操作時,應用程序然後會執行其他處理操作。當 read
的響應到達時,就會產生一個信號或執行一個基於線程的回調函數來完成這次 I/O 處理過程。
圖 6. 異步非阻塞I/O模型
在一個進程中爲了執行多個 I/O 請求而對計算操作和 I/O 處理進行重疊處理的能力利用了處理速度與 I/O 速度之間的差異。當一個或多個 I/O 請求掛起時,CPU 可以執行其他任務;或者更爲常見的是,在發起其他 I/O 的同時對已經完成的 I/O 進行操作。
各種I/O模型的比較
通過上面的討論可以清楚的看到,同步 I/O 總會有阻塞的過程,這就是“同步”最本質的特徵。而如前文所說,異步 I/O 的最大特點在於用戶進程均不阻塞。 用戶進程告知內核啓動某一 I/O 操作, 並讓內核全權代爲執行(包括等待數據及拷貝數據至用戶空間),此後用戶進程可以立即執行其它的任何操作。等到所有 I/O 過程執行完成後, 內核會通知用戶程。由此可見,在整個過程中,用戶進程均不阻塞。
圖 7. 各種I/O模型的比較1
圖 8 . I/O 模型比較2
I/O模型 | 讀寫操作和阻塞階段 |
阻塞I/O | 應用阻塞於讀寫函數 |
I/O複用 | 應用阻塞於I/O複用系統調用,但可同時監聽多個I/O事件。對I/O本身的讀寫操作是非阻塞的 |
信號驅動I/O | 信號觸發讀寫就緒事件,用戶程序執行讀寫操作。應用沒有阻塞階段 |
異步I/O | 內核執行讀寫操作並觸發讀寫完成事件。應用沒有阻塞階段 |
3 NIO編程
在傳統的I/O中查看JDK源碼的時候,關於read()方法可以在其註釋上看見
從輸入流中讀取下一個數據字節。 值字節作爲int返回,範圍爲0到255.如果沒有字節可用,因爲已到達流的末尾,則返回值-1。 此方法將阻塞,直到輸入數據可用,檢測到流的末尾或拋出異常
對於NIO有人會認爲N代表的是new一個新的IO,而更加貼切的說法是Non-block非阻塞。所以對你NIO來說是一種同步非阻塞式的IO模型。
基本概念
緩衝器Buffer
Buffer是一個對象,它包含要寫入和讀取的數據,在讀取數據的時候直接從Buffer中讀取,在寫入數據的時候,直接寫入到Buffer中區。Buffer實質是一個數組,通常是一個ByteBuffer字節數組,當然也可以是其他六種基本類型的數組,只不過其他數組到最後都要轉成ByteBuffer數組,所以最常用的就是ByteBuffer。
通道Channel
Channel是一個通道,像一個水管一樣,網絡數據通過Channel進行讀取和寫入的操作,不同於傳統的IO流,Channel是雙向適用於讀,寫或者兩者一起操作。
Channel主要有兩大類 適用於網絡通信的SelectableChannel和適用於文件的FileChannel,而Netty主要的是使用SelectableCahnnel,而在網絡通信中就有服務器和客戶端一說,服務器對應的是ServerSocketChannel,而客戶端對應的就是SocketChannel。
多路複用器Selector
多路複用器提供了選擇已經就緒的任務的能力,簡單來說就是Selector會不斷的輪詢註冊在其上的Channel,只要某個Channel發生讀或者寫,那麼這Channel就處於就緒狀態。一個多路複用器Selector可以同時輪詢多個Channel,這也就表明,只需要一個線程負責Selector的輪詢,就可以接入成千上萬的客戶端。
4 NIO客戶端和服務端序列圖
服務端
步驟一:打開ServerSocketChannel,用於監聽客戶端的連接,它是所有客戶端連接的父管道,代碼示例如下:
ServerSocketChannel acceptorSvr = ServerSocketChannel.open();
步驟二:綁定監聽端口,設置連接爲非阻塞模式,示例代碼如下:
acceptorSvr.socket().bind(new InetSocketAddress(InetAddress.getByName(“IP”), port));
acceptorSvr.configureBlocking(false);
步驟三:創建Reactor線程,創建多路複用器並啓動線程,代碼如下:
Selector selector = Selector.open();
New Thread(new ReactorTask()).start();
步驟四:將ServerSocketChannel註冊到Reactor線程的多路複用器Selector上,監聽ACCEPT事件,代碼如下:
SelectionKey key = acceptorSvr.register( selector, SelectionKey.OP_ACCEPT, ioHandler);
步驟五:多路複用器在線程run方法的無限循環體內輪詢準備就緒的Key,代碼如下:
int num = selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey)it.next();
// ... deal with I/O event ...
}
步驟六:多路複用器監聽到有新的客戶端接入,處理新的接入請求,完成TCP三次握手,建立物理鏈路,代碼示例如下:
SocketChannel channel = svrChannel.accept();
步驟七:設置客戶端鏈路爲非阻塞模式,示例代碼如下:
channel.configureBlocking(false);
channel.socket().setReuseAddress(true);
步驟八:將新接入的客戶端連接註冊到Reactor線程的多路複用器上,監聽讀操作,用來讀取客戶端發送的網絡消息,代碼如下:
SelectionKey key = socketChannel.register( selector, SelectionKey.OP_READ, ioHandler);
步驟九:異步讀取客戶端請求消息到緩衝區,示例代碼如下:
int readNumber = channel.read(receivedBuffer);
步驟十:對ByteBuffer進行編解碼,如果有半包消息指針reset,繼續讀取後續的報文,將解碼成功的消息封裝成Task,投遞到業務線程池中,進行業務邏輯編排,示例代碼如下:
Object message = null;
while(buffer.hasRemain())
{
byteBuffer.mark();
Object message = decode(byteBuffer);
if (message == null)
{
byteBuffer.reset();
break;
}
messageList.add(message );
}
if (!byteBuffer.hasRemain())
byteBuffer.clear();
else
byteBuffer.compact();
if (messageList != null & !messageList.isEmpty())
{
for(Object messageE : messageList)
handlerTask(messageE);
}
步驟十一:將POJO對象encode成ByteBuffer,調用SocketChannel的異步write接口,將消息異步發送給客戶端,示例代碼如下:
socketChannel.write(buffer);
客戶端
步驟一:打開SocketChannel,綁定客戶端本地地址(可選,默認系統會隨機分配一個可用的本地地址),示例代碼如下:
SocketChannel clientChannel = SocketChannel.open();
步驟二:設置SocketChannel爲非阻塞模式,同時設置客戶端連接的TCP參數,示例代碼如下:
clientChannel.configureBlocking(false);
socket.setReuseAddress(true);
socket.setReceiveBufferSize(BUFFER_SIZE);
socket.setSendBufferSize(BUFFER_SIZE);
步驟三:異步連接服務端,示例代碼如下:
boolean connected = clientChannel.connect(new InetSocketAddress(“ip”,port));
步驟四:判斷是否連接成功,如果連接成功,則直接註冊讀狀態位到多路複用器中,如果當前沒有連接成功(異步連接,返回false,說明客戶端已經發送sync包,服務端沒有返回ack包,物理鏈路還沒有建立),示例代碼如下:
if (connected)
{
clientChannel.register( selector, SelectionKey.OP_READ, ioHandler);
}
else
{
clientChannel.register( selector, SelectionKey.OP_CONNECT, ioHandler);
}
步驟五:向Reactor線程的多路複用器註冊OP_CONNECT狀態位,監聽服務端的TCP ACK應答,示例代碼如下:
clientChannel.register( selector, SelectionKey.OP_CONNECT, ioHandler);
步驟六:創建Reactor線程,創建多路複用器並啓動線程,代碼如下:
Selector selector = Selector.open();
New Thread(new ReactorTask()).start();
步驟七:多路複用器在線程run方法的無限循環體內輪詢準備就緒的Key,代碼如下:
int num = selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
if (key.isConnectable())
//handlerConnect();
}
步驟九:判斷連接結果,如果連接成功,註冊讀事件到多路複用器,示例代碼如下:
if (channel.finishConnect())
registerRead();
步驟十:註冊讀事件到多路複用器:
clientChannel.register( selector, SelectionKey.OP_READ, ioHandler);
步驟十一:異步讀客戶端請求消息到緩衝區,示例代碼如下:
int readNumber = channel.read(receivedBuffer);
步驟十二:對ByteBuffer進行編解碼,如果有半包消息接收緩衝區Reset,繼續讀取後續的報文,將解碼成功的消息封裝成Task,投遞到業務線程池中,進行業務邏輯編排,示例代碼如下:
Object message = null;
while(buffer.hasRemain())
{
byteBuffer.mark();
Object message = decode(byteBuffer);
if (message == null)
{
byteBuffer.reset();
break;
}
messageList.add(message );
}
if (!byteBuffer.hasRemain())
byteBuffer.clear();
else
byteBuffer.compact();
if (messageList != null & !messageList.isEmpty())
{
for(Object messageE : messageList)
handlerTask(messageE);
}
步驟十三:將POJO對象encode成ByteBuffer,調用SocketChannel的異步write接口,將消息異步發送給客戶端,示例代碼如下:
socketChannel.write(buffer);