URLConnection類概述與實例

絕大部分知識與實例來自O’REILLY的《Java網絡編程》(Java Network Programming,Fourth Edition,by Elliotte Rusty Harold(O’REILLY))。

URLConnection類簡介

URLConnection類是一個抽象類,每個URLConnection對象代表一個指向URL指定資源的活動連接。它與URL類最大的不同體現在以下兩點:

  • URLConnection類提供了更多方法,用於精細地控制與服務器的交互過程,比如檢察首部並作出相應;
  • URLConnection可以用POST、PUT等HTTP請求方法與服務器交互。

URLConnection類是Java的協議處理器機制的一部分。協議處理器將處理不同協議的細節和處理特定數據類型分離開,並提供相應的用戶接口。URLConnection是抽象類,想要實現一個特定的協議,就必須派生出子類,並覆蓋相應的方法。

獲取URLConnection對象

使用URLConnection類的方法大致如下:
(1)構造一個URL對象;
(2)調用這個URL對象的openConnection()方法獲取一個對應於該URL的協議的URLConnection對象;
(3)對該URLConnection進行配置;
(4)讀取首部字段;
(5)獲得輸入流並讀取數據,或是獲得輸出流並寫入數據;
(6)關閉連接。
URLConnection只提供了一個protected的構造方法:protected URLConnection(URL url),因此除非要繼承它創建新的子類,否則只能使用openConnection()方法獲取對象。
URLConnection對象被構造時,它是未連接的,只有調用了connect()方法才能真正連接本地主機和遠程主機。不過,所有需要打開連接才能工作的方法都會確認是否已經建立連接,若未建立則會調用connect()方法,因此一般不需要手動調用connect()方法建立連接。

讀取服務器數據

與URL的openStream()方法一樣,URLConnection類提供了getInputStream()方法用於獲取輸入流。事實上,URL的openStream()就是通過調用自己的URLConnection.getInputStream()實現的,因此這兩個方法完全等價。

讀取首部字段

URLConnection的一大特色就是能夠讀取首部信息。下面是獲取不同首部字段的一系列方法:

  • getContentType():返回響應主體的MIME內容類型,可能還會包含字符集類型(charset=xxx);
  • getContentLength()與getContentLengthLong(Java 7):返回內容的字節數,如果沒有該首部字段則返回-1。兩個方法的區別在於,可能資源的字節數超過了int的表示範圍,這種情況下getContentLength()會返回-1。此時就需要用getContentLengthLong()。
  • getContentEncoding():返回編碼方式,若未經編碼則返回null。
  • getDate():返回一個代表發送時間的long,表示按自格林尼治標準時間(GMT)1970年1月1日子夜12:00後過去了多少毫秒來給出。可以將其轉換爲一個java.util.Date對象(直接傳入構造器即可)。
  • getExpiration():返回過期日期,格式與getDate()一樣。如果沒有Expires字段則返回0,表示永不過期。
  • getLastModified():返回最後修改日期,若沒有該字段則返回0。

實際上,首部字段遠遠不止這麼幾種,這幾個方法是URLConnection爲幾種常用的首部字段特別進行包裝的結果。獲取任意名稱的首部字段的方法如下:

  • public String getHeaderField(String name)
  • public long getHeaderFieldDate(String name,long default)
  • public int getHeaderFieldInt(String name,int default)

這一組方法根據指定的字段名返回對應的值,若無該字段,第一個方法會返回null,而後面兩個會用default作爲默認值。

  • public String getHeaderFieldKey(int n)
  • public String getHeaderField(int n)

這兩個方法根據首部字段的編號n返回對應的鍵(字段名)和值。需要注意的是,在HTTP中,包含請求方法和路徑的起始行是第0個首部字段,實際的第1個首部字段編號爲1。

實例1:顯示所有首部字段

public static void showHeaders(URLConnection connection){
    if(connection == null){
        System.out.println("null");
        return;
    }
    for(int i = 1 ; ; i++){
        String header = connection.getHeaderField(i);
        if(header == null) break;
        System.out.println(
                connection.getHeaderFieldKey(i) + ": " + header);
    }
}

