1. 程式人生 > >Java網路程式設計之流的詳解

Java網路程式設計之流的詳解

前言

大部分網路程式做的事情就是接受輸入併產生輸出。讀伺服器傳送過來的資料與讀取本地檔案的資料並沒有多大的區別,同時伺服器將資料傳送給客戶端與寫資料到本地檔案也很像。

Java的IO操作基於streams實現的。輸入流讀資料,輸出流寫資料。

該系列文章是《Java網路程式設計》書籍的讀書筆記

輸出流(Output Streams)

所有輸出流的基類是OutputStream
定義的方法如下:

public abstract void write(int b) throws IOException
public void write(byte[] data) throws
IOException public void write(byte[] data, int offset, int length) throws IOException public void flush() throws IOException public void close() throws IOException

基礎的方法是write(int b),將引數b的低8位寫到輸出流中,高24位會被忽略。也就是說b的範圍是0~255(二進位制11111111是255)。其實就是傳了一個無符號位元組,因為Java沒有無符號位元組型別,所以通過int來替代。

但是每次如果只寫一個位元組很不方便,下面兩個write(byte[] data)

方法可以一次傳輸一個位元組陣列。

其實write(byte [] data)的核心程式碼還是回撥用write(int)方法:

for (int i = 0 ; i < len ; i++) {
    write(b[off + i]);
}

可以把需要傳送的資料快取起來,等到滿足一定條件或呼叫flush方法再發送。Java通過BufferedOutputStreamBufferedWriter來實現。BufferedOutputStream繼承自FilterOutputStream,而這個FilterOutputStream是裝飾器模式的一個例子,它底層維護了一個OutputStream

不管你覺得有沒有必要,呼叫flush方法就行了。在關閉stream之前,呼叫flush方法!

常見的關閉流的姿勢:

OutputStream out = null;
try {
    out = new FileOutputStream("/tmp/data.txt");
    // work with the output stream...
} catch (IOException ex) {
    System.err.println(ex.getMessage());
} finally {
    if (out != null) {
        try {
            out.close();
        } catch (IOException ex) {
            // ignore
        }
    }
}

Java7引入的更加清爽的姿勢:

try (OutputStream out = new FileOutputStream("/tmp/data.txt")) {
    // work with the output stream...
} catch (IOException ex) {
    System.err.println(ex.getMessage());
}

叫做try-with-resources 語句,如果有多個資源呢?

 try (InputStream fis = new FileInputStream(source);
        OutputStream fos = new FileOutputStream(target)){
        byte[] buf = new byte[8192];
        int i;
        while ((i = fis.read(buf)) != -1) {
            fos.write(buf, 0, i);
        }
    }
    catch (Exception e) {
        e.printStackTrace();
    }

資料流會在 try 執行完畢後自動被關閉,前提是,這些可關閉的資源必須實現 java.lang.AutoCloseable 介面。

輸入流(Input Stream)

所有輸入流的基類是InputStream
定義的方法如下:

public abstract int read() throws IOException
public int read(byte[] input) throws IOException
public int read(byte[] input, int offset, int length) throws IOException
public long skip(long n) throws IOException
public int available() throws IOException
public void close() throws IOException

基礎的是這個無引數的read()方法,從輸入流中讀取一個位元組,並以int型別(0~255)返回,如果讀到stream的某位,返回-1。

在輸入資料可用、檢測到檔案末尾或者丟擲異常前,此方法一直阻塞。

每次只讀一個位元組和每次只寫以位元組一樣不方便,read(byte[] input)方法就出來了。
read(byte[] input)方法嘗試填充input陣列,注意這裡的措辭是嘗試,當read的時候遇到了IOException或假設你傳入了1024大小的陣列,但是隻讀取了512個位元組,還有512個位元組正在傳輸中(你的網路比較慢啊老哥),這個方法就會返回實際讀取的位元組數。

byte[] input = new byte[1024];
int bytesRead = in.read(input);

考慮到上面所描述的,為了保證你能讀到1024位元組,將read放到迴圈裡面:

int bytesRead = 0;
int bytesToRead = 1024;
byte[] input = new byte[bytesToRead];
while (bytesRead < bytesToRead) {
    bytesRead += in.read(input, bytesRead, bytesToRead - bytesRead);
}

