java網路編程(三)----同步非阻塞nio及reactor模型__編程

來源:互聯網
上載者:User

很多剛接觸NIO的人,第一眼看到的就是Java相對晦澀的API,比如:Channel,Selector,Socket什麼的;然後就是一坨上百行的代碼來示範NIO的服務端Demo,所以這裡我們人性化地簡單介紹一下。

NIO我們一般認為是New I/O(也是官方的叫法),因為它是相對於老的I/O類庫新增的(其實在JDK 1.4中就已經被引入了,但這個名詞還會繼續用很久,即使它們在現在看來已經是“舊”的了,所以也提示我們在命名時,需要好好考慮),做了很大的改變。但民間跟多人稱之為Non-block I/O,即非阻塞I/O,因為這樣叫,更能體現它的特點。而下文中的NIO,不是指整個新的I/O庫,而是非阻塞I/O。

NIO提供了與傳統BIO模型中的Socket和ServerSocket相對應的SocketChannel和ServerSocketChannel兩種不同的通訊端通道實現。
新增的著兩種通道都支援阻塞和非阻塞兩種模式。阻塞模式使用就像傳統中的支援一樣,比較簡單,但是效能和可靠性都不好;非阻塞模式正好與之相反。
對於低負載、低並發的應用程式,可以使用同步阻塞I/O來提升開發速率和更好的維護性;對於高負載、高並發的(網路)應用,應使用NIO的非阻塞模式來開發。 java nio基礎實現

緩衝區 Buffer
Buffer是一個對象,包含一些要寫入或者讀出的資料。
在NIO庫中,所有資料都是用緩衝區處理的。在讀取資料時,它是直接讀到緩衝區中的;在寫入資料時,也是寫入到緩衝區中。任何時候訪問NIO中的資料,都是通過緩衝區進行操作。
緩衝區實際上是一個數組,並提供了對資料結構化訪問以及維護讀寫位置等資訊。
具體的緩衝區有這些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他們實現了相同的介面:Buffer。

通道 Channel
我們對資料的讀取和寫入要通過Channel,它就像水管一樣,是一個通道。通道不同於流的地方就是通道是雙向的,可以用於讀、寫和同時讀寫操作。
底層的作業系統的通道一般都是全雙工系統的,所以全雙工系統的Channel比流能更好的映射底層作業系統的API。
Channel主要分兩大類: SelectableChannel:使用者網路讀寫 FileChannel:用於檔案操作

後面代碼會涉及的ServerSocketChannel和SocketChannel都是SelectableChannel的子類。

多工器 Selector
Selector是Java NIO 編程的基礎。
Selector提供選擇已經就緒的任務的能力:Selector會不斷輪詢註冊在其上的Channel,如果某個Channel上面發生讀或者寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,然後通過SelectionKey可以擷取就緒Channel的集合,進行後續的I/O操作。

一個Selector可以同時輪詢多個Channel,只需要一個線程負責Selector的輪詢,就可以接入成千上萬的用戶端。 nio使用

回憶BIO模型,之所以需要多線程,是因為在進行I/O操作的時候,一是沒有辦法知道到底能不能寫、能不能讀,只能”傻等”,即使通過各種估算,算出來作業系統沒有能力進行讀寫,也沒法在socket.read()和socket.write()函數中返回,這兩個函數無法進行有效中斷。所以除了多開線程另起爐灶,沒有好的辦法利用CPU。

NIO的讀寫函數可以立刻返回,這就給了我們不開線程利用CPU的最好機會:如果一個串連不能讀寫(socket.read()返回0或者socket.write()返回0),我們可以把這件事記下來,記錄的方式通常是在Selector上註冊商標符號位,然後切換到其它就緒的串連(channel)繼續進行讀寫。

思考一下在Socket網路通訊中有什麼事件: 在伺服器端我們需要接收用戶端串連,這裡就有一個接收“Accept”事件; 而用戶端串連伺服器,串連有一個“Connect”事件 各自進行讀操作時,有一個“read”事件 各自進行寫操作時,有一個“write”事件

