第九章 Java I/O與NIO

Java IO

字節與字符

在Java中有輸入、輸出兩種IO流,每種輸入、輸出流又分爲字節流和字符流兩大類。關於字節,我們在學習8大基本數據類型中都有了解,每個字節(byte)有8bit組成,每種數據類型又幾個字節組成等。關於字符,我們可能知道代表一個漢字或者英文字母。
但是字節與字符之間的關係是怎樣的?
Java採用unicode編碼,2個字節來表示一個字符,這點與C語言中不同,C語言中採用ASCII,在大多數系統中,一個字符通常佔1個字節,但是在0~127整數之間的字符映射,unicode向下兼容ASCII。而Java採用unicode來表示字符,一箇中文或英文字符的unicode編碼都佔2個字節。但如果採用其他編碼方式,一個字符佔用的字節數則各不相同。可能有點暈,舉個例子解釋下。
例如:Java中的String類是按照unicode進行編碼的,當使用String(byte[] bytes, String encoding)構造字符串時,encoding所指的是bytes中的數據是按照那種方式編碼的,而不是最後產生的String是什麼編碼方式,換句話說,是讓系統把bytes中的數據由encoding編碼方式轉換成unicode編碼。如果不指明,bytes的編碼方式將由jdk根據操作系統決定。
getBytes(String charsetName)使用指定的編碼方式將此String編碼爲 byte 序列,並將結果存儲到一個新的 byte 數組中。如果不指定將使用操作系統默認的編碼方式,我的電腦默認的是GBK編碼。

public class Hel {  
    public static void main(String[] args){  
        String str = "你好hello";  
            int byte_len = str.getBytes().length;  
            int len = str.length();  
            System.out.println("字節長度爲:" + byte_len);  
        System.out.println("字符長度爲:" + len);  
        System.out.println("系統默認編碼方式:" + System.getProperty("file.encoding"));  
       }  
}

輸出結果

字節長度爲:9
字符長度爲:7
系統默認編碼方式:GBK

這是因爲:在 GB 2312 編碼或 GBK 編碼中,一個英文字母字符存儲需要1個字節,一個漢字字符存儲需要2個字節。 在UTF-8編碼中,一個英文字母字符存儲需要1個字節,一個漢字字符儲存需要3到4個字節。在UTF-16編碼中,一個英文字母字符存儲需要2個字節,一個漢字字符儲存需要3到4個字節(Unicode擴展區的一些漢字存儲需要4個字節)。在UTF-32編碼中,世界上任何字符的存儲都需要4個字節。
簡單來講,一個字符表示一個漢字或英文字母,具體字符與字節之間的大小比例視編碼情況而定。有時候讀取的數據是亂碼,就是因爲編碼方式不一致,需要進行轉換,然後再按照unicode進行編碼。

Java 編碼格式

計算機中存儲信息的最小單元是1byte即8bit,所以能表示的字符範圍是 0~255 個,人類要表示的符號太多,無法用一個字節來完全表示,需要一個新的數據結構char來表示這些字符。char與byte之間轉換需要編碼與解碼。
編碼:字節(Byte = 8bit 默認數據的最小單位)與字符(Character = 2byte)的轉換方式。
編碼就是把字符轉換爲字節,而解碼是把字節重新組合成字符。
如果編碼和解碼過程使用不同的編碼方式那麼就出現了亂碼。
編碼規範

  1. ASCII
    ASCII碼一共規定了128個字符的編碼,用一個字節的低 7 位表示,0~31 是控制字符如換行回車刪除等;32~126 是打印字符,可以通過鍵盤輸入並且能夠顯示出來。
  2. ISO-8859-1
    ISO 組織在 ASCII 碼基礎上又制定了一些列標準用來擴展 ASCII 編碼,共有256個字符。
  3. GB2312
  4. GBK
    中文字符佔 2 個字節,英文字符佔 1 個字節;
  5. UTF-16
    中文字符和英文字符都佔 2 個字節;
  6. UTF-8
    UTF-8就是在互聯網上使用最廣的一種Unicode的實現方式。UTF-8最大的一個特點,就是它是一種變長的編碼方式。它可以使用1~4個字節表示一個符號,根據不同的符號而變化字節長度。
    中文字符佔 3 個字節,英文字符佔 1 個字節;
    Java中需要編碼的場景包括:
  7. I/O操作中存在的編碼
    Reader類是Java I/O中讀字符的父類,InputStream類是讀字節的父類,通過InputStreamReader類將字節轉換爲字符。
    同理,Writer是寫字符的父類,OutputStram類是寫字節的父類,通過OutputStreamWriter將字符轉換爲字節。