public static void main(String[] args){
    String urlString = "https://osu.ppy.sh";
    try {
        URL url = new URL(urlString);
        showHeaders(url.openConnection());
    } catch (IOException e) {
        System.out.println("Fail to connect to " + urlString);
    }
}

輸出:
Date: Fri, 08 Sep 2017 08:27:28 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Pragma: public
Cache-Control: max-age=50
Expires: Fri, 08 Sep 2017 08:27:33 GMT
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains; preload
X-Content-Type-Options: nosniff
Server: cloudflare-nginx
CF-RAY: 39b087602ab26c28-SJC

緩存

緩存是一項非常常用的技術,用於降低服務器負載以及提高客戶端訪問速度。HTTP首部中提供了Cache-Control、Expires等首部字段來控制客戶端的緩存策略,以下是Cache-Control幾個常用的字段值:

  • max-age:緩存可用的時間,單位爲秒;
  • s-maxage:緩存項在共享緩存中可用的時間,單位爲秒;
  • public:可以在共享緩存中使用;
  • private:僅單個用戶可以使用;
  • no-cache:資源可以緩存,但是每次使用前必須和服務器進行確認,無論是否過期;
  • no-store:禁止緩存;
  • must-revalidate:資源過期後必須和服務器確認是否還有效。

Expires字段代表資源過期的時間,如果和Cache-Control同時出現會被覆蓋。

實例2:檢查緩存策略

public class CacheControl {
    private Date maxAge = null;
    private Date sMaxAge = null;
    private boolean mustRevalidate = false;
    private boolean noCache = false;
    private boolean noStore = false;
    private boolean proxyRevalidate = false;
    private boolean publicCache = false;
    private boolean privateCache = false;
    public CacheControl(String cacheControl) {
        if(cacheControl == null || !cacheControl.contains(":")){
            return;
        }
        Date now = new java.util.Date();
        String value = cacheControl.split(":")[1].trim();
        String[] components = value.split(",");
        for(String component : components){
            try{
                component = component.trim().toLowerCase(Locale.US);
                if(component.startsWith("max-age=")){
                    int index = component.indexOf("=");
                    String seconds = component.substring(index + 1).trim();
                    maxAge = new Date(now.getTime()
                            + 1000 * Integer.parseInt(seconds));
                }else if (component.equals("s-maxage=")) {
                    int index = component.indexOf("=");
                    String seconds = component.substring(index + 1).trim();
                    sMaxAge = new Date(now.getTime()
                            + 1000 * Integer.parseInt(seconds));
                }else if (component.equals("must-revalidate")) {
                    mustRevalidate = true;
                }else if (component.equals("proxy-revalidate")) {
                    proxyRevalidate = true;
                }else if (component.equals("no-cache")) {
                    noCache = true;
                }else if (component.equals("no-store")) {
                    noStore = true;
                }else if (component.equals("public")) {
                    publicCache = true;
                }else if (component.equals("private")) {
                    privateCache = true;
                }
            }catch (RuntimeException e) {
                continue;
            }
        }
    }

    public Date getMaxAge(){
        return maxAge;
    }

    public Date getSharedMaxAge(){
        return sMaxAge;
    }

    public boolean mustRevalidate(){
        return mustRevalidate;
    }

    public boolean proxyRevalidate(){
        return proxyRevalidate;
    }

    public boolean noCache(){
        return noCache;
    }

    public boolean noStore(){
        return noStore;
    }

    public boolean publicCache(){
        return publicCache;
    }

    public boolean privateCache(){
        return privateCache;
    }

    public static void main(String args[]){
        CacheControl control = new CacheControl("Cache-Control:public, max-age=50, must-revalidate, no-cache");
        System.out.println(control.publicCache());
        System.out.println(control.getMaxAge());
        System.out.println(control.mustRevalidate());
        System.out.println(control.noCache);
        System.out.println(control.privateCache());
    }
}