而上面我們說到串連被抽象為了Channel,這時我們就可以在多工器Selector上註冊通道和事件。 在伺服器端:ServerSocketChannel.register(Selector, SelectionKey.OP_ACCEPT);
將ServerSocketChannel註冊在Selector並綁定SelectionKey.OP_ACCEPT事件; 調用多工器Selector.select(); 如果channel已經發生了接收用戶端的事件,那麼就能被多工器select到,然後擷取到和用戶端串連的Socket,再將這個socket的讀寫事件註冊到Selector上。 同樣可以用戶端也是將其他事件註冊到Selector,然後事件被觸發後就可以被select()函數找到 最後程式不斷輪詢selector,根據select到的不同事件類型調用對應的handler進行處理。

select()函數調用的是系統底層函數,在Linux 2.6之前是select、poll,2.6之後是epoll,Windows是IOCP。

虛擬碼如下:

   interface ChannelHandler{      void channelReadable(Channel channel);      void channelWritable(Channel channel);   }   class Channel{     Socket socket;     Event event;//讀,寫或者串連   }   //IO線程主迴圈:   class IoThread extends Thread{   public void run(){   Channel channel;   //選擇就緒的事件和對應的串連   while(channel=Selector.select()){      if(channel.event==accept){          //觸發了accept事件的是新串連,          //我們需要為這個新串連註冊讀寫事件         registerNewChannelHandler(channel);      }      if(channel.event==write){          //如果可以寫,則執行寫事件         getChannelHandler(channel).channelWritable(channel);      }      if(channel.event==read){      //如果可以讀,則執行讀事件          getChannelHandler(channel).channelReadable(channel);      }    }   }   //所有channel的對應事件處理器   Map<Channel,ChannelHandler> handlerMap;  }

注意,select是阻塞的,無論是通過作業系統的通知(epoll)還是不停的輪詢(select,poll),這個函數是阻塞的。所以你可以放心大膽地在一個while(true)裡面調用這個函數而不用擔心CPU空轉。同時我們也可以通過設定周期的方式調用Selector.select(1000)(每一秒select一次)。

selector裡面有一個裝SelectionKey的集合,記錄著各個channel及其感興趣的事件,詳情請點擊:
http://blog.csdn.net/u010412719/article/details/52863704

參考了美團點評技術團隊的文章:https://tech.meituan.com/nio.html
(沒我總結的好啊哈哈哈所以看我的部落格就行)

實現nio的完整代碼在最後 nio與bio對比

所有的系統I/O都分為兩個階段:等待就緒和操作。舉例來說,讀函數,分為等待系統可讀和真正的讀;同理,寫函數分為等待網卡可以寫和真正的寫。

需要說明的是等待就緒的阻塞是不使用CPU的,是在“空等”;而真正的讀寫操作的阻塞是使用CPU的,真正在”幹活”,而且這個過程非常快,屬於memory copy,頻寬通常在1GB/s層級以上,可以理解為基本不耗時。

下圖是幾種常見I/O模型的對比:

java的nio便是第二或者第三種形式: 第二種:在一個while迴圈裡面不斷每隔一秒檢查selector裡面是否有事件被觸發Selector.select(1000) 第三種:Selector.select()會檢查是是否有就緒事件,沒有的話會阻塞直到有就緒事件函數才會返回。 Reactor

有一篇文章寫得很好:http://www.blogjava.net/DLevin/archive/2015/09/02/427045.html
你們可以看看,我這裡再強調一下重點。

圖解:
1. Handle:在網路編程中,這裡一般指Socket Handle,即一個網路連接(Connection,在Java NIO中的Channel)。這個Channel註冊到Synchronous Event Demultiplexer中,以監聽Handle中發生的事件,對ServerSocketChannnel可以是CONNECT事件,對SocketChannel可以是READ、WRITE、CLOSE事件等。
2. Synchronous Event Demultiplexer:多工器,阻塞等待一系列的Handle中的事件到來,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的執行返回的事件類型。這個模組一般使用作業系統的select來實現。在Java NIO中用Selector來封裝,當Selector.select()返回時,可以調用Selector的selectedKeys()方法擷取Set,一個SelectionKey表達一個有事件發生的Channel以及該Channel上的事件類型。
3. Initiation Dispatcher:Reactor模式的主要模組,事件調度器,通常被稱為reactor,用於管理Event Handler,即EventHandler的容器,用以註冊、移除EventHandler等;另外,它還作為Reactor模式的入口調用,在這個模組裡面調用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,當阻塞等待返回時,根據事件發生的Handle將其分發給對應的Event Handler處理,即回調EventHandler中的handle_event()方法。
4. 定義事件處理方法:handle_event(),以供InitiationDispatcher回調使用。

需要注意的是:在java nio中我們註冊了建立串連相關的“Accept”“Connect”事件後,再進行讀寫操作要將讀寫事件再註冊到多工器,下次讀寫事件發生時才會被select到。這就是傳說中的多工IO。

另外Reactor翻譯為“反應”器,名字中”反應“的由來:

“反應”即“倒置”,“控制逆轉”

具體事件處理常式不調用反應器,而是由反應器分配一個具體事件處理常式,具體事件處理常式對某個指定的事件發生做出反應;這種控制逆轉又稱為“好萊塢法則”(不要調用我,讓我來調用你) nio實現代碼

一、server端代碼

public class Server {    private static synchronized void start(int port){        ServerHandle serverHandle = new ServerHandle(port);        new Thread(serverHandle,"Server").start();    }    public static void main(String[] args){        start(12345);    }}

server處理類ServerHandle:

class ServerHandle implements Runnable {    private Selector m_Selector;    private ServerSocketChannel m_ServerChannel;    private volatile boolean m_Started;    public ServerHandle(int vPort) {        try {            m_Selector = Selector.open();            //伺服器監聽通道            ServerSocketChannel tServerChannel = ServerSocketChannel.open();            //如果為 true,則此通道將被置於阻塞模式;如果為 false,則此通道將被置於非阻塞模式            tServerChannel.configureBlocking(false);            tServerChannel.bind(new InetSocketAddress(vPort), 1024);            //註冊伺服器通道到selector,監聽ACCEPT事件            tServerChannel.register(m_Selector, SelectionKey.OP_ACCEPT);            m_Started = true;            System.out.println("伺服器已啟動,連接埠號碼:" + vPort);        } catch (IOException e) {            e.printStackTrace();        }    }    public void stop() {        m_Started = false;    }    @Override    public void run() {        while (m_Started) {            try {                //無論是否有讀寫事件發生,selector在1s後被喚醒一次                m_Selector.select(1000);                //或者通過下面這種方式:                //會阻塞直到selector裡面有就緒事件                //m_Selector.select();                Set<SelectionKey> tKeys = m_Selector.selectedKeys();                Iterator<SelectionKey> tIterator = tKeys.iterator();                SelectionKey tKey;                //輪詢                while (tIterator.hasNext()) {                    tKey = tIterator.next();                    tIterator.remove();                    try {                        handleInput(tKey);                    } catch (IOException e) {                        if (null != tKey) {                            tKey.cancel();                            if (null == tKey.channel()) {                                tKey.channel().close();                            }                        }                    }                }            } catch (IOException e) {                e.printStackTrace();            }        }        //完成後關閉select        //關閉Selector會自動關閉裡面的資源        if (null != m_Selector) {            try {                m_Selector.close();            } catch (IOException e) {                e.printStackTrace();            }        }    }    private void handleInput(SelectionKey vKey) throws IOException {        if (vKey.isValid()) {            if (vKey.isAcceptable()) {                ServerSocketChannel tServerChannel = (ServerSocketChannel) vKey.channel();                SocketChannel tSocketChannel = tServerChannel.accept();                tSocketChannel.configureBlocking(false);                //拿到新串連註冊讀事件                tSocketChannel.register(m_Selector, SelectionKey.OP_READ);            }        }        //讀訊息        if (vKey.isReadable()) {            SocketChannel tSocketChannel = (SocketChannel) vKey.channel();            //得用buffer            ByteBuffer tBuffer = ByteBuffer.allocate(1024);            int tReadBytes = tSocketChannel.read(tBuffer);            if (0 < tReadBytes) {                tBuffer.flip();                byte[] tBytes = new byte[tBuffer.remaining()];                tBuffer.get(tBytes);                String tExpression = new String(tBytes, "UTF-8");                System.out.println("伺服器收到訊息:" + tExpression);                String tResult = "這句話是伺服器發過來的";                doWrite(tSocketChannel, tResult);            }            //讀取不到訊息關閉資源            else if (0 > tReadBytes) {               //即使key被cancel了他的channel還在的                vKey.cancel();                tSocketChannel.close();            }        }    }//發送資料    private void doWrite(SocketChannel vChannel, String vResponse) throws IOException {        byte[] tBytes = vResponse.getBytes("UTF-8");        ByteBuffer tWriteBuffer = ByteBuffer.allocate(tBytes.length);        tWriteBuffer.put(tBytes);        tWriteBuffer.flip();        vChannel.write(tWriteBuffer);    }}

二、用戶端代碼:

public class Client {    private static String DEFAULT_HOST = "127.0.0.1";    private static int DEFAULT_PORT = 12345;    private static ClientHandle clientHandle;    public static void start(){        start(DEFAULT_HOST,DEFAULT_PORT);    }    public static synchronized void start(String ip,int port){        if(null != clientHandle) clientHandle.stop();        clientHandle = new ClientHandle(ip,port);        new Thread(clientHandle,"Server").start();    }    //向伺服器發送訊息    public static boolean sendMsg(String msg) throws Exception{        clientHandle.sendMsg(msg);        return true;    }    public static void main(String[] args) throws Exception {        start();        while(Client.sendMsg(new Scanner(System.in).nextLine()));    }}

用戶端處理類ServerHandle:

class ClientHandle implements Runnable {    private String m_Host;    private int m_Port;    private Selector m_Selector;    private SocketChannel m_SocketChannel;    private volatile boolean m_Started;    ClientHandle(String vIp, int vPort) {        m_Host = vIp;        m_Port = vPort;        try {            m_Selector = Selector.open();            m_SocketChannel = SocketChannel.open();            m_SocketChannel.configureBlocking(false);            m_Started = true;        } catch (IOException e) {            e.printStackTrace();        }    }    void stop() {        m_Started = false;    }    @Override    public void run() {        try {            doConnect();        } catch (IOException e) {            System.out.println("用戶端串連失敗");        }        while (m_Started) {            try {                m_Selector.select(1000);                Set<SelectionKey> tKeys = m_Selector.selectedKeys();                Iterator<SelectionKey> tIterator = tKeys.iterator();                SelectionKey tKey;                while (tIterator.hasNext()) {                    tKey = tIterator.next();                    tIterator.remove();                    try {                        handleInput(tKey);                    } catch (IOException e) {                        if (null != tKey) {                            tKey.cancel();                            if (null != tKey.channel()) {                                tKey.channel().close();                            }                        }                    }                }            } catch (IOException e) {                e.printStackTrace();            }        }        //關閉selector會關閉它裡面的所有資源        if (null != m_Selector) {            try {                m_Selector.close();            } catch (IOException e) {                e.printStackTrace();            }        }    }    private void handleInput(SelectionKey vKey) throws IOException {        if (vKey.isValid()) {            SocketChannel socketChannel = (SocketChannel) vKey.channel();            //如果是串連事件            if (vKey.isConnectable()) {                if (socketChannel.finishConnect())                    System.out.println("用戶端串連上伺服器端");                else System.exit(1);            }            //讀訊息            if (vKey.isReadable()) {                //建立ByteBuffer,並開闢一個1M的緩衝區                ByteBuffer buffer = ByteBuffer.allocate(1024);                //讀取請求碼流,返回讀取到的位元組數                int readBytes = socketChannel.read(buffer);                //讀取到位元組,對位元組進行編解碼                if (0 < readBytes) {                    //將緩衝區當前的limit設定為position=0,用於後續對緩衝區的讀取操作                    buffer.flip();                    //根據緩衝區可讀位元組數建立位元組數組                    byte[] bytes = new byte[buffer.remaining()];                    //將緩衝區可讀位元組數組複製到建立的數組中                    buffer.get(bytes);                    String result = new String(bytes, "UTF-8");                    System.out.println("用戶端收到訊息:" + result);                }                //鏈路已經關閉,釋放資源                else if (0 > readBytes) {                    vKey.cancel();                    socketChannel.close();                }            }        }    }    private void doWrite(SocketChannel channel, String request) throws IOException {        //將訊息編碼為位元組數組        byte[] bytes = request.getBytes();        //根據數組容量建立ByteBuffer        ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);        //將位元組數組複製到緩衝區        writeBuffer.put(bytes);        //flip操作        writeBuffer.flip();        //發送緩衝區的位元組數組        channel.write(writeBuffer);    }    private void doConnect() throws IOException {        //用戶端請求串連上伺服器,並註冊串連事件        m_SocketChannel.connect(new InetSocketAddress(m_Host, m_Port));        m_SocketChannel.register(m_Selector, SelectionKey.OP_CONNECT);    }    void sendMsg(String msg) throws Exception {        //發送了資料的話就註冊讀事件        m_SocketChannel.register(m_Selector, SelectionKey.OP_READ);        doWrite(m_SocketChannel, msg);    }}

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.