上面的程式碼有一個bug,它沒有考慮到實際上可能沒有1024位元組這麼多的資料,可能讀取了200位元組就返回-1了。

int bytesRead = 0;
int bytesToRead = 1024;
byte[] input = new byte[bytesToRead];
while (bytesRead < bytesToRead) {
    int result = in.read(input, bytesRead, bytesToRead - bytesRead);
    if (result == -1) break; // end of stream
    bytesRead += result;
}

呼叫available()方法可以得知有多少個位元組的資料能馬上讀取而不需要阻塞

int bytesAvailable = in.available();//注意,可能返回0
byte[] input = new byte[bytesAvailable];
int bytesRead = in.read(input, 0, bytesAvailable);

有時,你可能想跳過一些資料,這時可呼叫skip()方法來達到這個要求。

過濾流(Filter Streams)

Filter有兩種方式:filter stream,readers和writers。filter stream主要處理位元組資料:比如以二進位制的形式來處理位元組資料;readers和writers負責處理各種編碼的文字。

Filter通過鏈條的形式組織起來。鏈路中每個Filter從上個Filter(或stream)上接收資料,自己進行某些處理,然後傳遞給下一位。

上圖展示了一個加密、壓縮的文字檔案的傳輸過程。首先傳遞給TelnetInputStream,然後通過BufferedInputStream來快取資料以提交傳輸速度,接下來CipherInputStream解密傳遞過來的資料並交給GZIPInputStream來解壓,最後到了InputStreamReader將解壓且解密了的資料轉換為Unicode文字的形式。

連結Filter

Filter通過建構函式來接收stream。如下所示:

FileInputStream fin = new FileInputStream("data.txt");
BufferedInputStream bin = new BufferedInputStream(fin);

此時,既可以使用fin的read()方法又可以使用bin的read()方法來從檔案data.txt中讀取資料。然而,混合呼叫連線到同一個原始檔的不同stream違反了filter stream的約束。一般,應該使用鏈路上最後一個filter來進行讀寫操作。

一旦這種連結關係建立起來就無法脫離。

Buffered Streams

BufferedOutputStream儲存了需要寫的資料,直到buffer滿了或stream被flush了,然後它將資料一次性寫到底層的output stream。一次寫很多資料的速度遠超過多次寫很少的資料。尤其是在網路環境下。

BufferedInputStream類也有一個名為buf的protected位元組陣列作為緩衝區。 當呼叫其中一個流的read()方法時,它首先嚐試獲取從緩衝區請求的資料。 只有在緩衝區的資料用完時才會從底層源stream中讀取資料。因此,它讀取儘可能多的資料到緩衝區,而不管是否為馬上需要的資料。 在閱讀檔案時,從本地磁碟讀取幾百位元組的資料幾乎和讀取一個位元組的資料的速度一樣快。 因此,緩衝可以大幅度提高效能。

它們的建構函式定義如下:

public BufferedInputStream(InputStream in)
public BufferedInputStream(InputStream in, int bufferSize)


public BufferedOutputStream(OutputStream out)
public BufferedOutputStream(OutputStream out, int bufferSize)

PrintStream

PrintStream是大多數人遇到的第一個filter output stream。因為System.out是一個PrintStream。

PrintStream同樣也是通過建構函式來接收stream:

public PrintStream(OutputStream out)
public PrintStream(OutputStream out, boolean autoFlush)

autoFlush預設為false,如果true:byte陣列被寫入(呼叫了write(byte buf[])方法);println方法被呼叫;\n字元被寫入 都會觸發flush方法。

除了常用的write(),flush()和close()方法。PrintStream還過載了9個print()方法和10個println()方法。

public void print(long l) {
    write(String.valueOf(l));
}
public void print(float f) {
    write(String.valueOf(f));
}

和上面的程式碼類似,每個print()方法將它的引數轉變為string然後以預設的編碼寫到底層的output stream。

溫馨提示:網路程式設計時最好不要使用PrintStream!

Data Streams