輸出:
true
Sat Sep 09 09:15:02 CST 2017
true
true
false

上面部分完成了緩存策略的獲取,還沒有真正實現緩存。Java在默認情況下並不完成緩存,要安裝URL類使用的系統級緩存,還需要實現ResponseCache、CacheRequest、CacheResponse三個抽象類。
ResponseCache是實際的緩存類,可以使用ResponseCache.setDefault()來爲系統安裝一個默認緩存對象。該類有兩個方法需要實現:

public CacheResponse get(URI uri, String rqstMethod, Map<String, List<String>> rqstHeaders) throws IOException
public CacheRequest put(URI uri, URLConnection conn) throws IOException

一旦安裝了緩存,只要系統嘗試加載一個新的URL,它首先會在緩存中查找,如果緩存返回了需要的內容,URLConnection就不需要和遠程服務器連接。如果緩存中查找不到,系統纔會到遠程服務器下載數據,並將響應放在緩存中。
下面對這三個類作用原理的個人理解:

  • ResponseCache需要實現put(存儲緩存)和get(獲取緩存)兩個方法。put方法返回一個CacheRequest對象,這個對象需要實現一個getBody()方法,返回一個輸出流,緩存文件是通過這個輸出流進行保存的;get方法返回一個CacheResponse對象,這個對象需要實現一個getBody()方法,返回一個輸入流,緩存文件是通過這個輸入流進行讀取的。
  • URI充當了保存和獲取URI的鑰匙,一般會將URI對象作爲Map的key和對應的緩存對象聯繫起來。
  • 通過在CacheRequest和CacheResponse的getBody()實現中使用不同類型的輸入/輸出流,即可實現不同的緩存方式。

實例3 三個緩存相關類的簡單實現

public class SimpleCacheRequest extends CacheRequest {
    private ByteArrayOutputStream out = new ByteArrayOutputStream();
    @Override
    public OutputStream getBody() throws IOException {
        return out;
    }

    @Override
    public void abort() {
        out.reset();
    }

    public byte[] getData(){
        if(out.size() == 0){
            return null;
        }else {
            return out.toByteArray();
        }
    }
}

public class SimpleCacheResponse extends CacheResponse {
    private final Map<String, List<String>> headers;
    private final SimpleCacheRequest request;
    private final Date expires;
    private final CacheControl control;
    public SimpleCacheResponse(SimpleCacheRequest request,
            URLConnection connection,CacheControl control) {
        this.headers = Collections.unmodifiableMap(connection.getHeaderFields());
        this.request = request;
        this.expires = new Date(connection.getExpiration());
        this.control = control;
    }
    @Override
    public Map<String, List<String>> getHeaders() throws IOException {
        return headers;
    }

    @Override
    public InputStream getBody() throws IOException {
        return new ByteArrayInputStream(request.getData());
    }

    public boolean isExpired(){
        Date now = new Date();
        if(control.getMaxAge() == null){
            if(expires.before(now)){
                return false;
            }
        }else{
            if(control.getMaxAge().before(now)){
                return false;
            }
        }
        return true;
    }
}

public class SimpleResponseCache extends ResponseCache {
    private final Map<URI, SimpleCacheResponse> responses =
            new ConcurrentHashMap<>();
    private final int maxEntries = 100;
    @Override
    public CacheResponse get(URI uri, String rqstMethod, Map<String, List<String>> rqstHeaders) throws IOException {
        if("GET".equals(rqstMethod)){
            SimpleCacheResponse response = responses.get(uri);
            if(response != null && response.isExpired()){
                responses.remove(response);
                response = null;
            }
            return response;
        }else {
            return null;
        }
    }

    @Override
    public CacheRequest put(URI uri, URLConnection conn) throws IOException {
        if(responses.size() >= maxEntries){
            return null;
        }
        CacheControl control = 
                new CacheControl(conn.getHeaderField("Cache-Control"));
        if(control.noStore()){
            return null;
        }else if (!conn.getHeaderField(0).startsWith("GET")) {
            return null;
        }
        SimpleCacheRequest request = new SimpleCacheRequest();
        SimpleCacheResponse response = 
                new SimpleCacheResponse(request, conn, control);
        responses.put(uri, response);
        return request;
    }

}

