java網絡編程幾種IO模型的介紹和對比

本文通過實現一個簡單的時間服務器和客戶端,分別對JDK的BIO、NIO和JDK1.7中的NIO 2.0的使用進行介紹和對比,瞭解基於java的網絡編程的發展。本文內容主要參考《Netty權威指南》。

BIO

BIO即同步阻塞IO,採用BIO通信方式的服務器,對於每一個連接請求都由服務器創建一個新的線層來接受和處理請求,處理完成後銷燬線程。這就是典型的一請求一應答的模型。

同步阻塞IO服務端通信模型圖

這種方式的壞處是缺乏彈性伸縮的能力,當客戶端併發訪問量很大時,服務器需要創建大量線程來處理請求,而在短時間內創建大量線程就會導致服務器資源不足,進而引起僵死甚至宕機,無法提供服務。

 

同步阻塞IO的TimeServer

package bio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author elricboa on 2018/1/10.
 */
public class TimeServer {

    public static void main(String[] args) throws IOException {
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (NumberFormatException e) {
                //use the default value;
            }
        }

        try (ServerSocket server = new ServerSocket(port)) {
            System.out.println("The time server started on port : " + port);
            Socket socket = null;
            while (true) {
                socket = server.accept();
                new Thread(new TimeServerHandler(socket)).start();
            }
        } finally {
            System.out.println("The time server closed.");
        }

    }
}

服務器端程序啓動後,在一個無限循環中接收來自客戶端的連接,當沒有客戶端連接時,主線程則會阻塞在accept操作上。當有新的客戶端接入的時候,主線程以該socket爲參數創建一個TimeServerHandler對象,在新的線程中處理這條socket。

package bio;

import java.io.*;
import java.net.Socket;

/**
 * @author elricboa on 2018/1/10.
 */
public class TimeServerHandler implements Runnable {
    private Socket socket;

    public TimeServerHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try (BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
             PrintWriter out = new PrintWriter(
                     new OutputStreamWriter(socket.getOutputStream()))) {
            String currentTime = null;
            String body = null;
            while (true) {
                body = in.readLine();
                if (body == null) {
                    break;
                }
                System.out.println("The time server received order : " + body);
                if ("query time order".equalsIgnoreCase(body.trim())) {
                    currentTime = new java.util.Date().toString();
                } else {
                    currentTime = "bad order";
                }
                out.println(currentTime);
                out.flush();
            }

        } catch (IOException e) {
            e.printStackTrace();
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
                socket = null;
            }
        }
    }
}

TimeServerHanler包含處理請求的邏輯:讀取輸入並判斷是否爲合法查詢,如果是合法查詢就返回服務器當前時間,否則返回"bad order"。

package bio;

import java.io.*;
import java.net.Socket;
import java.net.UnknownHostException;

/**
 * @author elricboa on 2018/1/10.
 */
public class TimeClient {
    public static void main(String[] args){
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (NumberFormatException e) {
                //use the default value;
            }
        }

