說明:nio理論及例子,瞭解nio的可以跳過本文看Hadoop的rpc實現,建議新手看看
一、阻塞式BIO的缺點:
前面自己實現了一個阻塞式BIO服務,採 用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,接收到客戶端連接之後爲客戶端連接創建一個新的線程處理請求消 息,處理完成之後,返回應答消息給客戶端,線程銷燬,這就是典型的一請求一應答模型。該架構最大的問題就是不具備彈性伸縮能力,當併發訪問量增加後,服務 端的線程個數和併發訪問數成線性正比,由於線程是Java虛擬機非常寶貴的系統資源,當線程數膨脹之後,系統的性能急劇下降,隨着併發量的繼續增加,可能 會發生句柄溢出、線程堆棧溢出等問題,並導致服務器最終宕機。(http://www.open-open.com/lib/view/open1403057331075.html)
根據阻塞I/O通信模型,它的兩缺點:
1. 當客戶端多時,會創建大量的處理線程。且每個線程都要佔用棧空間和一些CPU時間
2. 阻塞可能帶來頻繁的上下文切換,且大部分上下文切換可能是無意義的。
(本圖來自: http://www.open-open.com/lib/view/open1403057331075.html)
二、異步非阻塞通信的引入
在 IO編程過程中,當需要同時處理多個客戶端接入請求時,可以利用多線程或者IO多路複用技術進行處理。IO多路複用技術通過把多個IO的阻塞複用到同一個 select的阻塞上,從而使得系統在單線程的情況下可以同時處理多個客戶端請求。與傳統的多線程/多進程模型比,I/O多路複用的最大優勢是系統開銷 小,系統不需要創建新的額外進程或者線程,也不需要維護這些進程和線程的運行,降低了系統的維護工作量,節省了系統資源。JDK1.4提供了對非阻塞IO(NIO)的支持,JDK1.5_update10版本使用epoll替代了傳統的select/poll,極大的提升了NIO通信的性能。
關於NIO知識詳細見:點擊打開鏈接 http://blog.csdn.net/lzlchangqi/article/details/412097191. 由一個專門的線程來處理所有的 IO 事件,並負責分發。
2. 事件驅動機制:事件到的時候觸發,而不是同步的去監視事件。
3. 線程通訊:線程之間通過 wait,notify 等方式通訊。保證每次上下文切換都是有意義的。減少無謂的線程切換。
如下圖:一個線程Reactor用來處理所有io,並分發read、write等事件
(本圖來自互聯網)
三:結合互聯網的例子進行分析NIO:
1、先看例子的源代碼,不妨debug調試下
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.net.ServerSocket;
- import java.nio.ByteBuffer;
- import java.nio.channels.SelectionKey;
- import java.nio.channels.Selector;
- import java.nio.channels.ServerSocketChannel;
- import java.nio.channels.SocketChannel;
- import java.util.Iterator;
- import java.util.Set;
- public class NIOServer {
- /*標識數字*/
- private int flag = 0;
- /*緩衝區大小*/
- private int BLOCK = 4096;
- /*接受數據緩衝區*/
- private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
- /*發送數據緩衝區*/
- private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
- private Selector selector;
- public NIOServer(int port) throws IOException {
- // 1、打開服務器套接字通道
- ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
- // 服務器配置爲非阻塞
- serverSocketChannel.configureBlocking(false);
- // 檢索與此通道關聯的服務器套接字
- ServerSocket serverSocket = serverSocketChannel.socket();
- //2、 進行服務的綁定
- serverSocket.bind(new InetSocketAddress(port));
- //3、 通過open()方法找到Selector
- selector = Selector.open();
- //4、註冊到selector,等待連接
- serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
- System.out.println("Server Start----8888:");
- }
- // 監聽
- private void listen() throws IOException {
- while (true) {
- // 選擇一組鍵,並且相應的通道已經打開
- selector.select();
- // 返回此選擇器的已選擇鍵集。
- Set<SelectionKey> selectionKeys = selector.selectedKeys();
- Iterator<SelectionKey> iterator = selectionKeys.iterator();
- //5 輪詢就緒的key
- while (iterator.hasNext()) {
- SelectionKey selectionKey = iterator.next();
- iterator.remove();
- handleKey(selectionKey);
- }
- }
- }
- // 處理請求
- private void handleKey(SelectionKey selectionKey) throws IOException {
- // 接受請求
- ServerSocketChannel server = null;
- SocketChannel client = null;
- String receiveText;
- String sendText;
- int count=0;
- // 測試此鍵的通道是否已準備好接受新的套接字連接。
- //步驟6 handle connect 處理新的客戶接入
- if (selectionKey.isAcceptable()) {
- // 返回爲之創建此鍵的通道。
- //步驟7 設置新建連接的socket
- server = (ServerSocketChannel) selectionKey.channel();
- // 接受到此通道套接字的連接。
- // 此方法返回的套接字通道(如果有)將處於阻塞模式。
- client = server.accept();
- // 配置爲非阻塞
- client.configureBlocking(false);
- //步驟8 註冊到selector,等待連接
- client.register(selector, SelectionKey.OP_READ);
- } else if (selectionKey.isReadable()) {//步驟9:異步處理請求消息到ByteBuffer(),代碼中沒有步驟10
- // 返回爲之創建此鍵的通道。
- client = (SocketChannel) selectionKey.channel();
- //將緩衝區清空以備下次讀取
- receivebuffer.clear();
- //讀取服務器發送來的數據到緩衝區中
- count = client.read(receivebuffer);
- if (count > 0) {
- receivebuffer.flip();
- byte[] bytes = new byte[receivebuffer.remaining()];
- receivebuffer.get(bytes);
- receiveText = new String(bytes,"utf-8");
- System.out.println("服務器端接受客戶端數據--:"+receiveText);
- client.register(selector, SelectionKey.OP_WRITE);
- }
- } else if (selectionKey.isWritable()) {//步驟11 異步寫
- //將緩衝區清空以備下次寫入
- sendbuffer.clear();
- // 返回爲之創建此鍵的通道。
- client = (SocketChannel) selectionKey.channel();
- sendText="message from server--" + flag++;
- //向緩衝區中輸入數據
- sendbuffer.put(sendText.getBytes());
- //將緩衝區各標誌復位,因爲向裏面put了數據標誌被改變要想從中讀取數據發向服務器,就要復位
- sendbuffer.flip();
- //輸出到通道
- client.write(sendbuffer);
- System.out.println("服務器端向客戶端發送數據--:"+sendText);
- client.register(selector, SelectionKey.OP_READ);
- }
- }
- /**
- * @param args
- * @throws IOException
- */
- public static void main(String[] args) throws IOException {
- // TODO Auto-generated method stub
- int port = 8888;
- NIOServer server = new NIOServer(port);
- server.listen();
- }
- }
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.nio.ByteBuffer;
- import java.nio.channels.SelectionKey;
- import java.nio.channels.Selector;
- import java.nio.channels.SocketChannel;
- import java.util.Iterator;
- import java.util.Set;
- public class NIOClient {
- /*標識數字*/
- private static int flag = 0;
- /*緩衝區大小*/
- private static int BLOCK = 4096;
- /*接受數據緩衝區*/
- private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
- /*發送數據緩衝區*/
- private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
- /*服務器端地址*/
- private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
- "localhost", 8888);
- public static void main(String[] args) throws IOException {
- // TODO Auto-generated method stub
- //步驟1 打開socket的通道socketChannel
- SocketChannel socketChannel = SocketChannel.open();
- //步驟2 設置爲非阻塞方式,同時設置tcp參數
- socketChannel.configureBlocking(false);
- Selector selector = null;
- // 異步連接服務器
- if (socketChannel.connect(SERVER_ADDRESS)) {
- }
- else {
- //步驟5 註冊連接服務端socket動作
- selector = Selector.open();
- socketChannel.register(selector, SelectionKey.OP_CONNECT);
- }
- // 分配緩衝區大小內存
- Set<SelectionKey> selectionKeys;
- Iterator<SelectionKey> iterator;
- SelectionKey selectionKey;
- SocketChannel client;
- String receiveText;
- String sendText;
- int count=0;
- //步驟6 啓動線程
- while (true) {
- //選擇一組鍵,其相應的通道已爲 I/O 操作準備就緒。
- //此方法執行處於阻塞模式的選擇操作。
- int ret = selector.select();
- //System.out.println(ret);
- //返回此選擇器的已選擇鍵集。
- selectionKeys = selector.selectedKeys();
- //System.out.println(selectionKeys.size());
- iterator = selectionKeys.iterator();
- while (iterator.hasNext()) {//7 輪詢就緒的key
- selectionKey = iterator.next();
- //4 判斷連接結果,如果連接成功,跳到步驟10,如果不成功,執行步驟5
- if (selectionKey.isConnectable()) {
- System.out.println("client connect");
- client = (SocketChannel) selectionKey.channel();
- // 判斷此通道上是否正在進行連接操作。
- // 完成套接字通道的連接過程。8 handle connect()
- if (client.isConnectionPending()) {
- client.finishConnect();//9 判斷連接完成,完成連接
- System.out.println("完成連接!");
- sendbuffer.clear();
- sendbuffer.put("Hello,Server".getBytes());
- sendbuffer.flip();
- client.write(sendbuffer);
- }
- //步驟10 向多路複用器註冊 OP_READ
- client.register(selector, SelectionKey.OP_READ);
- } else if (selectionKey.isReadable()) {//步驟11 handle read() 異步讀請求消息到ByteBuffer
- client = (SocketChannel) selectionKey.channel();
- //將緩衝區清空以備下次讀取
- receivebuffer.clear();
- //讀取服務器發送來的數據到緩衝區中
- count=client.read(receivebuffer);
- if(count>0){
- receiveText = new String( receivebuffer.array(),0,count);
- System.out.println("客戶端接受服務器端數據--:"+receiveText);
- client.register(selector, SelectionKey.OP_WRITE);
- }
- } else if (selectionKey.isWritable()) {//步驟13 異步寫ByteBuffer到SocketChannel
- sendbuffer.clear();
- client = (SocketChannel) selectionKey.channel();
- sendText = "message from client--" + (flag++);
- sendbuffer.put(sendText.getBytes());
- //將緩衝區各標誌復位,因爲向裏面put了數據標誌被改變要想從中讀取數據發向服務器,就要復位
- sendbuffer.flip();
- client.write(sendbuffer);
- System.out.println("客戶端向服務器端發送數據--:"+sendText);
- client.register(selector, SelectionKey.OP_READ);
- }
- }
- selectionKeys.clear();
- }
- }
- }
2、代碼可用如下圖說明(圖片來自:http://www.open-open.com/lib/view/open1403057331075.html)
注意:通過debug可以發現Server端handle read後,就直接進行了步驟13,異步寫操作,這是因爲在步驟11進行了write的註冊,因此它不需要client的觸發,這就是selector輪詢的作用。
按照Reactor模式設計和實現,它的服務端通信序列圖如下:
客戶端通信序列圖如下:
客戶端步驟4-6的標註有些牽強,其實在大的程序中是這樣的,如hadoop的ipc代碼中就是這樣,稍後文章會講解hadoop如何使用nio進行rpc通信。