DataInputStream和DataOutputStream提供了以二進位制形式讀取和寫入原始資料型別和string的方法。
DataOutputStream為特定的型別提供了11種寫入方法:

public final void writeBoolean(boolean b) throws IOException
public final void writeByte(int b) throws IOException
public final void writeShort(int s) throws IOException
public final void writeChar(int c) throws IOException
public final void writeInt(int i) throws IOException
public final void writeLong(long l) throws IOException
public final void writeFloat(float f) throws IOException
public final void writeDouble(double d) throws IOException
public final void writeChars(String s) throws IOException
public final void writeBytes(String s) throws IOException
public final void writeUTF(String s) throws IOException

writeUTF()方法以UTF-8來編碼string。

DataInputStream也提供了類似的讀取方法,就不在這裡列出了。

Readers和Writers

Writer類與Reader類是以字元流傳輸資料,一個字元兩個位元組。

字元流除了是以字元方式(兩個位元組)傳輸資料外,另外一點與位元組流不同的是字元流使用緩衝區,通過緩衝區再對檔案進行操作。位元組流位元組對檔案進行操作。使用字元流類時關閉字元流會強制將字元流緩衝區的類容輸出,如果不想關閉也將字元流進行輸出,使用Writer類的flush()方法。

Writers

下面列出幾個方法:

public abstract void write(char[] text, int offset, int length) throws IOException
public void write(String s) throws IOException
public void write(String s, int offset, int length) throws IOException

給你一個writer物件,和一個String “Network”,可以通過下面的方法執行寫出操作:

char[] network = {'N', 'e', 't', 'w', 'o', 'r', 'k'};
w.write(network, 0, network.length);

類似的也可以這樣:

String network = "Network";
w.write(network);
for (int i = 0; i < network.length; i++) w.write(network[i]);
w.write("Network");
w.write("Network", 0, 7);

Writers底層有一個Buffered Stream,因此任何write操作都會被快取,可以呼叫flush()方法強制flush stream。

OutputStreamWriter

OutputStreamWriter是最重要的Writer子類。

OutputStreamWriter從Java程式中接收字元,然後通過特定的編碼轉換為位元組並將這些位元組寫到底層的輸出流。

通過建構函式來指定底層的輸出流和字元編碼:

public OutputStreamWriter(OutputStream out, String encoding) throws UnsupportedEncodingException

除了建構函式和常用的Writer類方法,OutputStreamWriter提供了一個返回編碼的方法getEncoding()

Readers

public abstract int read(char[] text, int offset, int length) throws IOException
public int read() throws IOException
public int read(char[] text) throws IOException

它的大多數方法可以與InputStream 中的方法對應。 read()方法以int型(從0到65,535)返回一個單一的Unicode字元或讀到流結束時返回-1。

同樣InputStreamReader也是Reader子類中最重要的一個。InputStreamReader從底層的輸入流中讀取位元組,然後通過特定的編碼轉換為字元。
建構函式:

public InputStreamReader(InputStream in)
public InputStreamReader(InputStream in, String encoding) throws UnsupportedEncodingException

舉個例子,下面的方法從in中讀取位元組並以MacCyrillic編碼轉換為Unicode字串。

public static String getMacCyrillicString(InputStream in) throws IOException {
    InputStreamReader r = new InputStreamReader(in, "MacCyrillic");
    StringBuilder sb = new StringBuilder();
    int c;
    while ((c = r.read()) != -1) sb.append((char) c);
        return sb.toString();
    } 

Filter Readers and Writers

BufferedReader有一個readLine()方法,讀取一行文字並以String的形式返回:
public String readLine() throws IOException

BufferedWriter類加了一個新的方法newLine()

public void newLine() throws IOException
該方法根據不同的平臺插入不同的換行符到輸出流。

InputStreamReader和OutputStreamWriter,IO中的類要麼以Stream結尾,要麼以Reader或者Writer結尾,那這兩個同時以位元組流和字元流的類名字尾結尾的類是什麼用途呢?簡單來說,這兩個類把位元組流轉換成字元流,中間做了資料的轉換,類似介面卡模式的思想。

PrintWriter

PrintWriter用以取代PrintStream來處理多種格式的字符集和國際化文字。