從I/O模型到Netty(二)

Java NIO

在上一篇文章中對于I/O模型已經講的比較清楚了,在I/O密集型應用中使用Reactor模式可以明顯提高系統的性能(我們這里談到的性能很大程度上指的是吞吐量),但是在具體的開發過程中模式還是要落地成真實的代碼,使用傳統的I/O庫肯定是不行的,在Java中需要使用java.nio包下的庫。

雖然是講NIO的實現,但本文將不會把所有Java NIO中的主要API全部過一遍,而是通過例子理清NIO到底可以做什么事情。

本文中提到的JDK源代碼都可以在%JAVA_HOME%/jre/lib/rt.jar中看到。

Java NIO最初在Java4中被引入,但是到今天還是有很大部分的開發者從來沒使用過NIO的API,因為基礎I/O已經能滿足了我們日常的開發需求。但如果要開發I/O密集型應用的場景下,NIO可以明顯的提升程序的性能,另外NIO與基礎I/O有本質思想上的區別。
本文主要講Java中的NIO,內容包含:

  1. Oracle官方對NIO的說法
  2. Java中NIO的歷史進程
  3. NIO和NIO.2的區別在哪里
  4. NIO中的主要類的介紹
  5. 使用NIO的API構建一個Socket服務器

Oracle官方對NIO的說法

首先看看Oracle的官方文檔中是怎么說的:

Java中對于I/O的支持主要包括java.iojava.nio兩個包的內容,它們共同提供了如下特性:

  1. 通過數據流和序列化從文件系統中讀取和寫數據。
  2. 提供Charsets,解碼器和編碼器,用于在字節和Unicode字符之間的翻譯。
  3. 訪問文件、文件的屬性、文件系統。
  4. 提供異步的或者非阻塞多路復用I/O的API,用于構建可擴展的服務器程序。

這里并沒有提到網絡I/O的東西,在Java1.4以前,網絡I/O的API都是被放在java.net包下,在NIO中才被一起放入了java.nio包下。

Java中NIO的歷史進程

  1. 最開始Java中使用I/O來訪問文件系統只有通過java.io.File類來做,其中包含了一些對文件和目錄基本的操作。對于開發中常碰到的I/O需求一般都能覆蓋到,所以這也是日常開發工作中最常使用的I/O API。官方文檔中稱之為基礎I/O(Basic I/O)。
    基礎I/O是基于各種流的概念的,其基本模型就是上一篇中講到的阻塞I/O。
  2. 為了進一步豐富I/O操作的API,也是為了提升在I/O密集型應用中的性能,基于Reactor模式,在Java1.4中引入了java.nio包,其中重點包含幾個類:
  • java.nio.Buffer,用來存儲各種緩沖數據的容器。
  • java.nio.channels.Channel,用于連接程序和I/O設備的數據通道。
  • java.nio.channels.Selector,多路復用選擇器,在上一篇中講到過。
  • java.nio.charset.Charset,用來編解碼。
  1. 在Java7中引入了NIO.2,引入了一系列新的API(主要在新加入的包Java.nio.file),對于訪問文件系統提供了更多的API實現,更加豐富的文件屬性類,增加了一些異步I/O的API。同時,還添加了很多實用方法。

例如:以前簡單的拷貝一個文件就必須要寫一大堆的代碼,現在實用java.nio.file.Files.copy(Path, Path, CopyOption...)就可以很輕松的做到了

NIO和NIO.2的區別在哪里

