一文看懂java io系統

原文:chenmingyu.top/nio/html

學習java IO系統,重點是學會IO模型,瞭解了各類IO模型以後就能夠更好的理解java IOjava

Java IO 是一套Java用來讀寫數據(輸入和輸出)的API。大部分程序都要處理一些輸入,並由輸入產生一些輸出。Java爲此提供了java.io包編程

java中io系統能夠分爲Bio,Nio,Aio三種io模型數組

  1. 關於Bio,咱們須要知道什麼是同步阻塞IO模型,Bio操做的對象:流,以及如何使用Bio進行網絡編程,使用Bio進行網絡編程的問題
  2. 關於Nio,咱們須要知道什麼是同步非阻塞IO模型,什麼是多路複用Io模型,以及Nio中的Buffer,Channel,Selector的概念,以及如何使用Nio進行網絡編程
  3. 關於Aio,咱們須要知道什麼是異步非阻塞IO模型,Aio可使用幾種方式實現異步操做,以及如何使用Aio進行網絡編程

BIO

BIO是同步阻塞IO,JDK1.4以前只有這一個IO模型,BIO操做的對象是流,一個線程只能處理一個流的IO請求,若是想要同時處理多個流就須要使用多線程緩存

流包括字符流和字節流,流從概念上來講是一個連續的數據流。當程序須要讀數據的時候就須要使用輸入流讀取數據,當須要往外寫數據的時候就須要輸出流bash

阻塞IO模型

在Linux中,當應用進程調用recvfrom方法調用數據的時候,若是內核沒有把數據準備好不會馬上返回,而是會經歷等待數據準備就緒,數據從內核複製到用戶空間以後再返回,這期間應用進程一直阻塞直到返回,因此被稱爲阻塞IO模型服務器

BIO中操做的流主要有兩大類,字節流和字符流,兩類根據流的方向均可以分爲輸入流和輸出流markdown

按照類型和輸入輸出方向可分爲:網絡

  1. 輸入字節流:InputStream
  2. 輸出字節流:OutputStream
  3. 輸入字符流:Reader
  4. 輸出字符流:Writer

字節流主要用來處理字節或二進制對象,字符流用來處理字符文本或字符串多線程

使用InputStreamReader能夠將輸入字節流轉化爲輸入字符流

Reader reader  =  new InputStreamReader(inputStream);
複製代碼

使用OutputStreamWriter能夠將輸出字節流轉化爲輸出字符流

Writer writer = new OutputStreamWriter(outputStream)
複製代碼

咱們能夠在程序中經過InputStream和Reader從數據源中讀取數據,而後也能夠在程序中將數據經過OutputStream和Writer輸出到目標媒介中

在使用字節流的時候,InputStream和OutputStream都是抽象類,咱們實例化的都是他們的子類,每個子類都有本身的做用範圍

在使用字符流的時候也是,Reader和Writer都是抽象類,咱們實例化的都是他們的子類,每個子類都有本身的做用範圍

以讀寫文件爲例

從數據源中讀取數據

輸入字節流:InputStream

public static void main(String[] args) throws Exception{
    File file = new File("D:/a.txt");
    InputStream inputStream = new FileInputStream(file);
    byte[] bytes = new byte[(int) file.length()];
    inputStream.read(bytes);
    System.out.println(new String(bytes));
    inputStream.close();
}
複製代碼

輸入字符流:Reader

public static void main(String[] args) throws Exception{
    File file = new File("D:/a.txt");
    Reader reader = new FileReader(file);
    char[] bytes = new char[(int) file.length()];
    reader.read(bytes);
    System.out.println(new String(bytes));
    reader.close();
}
複製代碼

輸出到目標媒介

輸出字節流:OutputStream

public static void main(String[] args) throws Exception{
    String var = "hai this is a test";
    File file = new File("D:/b.txt");
    OutputStream outputStream = new FileOutputStream(file);
    outputStream.write(var.getBytes());
    outputStream.close();
}
複製代碼

輸出字符流:Writer

public static void main(String[] args) throws Exception{
    String var = "hai this is a test";
    File file = new File("D:/b.txt");
    Writer writer = new FileWriter(file);
    writer.write(var);
    writer.close();
}
複製代碼

BufferedInputStream