        try(Socket socket = new Socket("127.0.0.1", port);
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()))){
            out.println("QUERY TIME ORDER");
            out.flush();
            System.out.println("send order to server");
            String resp = in.readLine();
            System.out.println("Now is : " + resp);
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

imeClient創建了連接到服務器的socket之後,向服務器發送請求,將收到的返回結果打印之後退出。

BIO的主要問題在於每一個客戶端連接都需要一個線程來處理,一個處理線程也只能處理一個客戶端連接。而在高性能服務器應用領域,往往要面對成千上萬個併發連接,這種模型顯然無法滿足高性能、高併發的接入場景。

 

採用線程池做緩衝的同步阻塞IO

爲了解決BIO方式中每一個請求都需要一個線程來處理的問題,有人對它的線程模型進行了優化,在服務器端採取線程池的形式來處理客戶端的多個請求,這樣就可以有M個服務器線程來處理N個客戶端請求。這裏的M可以遠遠大於N,這樣一來可以根據服務器情況靈活配置M和N的比例,防止創建海量的併發訪問耗盡服務器資源。

當一個用戶請求來到服務器時,服務器會將客戶端Socket封裝成一個Task(繼承了java.lang.Runnable接口),然後將Task交由服務器端的線程池處理。服務器維護一個消息隊列和若干個worker線程,worker線程從消息隊列中取出Task執行。由於消息隊列和worker線程的數量都是靈活可控的,它們佔用的資源也是可控的,所以不用擔心會耗盡服務器資源。

僞異步IO服務端通信模型

 

這種解決方案來自於在JDK NIO沒有流行之前,爲了解決Tomcat通信線程同步I/O導致業務線程被掛住的情況,實現者在通信線程和業務線程之間加了一個緩衝區,用於隔離I/O線程和業務線程間的直接訪問,這樣業務線程就不會被I/O線程阻塞。

採用線程池做緩衝的TimeServer

package fnio;

import bio.TimeServerHandler;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * @author elricboa on 2018/1/10.
 */
public class TimeServer {

    public static void main(String[] args) throws IOException {
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (NumberFormatException e) {
                //use the default value;
            }
        }

        try (ServerSocket server = new ServerSocket(port)) {
            System.out.println("The time server started on port : " + port);
            TimeServerExecutePool singleExecutor = new TimeServerExecutePool(50,
                    1000);
            Socket socket = null;
            while (true) {
                socket = server.accept();
                singleExecutor.execute(new TimeServerHandler(socket));
            }
        } finally {
            System.out.println("The time server closed");
        }

    }
}
package fnio;

import bio.TimeServerHandler;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author elricboa on 2018/1/10.
 */
public class TimeServerExecutePool {
    private Executor executor;

    public TimeServerExecutePool(int maxPoolSize, int queueSize) {
        this.executor = new ThreadPoolExecutor(
                Runtime.getRuntime().availableProcessors(), maxPoolSize, 120L,
                TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize));
    }

    public void execute(TimeServerHandler timeServerHandler) {
        executor.execute(timeServerHandler);
    }
}

TimeServer的主函數實現發生了一點變化,首先創建了一個線程池TimeServerExecutePool,每次接收一個socket,就將其封裝爲Task,交由線程池處理,從而避免了每一個連接都創建新線程的操作。

 

雖然通過線程池的方式避免了過多的線程耗盡資源,但這種實現方式底層通信仍是同步阻塞模型,在處理輸入和輸出的時候,調用的仍然是會發生阻塞的api。當消息的接收方處理較慢的時候,TCP緩衝區剩餘空間會減少,接收窗口的大小也會不斷減小,知道爲零。當雙方處於keep-alive狀態,這時發送方將無法再向TCP緩衝區寫入數據,這時調用阻塞IO,write操作將會無限期阻塞,直到發送窗口大於零或者發生IO異常。

NIO

jdk1.4版本推出了NIO,意思是new I/O,NIO最大的特性是支持了非阻塞I/O,所以NIO也可以解釋爲non-blocking I/O,在這裏採用的是後面一種意思。

NIO彌補了原來同步阻塞I/O的不足,提供了高效的,面向塊的I/O,爲此NIO引入了一些新的概念。

  • Buffer(緩衝區)
    Buffer是一個不同於原來同步阻塞I/O的新的概念,它包含一些數據,這些數據可以從其他地方讀入也可以輸出到其他地方。在NIO庫中,所有的數據操作都發生在Buffer裏,讀取或者寫入數據時都先將數據放到緩衝區中。Buffer的本質通常是一個字節數組或者其他類型的數組,但Buffer不僅僅是一個數組,它還提供數據的結構化訪問和讀寫位置等信息,最常用的是ByteBuffer,顧名思義它的底層是一個字節數組。
  • Channel(通道)
    Channel類似於流,Buffer的數據都是從Channel讀取和寫入。與流不同的地方在於Channel是全雙工的,而流的數據只能沿一個方向。
  • Selector(選擇器)
    《netty權威指南》一書中把selector翻譯爲多路複用器,因爲JDK的Selector基於select/poll模型實現,它是基於多路I/O複用的實現。Selector在java NIO編程中是一個比較重要的概念,Selector可以提供已經就緒的任務。一個Selector上可以註冊多個Channel,然後Selector會不斷輪詢註冊在其上的Channel,一旦一個Channel上有事件發生(連接建立、讀、寫等),這個Channel就進入就緒狀態,這時候可以通過Selector獲得這個Channel,進行後續的操作。
    Selector的領先之處在於它採用一個線程來輪詢多個Channel,並且從Selector獲得Channel的操作可以是非阻塞的。使用者只需向Selector註冊感興趣的事件,不必阻塞在獲取Channel的地方。當有感興趣的事件發生時,就可以從Selector獲得相應的Channel。