public class File_Stream {
    public static void main(String[] args) throws IOException {
        Scanner sca=new Scanner(System.in);
        //寫文件
        System.out.print("請輸入文件名:");
        String name=sca.next();
        File file=new File(name+".txt");//文件名        相對路徑(項目名根目錄下)
//      FileOutputStream fs=new FileOutputStream(file);//如果文件存在 覆蓋
        FileOutputStream fos=new FileOutputStream(file,true);//true表示追加,如果文件存在 向裏面繼續添加內容
        System.out.println("請輸入寫入的內容:");
        String str=sca.next();
        byte bytes[]=str.getBytes();         //FileOutputStream 是基於字節流  把要寫入的信息 保存到字節數組中
        fos.write(bytes,0,bytes.length);//將字節數組中全部內容寫到文件中   從0—數組的長度
        fos.close();//關閉流
        System.out.println("文件寫入成功!");
 
        //讀文件
        FileInputStream fis=new FileInputStream(file);
        byte bt[]=new byte[1024];//1KB       每次最多讀取的1KB  根據文件大小而定
        int temp=0;
        while((temp=fis.read(bt))!=-1){    //將數據保存到數組(緩衝區)中 並返回讀取的字節數  -1表示讀完了
            System.out.println(new String(bt,0,temp));//輸出數組中保存內容 按照每次讀取的字節數
        }
        fis.close();
    }
}
  1. 內存中的編碼(String 的 編碼方式)
    Java 中用 String 表示字符串,所以 String 類就提供轉換到字節的方法,也支持將字節轉換爲字符串的構造函數。
String s = "這是一段中文字符串";    
 byte[] b = s.getBytes("UTF-8");    // (字符轉字節)將字符串所表示的字符按照charset編碼,並以字節方式表示。
 String n = new String(b,"UTF-8");	// (字節轉字符)將字節數組按照charset編碼進行組合識別,最後轉換爲unicode存儲

File類

File類是java.io包下代表與平臺無關的文件和目錄,也就是說,如果希望在程序中操作文件和目錄,都可以通過File類來完成。
① 構造函數

//構造函數File(String pathname)
File f1 =new File("c:\\abc\\1.txt");
//File(String parent,String child)
File f2 =new File("c:\\abc","2.txt");
//File(File parent,String child)
File f3 =new File("c:"+File.separator+"abc");//separator 跨平臺分隔符
File f4 =new File(f3,"3.txt");
System.out.println(f1);//c:\abc\1.txt

路徑分隔符: windows: “/” “” 都可以 linux/unix: “/”
注意:如果windows選擇用"“做分割符的話,那麼請記得替換成”",因爲Java中"“代表轉義字符
所以推薦都使用”/",也可以直接使用代碼File.separator,表示跨平臺分隔符。
路徑:

  • 相對路徑:
    ./表示當前路徑(默認情況下,java.io 包中的類總是根據當前用戶目錄來分析相對路徑名。此目錄由系統屬性user.dir 指定,通常是 Java 虛擬機的調用目錄。)
    …/表示上一級路徑
  • 絕對路徑:
    絕對路徑名是完整的路徑名,不需要任何其他信息就可以定位自身表示的文件

② 創建與刪除方法