在使用InputStream的時候,都是一個字節一個字節的讀或寫,而BufferedInputStream爲輸入字節流提供了緩衝區,讀數據的時候會一次讀取一塊數據放到緩衝區裏,當緩衝區裏的數據被讀完以後,輸入流會再次填充數據緩衝區,直到輸入流被讀完,有了緩衝區就可以提升不少io速度

使用方式將輸入流包裝到BufferedInputStream中

/** * inputStream 輸入流 * 1024 內部緩衝區大小爲1024byte */
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream,1024);
複製代碼

BufferedOutputStream

BufferedOutputStream能夠爲輸出字節流提供緩衝區,做用與BufferedInputStream相似

使用方式將輸出流包裝到BufferedOutputStream中

/** * outputStream 輸出流 * 1024 內部緩衝區大小爲1024byte */
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream,1024);
複製代碼

字節流提供了帶緩衝區的,那字符流確定也提供了BufferedReader和BufferedWriter

BufferedReader

爲輸入字符流提供緩衝區,使用方式以下

BufferedReader bufferedReader = new BufferedReader(reader,1024);
複製代碼

BufferedWriter

爲輸出字符流提供緩衝區,使用方式以下

BufferedWriter bufferedWriter = new BufferedWriter(writer,1024);
複製代碼

BIO模型 網絡編程

當使用BIO模型進行Socket編程的時候,服務端一般使用while循環中調用accept方法,在沒有客戶端請求時,accept方法會一直阻塞,直到接收到請求並返回處理的相應,這個過程都是線性的,只有處理完當前的請求以後纔會接受處理後面的請求,這樣一般會致使通訊線程被長時間阻塞

BIO模型處理多個鏈接:

在這種模式中咱們一般用一個線程去接受請求,而後用一個線程池去處理請求,用這種方式併發管理多個Socket客戶端鏈接,像這樣:

使用BIO模型進行網絡編程的問題在於缺少彈性伸縮能力,客戶端併發訪問數量和服務器線程數量是1:1的關係,並且平時因爲阻塞會有大量的線程處於等待狀態,等待輸入或者輸出數據就緒,形成資源浪費,在面對大量併發的狀況下,若是不使用線程池直接new線程的話,就會大體線程膨脹,系統性能降低,有可能致使堆棧的內存溢出,並且頻繁的建立銷燬線程,更浪費資源

使用線程池多是更優一點的方案,可是沒法解決阻塞IO的阻塞問題,並且還須要考慮若是線程池的數量設置較小就會拒絕大量的Socket客戶端的鏈接,若是線程池數量設置較大的時候,會致使大量的上下文切換,並且程序要爲每一個線程的調用棧都分配內存,其默認值大小區間爲 64 KB 到 1 MB,浪費虛擬機內存

BIO模型適用於連接數目固定並且比較少的架構,可是使用這種模型寫的代碼更直觀簡單易於理解

NIO

JDK 1.4版本以來,JDK發佈了全新的I/O類庫,簡稱NIO,是一種同步非阻塞IO模型

非阻塞IO模型

同步非阻塞IO模型實現:

非阻塞IO模型

應用進程調用recvfrom系統調用,若是內核數據沒有準備好,會直接返回一個EWOULDBLOCK錯誤,應用進程不會阻塞,可是須要應用進程不斷的輪詢調用recvfrom,直到內核數據準備就緒,以後等待數據從內核複製到用戶空間(這段時間會阻塞,可是耗時極小),複製完成後返回

IO複用模型

IO複用模型,利用Linux系統提供的select,poll系統調用,將一個或者多個文件句柄(網絡編程中的客戶端連接)傳遞給select或者poll系統調用,應用進程阻塞在select上,這樣就造成了一個進程對應多個Socket連接,而後select/poll會線性掃描這個Socket連接的集合,當只有少數socket有數據的時候,會致使效率降低,並且select/poll受限於所持有的文件句柄數量,默認值是1024個

信號驅動 IO模型

系統調用sigaction執行一個信號處理函數,這個系統調用不會阻塞應用進程,當數據準備就緒的時候,就爲該進程生成一個SIGIO信號,經過信號回調通知應用程序調用recvfrom來讀取數據

NIO的核心概念

Buffer(緩衝區)

Buffer是一個對象,它包含一些要寫入或者讀出的數據,在NIO中全部數據都是用緩存區處理的,在讀數據的時候要從緩衝區中讀,寫數據的時候會先寫到緩衝區中,緩衝區本質上是一塊能夠寫入數據,而後能夠從中讀取數據的一個數組,提供了對數據的結構化訪問以及在內部維護了讀寫位置等信息