採用NIO實現的TimeServer

NIO服務端通信序列圖


package nio;

import java.io.IOException;

/**
 * @author elricboa on 2018/1/10.
 */
public class TimeServer {

    public static void main(String[] args) throws IOException {
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (NumberFormatException e) {
                //use the default value;
            }
        }

        MultiplexerTimeServer server = new MultiplexerTimeServer(port);
        new Thread(server, "NIO-MultiplexerTimeServer--001").start();
    }
}

TimeServer在主函數中創建了一個MultiplexerTimeSever的多路複用類,它的是一個獨立的線程,負責輪詢Selector,處理多個客戶端的併發接入。

package nio;

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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;

/**
 * @author elricboa on 2018/1/10.
 */
public class MultiplexerTimeServer implements Runnable{
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private volatile boolean stop;

    public MultiplexerTimeServer(int port) {
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.socket()
                    .bind(new InetSocketAddress(port), 1024);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("The time server started on port : " + port);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    public void stop() {
        this.stop = true;
    }

    @Override
    public void run() {
        while (!stop) {
            try {
                selector.select(1000);
                Set<SelectionKey> selectionKeySet = selector.selectedKeys();
                SelectionKey key = null;
                for (Iterator<SelectionKey> itr = selectionKeySet
                        .iterator(); itr.hasNext(); ) {
                    key = itr.next();
                    itr.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }

            } catch (Throwable t) {
                t.printStackTrace();
            }
        }

        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            if (key.isAcceptable()) {
                ServerSocketChannel ssc = ((ServerSocketChannel) key.channel());
                SocketChannel sc = ssc.accept();
                sc.configureBlocking(false);
                sc.register(selector, SelectionKey.OP_READ);
            }

            if (key.isReadable()) {
                SocketChannel sc = ((SocketChannel) key.channel());
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0) {
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println(
                            "The time server received order : " + body);
                    String currentTime = "query time order"
                            .equalsIgnoreCase(body.trim()) ?
                            new Date().toString() :
                            "bad order";
                    doWrite(sc, currentTime);
                } else if (readBytes < 0) {
                    key.cancel();
                    sc.close();
                }
            }
        }
    }

    private void doWrite(SocketChannel channel, String response)
            throws IOException {
        if (channel != null && response != null
                && response.trim().length() > 0) {
            byte[] bytes = response.getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            channel.write(writeBuffer);
        }
    }
}

MultiplexerTimeServer在初始化時,會創建Selector和SeverSocketChannel,然後將ServerSocketChannel註冊到Selector上,監聽SelectionKey.OP_ACCEPT事件。在run方法遍歷selector,無論是否有事件發生,selector會每隔一秒被喚醒,如果有事件發生,則對就緒狀態的Channel集合進行迭代操作。在handleInput方法中處理客戶端的請求信息,先根據當前的SelectionKey判斷事件類型,然後建立SocketChannel,創建ByteBuffer接收客戶端的請求數據,解碼後判斷請求數據的合法性,最後將響應數據異步地發送給客戶端。

 

NIO客戶端序列圖

package nio;

/**
 * @author elricboa on 2018/1/10.
 */
public class TimeClient {
    public static void main(String[] args) {
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (NumberFormatException e) {
                //use the default value;
            }
        }

        new Thread(new TimeClientHandle("127.0.0.1", port), "TimeClient-001")
                .start();
    }
}

package nio;

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;

/**
 * @author elricboa on 2018/1/11.
 */