//如果文件存在返回false,否則返回true並且創建文件 
boolean createNewFile();
//創建一個File對象所對應的目錄,成功返回true,否則false。且File對象必須爲路徑而不是文件。只會創建最後一級目錄,如果上級目錄不存在就拋異常。
boolean mkdir();
//創建一個File對象所對應的目錄,成功返回true,否則false。且File對象必須爲路徑而不是文件。創建多級目錄,創建路徑中所有不存在的目錄
boolean mkdirs()    ;
//如果文件存在返回true並且刪除文件,否則返回false
boolean delete();
//在虛擬機終止時,刪除File對象所表示的文件或目錄。
void deleteOnExit();

③ 判斷方法

boolean canExecute()    ;//判斷文件是否可執行
boolean canRead();//判斷文件是否可讀
boolean canWrite();//判斷文件是否可寫
boolean exists();//判斷文件是否存在
boolean isDirectory();//判斷是否是目錄
boolean isFile();//判斷是否是文件
boolean isHidden();//判斷是否是隱藏文件或隱藏目錄
boolean isAbsolute();//判斷是否是絕對路徑 文件不存在也能判斷

③獲取方法

String getName();//返回文件或者是目錄的名稱
String getPath();//返回路徑
String getAbsolutePath();//返回絕對路徑
String getParent();//返回父目錄,如果沒有父目錄則返回null
long lastModified();//返回最後一次修改的時間
long length();//返回文件的長度
File[] listRoots();// 列出所有的根目錄(Window中就是所有系統的盤符)
String[] list() ;//返回一個字符串數組,給定路徑下的文件或目錄名稱字符串
String[] list(FilenameFilter filter);//返回滿足過濾器要求的一個字符串數組
File[]  listFiles();//返回一個文件對象數組,給定路徑下文件或目錄
File[] listFiles(FilenameFilter filter);//返回滿足過濾器要求的一個文件對象數組

其中包含了一個重要的接口FileNameFilter,該接口是個文件過濾器,包含了一個accept(File dir,String name)方法,該方法依次對指定File的所有子目錄或者文件進行迭代,按照指定條件,進行過濾,過濾出滿足條件的所有文件。

        // 文件過濾
        File[] files = file.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File file, String filename) {
                return filename.endsWith(".mp3");
            }
        });

file目錄下的所有子文件如果滿足後綴是.mp3的條件的文件都會被過濾出來。

RandomAccessFile

  • 簡介

RandomAccessFile既可以讀取文件內容,也可以向文件輸出數據。同時,RandomAccessFile支持“隨機訪問”的方式,程序快可以直接跳轉到文件的任意地方來讀寫數據。
由於RandomAccessFile可以自由訪問文件的任意位置,所以如果需要訪問文件的部分內容,而不是把文件從頭讀到尾,使用RandomAccessFile將是更好的選擇。
與OutputStream、Writer等輸出流不同的是,RandomAccessFile允許自由定義文件記錄指針,RandomAccessFile可以不從開始的地方開始輸出,因此RandomAccessFile可以向已存在的文件後追加內容。如果程序需要向已存在的文件後追加內容,則應該使用RandomAccessFile。

RandomAccessFile的方法雖然多,但它有一個最大的侷限,就是隻能讀寫文件,不能讀寫其他IO節點。
RandomAccessFile的一個重要使用場景就是網絡請求中的多線程下載及斷點續傳。

  • 方法
  1. RandomAccessFile的構造函數
    RandomAccessFile類有兩個構造函數,其實這兩個構造函數基本相同,只不過是指定文件的形式不同——一個需要使用String參數來指定文件名,一個使用File參數來指定文件本身。除此之外,創建RandomAccessFile對象時還需要指定一個mode參數,該參數指定RandomAccessFile的訪問模式,一共有4種模式。