實例化一個ByteBuffer

//建立一個容量爲1024個byte的緩衝區
ByteBuffer buffer=ByteBuffer.allocate(1024);
複製代碼

如何使用Buffer:

  1. 寫入數據到Buffer
  2. 調用flip()方法將Buffer從寫模式切換到讀模式
  3. 從Buffer中讀取數據
  4. 調用clear()方法或者compact()方法清空緩衝區,讓它能夠再次被寫入

更多詳細信息看這個:ifeve.com/buffers/

Channel(通道)

Channel(通道)數據老是從通道讀取到緩衝區,或者從緩衝區寫入到通道中,Channel只負責運輸數據,而操做數據是Buffer

通道與流相似,不一樣地方:

  1. 在於條通道是雙向的,能夠同時進行讀,寫操做,而流是單向流動的,只能寫入或者讀取
  2. 流的讀寫是阻塞的,通道能夠異步讀寫

數據從Channel讀到Buffer

inChannel.read(buffer);
複製代碼

數據從Buffer寫到Channel

outChannel.write(buffer);
複製代碼

更多詳細信息看這個:<ifeve.com/channels/>

以複製文件爲例

FileInputStream fileInputStream=new FileInputStream(new File(src));
FileOutputStream fileOutputStream=new FileOutputStream(new File(dst));
//獲取輸入輸出channel通道
FileChannel inChannel=fileInputStream.getChannel();
FileChannel outChannel=fileOutputStream.getChannel();
//建立容量爲1024個byte的buffer
ByteBuffer buffer=ByteBuffer.allocate(1024);
while(true){
    //從inChannel裏讀數據,若是讀不到字節了就返回-1,文件就讀完了
    int eof =inChannel.read(buffer);
    if(eof==-1){
        break;
    }
    //將Buffer從寫模式切換到讀模式
    buffer.flip();
    //開始往outChannel寫數據
    outChannel.write(buffer);
    //清空buffer
    buffer.clear();
}
inChannel.close();
outChannel.close();
fileInputStream.close();
fileOutputStream.close();
複製代碼

Selector(多路複用選擇器)

Selector是NIO編程的基礎,主要做用就是將多個Channel註冊到Selector上,若是Channel上發生讀或寫事件,Channel就處於就緒狀態,就會被Selector輪詢出來,而後經過SelectionKey就能夠獲取到已經就緒的Channel集合,進行IO操做了

Selector與Channel,Buffer之間的關係

更多詳細信息看這個:<ifeve.com/selectors/

NIO模型 網絡編程

JDK中NIO使用多路複用的IO模型,經過把多個IO阻塞複用到一個select的阻塞上,實現系統在單線程中能夠同時處理多個客戶端請求,節省系統開銷,在JDK1.4和1.5 update10版本以前,JDK的Selector基於select/poll模型實現,在JDK 1.5 update10以上的版本,底層使用epoll代替了select/poll

epoll較select/poll的優勢在於:

  1. epoll支持打開的文件描述符數量不在受限制,select/poll能夠打開的文件描述符數量有限
  2. select/poll使用輪詢方式遍歷整個文件描述符的集合,epoll基於每一個文件描述符的callback函數回調

select,poll,epoll都是IO多路複用的機制。I/O多路複用就是經過一種機制,一個進程能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做。但select,poll,epoll本質上都是同步I/O,由於他們都須要在讀寫事件就緒後本身負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需本身負責進行讀寫

NIO提供了兩套不一樣的套接字通道實現網絡編程,服務端:ServerSocketChannel和客戶端SocketChannel,兩種通道都支持阻塞和非阻塞模式

服務端代碼

服務端接受客戶端發送的消息輸出,並給客戶端發送一個消息

