Java學習102:HTTP編程

什麼是HTTP?HTTP就是目前使用最廣泛的Web應用程序使用的基礎協議,例如,瀏覽器訪問網站,手機App訪問後臺服務器,都是通過HTTP協議實現的。

HTTP是HyperText Transfer Protocol的縮寫,翻譯爲超文本傳輸協議,它是基於TCP協議之上的一種請求-響應協議。

我們來看一下瀏覽器請求訪問某個網站時發送的HTTP請求-響應。當瀏覽器希望訪問某個網站時,瀏覽器和網站服務器之間首先建立TCP連接,且服務器總是使用80端口和加密端口443,然後,瀏覽器向服務器發送一個HTTP請求,服務器收到後,返回一個HTTP響應,並且在響應中包含了HTML的網頁內容,這樣,瀏覽器解析HTML後就可以給用戶顯示網頁了。一個完整的HTTP請求-響應如下:
在這裏插入圖片描述
HTTP請求的格式是固定的,它由HTTP Header和HTTP Body兩部分構成。第一行總是請求方法 路徑 HTTP版本,例如,GET / HTTP/1.1表示使用GET請求,路徑是/,版本是HTTP/1.1。

後續的每一行都是固定的Header: Value格式,我們稱爲HTTP Header,服務器依靠某些特定的Header來識別客戶端請求,例如:

  • Host:表示請求的域名,因爲一臺服務器上可能有多個網站,因此有必要依靠Host來識別用於請求;
  • User-Agent:表示客戶端自身標識信息,不同的瀏覽器有不同的標識,服務器依靠User-Agent判斷客戶端類型;
  • Accept:表示客戶端能處理的HTTP響應格式,*/*表示任意格式,text/*表示任意文本,image/png表示PNG格式的圖片;
  • Accept-Language:表示客戶端接收的語言,多種語言按優先級排序,服務器依靠該字段給用戶返回特定語言的網頁版本。

如果是GET請求,那麼該HTTP請求只有HTTP Header,沒有HTTP Body。如果是POST請求,那麼該HTTP請求帶有Body,以一個空行分隔。一個典型的帶Body的HTTP請求如下:

POST /login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30

username=hello&password=123456

POST請求通常要設置Content-Type表示Body的類型,Content-Length表示Body的長度,這樣服務器就可以根據請求的Header和Body做出正確的響應。

此外,GET請求的參數必須附加在URL上,並以URLEncode方式編碼,例如:http://www.example.com/?a=1&b=K%26R,參數分別是a=1和b=K&R。因爲URL的長度限制,GET請求的參數不能太多,而POST請求的參數就沒有長度限制,因爲POST請求的參數必須放到Body中。並且,POST請求的參數不一定是URL編碼,可以按任意格式編碼,只需要在Content-Type中正確設置即可。常見的發送JSON的POST請求如下:

POST /login HTTP/1.1
Content-Type: application/json
Content-Length: 38

{"username":"bob","password":"123456"}

HTTP響應也是由Header和Body兩部分組成,一個典型的HTTP響應如下:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 133251

<!DOCTYPE html>
<html><body>
<h1>Hello</h1>
...

響應的第一行總是HTTP版本 響應代碼 響應說明,例如,HTTP/1.1 200 OK表示版本是HTTP/1.1,響應代碼是200,響應說明是OK。客戶端只依賴響應代碼判斷HTTP響應是否成功。HTTP有固定的響應代碼:

  • 1xx:表示一個提示性響應,例如101表示將切換協議,常見於WebSocket連接;
  • 2xx:表示一個成功的響應,例如200表示成功,206表示只發送了部分內容;
  • 3xx:表示一個重定向的響應,例如301表示永久重定向,303表示客戶端應該按指定路徑重新發送請求;
  • 4xx:表示一個因爲客戶端問題導致的錯誤響應,例如400表示因爲Content-Type等各種原因導致的無效請求,404表示指定的路徑不存在;
  • 5xx:表示一個因爲服務器問題導致的錯誤響應,例如500表示服務器內部故障,503表示服務器暫時無法響應。

當瀏覽器收到第一個HTTP響應後,它解析HTML後,又會發送一系列HTTP請求,例如,GET /logo.jpg HTTP/1.1請求一個圖片,服務器響應圖片請求後,會直接把二進制內容的圖片發送給瀏覽器:

HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Length: 18391

????JFIFHH??XExifMM?i&??X?...(二進制的JPEG圖片)

因此,服務器總是被動地接收客戶端的一個HTTP請求,然後響應它。客戶端則根據需要發送若干個HTTP請求。

對於最早期的HTTP/1.0協議,每次發送一個HTTP請求,客戶端都需要先創建一個新的TCP連接,然後,收到服務器響應後,關閉這個TCP連接。由於建立TCP連接就比較耗時,因此,爲了提高效率,HTTP/1.1協議允許在一個TCP連接中反覆發送-響應,這樣就能大大提高效率:
在這裏插入圖片描述
因爲HTTP協議是一個請求-響應協議,客戶端在發送了一個HTTP請求後,必須等待服務器響應後,才能發送下一個請求,這樣一來,如果某個響應太慢,它就會堵住後面的請求。

所以,爲了進一步提速,HTTP/2.0允許客戶端在沒有收到響應的時候,發送多個HTTP請求,服務器返回響應的時候,不一定按順序返回,只要雙方能識別出哪個響應對應哪個請求,就可以做到並行發送和接收:
在這裏插入圖片描述
可見,HTTP/2.0進一步提高了效率。

HTTP編程
既然HTTP涉及到客戶端和服務器端,和TCP類似,我們也需要針對客戶端編程和針對服務器端編程。

本節我們不討論服務器端的HTTP編程,因爲服務器端的HTTP編程本質上就是編寫Web服務器,這是一個非常複雜的體系,也是JavaEE開發的核心內容,我們在後面的章節再仔細研究。

本節我們只討論作爲客戶端的HTTP編程。

因爲瀏覽器也是一種HTTP客戶端,所以,客戶端的HTTP編程,它的行爲本質上和瀏覽器是一樣的,即發送一個HTTP請求,接收服務器響應後,獲得響應內容。只不過瀏覽器進一步把響應內容解析後渲染並展示給了用戶,而我們使用Java進行HTTP客戶端編程僅限於獲得響應內容。

我們來看一下Java如果使用HTTP客戶端編程。

Java標準庫提供了基於HTTP的包,但是要注意,早期的JDK版本是通過HttpURLConnection訪問HTTP,典型代碼如下:

URL url = new URL("http://www.example.com/path/to/target?a=1&b=2");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setUseCaches(false);
conn.setConnectTimeout(5000); // 請求超時5秒
// 設置HTTP頭:
conn.setRequestProperty("Accept", "*/*");
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (compatible; MSIE 11; Windows NT 5.1)");
// 連接併發送HTTP請求:
conn.connect();
// 判斷HTTP響應是否200:
if (conn.getResponseCode() != 200) {
    throw new RuntimeException("bad response");
}		
// 獲取所有響應Header:
Map<String, List<String>> map = conn.getHeaderFields();
for (String key : map.keySet()) {
    System.out.println(key + ": " + map.get(key));
}
// 獲取響應內容:
InputStream input = conn.getInputStream();
...