“r”: 以只讀方式打開。調用結果對象的任何 write 方法都將導致拋出 IOException。
“rw”: 打開以便讀取和寫入。
“rws”: 打開以便讀取和寫入。相對於 “rw”,“rws” 還要求對“文件的內容”或“元數據”的每個更新都同步寫入到基礎存儲設備。
“rwd” : 打開以便讀取和寫入,相對於 “rw”,“rwd” 還要求對“文件的內容”的每個更新都同步寫入到基礎存儲設備。

  1. RandomAccessFile的重要方法
    RandomAccessFile既可以讀文件,也可以寫文件,所以類似於InputStream的read()方法,以及類似於OutputStream的write()方法,RandomAccessFile都具備。除此之外,RandomAccessFile具備兩個特有的方法,來支持其隨機訪問的特性。
    RandomAccessFile對象包含了一個記錄指針,用以標識當前讀寫處的位置,當程序新創建一個RandomAccessFile對象時,該對象的文件指針記錄位於文件頭(也就是0處),當讀/寫了n個字節後,文件記錄指針將會後移n個字節。除此之外,RandomAccessFile還可以自由移動該記錄指針。下面就是RandomAccessFile具有的兩個特殊方法,來操作記錄指針,實現隨機訪問:

long getFilePointer( ):返回文件記錄指針的當前位置
void seek(long pos):將文件指針定位到pos位置

  • 使用

利用RandomAccessFile實現文件的多線程下載,即多線程下載一個文件時,將文件分成幾塊,每塊用不同的線程進行下載。下面是一個利用多線程在寫文件時的例子,其中預先分配文件所需要的空間,然後在所分配的空間中進行分塊,然後寫入:

/** 
 * 測試利用多線程進行文件的寫操作 
 */  
public class Test {  

    public static void main(String[] args) throws Exception {  
        // 預分配文件所佔的磁盤空間,磁盤中會創建一個指定大小的文件  
        RandomAccessFile raf = new RandomAccessFile("D://abc.txt", "rw");  
        raf.setLength(1024*1024); // 預分配 1M 的文件空間  
        raf.close();  

        // 所要寫入的文件內容  
        String s1 = "第一個字符串";  
        String s2 = "第二個字符串";  
        String s3 = "第三個字符串";  
        String s4 = "第四個字符串";  
        String s5 = "第五個字符串";  

        // 利用多線程同時寫入一個文件  
        new FileWriteThread(1024*1,s1.getBytes()).start(); // 從文件的1024字節之後開始寫入數據  
        new FileWriteThread(1024*2,s2.getBytes()).start(); // 從文件的2048字節之後開始寫入數據  
        new FileWriteThread(1024*3,s3.getBytes()).start(); // 從文件的3072字節之後開始寫入數據  
        new FileWriteThread(1024*4,s4.getBytes()).start(); // 從文件的4096字節之後開始寫入數據  
        new FileWriteThread(1024*5,s5.getBytes()).start(); // 從文件的5120字節之後開始寫入數據  
    }  

    // 利用線程在文件的指定位置寫入指定數據  
    static class FileWriteThread extends Thread{  
        private int skip;  
        private byte[] content;  

        public FileWriteThread(int skip,byte[] content){  
            this.skip = skip;  
            this.content = content;  
        }  

        public void run(){  
            RandomAccessFile raf = null;  
            try {  
                raf = new RandomAccessFile("D://abc.txt", "rw");  
                raf.seek(skip);  
                raf.write(content);  
            } catch (FileNotFoundException e) {  
                e.printStackTrace();  
            } catch (IOException e) {  
                // TODO Auto-generated catch block  
                e.printStackTrace();  
            } finally {  
                try {  
                    raf.close();  
                } catch (Exception e) {  
                }  
            }  
        }  
    }  

}

當RandomAccessFile向指定文件中插入內容時,將會覆蓋掉原有內容。如果不想覆蓋掉,則需要將原有內容先讀取出來,然後先把插入內容插入後再把原有內容追加到插入內容後。

IO流

IO流 簡介