//建立多路複用選擇器Selector
        Selector selector=Selector.open();
        //建立一個通道對象Channel,監聽9001端口
        ServerSocketChannel channel = ServerSocketChannel.open().bind(new InetSocketAddress(9001));
        //設置channel爲非阻塞
        channel.configureBlocking(false);
        //
        /** * 1.SelectionKey.OP_CONNECT:鏈接事件 * 2.SelectionKey.OP_ACCEPT:接收事件 * 3.SelectionKey.OP_READ:讀事件 * 4.SelectionKey.OP_WRITE:寫事件 * * 將channel綁定到selector上並註冊OP_ACCEPT事件 */
        channel.register(selector,SelectionKey.OP_ACCEPT);

        while (true){
            //只有當OP_ACCEPT事件到達時,selector.select()會返回(一個key),若是該事件沒到達會一直阻塞
            selector.select();
            //當有事件到達了,select()不在阻塞,而後selector.selectedKeys()會取到已經到達事件的SelectionKey集合
            Set keys = selector.selectedKeys();
            Iterator iterator = keys.iterator();
            while (iterator.hasNext()){
                SelectionKey key = (SelectionKey) iterator.next();
                //刪除這個SelectionKey,防止下次select方法返回已處理過的通道
                iterator.remove();
                //根據SelectionKey狀態判斷
                if (key.isConnectable()){
                    //鏈接成功
                } else if (key.isAcceptable()){
                    /** * 接受客戶端請求 * * 由於咱們只註冊了OP_ACCEPT事件,因此有客戶端連接上,只會走到這 * 咱們要作的就是去讀取客戶端的數據,因此咱們須要根據SelectionKey獲取到serverChannel * 根據serverChannel獲取到客戶端Channel,而後爲其再註冊一個OP_READ事件 */
                    // 1,獲取到ServerSocketChannel
                    ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                    // 2,由於已經肯定有事件到達,因此accept()方法不會阻塞
                    SocketChannel clientChannel = serverChannel.accept();
                    // 3,設置channel爲非阻塞
                    clientChannel.configureBlocking(false);
                    // 4,註冊OP_READ事件
                    clientChannel.register(key.selector(),SelectionKey.OP_READ);
                } else if (key.isReadable()){
                    // 通道能夠讀數據
                    /** * 由於客戶端連上服務器以後,註冊了一個OP_READ事件發送了一些數據 * 因此首先仍是須要先獲取到clientChannel * 而後經過Buffer讀取clientChannel的數據 */
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
                    long bytesRead = clientChannel.read(byteBuffer);
                    while (bytesRead>0){
                        byteBuffer.flip();
                        System.out.println("client data :"+new String(byteBuffer.array()));
                        byteBuffer.clear();
                        bytesRead = clientChannel.read(byteBuffer);
                    }

                    /** * 咱們服務端收到信息以後,咱們再給客戶端發送一個數據 */
                    byteBuffer.clear();
                    byteBuffer.put("客戶端你好,我是服務端,你看這NIO多難".getBytes("UTF-8"));
                    byteBuffer.flip();
                    clientChannel.write(byteBuffer);
                } else if (key.isWritable() && key.isValid()){
                    //通道能夠寫數據
                }

            }
        }
複製代碼

客戶端代碼

客戶端鏈接上服務端後,先給服務端發送一個消息,並接受服務端發送的消息

Selector selector = Selector.open();
SocketChannel clientChannel = SocketChannel.open();
//將channel設置爲非阻塞
clientChannel.configureBlocking(false);
//鏈接服務器
clientChannel.connect(new InetSocketAddress(9001));
//註冊OP_CONNECT事件
clientChannel.register(selector, SelectionKey.OP_CONNECT);
while (true){
    //若是事件沒到達就一直阻塞着
    selector.select();
    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
    while (iterator.hasNext()){
        SelectionKey key = iterator.next();
        iterator.remove();
        if (key.isConnectable()){
            /** * 鏈接服務器端成功 * * 首先獲取到clientChannel,而後經過Buffer寫入數據,而後爲clientChannel註冊OP_READ時間 */
            clientChannel = (SocketChannel) key.channel();
            if (clientChannel.isConnectionPending()){
                clientChannel.finishConnect();
            }
            clientChannel.configureBlocking(false);
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            byteBuffer.clear();
            byteBuffer.put("服務端你好,我是客戶端,你看這NIO難嗎".getBytes("UTF-8"));
            byteBuffer.flip();
            clientChannel.write(byteBuffer);
            clientChannel.register(key.selector(),SelectionKey.OP_READ);
        } else if (key.isReadable()){
            //通道能夠讀數據
            clientChannel = (SocketChannel) key.channel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
            long bytesRead = clientChannel.read(byteBuffer);
            while (bytesRead>0){
                byteBuffer.flip();
                System.out.println("server data :"+new String(byteBuffer.array()));
                byteBuffer.clear();
                bytesRead = clientChannel.read(byteBuffer);
            }
        } else if (key.isWritable() && key.isValid()){
            //通道能夠寫數據
        }
    }
}
複製代碼