上述代碼編寫比較繁瑣,並且需要手動處理InputStream,所以用起來很麻煩。

從Java 11開始,引入了新的HttpClient,它使用鏈式調用的API,能大大簡化HTTP的處理。

我們來看一下如何使用新版的HttpClient。首先需要創建一個全局HttpClient實例,因爲HttpClient內部使用線程池優化多個HTTP連接,可以複用:

static HttpClient httpClient = HttpClient.newBuilder().build();

使用GET請求獲取文本內容代碼如下

import java.net.URI;
import java.net.http.*;
import java.net.http.HttpClient.Version;
import java.time.Duration;
import java.util.*;
public class Main {
    // 全局HttpClient:
    static HttpClient httpClient = HttpClient.newBuilder().build();

    public static void main(String[] args) throws Exception {
        String url = "https://www.sina.com.cn/";
        HttpRequest request = HttpRequest.newBuilder(new URI(url))
            // 設置Header:
            .header("User-Agent", "Java HttpClient").header("Accept", "*/*")
            // 設置超時:
            .timeout(Duration.ofSeconds(5))
            // 設置版本:
            .version(Version.HTTP_2).build();
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        // HTTP允許重複的Header,因此一個Header可對應多個Value:
        Map<String, List<String>> headers = response.headers().map();
        for (String header : headers.keySet()) {
            System.out.println(header + ": " + headers.get(header).get(0));
        }
        System.out.println(response.body().substring(0, 1024) + "...");
    }
}

如果我們要獲取圖片這樣的二進制內容,只需要把HttpResponse.BodyHandlers.ofString()換成HttpResponse.BodyHandlers.ofByteArray(),就可以獲得一個HttpResponse<byte[]>對象。如果響應的內容很大,不希望一次性全部加載到內存,可以使用HttpResponse.BodyHandlers.ofInputStream()獲取一個InputStream流。

要使用POST請求,我們要準備好發送的Body數據並正確設置Content-Type:

String url = "http://www.example.com/login";
String body = "username=bob&password=123456";
HttpRequest request = HttpRequest.newBuilder(new URI(url))
    // 設置Header:
    .header("Accept", "*/*")
    .header("Content-Type", "application/x-www-form-urlencoded")
    // 設置超時:
    .timeout(Duration.ofSeconds(5))
    // 設置版本:
    .version(Version.HTTP_2)
    // 使用POST並設置Body:
    .POST(BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
String s = response.body();

可見發送POST數據也十分簡單。

小結
Java提供了HttpClient作爲新的HTTP客戶端編程接口用於取代老的HttpURLConnection接口;

HttpClient使用鏈式調用並通過內置的BodyPublishers和BodyHandlers來更方便地處理數據。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章