Java的IO流是實現輸入/輸出的基礎,它可以方便地實現數據的輸入/輸出操作,在Java中把不同的輸入/輸出源抽象表述爲"流"。流是一組有順序的,有起點和終點的字節集合,是對數據傳輸的總稱或抽象。即數據在兩設備間的傳輸稱爲流,流的本質是數據傳輸,根據數據傳輸特性將流抽象爲各種類,方便更直觀的進行數據操作。
流有輸入和輸出,輸入時是流從數據源流向程序。輸出時是流從程序傳向數據源,而數據源可以是內存,文件,網絡或程序等。

IO流 分類

  1. 輸入流和輸出流

根據數據流向不同分爲:輸入流和輸出流。
輸入流:程序從數據源中讀取數據。
輸出流:程序向數據源中寫入數據。
如下如所示:對程序而言,向右的箭頭,表示輸入,向左的箭頭,表示輸出。
在這裏插入圖片描述
2. 字節流和字符流

字節流和字符流和用法幾乎完全一樣,區別在於字節流和字符流所操作的數據單元不同。
字符流的由來: 因爲數據編碼的不同,而有了對字符進行高效操作的流對象。本質其實就是基於字節流讀取時,去查了指定的碼錶。字節流和字符流的區別:
(1)讀寫單位不同:字節流以字節(8bit)爲單位,字符流以字符爲單位,根據碼錶映射字符,一次可能讀多個字節。
(2)處理對象不同:字節流能處理所有類型的數據(如圖片、avi等),而字符流只能處理字符類型的數據。
只要是處理純文本數據,就優先考慮使用字符流。 除此之外都使用字節流。

  1. 節點流和處理流

按照流的角色來分,可以分爲節點流和處理流。
可以從/向一個特定的IO設備(如磁盤、網絡)讀/寫數據的流,稱爲節點流,節點流也被成爲低級流。
處理流是對一個已存在的流進行連接或封裝,通過封裝後的流來實現數據讀/寫功能,處理流也被稱爲高級流。

//節點流,直接傳入的參數是IO設備
FileInputStream fis = new FileInputStream("test.txt");
//處理流,直接傳入的參數是流對象
BufferedInputStream bis = new BufferedInputStream(fis);

在這裏插入圖片描述
當使用處理流進行輸入/輸出時,程序並不會直接連接到實際的數據源,沒有和實際的輸入/輸出節點連接。使用處理流的一個明顯好處是,只要使用相同的處理流,程序就可以採用完全相同的輸入/輸出代碼來訪問不同的數據源,隨着處理流所包裝節點流的變化,程序實際所訪問的數據源也相應地發生變化。
實際上,Java使用處理流來包裝節點流是一種典型的裝飾器設計模式,通過使用處理流來包裝不同的節點流,既可以消除不同節點流的實現差異,也可以提供更方便的方法來完成輸入/輸出功能。

IO流 四大基類

根據流的流向以及操作的數據單元不同,將流分爲了四種類型,每種類型對應一種抽象基類。這四種抽象基類分別爲:InputStream,Reader,OutputStream以及Writer。四種基類下,對應不同的實現類,具有不同的特性。在這些實現類中,又可以分爲節點流和處理流。下面就是整個由着四大基類支撐下,整個IO流的框架圖。
在這裏插入圖片描述
InputStream,Reader,OutputStream以及Writer,這四大抽象基類,本身並不能創建實例來執行輸入/輸出,但它們將成爲所有輸入/輸出流的模版,所以它們的方法是所有輸入/輸出流都可以使用的方法。類似於集合中的Collection接口。

  1. InputStream
    InputStream 是所有的輸入字節流的父類,它是一個抽象類,主要包含三個方法:
//讀取一個字節並以整數的形式返回(0~255),如果返回-1已到輸入流的末尾。 
int read()//讀取一系列字節並存儲到一個數組buffer,返回實際讀取的字節數,如果讀取前已到輸入流的末尾返回-1。 
int read(byte[] buffer)//讀取length個字節並存儲到一個字節數組buffer,從off位置開始存,最多len, 返回實際讀取的字節數,如果讀取前以到輸入流的末尾返回-1。 
int read(byte[] buffer, int off, int len)