在上一節中已經簡單介紹了這兩個概念的不同,這里再簡單羅列一下。NIO中引入的一個重要概念就是Reactor模式,而NIO.2對NIO本身不是一次升級,而是一次擴充,NIO.2中新增了很多實用方法(utilities),以支持更多的功能需求,并不是說能夠提升多少的性能。主要增加了如下兩點:

  1. 新的訪問文件的API。
    訪問文件從簡單到復雜的方法

    Java.nio.file包和其子包中新增了大量的與訪問文件相關的類,其中比較重要的有以下幾個,更完整的更新可以在Oracle的官網文檔中查看。
  • java.nio.file.Path,它可以用來取代早期的java.io.File用來訪問文件。
  • java.nio.file.Files,其中包含了大量的對文件操作的API。
  1. 異步I/O的API
    在NIO原來的API的基礎上,增加了對Proactor模式的支持,可以在包java.nio.channels中看到新加入的java.nio.channels.AsynchronousChanneljava.nio.channels.CompletionHandler<V, A>。使用這些類可以實現異步編程,如代碼1中所示:

     //代碼1
     //定義一個處理文件內容的函數式接口
     @FunctionalInterface
     static interface ProcessBuffer{
         void process(int result, ByteBuffer bb);
     }
     //遞歸地讀取文件的全部內容
     static void readFileThrough(AsynchronousFileChannel ch, ProcessBuffer runn, int position) {
         
         ByteBuffer bb = ByteBuffer.allocate(512);
         ch.read(bb, position, null, new CompletionHandler<Integer, Object>() {
    
             @Override
             public void completed(Integer result, Object attachment) {
                 System.out.println("成功了");
                 bb.flip();
                 runn.process(result, bb);
                 bb.clear();
                 if (result == bb.capacity())
                     readFileThrough(ch, runn, position + result);
             }
    
             @Override
             public void failed(Throwable exc, Object attachment) {
                 System.err.println("失敗了!!!");
             }
         });
     }
     //讀取文件內容,并打印
     static void testAIOReadFile() throws IOException {
         Path p = Paths.get(fileDir, fileName);
         AsynchronousFileChannel channel = AsynchronousFileChannel.open(p, StandardOpenOption.READ);
    
         Thread daemon = new Thread(() -> {
             try {
                 System.out.println("守護");
                 Thread.sleep(10000);
             } catch (Exception e) {
             
             }
         });
     
         readFileThrough(channel, (result, bb) -> {
             if (result < bb.capacity()) {
                 System.out.println(new String(Arrays.copyOf(bb.array(), result)));
                 System.out.println("已讀完。。。");
                 daemon.interrupt();
             }else {
                 System.out.print(new String(bb.array()));
             }
         }, 0);
     
         daemon.start();
     }
    

NIO中的主要類的介紹

NIO的基本思想是要構建一個Reactor模式的實現,具體落實到API,在Java中主要有以下幾個類:

1. java.nio.Buffer

這是一個容器類,用來存儲「基礎數據類型」,所有從Channel中讀取出來的數據都要使用Buffer的子類來作為存儲單元,可以把它想象成一個帶著很多屬性的數組(和ArrayList很類似,其實它的實現機制也差不多就是這樣)。

第一次看到介紹Buffer是在一本書上,書上畫了好多方框和指向這些方框的屬性值,看著就頭暈。其實很簡單,Buffer就是一個數組。

在讀寫交換時,必不可少的要批量地去讀取并寫入到目標對象,這個道理是不變的。在基礎I/O中如果我們要把一個輸入流寫入一個輸出流,可能會這么做:

//代碼2
public static void copy(File src, File dest) throws IOException {
    FileInputStream in = new FileInputStream(src);
    FileOutputStream out = new FileOutputStream(dest);
    byte[] buffer = new byte[1024];
    int bytes = 0;
    while ((bytes = in.read(buffer)) > -1){
        out.write(buffer, 0, bytes);
    }
    out.close();
    in.close();
}

以上代碼中使用了一個真實的數組用來做讀寫切換,從而達到批量(緩沖)讀寫的目標。
而在NIO中(如代碼1),讀寫切換也同樣是使用了一個數組進行暫存(緩沖),只不過在這個數組之上,封裝了一些屬性(java.nio.Buffer源碼中的一些屬性如代碼3所示)和操作。