public class TimeClientHandle implements Runnable {
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile boolean stop;

    public TimeClientHandle(String host, int port) {
        this.host = host == null ? "127.0.0.1" : host;
        this.port = port;

        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    @Override public void run() {
        try {
            doConnect();
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }

        while (!stop) {
            try {
                selector.select(1000);
                Set<SelectionKey> selectionKeySet = selector.selectedKeys();
                SelectionKey key = null;
                for (Iterator<SelectionKey> itr = selectionKeySet
                        .iterator(); itr.hasNext(); ) {
                    key = itr.next();
                    itr.remove();
                    try {
                        handleInput(key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void doConnect() throws IOException {
        if (socketChannel.connect(new InetSocketAddress(host, port))) {
            socketChannel.register(selector, SelectionKey.OP_READ);
            doWrite(socketChannel);
        } else {
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }

    private void handleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            SocketChannel sc = ((SocketChannel) key.channel());
            if (key.isConnectable()) {
                if (sc.finishConnect()) {
                    sc.register(selector, SelectionKey.OP_READ);
                    doWrite(sc);
                } else {
                    System.exit(1);
                }
            }

            if (key.isReadable()) {
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0) {
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("Now is : " + body);
                    this.stop = true;
                } else if (readBytes < 0) {
                    key.cancel();
                    sc.close();
                }
            }
        }
    }

    private void doWrite(SocketChannel sc) throws IOException {
        byte[] request = "query time order".getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(request.length);
        writeBuffer.put(request);
        writeBuffer.flip();
        sc.write(writeBuffer);
        if(!writeBuffer.hasRemaining()){
            System.out.println("send order to server succeed");
        }
    }
}

客戶端與服務端實現類似,不再贅述。

 

注意:由於NIO中的SocketChannel是異步非阻塞的,在執行發送操作時不能保證將需要發送的數據發送完,所以會存在“半包寫”的問題,我們需要註冊OP_WRITE,不斷輪詢selector將未發送完的數據發送完畢,可以通過ByteBuffer的hasRemaining方法判斷髮送是否完成。因爲此處只是簡單示例,並沒有處理這個問題,後續文章會有詳細介紹。

嚴格來說,NIO只能被稱爲非阻塞IO,而並不能被稱爲異步非阻塞IO。因爲在JDK1.5 update10之前,它的底層實現是基於I/O複用技術的非阻塞I/O,而不是異步I/O。在JDK1.5 update10和Linux core2.6以上版本,sun優化了Selector的實現,在底層使用epoll替換了select/poll,上層的API並沒有變化,可以認爲是一次性能優化,但並沒有改變其I/O模型。

NIO的優點總結:1.客戶端建立連接的過程是非阻塞的。建立連接時只需在Selector上註冊相應的事件,而不需要同步阻塞。2.網絡讀寫是非阻塞的,如果沒有可用的數據不會等待,而是直接返回,這樣I/O通信線程可以處理其他鏈路,不必等到這個鏈路可用。3.線程模型的優化:JDK的Selector在Unix等主流系統的底層實現爲epoll,這樣的好處是它沒有連接句柄數的限制,這意味着一個Selector可以處理成千上萬個連接而性能不會隨着連接數上升而線性下降。

AIO

jdk1.7版本升級了NIO類庫,被稱爲NIO 2.0,它是真正的異步IO,在異步操作是可以通過傳遞信號量,當操作完成時會執行相關的回調方法。NIO 2.0提供了真正的異步文件I/O操作,並提供了與Unix網絡編程事件驅動I/O對應的AIO。NIO 2.0提供了異步文件通道和異步套接字通道的實現,異步通道通過兩種方式提供操作的返回結果:

  • 用java.util.concurrent.Future來表示異步操作的結果
    提交一個 I/O 操作請求,返回一個 Future。然後通過對 Future 進行檢查,確定它是否完成,或者阻塞 IO 操作直到操作正常完成或者超時異常。需要注意的是獲取Future結果的Future.get()是阻塞的,所以如果不仔細考慮應用場景,很容易變成同步的編程模式。
  • 在開始異步操作是傳入一個java.nio.channels.CompletionHandler實現類來執行回調
    提交一個I/O操作請求,並在開始時制定一個CompletionHandler。當I/O操作完成時,便會發送一個通知,這時候指定的CompletionHandler的相應方法便會被調用。

AIO 實現的TimeServer

package aio;

/**
 * @author elricboa on 2018/1/11.
 */
public class TimeServer {
    public static void main(String[] args) {
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (NumberFormatException e) {
                //use the default value;
            }
        }

        AsyncTimeServerHandler timeServer = new AsyncTimeServerHandler(port);
        new Thread(timeServer, "AIO-AsyncTimeServerHandler-001").start();
    }
}

package aio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.CountDownLatch;

/**
 * @author elricboa on 2018/1/11.
 */
public class AsyncTimeServerHandler implements Runnable {
    private int port;
    CountDownLatch latch;
    AsynchronousServerSocketChannel asynchronousServerSocketChannel;

    public AsyncTimeServerHandler(int port) {
        this.port = port;
        try {
            asynchronousServerSocketChannel = AsynchronousServerSocketChannel
                    .open();
            asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        latch = new CountDownLatch(1);
        doAccept();
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void doAccept() {
        asynchronousServerSocketChannel
                .accept(this, new AcceptCompletionHandler());
    }

}

AsyncTimeServerHandler在構造方法中先創建一個異步的服務端通道AsynchronousServerSocketChannel,然後綁定端口。在run方法中,初始化一個CountDownLatch對象,以阻塞當前線程,防止主線程在執行完成前退出。doAccept方法用於接收客戶端連接,在異步操作是傳入一個CompletionHandler的實例,用於接收accpet操作成功的通知消息,

package aio;

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;

/**
 * @author elricboa on 2018/1/11.
 */
public class AcceptCompletionHandler implements
        CompletionHandler<AsynchronousSocketChannel, AsyncTimeServerHandler> {
    @Override public void completed(AsynchronousSocketChannel result,
                                    AsyncTimeServerHandler attachment) {
        attachment.asynchronousServerSocketChannel.accept(attachment, this);
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        result.read(readBuffer, readBuffer, new ReadCompletionHandler(result));
    }

    @Override public void failed(Throwable exc,
                                 AsyncTimeServerHandler attachment) {
        exc.printStackTrace();
        attachment.latch.countDown();
    }
}

AcceptCompletionHandler在completed方法中,先從傳入的attachment獲取channel,然後調用其accept方法,因爲服務端要接收多個客戶端連接,所以在接收成功是需繼續調用其accept方法,接收其他客戶端連接,形成一個循環。在連接建立之後,先分配一個1M的緩衝區,再將客戶端連接的數據異步讀取到緩衝區中,在讀取操作是傳入一個CompletionHandler用於接收通知回調。

package aio;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.Date;

/**
 * @author elricboa on 2018/1/11.
 */
public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
    private AsynchronousSocketChannel channel;

    public ReadCompletionHandler(AsynchronousSocketChannel result) {
        if (this.channel == null) {
            this.channel = result;
        }
    }

    @Override 
    public void completed(Integer result, ByteBuffer attachment) {
        attachment.flip();
        byte[] body = new byte[attachment.remaining()];
        attachment.get(body);
        try {
            String request = new String(body, "UTF-8");
            System.out.println("The time server received order : " + request);
            String currentTime = "query time order"
                    .equalsIgnoreCase(request.trim()) ?
                    new Date().toString() :
                    "bad order";
            System.out.println(currentTime);
            doWrite(currentTime);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    private void doWrite(String currentTime) {
        if(currentTime != null && currentTime.trim().length() > 0) {
            byte[] response = currentTime.getBytes();
            final ByteBuffer writeBuffer = ByteBuffer.allocate(response.length);
            writeBuffer.put(response);
            writeBuffer.flip();
            channel.write(writeBuffer, writeBuffer, new CompletionHandler<Integer, ByteBuffer>() {
                @Override public void completed(Integer result,
                                                ByteBuffer attachment) {
                    if(attachment.hasRemaining()){
                        channel.write(attachment, attachment, this);
                    }
                }

                @Override public void failed(Throwable exc,
                                             ByteBuffer attachment) {
                    try {
                        channel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    @Override 
    public void failed(Throwable exc, ByteBuffer attachment) {
        try {
            this.channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

ReadCompletionHandler用於異步讀取客戶端的請求數據,進行合法性判斷之後再異步地寫入返回數據。

TimeClient實現:

package aio;

/**
 * @author elricboa on 2018/1/10.
 */
public class TimeClient {
    public static void main(String[] args) {
        int port = 8080;
        if (args != null && args.length > 0) {
            try {
                port = Integer.parseInt(args[0]);
            } catch (NumberFormatException e) {
                //use the default value;
            }
        }

        new Thread(new AsyncTimeClientHandler("127.0.0.1", port),
                "AIO-AsyncTimeClientHandler-001").start();
    }
}
AsyncTimeClientHandler.java實現:
package aio;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;

/**
 * @author elricboa on 2018/1/11.
 */
public class AsyncTimeClientHandler implements CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {
    private AsynchronousSocketChannel client;
    private String host;
    private int port;
    private CountDownLatch latch;

    public AsyncTimeClientHandler(String host, int port) {
        this.host = host;
        this.port = port;

        try {
            client = AsynchronousSocketChannel.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        latch = new CountDownLatch(1);
        client.connect(new InetSocketAddress(host, port), this, this);

        try {
            latch.await();
            client.close();
        } catch (InterruptedException | IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void completed(Void result,
                                    AsyncTimeClientHandler attachment) {
        byte[] request = "query time order".getBytes();
        final ByteBuffer writeBuffer = ByteBuffer.allocate(request.length);
        writeBuffer.put(request);
        writeBuffer.flip();
        client.write(writeBuffer, writeBuffer,
                new CompletionHandler<Integer, ByteBuffer>() {
                    @Override
                    public void completed(Integer result,
                                                    ByteBuffer attachment) {
                        if (writeBuffer.hasRemaining()) {
                            client.write(writeBuffer, writeBuffer, this);
                        } else {
                            final ByteBuffer readBuffer = ByteBuffer
                                    .allocate(1024);
                            client.read(readBuffer, readBuffer,
                                    new CompletionHandler<Integer, ByteBuffer>() {
                                        @Override
                                        public void completed(
                                                Integer result,
                                                ByteBuffer attachment) {
                                            attachment.flip();
                                            byte[] bytes = new byte[attachment
                                                    .remaining()];
                                            readBuffer.get(bytes);
                                            try {
                                                String body = new String(bytes,
                                                        "UTF-8");
                                                System.out.println(
                                                        "Now is : " + body);
                                                latch.countDown();
                                            } catch (UnsupportedEncodingException e) {
                                                e.printStackTrace();
                                            }
                                        }

                                        @Override
                                        public void failed(
                                                Throwable exc,
                                                ByteBuffer attachment) {
                                            try {
                                                client.close();
                                                latch.countDown();
                                            } catch (IOException e) {
                                                e.printStackTrace();
                                            }
                                        }
                                    });
                        }
                    }

                    @Override
                    public void failed(Throwable exc,
                                                 ByteBuffer attachment) {
                        try {
                            client.close();
                            latch.countDown();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
    }

    @Override
    public void failed(Throwable exc,
                                 AsyncTimeClientHandler attachment) {
        exc.printStackTrace();
        try {
            client.close();
            latch.countDown();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

對比

 
同步阻塞I/O
帶緩衝區的同步I/O
非阻塞I/O
異步I/O
客戶端數:I/O線程 1:1 M:N(其中M可以大於N) M:1(1個I/O線程處理多個連接) M:0(無需額外的I/O線程,被動回調)
I/O類型(阻塞) 阻塞 阻塞 非阻塞 非阻塞
I/O類型(同步) 同步 同步 同步(I/O多路複用) 異步
API使用難度 簡單 簡單 複雜 複雜
調試難度 簡單 簡單 複雜 複雜
可靠性 非常差
吞吐量









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