實例

        // 讀取f盤下該文件f://hell/test.txt
        InputStream inputStream = new FileInputStream(new File("f://hello//test.txt"));
        int i = 0;
        //一次讀取一個字節
        while ((i = inputStream.read()) != -1) {
            System.out.print((char) i + " ");
        }
        //關閉IO流
        inputStream.close();
  1. Reader
    Reader 是所有的輸入字符流的父類,它是一個抽象類,主要包含三個方法:
//讀取一個字符並以整數的形式返回(0~255),如果返回-1已到輸入流的末尾。 
int read()//讀取一系列字符並存儲到一個數組buffer,返回實際讀取的字符數,如果讀取前已到輸入流的末尾返回-1。 
int read(char[] cbuf)//讀取length個字符,並存儲到一個數組buffer,從off位置開始存,最多讀取len,返回實際讀取的字符數,如果讀取前以到輸入流的末尾返回-1。 
int read(char[] cbuf, int off, int len)

實例

        //使用默認編碼        
        FileReader reader = new FileReader("test.txt");
        int len;
        while ((len = reader.read()) != -1) {
            System.out.print((char) len);
        }
        reader.close();

對比InputStream和Reader所提供的方法,就不難發現兩個基類的功能基本一樣的,只不過讀取的數據單元不同。
在執行完流操作後,要調用close()方法來關係輸入流,因爲程序裏打開的IO資源不屬於內存資源,垃圾回收機制無法回收該資源,所以應該顯式關閉文件IO資源。

  1. OutputStream
    OutputStream 是所有的輸出字節流的父類,它是一個抽象類,主要包含如下四個方法:
//向輸出流中寫入一個字節數據,該字節數據爲參數b的低8位。 
void write(int b) ; 
//將一個字節類型的數組中的數據寫入輸出流。 
void write(byte[] b); 
//將一個字節類型的數組中的從指定位置(off)開始的,len個字節寫入到輸出流。 
void write(byte[] b, int off, int len); 
//將輸出流中緩衝的數據全部寫出到目的地。 
void flush();

實例

        OutputStream outputStream = new FileOutputStream(new File("test.txt"));
        // 寫入數據
        outputStream.write("ABCD".getBytes());
        // 關閉IO流
        outputStream.close();
  1. Writer
    Writer 是所有的輸出字符流的父類,它是一個抽象類,主要包含如下六個方法:
//向輸出流中寫入一個字符數據,該字節數據爲參數b的低16位。 
void write(int c); 
//將一個字符類型的數組中的數據寫入輸出流, 
void write(char[] cbuf) 
//將一個字符類型的數組中的從指定位置(offset)開始的,length個字符寫入到輸出流。 
void write(char[] cbuf, int offset, int length); 
//將一個字符串中的字符寫入到輸出流。 
void write(String string); 
//將一個字符串從offset開始的length個字符寫入到輸出流。 
void write(String string, int offset, int length); 
//將輸出流中緩衝的數據全部寫出到目的地。 
void flush()

實例

        FileWriter writer = new FileWriter("test.txt");
        // 寫入數據
        writer.write("ABCD".getBytes());
        // 關閉IO流
        writer.close();

可以看出,Writer比OutputStream多出兩個方法,主要是支持寫入字符和字符串類型的數據。
使用Java的IO流執行輸出時,不要忘記關閉輸出流,關閉輸出流除了可以保證流的物理資源被回收之外,還能將輸出流緩衝區的數據flush到物理節點裏(因爲在執行close()方法之前,自動執行輸出流的flush()方法)

Java NIO

NIO 簡介