//代碼3 - Buffer類中定義的一些屬性
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;

關于Buffer類詳細的繼承關系和其主要方法,可以參考下圖:


Buffer的繼承關系

2. java.nio.channels.Channel

Channel可以看做是代碼2中InputStream和OutStream的合體,在實際使用中,我們往往針對同一個I/O設備同時存在讀和寫的操作,在基礎I/O中我們就需要針對同一個目標對象生成一個輸入流和輸出流的對象,可是在NIO中就可以只建立一個Channel對象了。

Channel抽象的概念是對于某個I/O設備的「連接」,可以使用這個連接進行一些I/O操作,java.nio.channels.Channel本身是一個接口,只有兩個方法,但是在Java的的環境中,往往最簡單的接口最煩人,因為它的實現類總是會異常的多。

//代碼4 - 去除了所有注釋的Channel類
package java.nio.channels;

import java.io.IOException;
import java.io.Closeable;

public interface Channel extends Closeable {

    public boolean isOpen();

    public void close() throws IOException;

}

當然,這是享受多態帶來的好處的同時必須承受的。詳細的Channel繼承和實現關系如下:

Channel的繼承和實現關系

3. java.nio.channels.Selector

如果你是使用NIO來做網絡I/O,Selector是JavaNIO中最重要的類,正如它的注釋里第一句說的,Selector是SelectableChannel的「多路復用器」。

SelectableChannel的實現類