之後只需要調用ResponseCache.setDefaultCache(new SimpleResponseCache)即可完成安裝。

Cookie技術用於存儲持久的客戶端狀態。Java中實現簡單的Cookie功能非常容易,只需要下面兩行代碼即可完成安裝:

CookieManager manager = new CookieManager();
CookieHandler.setDefault(manager);

如果想要控制cookie的接收,可以使用CookiePolicy:

  • CookiePolicy.ACCEPT_ALL:接受所有cookie;
  • CookiePolicy.ACCEPT_NONT:不接受任何cookie;
  • CookiePolicy.ACCEPT_ORIGINAL_SERVER:只接受第一方cookie。

只需要調用manager.setCookiePolicy並傳入上面的某個參數即可,當然也可以通過實現CookiePolicy接口來自定義cookie接收策略。
如果想要在本地存放cookie(比如保存在磁盤上),可以通過manager.getCookieStore()方法獲取CookieStore對象,裏面存放着所有cookie,每個cookie封裝在一個HttpCookie對象中。

實例4:啓用Cookie功能並打印cookies

public static void main(String[] args){
    CookieManager manager = new CookieManager();
    CookieHandler.setDefault(manager);

    String urlString = "https://www.baidu.com";
    try {
        URL url = new URL(urlString);
        URLConnection connection = url.openConnection();
        try {
            List<HttpCookie> cookies = manager.getCookieStore().get(url.toURI());
            for(HttpCookie cookie : cookies){
                System.out.println(cookie);
            }
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    } catch (IOException e) {
        System.out.println("Fail to connect to " + urlString);
    }
}

配置連接

URLConnection類提供了以下字段,用於定義如何向服務器發出請求:

  • protected URL url;
  • protected boolean doInput = true;
  • protected boolean doOutout = false;
  • protected boolean allowUserInteracton = defaultAllowUserInteraction;
  • protected boolean useCaches = defaultUseCaches;
  • protected long ifModifiedSince = 0;
  • protected boolean connected = false;

這些字段都是protected的,因此需要通過它們的getter和setter來訪問。這裏有兩個例外,一是url字段只有getter而沒有setter,因此連接在打開之後就無法改變指向的url,二是connected既沒有getter也沒有setter。
除此之外,還有一些方法,用於定義URLConnection的默認行爲:

public static boolean getDefaultAllowUserInteraction()
public static void setDefaultAllowUserInteraction(boolean defaultallowuserinteraction)
public boolean getDefaultUseCaches()
public void setDefaultUseCaches(boolean defaultusecaches)

新的默認值只在調用這些方法之後的URLConnection對象中生效。
下面是各個字段的功能介紹:
(1)url:指定了這個URLConnection對象連接的URL。這個字段在構造函數中賦值,此後不能再改變。
(2)connected:如果連接已經打開,字段值爲true;如果連接關閉,字段值爲false。這個字段無法被設置,也無法讀取,只能由URLConnection的子類訪問。任何導致URLConnection連接的方法都會將這個字段設置爲true(connect()、getInputStream()、getOutputStream()),任何導致連接斷開的方法都會將這個字段設置爲false(URLConnection中沒有這樣的方法,但是其他子類中可能有)。
(3)allowUserInteraction:該字段指示了是否允許用戶交互(比如輸入用戶名和密碼),默認false。
(4)doInput:如果URLConnection可以用來讀取,則該字段爲true,否則爲false。默認true。
(5)doOutput:如果URLConnection可以用來寫入,則該字段爲true,否則爲false。默認false。
(6)ifModifiedSince:long類型,表示上一次修改的時間。這個字段會被放在If-Modified_Since字段中,默認是1970年1月1日子夜12:00。
(7)useCaches:確定是否可以使用緩存,true代表可以,false代表不可以,默認true。

超時

有4個方法可以查詢和修改連接的超時值:

public void setConnectTimeout(int timeout)
public int getConnectTimeout()
public void setReadTimeout(int timeout)
public int getReadTimeout()

其中,ConnectTimeout指的是等待建立連接的時間,ReadTimeout指的是等待數據到達的時間。這些方法都用毫秒作爲單位,並且將0解釋爲永不超時。

自定義請求首部字段

在發送請求報文時,系統會自動加上必要的首部字段。如果需要添加自定義的首部字段,可以使用addRequestProperty(String name,String value)方法。獲取自定義的首部字段集(不包括系統加上的默認部分),可以使用getRequestProperties()方法。

向服務器寫入數據

有時候會需要向URLConnection寫入數據,最常見的是使用POST方法向Web服務器提交表單,一般流程如下:

  1. (需要在編程之前完成)根據網頁源代碼確定提交表單的格式以及表單提交的地址,並構建出包含表單內容的字符串;
  2. 根據表單提交地址創建一個URL對象,並獲取URLConnection對象;
  3. 調用setDoOutput(true)設置URLConnection爲可以寫入,此時方法會自動變成POST;
  4. 調用getOutputStream()獲取輸出流,並寫入表單內容,注意內容最後要加上“\r\n”;
  5. 調用getInputStream()獲取輸入流,讀取服務器響應。

實例5:

//用於構造表單內容
public class QueryString {

    StringBuilder query = new StringBuilder();

    public synchronized void add(String name, String value){
        if(query.length() != 0){
            query.append("&");
        }
        encode(name, value);
    }

    private synchronized void encode(String name,String value){
        try {
            query.append(URLEncoder.encode(name,"UTF-8"));
            query.append('=');
            query.append(URLEncoder.encode(value,"UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    public void clear(){
        query = new StringBuilder();
    }

    @Override
    public String toString() {
        return query.toString();
    }
}

//用於發送表單
public class FormPoster{
    private URL url;
    private URLConnection connection = null;
    private QueryString query;

    public FormPoster(URL url) throws IOException {
        String protocol = url.getProtocol().toLowerCase(Locale.US);
        if(!protocol.equals("http") && !protocol.equals("https")){
            throw new IllegalArgumentException("Only works for HTTP and HTTPS!");
        }
        this.url = url;
        connection = url.openConnection();
        query = new QueryString();
    }

    public void add(String name, String value){
        query.add(name, value);
    }

    public void addRequestProperty(String key, String value){
        connection.addRequestProperty(key, value);
    }

    public URL getURL(){
        return url;
    }

    public String getQuery(){
        return query.toString();
    }

    public URLConnection getConnection(){
        return connection;
    }

    public InputStream post() throws IOException{
        connection.setDoOutput(true);
        try(OutputStreamWriter out = 
                new OutputStreamWriter(connection.getOutputStream(), "UTF-8")){
            out.write(query.toString());
            out.write("\r\n");
            out.flush();
        }

        return connection.getInputStream();
    }
}

HttpURLConnection

HttpURLConnection是URLConnection的抽象子類,提供了一些額外的方法。事實上,當使用http URL時,調用URL的openConnection()方法返回的就是HttpURLConnection的一個實例,因此可以將其強制轉換成HttpURLConnection類型。
下面列舉HttpURLConnection類相比於URLConnection類新增的方法:
(1)setRequestMethod(),用於設置請求方法;
(2)getResponseCode()與getResponseMessage(),用於獲取響應碼和相應消息(404 Not Found);
(3)getErrorStream(),用於獲取在服務器遇到錯誤時返回的信息(如404時出現的頁面);
(4)setFollowRedirects(),用於設置是否跟隨重定向(全局);setInstanceFollowRedirects(),用於設置單個實例是否跟隨重定向;
(5)setChunkedStreamingMode(int chunkedLength),啓用分塊傳輸模式,並設置分塊大小。

發佈了42 篇原創文章 · 獲贊 28 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章