1. 程式人生 > >Java網路程式設計(輸入流 InputStream)

Java網路程式設計(輸入流 InputStream)

Java的基本輸入類是java.io.InputStream:

public abstract class InputStream

這個類提供將資料都取為原始位元組的基本方法。這些方法包括:

public abstract int read() throws IOException


pulic int read(byte[] input) throws IOException


pulic int read(byte[] input, int offset, int length) throws IOException


public long skip(long n) throws IOException


public int available()throws IOException


public int close()throws IOException

InputStream的具體子類使用這些方法從某種特定介質中讀取資料。

例如:

FileInputStream從檔案中讀取資料。

TelnetInputStream從網路連線中讀取資料。

ByteArrayInputStream從位元組陣列中讀取資料。

但無論哪種資料來源主要只使用以上這六個方法。

有時候,你不知道正在讀取的流具體是什麼型別。例如,TelnetInputStream類隱藏在sun.net包中沒有提供相關文件。java.net包中有很多方法都會返回這個類的例項(例如:java.net.URL的openStream()方法)。不過,這些方法宣告會只返回InputStream,而不是特定的子類TelnetInputStream。這又是多型在起作用。子類的例項可以透明地作為超類的例項來使用。並不需要子類特定的知識。

InputStream的基本方法是沒有引數的read()方法。這個方法從輸入流的源中讀取1位元組的資料,作為一個0-255的int返回。流的結束通過-1來表示。read()方法會等待並阻塞其後任何程式碼的執行,直到有1位元組的資料可供讀取。輸入和輸出可能很慢,所以如果程式在做其他事情,要儘量將I/O放在單獨的執行緒中。

read方法作為抽象方法,因為各個子類需要修改這個方法來處理特定的介質。

例如:

ByteArrayInputStream會用java純程式碼實現這個方法,從其陣列進行復制位元組。

不過,TelnetInputStream需要使用一個原生庫,它知道如何從主機平臺的網路介面讀取資料。

下面的程式碼段從InputStream in中讀取10個位元組,儲存到byte陣列input中。不過,如果檢測到流結束,迴圈就會提前終止:

byte[] input = new byte[10];

for(int i = 0; i < input . length; i++){

    int b = in.read();

    if(b == -1){

        break;

    }

    input[i] = (byte) b;

}

雖然read()只讀取1個位元組,但它會返回一個int。這樣把結果儲存在位元組陣列之前就要進行型別轉換。

當然,這會差生一個-128-127之間的有符號位元組,而不是read()方法返回0-255之間一個無符號位元組。不過,只要你清除在做什麼,這就不是大問題。你可以如下將一個有符號位元組轉換為無符號位元組:


int i = b >0 ? b : 256+b;

與一次寫入1位元組的資料一樣,一次讀取1位元組的效率也不高。因此,有兩個過載的read()方法,可以從流中讀取多位元組的資料填充到一個指定的陣列:read(byte[] input)和read(byte[] input, int dffset, int length)。第一個方法嘗試填充指定得陣列input。第二個方法嘗試填充指定的input陣列中從offset開始連續length位元組的子陣列。

注意這裡說的是嘗試填充陣列,但不一定會成功。嘗試可能會以許多形式失敗。例如,你可能聽說過,當你的程式正在通過DSL從遠端Web伺服器讀取資料時,由於電話中心辦公室的交換機存在bug,這會斷開你與其他地方數百個鄰居的連線。這會導致一個IOException異常。但更常見的是,讀嘗試可能不會完全失敗但也不會完全成功。可能讀取到一些請求的位元組但是未能全部讀到。例如,你可能嘗試從一個網路連線中讀取1024位元組,但是實際上只有512個位元組到達,但此時卻不可用,考慮到這一點,讀取多位元組檔案的方法會返回實際讀取的位元組數。例如,考慮下面的程式碼段:

byte[] input = new byte[1024];

int byteRread = in.read(input);

它嘗試從InputStream in向陣列input中讀入1024個位元組。不過,如果只有512個位元組可用,它只會讀取這麼多,byteRead將會設定為512。為保障你希望的所有資料都真正的讀取到,要把讀取方法放到迴圈中,這樣會重複讀取,直到陣列填滿為止。例如:

int bytesRead = 0;

int bytesToRead = 1024;

byte[] input = new byte[bytesToPead];

while(byteRead < bytesToRead){

    bytesRead += in.read(input, bytesRead, bytesToRead - bytesRead);

}