多路復用,這是在上一篇介紹過的概念,在不同的操作系統也有不同的底層實現。用戶也可以自己實現自己的Selector(通過類java.nio.channels.spi.SelectorProvider

//代碼5 - provider構造方法
public static SelectorProvider provider() {
    synchronized (lock) {
        if (provider != null)
            return provider;
        return AccessController.doPrivileged(
            new PrivilegedAction<SelectorProvider>() {
                public SelectorProvider run() {
                        if (loadProviderFromProperty())
                            //如果設置了屬性java.nio.channels.spi.SelectorProvider,則會載入響應的類
                            return provider;
                        if (loadProviderAsService())
                            return provider;
                        provider = sun.nio.ch.DefaultSelectorProvider.create();
                        return provider;
                    }
                });
    }
}

如果你不實現自己的SelectorProvidor,在代碼5中可以看到JDK會使用類sun.nio.ch.DefaultSelectorProvider來創建,這里會根據你的操作系統的類別不同而選擇不同的實現類。openJDK中也有相應的實現,有興趣的可以去GrepCode查看一下,Mac OS下是使用KQueueSelectorProvider

Selector的使用比較簡單,同時要配合SelectionKey使用,它們的繼承結構圖也比較簡單,如下:

Selector繼承關系

4. 其他

其他一些類如Charset個人感覺屬于實用性很強的類,但是在NIO與基礎I/O的比較中就顯得不那么重要了。

使用NIO的API構建一個Socket服務器

Java1.4引入的NIO中已經可以實現Reactor模式,在NIO.2中又引入了AIO的API,所以本節將分別使用兩種模式來實現一個Socket服務器,這里重點介紹Java中NIO API的使用,至于NIO和基礎I/O的性能對比,網上有很多,這里就不再做比較了。

首先定義一些基礎類,將從Socket中獲取的數據解析成TestRequest對象,然后再找到響應的Handler。看代碼:

我這里為了偷懶,將很多基礎類和方法定義在了一個類中,這種方法其實十分不可取。

//代碼6 
/**
 * 執行計算工作的線程池
 */
private static ExecutorService workers = Executors.newFixedThreadPool(10);

/**
 * 解析出來的請求對象
 * @author lk
 *
 */
public static class TestRequest{
    
    /**
     * 根據解析到的method來獲取響應的Handler
     */
    String method;
    String args;
    public static TestRequest parseFromString(String req) {
        System.out.println("收到請求:" + req);
        TestRequest request = new TestRequest();
        request.method = req.substring(0, 512);
        request.args = req.substring(512, req.length());
        return request;
    }
}


/**
 * 具體的邏輯需要實現此接口
 * @author lk
 *
 */
public static interface SockerServerHandler {
    ByteBuffer handle(TestRequest req);
}

主要的邏輯其實就是使用ServerSocketChannel的實例監聽本地端口,并且設置其為非阻塞(默認為阻塞模式)。代碼7中的parse()函數是一個典型的「使用Buffer讀取Channel中數據」的方法,這里為了簡(tou)單(lan),默認只讀取1024個字節,所以并沒有實際去循環讀取。

//代碼7
private static void useNIO() {
    Selector dispatcher = null;
    ServerSocketChannel serverChannel = null;
    try {
        dispatcher = Selector.open();
        serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        
        serverChannel.socket().setReuseAddress(true);
        serverChannel.socket().bind(LOCAL_8080);
        
        //ServerSocketChannel只支持這一種key,因為server端的socket只能去accept
        serverChannel.register(dispatcher, SelectionKey.OP_ACCEPT);
        
        while (dispatcher.select() > 0) {
            operate(dispatcher);
        }
        
    } catch (Exception e) {
        e.printStackTrace();
    }
}
/**
 * 在分發器上循環獲取連接事件
 * @param dispatcher
 * @throws IOException
 */
private static void operate(Selector dispatcher) throws IOException {
    //Set<SelectionKey> keys = dispatcher.keys();
    Set<SelectionKey> keys = dispatcher.selectedKeys();
    Iterator<SelectionKey> ki = keys.iterator();
    while(ki.hasNext()) {
        SelectionKey key = ki.next();
        ki.remove();
        if (key.isAcceptable()) {
            ServerSocketChannel channel = (ServerSocketChannel) key.channel();
            //針對此socket的IO就是BIO了
            final SocketChannel socket = channel.accept();
            workers.submit(() -> {
                try {
                    
                    TestRequest request = TestRequest.parseFromString(parse(socket));
                    SockerServerHandler handler = (SockerServerHandler) Class.forName(getClassNameForMethod(request.method)).newInstance();
                
                    socket.write(handler.handle(request));
                    
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }   
            });
        }
    }
}

private static String parse(SocketChannel socket) throws IOException {
    String req = null;
    
    try {
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
        byte[] bytes;
        int count = 0;
        if ((count = socket.read(buffer)) >= 0) {
            buffer.flip();
            bytes = new byte[count];
            buffer.get(bytes);
            req = new String(bytes, Charset.forName("utf-8"));
            buffer.clear();
        }
        
    } finally {
        socket.socket().shutdownInput();
    }
    return req;
}

Java的程序有個通病,寫出來的程序又臭又長,同樣是使用JavaNIO的API實現一個非阻塞的Socket服務器,使用NIO.2中AIO(異步I/O)的API就很簡單了,但是卻陷入了回調地獄(當然可以通過別的方式避免回調,但是其本質還是一樣的)。和上邊介紹的Reactor模式相比,簡直就是拿核武器比步槍,有點降維攻擊的意味了。Reactor中那么復雜的概念和邏輯所實現的功能,使用AIO的API很輕松就搞定了,而且概念比較少,邏輯更清晰。

//代碼8
private static void useAIO() {
    AsynchronousServerSocketChannel server;
    try {
        server = AsynchronousServerSocketChannel.open();
        server.bind(LOCAL_8080);
        while (true) {
            Future<AsynchronousSocketChannel> socketF = server.accept();
            try {
                final AsynchronousSocketChannel socket  = socketF.get();
                workers.submit(() -> {
                    try {
                        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
                        
                        socket.read(buffer, null, new CompletionHandler<Integer, Object>() {

                            @Override
                            public void completed(Integer count, Object attachment) {
                                byte[] bytes;
                                if (count >= 0) {
                                    buffer.flip();
                                    bytes = new byte[count];
                                    buffer.get(bytes);
                                    String req = new String(bytes, Charset.forName("utf-8"));
                                    TestRequest request = TestRequest.parseFromString(req);
                                    try {
                                        SockerServerHandler handler = (SockerServerHandler) Class.forName(getClassNameForMethod(request.method)).newInstance();
                                        ByteBuffer bb = handler.handle(request);
                                        socket.write(bb, null, null);
                                    } catch (InstantiationException | IllegalAccessException
                                            | ClassNotFoundException e) {
                                        // TODO Auto-generated catch block
                                        e.printStackTrace();
                                    }
                                    buffer.clear();
                                }
                            }

                            @Override
                            public void failed(Throwable exc, Object attachment) {
                                // TODO Auto-generated method stub
                                
                            }
                        });
                        
                        
                    } catch (Exception e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    } finally {
                        
                    }       
                });
            } catch (InterruptedException | ExecutionException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
                break;
            }
        }
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

最后是測試用的客戶端程序,NIO在客戶端同樣也可以發揮很重要的作用,這里就先略過了,代碼9中客戶端測試使用的是基礎I/O:

//代碼9
private volatile static int succ = 0;

public static void main(String[] args) throws UnknownHostException, IOException {
    CountDownLatch latch = new CountDownLatch(100);
    for (int i = 0; i < 100; i++) {
        new Thread( () -> {
            Socket soc;
            try {
                soc = new Socket("localhost", 8080);

                if (soc.isConnected()) {
                    OutputStream out = soc.getOutputStream();
                    byte[] req = "hello".getBytes("utf-8");
                    
                    out.write(Arrays.copyOf(req, 1024));
                    InputStream in = soc.getInputStream();
                    byte[] resp = new byte[1024];
                    in.read(resp, 0, 1024);
                    String result = new String(resp, "utf-8");
                    if (result.equals("haha")) {
                        succ++;
                    }
                    System.out.println(Thread.currentThread().getName() + "收到回復:" + result);
                    out.flush();
                    out.close();
                    in.close();
                    soc.close();
                }
                try {
                    System.out.println(Thread.currentThread().getName() + "去睡覺等待。。。");
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();

        latch.countDown();
    }
    Runnable hook = () -> {
        System.out.println("成功個數:" + succ);
    };
    Runtime.getRuntime().addShutdownHook(new Thread(hook));
}

總結

原本只是想寫一篇Netty在RPC框架中的使用,寫著寫著就寫多了。本文從Java中引入NIO的歷史講起,梳理了Java對NIO支持的具體的API,最后通過一個典型的Socket服務器的例子具體的展示了Java中NIO相關API的使用,將Reactor模式和Proactor模式從理論落地到實際的代碼。

由于作者比較懶,貼圖全部都是在網上找的(代碼大部分是自己寫的),如侵刪。下一篇將講到比較火的一個NIO框架Netty的實現與使用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,830評論 18 139
  • 零、寫在前面 本文雖然是講Netty,但實際更關注的是Netty中的NIO的實現,所以對于Netty中的OIO(O...
    TheAlchemist閱讀 3,310評論 1 34
  • I/O是任何一個程序設計者都無法忽略的存在,很多高級編程語言都在嘗試使用巧妙的設計屏蔽I/O的實際存在,減小它對程...
    TheAlchemist閱讀 2,195評論 1 27
  • NIO(Non-blocking I/O,在Java領域,也稱為New I/O),是一種同步非阻塞的I/O模型,也...
    閃電是只貓閱讀 3,137評論 0 7
  • 六月初,淬淚殤;憂傷郁,韻月痕。 微涼的氣息沁入心脾,隨著呼吸蔓延全身,雨后清新混著泥土的新鮮味道讓人迷醉。。 嬌...
    走過歲月的朝夕閱讀 177評論 0 0