使用原生NIO類庫十分複雜,NIO的類庫和Api繁雜,使用麻煩,須要對網絡編程十分熟悉,才能編寫出高質量的NIO程序,因此並不建議直接使用原生NIO進行網絡編程,而是使用一些成熟的框架,好比Netty

AIO

JDK1.7升級了Nio類庫,成爲Nio2.0,最主要的是提供了異步文件的IO操做,以及事件驅動IO,AIO的異步套接字通道是真正的異步非阻塞IO

異步IO模型

在Linux系統中,應用進程發起read操做,馬上能夠去作其餘的事,內核會將數據準備好而且複製到用空間後告訴應用進程,數據已經複製完成read操做

aio模型 網絡編程

異步操做

aio不須要經過多路複用器對註冊的通道進行輪詢操做就能夠實現異步讀寫,從而簡化了NIO的編程模型

aio經過異步通道實現異步操做,異步通道提供了兩種方式獲取操做結果:

  1. 經過Future類來獲取異步操做的結果,不過要注意的是future.get()是阻塞方法,會阻塞線程
  2. 經過回調的方式進行異步,經過傳入一個CompletionHandler的實現類進行回調,CompletionHandler定義了兩個方法,completed和failed兩方法分別對應成功和失敗

Aio中的Channel都支持以上兩種方式

AIO提供了對應的異步套接字通道實現網絡編程,服務端:AsynchronousServerSocketChannel和客戶端AsynchronousSocketChannel

服務端

服務端向客戶端發送消息,並接受客戶端發送的消息

AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("127.0.0.1", 9001));
//異步接受請求
server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
    //成功時
    @Override
    public void completed(AsynchronousSocketChannel result, Void attachment) {
        try {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.put("我是服務端,客戶端你好".getBytes());
            buffer.flip();
            result.write(buffer, null, new CompletionHandler<Integer, Void>(){
                @Override
                public void completed(Integer result, Void attachment) {
                    System.out.println("服務端發送消息成功");
                }

                @Override
                public void failed(Throwable exc, Void attachment) {
                    System.out.println("發送失敗");
                }
            });

            ByteBuffer readBuffer = ByteBuffer.allocate(1024);
            result.read(readBuffer, null, new CompletionHandler<Integer, Void>() {
                //成功時調用
                @Override
                public void completed(Integer result, Void attachment) {
                    System.out.println(new String(readBuffer.array()));
                }
                //失敗時調用
                @Override
                public void failed(Throwable exc, Void attachment) {
                    System.out.println("讀取失敗");
                }
            });

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    //失敗時
    @Override
    public void failed(Throwable exc, Void attachment) {
        exc.printStackTrace();
    }
});
//防止線程執行完
TimeUnit.SECONDS.sleep(1000L);
複製代碼

客戶端

客戶端向服務端發送消息,並接受服務端發送的消息

AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
Future<Void> future = client.connect(new InetSocketAddress("127.0.0.1", 9001));
//阻塞,獲取鏈接
future.get();

ByteBuffer buffer = ByteBuffer.allocate(1024);
//讀數據
client.read(buffer, null, new CompletionHandler<Integer, Void>() {
    //成功時調用
    @Override
    public void completed(Integer result, Void attachment) {
        System.out.println(new String(buffer.array()));
    }
    //失敗時調用
    @Override
    public void failed(Throwable exc, Void attachment) {
        System.out.println("客戶端接收消息失敗");
    }
});

ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
writeBuffer.put("我是客戶端,服務端你好".getBytes());
writeBuffer.flip();
//阻塞方法
Future<Integer> write = client.write(writeBuffer);
Integer r = write.get();
if(r>0){
    System.out.println("客戶端消息發送成功");
}
//休眠線程
TimeUnit.SECONDS.sleep(1000L);
複製代碼

總結

各IO模型對比:

僞異步IO是指使用線程池處理請求的Bio模型

參考:

netty權威指南 第二版

ifeve.com/java-nio-al… 併發編程網

tech.meituan.com/2016/11/04/… 美團技術團隊

文中圖片若有侵權,聯繫我刪除

相關文章
相關標籤/搜索