這項 技術對於網路流 尤為重要。一般來講如果一個檔案完全可用,那麼檔案的所有位元組也都可用。不過,由於網路比cpu慢的多,所以程式很容易在 所有資料到達前清空網路緩衝區。它通常會返回0,表示沒有資料可用,但流還沒有關閉。這往往比單位元組的read()方法要好,在這種情況下單位元組方法會阻塞正在執行的執行緒。

所有3個read()方法都會用返回-1來表示流結束。如果流結束,而又沒有讀取的資料,多位元組read()方法會返回這些資料,直到緩衝區清空。其後任何一個read()方法呼叫會返回-1。-1永遠不會進陣列。陣列只包含實際的資料元素。

前面的程式碼存在一個bug,因為它沒有考慮到1024永遠無法達到的情況。要修復這個bug,需要先測試read的返回值,然後再增加到bytesRead中。例如:

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;//流結束

    bytesRead += result;

}

如果你不想等待所需的全部位元組都立刻可用,可以使用available()方法來確定不阻塞的情況下有多少位元組可以讀取。它會返回可以讀取的最小位元組數。事實上還能讀取更多的位元組,但至少可以讀取available()建議的位元組數。

例如:  

int bytesAvailable = in.avaliable();


byte[] input = new byte[bytesAvailable];

int byteRead = in.read(input, 0, bytesAvailable); 

在這種情況下,可以認為bytresRead與bytesAvaliable相等。不過不能期望bytesRead大於0,有可能沒有可用的位元組。在流的最後,avaliable會返回0。一般來說,read(byte[] inpue, int offset, int length)在流結束之前返回-1;但是如果length是0,那麼它不會注意流的結束,而是返回0。

在少數情況下,你可以希望跳過資料不進行讀取。skip()方法會完成這項任務。與讀取檔案相比它在網路連線中用處不大。網路連線是順序的,一般情況下都會很慢,所以與跳過資料相比,讀取檔案並不會耗費太久時間。檔案訪問是隨機的,所以要跳過資料,可以簡單實現為重新指定檔案指標位置,而不再需要處理要跳過的各個位元組。

與輸出流一樣,一旦結束對輸入流的操作,應當呼叫它的close()方法進行關閉。

標記和重置

InputStream類還有3個不太常用的方法,允許程式備份和重新讀取已經讀取的資料。

這些方法是:

public void mark(int readAheadLimmit)


public void reset() throws IOException


public bolean markSupported()

為了重新讀取資料,要用mark()方法標記流的當前位置。在以後的某個時刻,可以用reset()方法把流重置到之前標記的位置上。接下來的讀取操作會返回從標記位置開始的資料。

不過,不能隨心所欲的向前標記任意遠的位置。從標記處讀取重置的位元組數由mark的readAheadLimit引數確定。如果試圖示記的太遠,就會丟擲IOException異常。

此外,一個流任何時刻都只能有一個標記,標記第二個時第一個會清除。

標記和重置通常通過將標誌位置之後的所有位元組儲存到一個內部快取區來實現。不過,不是所有的流都支援這一點。

在嘗試使用標記重置的時候,要檢查markSupported方法是否都返回true。如果返回truue,那麼這個流確實支援標記和重置。

否則mark()什麼也不會去做,而reset()會丟擲IOException 異常。

java.io中僅有兩個始終支援標的輸入流是BufferedInputStream和ByteArrayInputStream。

而其他流如果先串連到緩衝的輸入流才支援標記。

一個檔案複製的例子:

package com.hello.java04;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

public class FileCopy {

      public static void main(String[] args) {

            

            byte[] buffer=new byte[1024];

            int numberRead = 0;

            FileInputStream input = null;

            FileOutputStream output = null;

            try {

                  input = new FileInputStream("E:/text/1.txt");

                  

                  output = new FileOutputStream("E:/text/2.txt");

                  try {

                        while((numberRead=input.read(buffer))!=-1 ){

                              

                              output.write(buffer, 0, numberRead);

                        }

                        

                  } catch (IOException e) {

                        // TODO Auto-generated catch block

                        e.printStackTrace();

                  }

            } catch (FileNotFoundException e) {

                  // TODO Auto-generated catch block

                  e.printStackTrace();

            }

            finally{

                  try {

                        input.close();

                        output.close();

                  } catch (IOException e) {

                        // TODO Auto-generated catch block

                        e.printStackTrace();

                  }

                  

            }

            

            

      }

}