新的輸入/輸出 (NIO) 庫是在 JDK 1.4 中引入的,彌補了原來的 I/O 的不足,提供了高速的、面向塊的 I/O。
NIO 與普通 I/O 的區別主要有以下兩點:

  • NIO 是非阻塞的;
    當一個read操作發生時,它會經歷兩個階段:
    1 等待數據準備 (Waiting for the data to be ready)
    2 將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)
    blocking IO的特點就是在IO執行的兩個階段都被block了。
    non-blocking IO的特點是當用戶進程發出read操作時,如果kernel中的數據還沒有準備好,那麼它並不會block用戶進程,而是立刻返回一個error。用戶進程判斷結果是一個error時,它就知道數據還沒有準備好,於是它可以再次發送read操作。一旦kernel中的數據準備好了,並且又再次收到了用戶進程的system call,那麼它馬上就將數據拷貝到了用戶內存,然後返回。所以,用戶進程其實是需要不斷的主動詢問kernel數據好了沒有。
  • NIO 面向塊,I/O 面向流。
    I/O 與 NIO 最重要的區別是數據打包和傳輸的方式,I/O 以流的方式處理數據,而 NIO 以塊的方式處理數據。按塊處理數據比按流處理數據要快得多。但是面向塊的 I/O 缺少一些面向流的 I/O 所具有的優雅性和簡單性。

通道與緩衝區

  1. 通道
    通道 Channel 是對原 I/O 包中的流的模擬,可以通過它讀取和寫入數據。

通道與流的不同之處在於,流只能在一個方向上移動(一個流必須是 InputStream 或者 OutputStream 的子類),而通道是雙向的,可以用於讀、寫或者同時用於讀寫。

通道包括以下類型:

  • FileChannel:從文件中讀寫數據;
  • DatagramChannel:通過 UDP 讀寫網絡中數據;
  • SocketChannel:通過 TCP 讀寫網絡中數據;
  • ServerSocketChannel:可以監聽新進來的 TCP 連接,對每一個新進來的連接都會創建一個 SocketChannel。
  1. 緩衝區

發送給一個通道的所有數據都必須首先放到緩衝區中,同樣地,從通道中讀取的任何數據都要先讀到緩衝區中。也就是說,不會直接對通道進行讀寫數據,而是要先經過緩衝區。

緩衝區實質上是一個數組,但它不僅僅是一個數組。緩衝區提供了對數據的結構化訪問,而且還可以跟蹤系統的讀/寫進程。

緩衝區包括以下類型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

緩衝區狀態變量包括:

  • capacity:最大容量;
  • position:當前已經讀寫的字節數;
  • limit:還可以讀寫的字節數。

狀態變量的改變過程舉例:

① 新建一個大小爲 8 個字節的緩衝區,此時 position 爲 0,而 limit = capacity = 8。capacity 變量不會改變,下面的討論會忽略它。

② 從輸入通道中讀取 5 個字節數據寫入緩衝區中,此時 position 爲 5,limit 保持不變。

③ 在將緩衝區的數據寫到輸出通道之前,需要先調用 flip() 方法,這個方法將 limit 設置爲當前 position,並將 position 設置爲 0。

④ 從緩衝區中取 4 個字節到輸出緩衝中,此時 position 設爲 4。

⑤ 最後需要調用 clear() 方法來清空緩衝區,此時 position 和 limit 都被設置爲最初位置。

選擇器

  1. 簡介
    NIO 常常被叫做非阻塞 IO,主要是因爲 NIO 在網絡通信中的非阻塞特性被廣泛使用。

NIO 實現了 IO 多路複用中的 Reactor 模型,一個線程 Thread 使用一個選擇器 Selector 通過輪詢的方式去監聽多個通道 Channel 上的事件,從而讓一個線程就可以處理多個事件。

通過配置監聽的通道 Channel 爲非阻塞,那麼當 Channel 上的 IO 事件還未到達時,就不會進入阻塞狀態一直等待,而是繼續輪詢其它 Channel,找到 IO 事件已經到達的 Channel 執行。

因爲創建和切換線程的開銷很大,因此使用一個線程來處理多個事件而不是一個線程處理一個事件,對於 IO 密集型的應用具有很好地性能。

應該注意的是,只有Socket Channel 才能配置爲非阻塞,而 FileChannel 不能,爲 FileChannel 配置非阻塞也沒有意義。

  1. 原理
    IO multiplexing —— 整體 blocking 局部 non-blocking
    當用戶進程調用了select,那麼整個進程會被block,而同時,kernel通過不斷輪詢,“監視”所有select負責的所有kernel(socket),當任何一個kernel(socket)中的數據準備好了,select就會返回。這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程。
    使用IO multiplexing性能不一定比使用multi-threading + blocking IO效率高,使用select的優勢在於它可以同時處理多個connection。
    在IO multiplexing Model中,實際中,對於每一個socket channel,一般都設置成爲non-blocking,但是,整個用戶的process其實是一直被block的。只不過process是被select這個函數block,而不是被socket channel IO給block。
  2. 使用
  • 創建選擇器
Selector selector = Selector.open();
  • 將通道註冊到選擇器上
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

通道必須配置爲非阻塞模式,否則使用選擇器就沒有任何意義了,因爲如果通道在某個事件上被阻塞,那麼服務器就不能響應其它事件,必須等待這個事件處理完畢才能去處理其它事件,顯然這和選擇器的作用背道而馳。

在將通道註冊到選擇器上時,還需要指定要註冊的具體事件,主要有以下幾類:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

它們在 SelectionKey 的定義如下:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

可以看出每個事件可以被當成一個位域,從而組成事件集整數。例如:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
  • 監聽事件
int num = selector.select();

使用 select() 來監聽到達的事件,它會一直阻塞直到有至少一個事件到達。

  • 獲取到達的事件
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
        // ...
    } else if (key.isReadable()) {
        // ...
    }
    keyIterator.remove();
}
  • 事件循環

因爲一次 select() 調用不能處理完所有的事件,並且服務器端有可能需要一直監聽事件,因此服務器端處理事件的代碼一般會放在一個死循環內。

while (true) {
    int num = selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = keys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isAcceptable()) {
            // ...
        } else if (key.isReadable()) {
            // ...
        }
        keyIterator.remove();
    }
}
  • 套接字 NIO 實例
public class NIOServer {

    public static void main(String[] args) throws IOException {

        Selector selector = Selector.open();

        ServerSocketChannel ssChannel = ServerSocketChannel.open();
        ssChannel.configureBlocking(false);
        ssChannel.register(selector, SelectionKey.OP_ACCEPT);

        ServerSocket serverSocket = ssChannel.socket();
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
        serverSocket.bind(address);

        while (true) {

            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = keys.iterator();

            while (keyIterator.hasNext()) {

                SelectionKey key = keyIterator.next();

                if (key.isAcceptable()) {

                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();

                    // 服務器會爲每個新連接創建一個 SocketChannel
                    SocketChannel sChannel = ssChannel1.accept();
                    sChannel.configureBlocking(false);

                    // 這個新連接主要用於從客戶端讀取數據
                    sChannel.register(selector, SelectionKey.OP_READ);

                } else if (key.isReadable()) {

                    SocketChannel sChannel = (SocketChannel) key.channel();
                    System.out.println(readDataFromSocketChannel(sChannel));
                    sChannel.close();
                }

                keyIterator.remove();
            }
        }
    }

    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        StringBuilder data = new StringBuilder();

        while (true) {

            buffer.clear();
            int n = sChannel.read(buffer);
            if (n == -1) {
                break;
            }
            buffer.flip();
            int limit = buffer.limit();
            char[] dst = new char[limit];
            for (int i = 0; i < limit; i++) {
                dst[i] = (char) buffer.get(i);
            }
            data.append(dst);
            buffer.clear();
        }
        return data.toString();
    }
}
public class NIOClient {

    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 8888);
        OutputStream out = socket.getOutputStream();
        String s = "hello world";
        out.write(s.getBytes());
        out.close();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章