1. 程式人生 > >Java 9 揭祕(14. HTTP/2 Client API)

Java 9 揭祕(14. HTTP/2 Client API)

Tips
做一個終身學習的人。

Java 9

在此章中,主要介紹以下內容:

  • 什麼是HTTP/2 Client API
  • 如何建立HTTP客戶端
  • 如何使HTTP請求
  • 如何接收HTTP響應
  • 如何建立WebSocket的endpoints
  • 如何將未經請求的資料從伺服器推送到客戶端

JDK 9將HTTP/2 Client API作為名為jdk.incubator.httpclient的孵化器模組。 該模組匯出包含所有公共API的jdk.incubator.http包。 孵化器模組不是Java SE的一部分。 在Java SE 10中,它將被標準化,併成為Java SE 10的一部分,否則將被刪除。 請參閱 http://openjdk.java.net/jeps/11

上的網頁,以瞭解有關JDK中孵化器模組的更多資訊。

孵化器模組在編譯時或執行時未被預設解析,因此需要使用--add-modules選項將jdk.incubator.httpclient模組新增到預設的根模組中,如下所示:

<javac|java|jmod...> -add-modules jdk.incubator.httpclient ...

如果另一個模組讀取並解析了第二個模組,則也相應解析了孵化器模組。 在本章中,將建立一個讀取jdk.incubator.httpclient模組的模組,不必使用-add-modules選項來解析。

因為孵化器模組提供的API還不是最終的,當在編譯時或執行時使用孵化器模組時,會在標準錯誤上列印警告。 警告資訊如下所示:

WARNING: Using incubator modules: jdk.incubator.httpclient

孵化器模組的名稱和包含孵化器API的軟體包以jdk.incubator開始。 一旦它們被標準化幷包含在Java SE中,它們的名稱將被更改為使用標準的Java命名約定。 例如,模組名稱jdk.incubator.httpclient可能會在Java SE 10中成為java.httpclient。

因為jdk.incubator.httpclient模組不在Java SE中,所以將不會為此模組找到Javadoc。 為了生成此模組的Javadoc,並將其包含在本書的原始碼中。 可以使用下載的原始碼中的Java9Revealed/jdk.incubator.httpclient/dist/javadoc/index.html檔案訪問Javadoc。 使用JDK 9早期訪問構建158的JDK版本來生成Javadoc。 API可能會改變,可能需要重新生成Javadoc。 以下是具體的步驟:

  1. 原始碼包含與專案名稱相同目錄中的jdk.incubator.httpclient NetBeans專案。
  2. 安裝JDK 9時,其原始碼將作為src.zip檔案複製到安裝目錄中。 將所有內容從src.zip檔案中的jdk.incubator.httpclient目錄複製到下載的原始碼中的Java9revealed\jdk.incubator.httpclient\src目錄中。
  3. 在NetBeans中開啟jdk.incubator.httpclient專案。
  4. 右鍵單擊NetBeans中的專案,然後選擇“生成Javadoc”選項。 你會收到錯誤和警告,可以忽略。 它將在Java9Revealed/jdk.incubator.httpclient/dist/javadoc目錄中生成Javadoc。 開啟此目錄中的index.html檔案,檢視jdk.incubator.httpclient模組的Javadoc。

一. 什麼是HTTP/2 Client API?

自JDK 1.0以來,Java已經支援HTTP/1.1。 HTTP API由java.net包中的幾種型別組成。 現有的API有以下問題:

  • 它被設計為支援多個協議,如http,ftp,gopher等,其中許多協議不再被使用。
  • 太抽象了,很難使用。
  • 它包含許多未公開的行為。
  • 它只支援一種模式,阻塞模式,這要求每個請求/響應有一個單獨的執行緒。

2015年5月,IETF(Internet Engineering Task Force)釋出了HTTP/2規範。 有關HTTP/2規範的完整文字,請訪問https://tools.ietf.org/html/rfc7540。 HTTP/2不會修改應用程式級語義。 也就是說,對應用程式中的HTTP協議的瞭解和使用情況並沒有改變。 它具有更有效的方式準備資料包,然後傳送到客戶端和伺服器之間的電線。 所有之前知道的HTTP,如HTTP頭,方法,狀態碼,URL等都保持不變。 HTTP/2嘗試解決與HTTP/1連線所面臨的許多效能相關的問題:

  • HTTP/2支援二進位制資料交換,來代替HTTP/1.1支援的文字資料。
  • HTTP/2支援多路複用和併發,這意味著多個數據交換可以同時發生在TCP連線的兩個方向上,而對請求的響應可以按順序接收。 這消除了在對等體之間具有多個連線的開銷,這在使用HTTP/1.1時通常是這種情況。 在HTTP/1.1中,必須按照發送請求的順序接收響應,這稱為head-of-line阻塞。 HTTP/2通過在同一TCP連線上進行復用來解決線路阻塞問題。
  • 客戶端可以建議請求的優先順序,伺服器可以在對響應進行優先順序排序時予以遵守。
  • HTTP首部(header)被壓縮,這大大降低了首部大小,從而降低了延遲。
  • 它允許從伺服器到客戶端的資源推送。

JDK 9不是更新現有的HTTP/1.1 API,而是提供了一個支援HTTP/1.1和HTTP/2的HTTP/2 Client API。 該API旨在最終取代舊的API。 新API還包含使用WebSocket協議開發客戶端應用程式的類和介面。 有關完整的WebSocket協議規範,請訪問https://tools.ietf.org/html/rfc6455。 新的HTTP/2客戶端API與現有的API相比有以下幾個好處:

  • 在大多數常見情況下,學習和使用簡單易用。
  • 它提供基於事件的通知。 例如,當收到首部資訊,收到正文併發生錯誤時,會生成通知。
  • 它支援伺服器推送,這允許伺服器將資源推送到客戶端,而客戶端不需要明確的請求。 它使得與伺服器的WebSocket通訊設定變得簡單。
  • 它支援HTTP/2和HTTPS/TLS協議。
  • 它同時工作在同步(阻塞模式)和非同步(非阻塞模式)模式。

新的API由不到20種類型組成,其中有四種是主要型別。 當使用這四種類型時,會使用其他型別。 新API還使用舊API中的幾種型別。 新的API位於jdk.incubator.httpclient模組中的jdk.incubator.http包中。 主要型別有三個抽象類和一個介面:

HttpClient class
HttpRequest class
HttpResponse class
WebSocket interface

HttpClient類的例項是用於儲存可用於多個HTTP請求的配置的容器,而不是為每個HTTP請求單獨設定它們。 HttpRequest類的例項表示可以傳送到伺服器的HTTP請求。 HttpResponse類的例項表示HTTP響應。 WebSocket介面的例項表示一個WebSocket客戶端。 可以使用Java EE 7 WebSocket API建立WebSocket伺服器。

使用構建器建立HttpClientHttpRequestWebSocket的例項。 每個型別都包含一個名為Builder的巢狀類/介面,用於構建該型別的例項。 請注意,不用建立HttpResponse,它作為所做的HTTP請求的一部分返回。 新的HTTP/2 Client API非常簡單,只需在一個語句中讀取HTTP資源! 以下程式碼段使用GET請求,以URL https://www.google.com/作為字串讀取內容:

String responseBody = HttpClient.newHttpClient()
         .send(HttpRequest.newBuilder(new URI("https://www.google.com/"))
               .GET()
               .build(), BodyHandler.asString())
         .body();

處理HTTP請求的典型步驟如下:

  • 建立HTTP客戶端物件以儲存HTTP配置資訊。
  • 建立HTTP請求物件並使用要傳送到伺服器的資訊進行填充。
  • 將HTTP請求傳送到伺服器。
  • 接收來自伺服器的HTTP響應物件作為響應。
  • 處理HTTP響應。

二. 設定案例

在本章中使用了許多涉及與Web伺服器互動的例子。 不是使用部署在Internet上的Web應用程式,而是在NetBeans中建立了一個可以在本地部署的Web應用程式專案。 如果更喜歡使用其他Web應用程式,則需要更改示例中使用的URL。

NetBeans Web應用程式位於原始碼的webapp目錄中。 通過在GlassFish伺服器4.1.1和Tomcat 8/9上部署Web應用程式來測試示例。 可以從https://netbeans.org/下載帶有GlassFish伺服器的NetBeans IDE。 在8080埠的GlassFish伺服器上執行HTTP監聽器。如果在另一個埠上執行HTTP監聽器,則需要更改示例URL中的埠號。

本章的所有HTTP客戶端程式都位於com.jdojo.http.client模組中,其宣告如下所示。

// module-info.java
module com.jdojo.http.client {
    requires jdk.incubator.httpclient;
}

三. 建立HTTP客戶端

HTTP請求需要將配置資訊傳送到伺服器,以便伺服器知道要使用的身份驗證器,SSL配置詳細資訊,要使用的cookie管理器,代理資訊,伺服器重定向請求時的重定向策略等。 HttpClient類的例項儲存這些特定於請求的配置,它們可以重用於多個請求。 可以根據每個請求覆蓋其中的一些配置。 傳送HTTP請求時,需要指定將提供請求的配置資訊的HttpClient物件。 HttpClient包含用於所有HTTP請求的以下資訊:驗證器,cookie管理器,執行器,重定向策略,請求優先順序,代理選擇器,SSL上下文,SSL引數和HTTP版本。

認證者是java.net.Authenticator類的例項。 它用於HTTP身份驗證。 預設是不使用驗證器。

Cookie管理器用於管理HTTP Cookie。 它是java.net.CookieManager類的一個例項。 預設是不使用cookie管理器。

執行器是java.util.concurrent.Executor介面的一個例項,用於傳送和接收非同步HTTP請求和響應。 如果未指定,則提供預設執行程式。

重定向策略是HttpClient.Redirect列舉的常量,它指定如何處理伺服器的重定向問題。 預設值NEVER,這意味著伺服器發出的重定向不會被遵循。

請求優先順序是HTTP/2請求的預設優先順序,可以在1到256(含)之間。 這是伺服器優先處理請求的一個提示。 更高的值意味著更高的優先順序。

代理選擇器是java.net.ProxySelector類的一個例項,用於選擇要使用的代理伺服器。 預設是不使用代理伺服器。

SSL上下文是提供安全套接字協議實現的javax.net.ssl.SSLContext類的例項。當不需要指定協議或不需要客戶端身份驗證時, 提供了一個預設的SSLContext,此選項將起作用。

SSL引數是SSL/TLS/DTLS連線的引數。 它們儲存在javax.net.ssl.SSLParameters類的例項中。

HTTP版本是HTTP的版本,它是1.1或2.它被指定為HttpClient.Version列舉的常量:HTTP_1_1和HTTP_2。 它儘可能請求一個特定的HTTP協議版本。 預設值為HTTP_1_1。

Tips
HttpClient是不可變的。 當構建這樣的請求時,儲存在HttpClient中的一些配置可能會被HTTP請求覆蓋。

HttpClient類是抽象的,不能直接建立它的物件。 有兩種方法可以建立一個HttpClient物件:

  • 使用HttpClient類的newHttpClient()靜態方法
  • 使用HttpClient.Builder類的build()方法

以下程式碼段獲取預設的HttpClient物件:

// Get the default HttpClient
HttpClient defaultClient = HttpClient.newHttpClient();

也可以使用HttpClient.Builder類建立HttpClientHttpClient.newBuilder()靜態方法返回一個新的HttpClient.Builder類例項。 HttpClient.Builder類提供了設定每個配置值的方法。 配置的值被指定為方法的引數,該方法返回構建器物件本身的引用,因此可以連結多個方法。 最後,呼叫返回HttpClient物件的build()方法。 以下語句建立一個HttpClient,重定向策略設定為ALWAYS,HTTP版本設定為HTTP_2:

// Create a custom HttpClient
HttpClient httpClient = HttpClient.newBuilder()                      .followRedirects(HttpClient.Redirect.ALWAYS)
                      .version(HttpClient.Version.HTTP_2)
                      .build();

HttpClient類包含對應於每個配置設定的方法,該設定返回該配置的值。 這些方法如下:

Optional<Authenticator> authenticator()
Optional<CookieManager> cookieManager()
Executor executor()
HttpClient.Redirect followRedirects()
Optional<ProxySelector> proxy()
SSLContext sslContext()
Optional<SSLParameters> sslParameters()
HttpClient.Version version()

請注意,HttpClient類中沒有setter方法,因為它是不可變的。 不能使用HttpClient自己本身的物件。 在使用HttpClient物件向伺服器傳送請求之前,需要使用HttpRequest物件。HttpClient類包含以下三種向伺服器傳送請求的方法:

<T> HttpResponse<T> send(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)
<T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)
<U,T> CompletableFuture<U> sendAsync(HttpRequest req, HttpResponse.MultiProcessor<U,T> multiProcessor)

send()方法同步傳送請求,而sendAsync()方法非同步傳送請求。

四. 處理HTTP請求

客戶端應用程式使用HTTP請求與Web伺服器進行通訊。 它向伺服器傳送一個請求,伺服器發回對應的HTTP響應。 HttpRequest類的例項表示HTTP請求。 以下是處理HTTP請求所需執行的步驟:

  • 獲取HTTP請求構建器(builder)
  • 設定請求的引數
  • 從構建器建立HTTP請求
  • 將HTTP請求同步或非同步傳送到伺服器
  • 處理來自伺服器的響應

1. 獲取HTTP請求構建器

需要使用構建器物件,該物件是HttpRequest.Builder類的例項來建立一個HttpRequest。 可以使用HttpRequest類的以下靜態方法獲取HttpRequest.Builder

HttpRequest.Builder newBuilder()
HttpRequest.Builder newBuilder(URI uri)

以下程式碼片段顯示瞭如何使用這些方法來獲取HttpRequest.Builder例項:

// A URI to point to google
URI googleUri = new URI("http://www.google.com");
// Get a builder for the google URI
HttpRequest.Builder builder1 = HttpRequest.newBuilder(googleUri);
// Get a builder without specifying a URI at this time
HttpRequest.Builder builder2 = HttpRequest.newBuilder();

2. 設定HTTP請求引數

擁有HTTP請求構建器後,可以使用構建器的方法為請求設定不同的引數。 所有方法返回構建器本身,因此可以連結它們。 這些方法如下:

HttpRequest.Builder DELETE(HttpRequest.BodyProcessor body)
HttpRequest.Builder expectContinue(boolean enable)
HttpRequest.Builder GET()
HttpRequest.Builder header(String name, String value)
HttpRequest.Builder headers(String... headers)
HttpRequest.Builder method(String method, HttpRequest.BodyProcessor body)
HttpRequest.Builder POST(HttpRequest.BodyProcessor body)
HttpRequest.Builder PUT(HttpRequest.BodyProcessor body)
HttpRequest.Builder setHeader(String name, String value)
HttpRequest.Builder timeout(Duration duration)
HttpRequest.Builder uri(URI uri)
HttpRequest.Builder version(HttpClient.Version version)

使用HttpClientHttpRequest傳送到伺服器。 當構建HTTP請求時,可以使用version()方法通過HttpRequest.Builder物件設定HTTP版本值,該方法將在傳送此請求時覆蓋HttpClient中設定的HTTP版本。 以下程式碼片段將HTTP版本設定為2.0,以覆蓋預設HttpClient物件中的NEVER的預設值:

// By default a client uses HTTP 1.1. All requests sent using this
// HttpClient will use HTTP 1.1 unless overridden by the request
HttpClient client = HttpClient.newHttpClient();
        
// A URI to point to google
URI googleUri = new URI("http://www.google.com");
// Get an HttpRequest that uses HTTP 2.0
HttpRequest request = HttpRequest.newBuilder(googleUri)
                                 .version(HttpClient.Version.HTTP_2)
                                 .build();
// The client object contains HTTP version as 1.1 and the request
// object contains HTTP version 2.0. The following statement will
// send the request using HTTP 2.0, which is in the request object.
HttpResponse<String> r = client.send(request, BodyHandler.asString());

timeout()方法指定請求的超時時間。 如果在指定的超時時間內未收到響應,則會丟擲HttpTimeoutException異常。

HTTP請求可能包含名為expect的首部欄位,其值為“100-Continue”。 如果設定了此首部欄位,則客戶端只會向伺服器傳送標頭檔案,並且預計伺服器將發回錯誤響應或100-Continue響應。 收到此響應後,客戶端將請求主體傳送到伺服器。 在客戶端傳送實際請求體之前,客戶端使用此技術來檢查伺服器是否可以基於請求的首部處理請求。 預設情況下,此首部欄位未設定。 需要呼叫請求構建器的expectContinue(true)方法來啟用此功能。 請注意,呼叫請求構建器的header("expect", "100-Continue")方法不會啟用此功能。 必須使用expectContinue(true)方法啟用它。

// Enable the expect=100-Continue header in the request
HttpRequest.Builder builder = HttpRequest.newBuilder()                                                               
                                         .expectContinue(true);

五. 設定請求首部

HTTP請求中的首部(header)是鍵值對的形式。 可以有多個首部欄位。 可以使用HttpRequest.Builder類的header()headers()setHeader()方法向請求新增首部欄位。 如果header()headers()方法尚未存在,則會新增首部欄位。 如果首部欄位已經新增,這些方法什麼都不做。 setHeader()方法如果存在,將替換首部欄位; 否則,它會新增首部欄位。

header()setHeader()方法允許一次新增/設定一個首部欄位,而headers()方法可以新增多個。headers()方法採用一個可變引數,它應該按順序包含鍵值對。 以下程式碼片段顯示瞭如何為HTTP請求設定首部欄位:

// Create a URI
URI calc = new URI("http://localhost:8080/webapp/Calculator");
// Use the header() method
HttpRequest.Builder builder1 = HttpRequest.newBuilder(calc)
    .header("Content-Type", "application/x-www-form-urlencoded")
    .header("Accept", "text/plain");
// Use the headers() method
HttpRequest.Builder builder2 = HttpRequest.newBuilder(calc)                
    .headers("Content-Type", "application/x-www-form-urlencoded",
             "Accept", "text/plain");
// Use the setHeader() method
HttpRequest.Builder builder3 = HttpRequest.newBuilder(calc)                
    .setHeader("Content-Type", "application/x-www-form-urlencoded")
    .setHeader("Accept", "text/plain");

六. 設定請求內容實體

一些HTTP請求的主體包含使用POST和PUT方法的請求等資料。 使用主體處理器設定HTTP請求的內容實體,該體處理器是HttpRequest.BodyProcessor的靜態巢狀介面。

HttpRequest.BodyProcessor介面包含以下靜態工廠方法,它們返回一個HTTP請求的處理器,請求特定型別的資源(例如Stringbyte []File):

HttpRequest.BodyProcessor fromByteArray(byte[] buf)
HttpRequest.BodyProcessor fromByteArray(byte[] buf, int offset, int length)
HttpRequest.BodyProcessor fromByteArrays(Iterable<byte[]> iter)
HttpRequest.BodyProcessor fromFile(Path path)
HttpRequest.BodyProcessor fromInputStream(Supplier<? extends InputStream> streamSupplier)
HttpRequest.BodyProcessor fromString(String body)
HttpRequest.BodyProcessor fromString(String s, Charset charset)

這些方法的第一個引數表示請求的內容實體的資料來源。 例如,如果String物件提供請求的內容實體,則使用fromString(String body)方法獲取一個處理器。

Tips
HttpRequest類包含noBody()靜態方法,該方法返回一個HttpRequest.BodyProcessor,它不處理請求內容實體。 通常,當HTTP方法不接受正文時,此方法可以與method()方法一起使用,但是method()方法需要傳遞一個實體處理器。

一個請求是否可以擁有一個內容實體取決於用於傳送請求的HTTP方法。 DELETE,POST和PUT方法都有一個實體,而GET方法則沒有。HttpRequest.Builder類包含一個與HTTP方法名稱相同的方法來設定請求的方法和實體。 例如,要使用POST方法與主體,構建器有POST(HttpRequest.BodyProcessor body)方法。

還有許多其他HTTP方法,如HEAD和OPTIONS,它們沒有HttpRequest.Builder類的相應方法。 該類包含一個可用於任何HTTP方法的method(String method, HttpRequest.BodyProcessor body)。 當使用method()方法時,請確保以大寫的方式指定方法名稱,例如GET,POST,HEAD等。以下是這些方法的列表:

HttpRequest.Builder DELETE(HttpRequest.BodyProcessor body)
HttpRequest.Builder method(String method, HttpRequest.BodyProcessor body)
HttpRequest.Builder POST(HttpRequest.BodyProcessor body)
HttpRequest.Builder PUT(HttpRequest.BodyProcessor body)

以下程式碼片段從String中設定HTTP請求的內容實體,通常在將HTML表單釋出到URL時完成。 表單資料由三個n1n2op欄位組成。

URI calc = new URI("http://localhost:8080/webapp/Calculator");
// Compose the form data with n1 = 10, n2 = 20. And op = +      
String formData = "n1=" + URLEncoder.encode("10","UTF-8") +
                  "&n2=" + URLEncoder.encode("20","UTF-8") +
                  "&op=" + URLEncoder.encode("+","UTF-8")  ;
HttpRequest.Builder builder = HttpRequest.newBuilder(calc)                
    .header("Content-Type", "application/x-www-form-urlencoded")
    .header("Accept", "text/plain")
    .POST(HttpRequest.BodyProcessor.fromString(formData));

七. 建立HTTP請求

建立HTTP請求只需呼叫HttpRequest.Builder上的build()方法,該方法返回一個HttpRequest物件。 以下程式碼段建立了使用HTTP GET方法的HttpRequest

HttpRequest request = HttpRequest.newBuilder()
                                 .uri(new URI("http://www.google.com"))
                                 .GET()
                                 .build();

以下程式碼片段使用HTTP POST方法構建首部資訊和內容實體的Http請求:

// Build the URI and the form’s data
URI calc = new URI("http://localhost:8080/webapp/Calculator");               
String formData = "n1=" + URLEncoder.encode("10","UTF-8") +
                  "&n2=" + URLEncoder.encode("20","UTF-8") +
                  "&op=" + URLEncoder.encode("+","UTF-8");
// Build the HttpRequest object
HttpRequest request = HttpRequest.newBuilder(calc)   
   .header("Content-Type", "application/x-www-form-urlencoded")
   .header("Accept", "text/plain")   
   .POST(HttpRequest.BodyProcessor.fromString(formData))
   .build();

請注意,建立HttpRequest物件不會將請求傳送到伺服器。 需要呼叫HttpClient類的send()sendAsync()方法將請求傳送到伺服器。

以下程式碼片段使用HTTP HEAD請求方法建立一個HttpRequest物件。 請注意,它使用HttpRequest.Builder類的method()方法來指定HTTP方法。

HttpRequest request =
    HttpRequest.newBuilder(new URI("http://www.google.com"))   
               .method("HEAD", HttpRequest.noBody())
               .build();

八. 處理HTTP響應

一旦擁有HttpRequest物件,可以將請求傳送到伺服器並同步或非同步地接收響應。 HttpResponse<T>類的例項表示從伺服器接收到的響應,其中型別引數T表示響應內容實體的型別,例如Stringbyte []Path。 可以使用HttpRequest類的以下方法傳送HTTP請求並接收HTTP響應:

<T> HttpResponse<T> send(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)
<T> CompletableFuture<HttpResponse<T>> sendAsync(HttpRequest req, HttpResponse.BodyHandler<T> responseBodyHandler)
<U,T> CompletableFuture<U> sendAsync(HttpRequest req, HttpResponse.MultiProcessor<U,T> multiProcessor)

send()方法是同步的。 也就是說,它會一直阻塞,直到收到響應。 sendAsync()方法非同步處理響應。 它立即返回一個CompletableFuture<HttpResponse>,當響應準備好進行處理時,它就會完成。

1. 處理響應狀態和首部

HTTP響應包含狀態程式碼,響應首部和響應內容實體。 一旦從伺服器接收到狀態程式碼和首部,但在接收到正文之前,HttpResponse物件就可使用。 HttpResponse類的statusCode()方法返回響應的狀態程式碼,型別為intHttpResponse類的headers()方法返回響應的首部,作為HttpHeaders介面的例項。 HttpHeaders介面包含以下方法,通過名稱或所有首部方便地檢索首部的值作為Map <String,List <String >>型別:

List<String> allValues(String name)
Optional<String> firstValue(String name)
Optional<Long> firstValueAsLong(String name)
Map<String,List<String>> map()

下面包含一個完整的程式,用於向google傳送請求,並附上HEAD請求。 它列印接收到的響應的狀態程式碼和首部。 你可能得到不同的輸出。

// GoogleHeadersTest.java
package com.jdojo.http.client;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
import jdk.incubator.http.HttpResponse;
public class GoogleHeadersTest {
    public static void main(String[] args) {
        try {
            URI googleUri = new URI("http://www.google.com");
            HttpClient client = HttpClient.newHttpClient();
            HttpRequest request =
                HttpRequest.newBuilder(googleUri)
                           .method("HEAD", HttpRequest.noBody())
                           .build();
            HttpResponse<?> response =
              client.send(request, HttpResponse.BodyHandler.discard(null));
            // Print the response status code and headers
            System.out.println("Response Status Code:" +
                               response.statusCode());
            System.out.println("Response Headers are:");
            response.headers()
                    .map()
                    .entrySet()
                    .forEach(System.out::println);
        } catch (URISyntaxException | InterruptedException |
                 IOException e) {
            e.printStackTrace();
        }
    }
}

輸出的結果為:

WARNING: Using incubator modules: jdk.incubator.httpclient
Response Status Code:200
Response Headers are:
accept-ranges=[none]
cache-control=[private, max-age=0]
content-type=[text/html; charset=ISO-8859-1]
date=[Sun, 26 Feb 2017 16:39:36 GMT]
expires=[-1]
p3p=[CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info."]
server=[gws]
set-cookie=[NID=97=Kmz52m8Zdf4lsNDsnMyrJomx_2kD7lnWYcNEuwPWsFTFUZ7yli6DbCB98Wv-SlxOfKA0OoOBIBgysuZw3ALtgJjX67v7-mC5fPv88n8VpwxrNcjVGCfFrxVro6gRNIrye4dAWZvUVfY28eOM; expires=Mon, 28-Aug-2017 16:39:36 GMT; path=/; domain=.google.com; HttpOnly]
transfer-encoding=[chunked]
vary=[Accept-Encoding]
x-frame-options=[SAMEORIGIN]
x-xss-protection=[1; mode=block]

2. 處理響應內容實體

處理HTTP響應的內容實體是兩步過程:

  • 當使用HttpClient類的send()sendAsync()方法傳送請求時,需要指定響應主體處理程式,它是HttpResponse.BodyHandler<T>介面的例項。
  • 當接收到響應狀態程式碼和首部時,呼叫響應體處理程式的apply()方法。 響應狀態程式碼和首部傳遞給apply()方法。 apply()方法返回HttpResponse.BodyProcessor介面的例項,它讀取響應實體並將讀取的資料轉換為型別T。

不要擔心處理響應實體的這些細節。 提供了HttpResponse.BodyHandler<T>的幾個實現。 可以使用HttpResponse.BodyHandler介面的以下靜態工廠方法獲取其不同型別引數T的例項:

HttpResponse.BodyHandler<byte[]> asByteArray()
HttpResponse.BodyHandler<Void> asByteArrayConsumer(Consumer<Optional<byte[]>> consumer)
HttpResponse.BodyHandler<Path> asFile(Path file)
HttpResponse.BodyHandler<Path> asFile(Path file, OpenOption... openOptions)
HttpResponse.BodyHandler<Path> asFileDownload(Path directory, OpenOption... openOptions)
HttpResponse.BodyHandler<String> asString()
HttpResponse.BodyHandler<String> asString(Charset charset)
<U> HttpResponse.BodyHandler<U> discard(U value)

這些方法的簽名足夠直觀,可以告訴你他們處理什麼型別的響應實體。 例如,如果要將響應實體作為String獲取,請使用asString()方法獲取一個實體處理程式。 discard(U value)方法返回一個實體處理程式,它丟棄響應實體並返回指定的值作為主體。

HttpResponse<T>類的body()方法返回型別為T的響應實體。

以下程式碼段向google傳送GET請求,並以String形式檢索響應實體。 這裡忽略了了異常處理邏輯。

import java.net.URI;
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
import jdk.incubator.http.HttpResponse;
import static jdk.incubator.http.HttpResponse.BodyHandler.asString;
...
// Build the request
HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("http://google.com"))
                .GET()
                .build();
// Send the request and get a Response
HttpResponse<String> response = HttpClient.newHttpClient()
                                          .send(request, asString());
// Get the response body and print it
String body = response.body();
System.out.println(body);

輸出結果為:

WARNING: Using incubator modules: jdk.incubator.httpclient
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

該示例返回一個狀態程式碼為301的響應正文,表示URL已經移動。 輸出還包含移動的URL。 如果將HttpClient中的以下重定向策略設定為“ALWAYS”,則該請求將重新提交到已移動的URL。 以下程式碼片段可解決此問題:

// The request will follow the redirects issues by the server       
HttpResponse<String> response = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.ALWAYS)
    .build()
    .send(request, asString());

下面包含一個完整的程式,它顯示如何使用一個POST請求與內容實體,並非同步處理響應。 原始碼中的Web應用程式包含為Calculator的servlet。 Calculator servlet的原始碼不會在這裡顯示。 servlet接受請求中的三個引數,命名為n1,n2和op,其中n1和n2是兩個數字,op是一個運算子(+, - ,*或/)。 響應是一個純文字,幷包含了運算子及其結果。 程式中的URL假定你已在本機上部署了servlet,並且Web伺服器正在埠8080上執行。如果這些假設不正確,請相應地修改程式。 如果servlet被成功呼叫,你將得到這裡顯示的輸出。 否則,將獲得不同的輸出。

// CalculatorTest.java
package com.jdojo.http.client;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.HttpRequest;
import static jdk.incubator.http.HttpRequest.BodyProcessor.fromString;
import jdk.incubator.http.HttpResponse;
public class CalculatorTest {
    public static void main(String[] args) {
        try {
            URI calcUri =
                new URI("http://localhost:8080/webapp/Calculator");
            String formData = "n1=" + URLEncoder.encode("10","UTF-8") +
                              "&n2=" + URLEncoder.encode("20","UTF-8") +
                              "&op=" + URLEncoder.encode("+","UTF-8")  ;
            // Create a request
            HttpRequest request = HttpRequest.newBuilder()
                .uri(calcUri)
                .header("Content-Type", "application/x-www-form-urlencoded")
                .header("Accept", "text/plain")                
                .POST(fromString(formData))
                .build();
            // Process the response asynchronously. When the response
            // is ready, the processResponse() method of this class will
            // be called.
            HttpClient.newHttpClient()
                      .sendAsync(request,
                                 HttpResponse.BodyHandler.asString())
                      .whenComplete(CalculatorTest::processResponse);
            try {
                // Let the current thread sleep for 5 seconds,
                // so the async response processing is complete
                Thread.sleep(5000);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        } catch (URISyntaxException | IOException e) {
            e.printStackTrace();
        }
    }
    private static void processResponse(HttpResponse<String> response,
                                       Throwable t) {
         if (t == null ) {
             System.out.println("Response Status Code: " +  
                                 response.statusCode());
             System.out.println("Response Body: " + response.body());
         } else {
            System.out.println("An exception occurred while " +
                "processing the HTTP request. Error: " +  t.getMessage());
         }
     }
}

輸出結果為:

WARNING: Using incubator modules: jdk.incubator.httpclient
Response Status Code: 200
Response Body: 10 + 20 = 30.0

使用響應實體處理程式可以節省開發人員的大量工作。 在一個語句中,可以下載並將URL的內容儲存在檔案中。 以下程式碼片段將google的內容作為google.html的檔案儲存在當前目錄中。 下載完成後,列印下載檔案的路徑。 如果發生錯誤,則會列印異常的堆疊跟蹤。

HttpClient.newBuilder()
          .followRedirects(HttpClient.Redirect.ALWAYS)
          .build()
          .sendAsync(HttpRequest.newBuilder()           
                                .uri(new URI("http://www.google.com"))
                                .GET()
                                .build(),
                                asFile(Paths.get("google.html")))
           .whenComplete((HttpResponse<Path> response,
                          Throwable exception) -> {
               if(exception == null) {
                  System.out.println("File saved to " +
                                     response.body().toAbsolutePath());
              } else {
                  exception.printStackTrace();
              }
            });

3. 處理響應的Trailer

HTTP Trailer是HTTP響應結束後由伺服器傳送的鍵值列表。 許多伺服器通常不使用HTTP Trailer。 HttpResponse類包含一個trailers()方法,它作為CompletableFuture <HttpHeaders>的例項返回響應Trailer。 注意返回的物件型別的名稱——HttpHeaders。 HTTP/2 Client API確實有一個名為HttpTrailers的型別。 需要檢索響應實體,然後才能檢索Trailer。 目前,HTTP/2 Client API不支援處理HTTP Trailer了。 以下程式碼片段顯示瞭如何在API支援時列印所有響應Trailer:

// Get an HTTP response
HttpResponse<String> response = HttpClient.newBuilder()
                  .followRedirects(HttpClient.Redirect.ALWAYS)
                  .build()
                  .send(HttpRequest.newBuilder()           
                                   .uri(new URI("http://www.google.com"))
                                   .GET()
                                   .build(),
                                   asString());
// Read the response body
String body = response.body();
// Process trailers
response.trailers()
        .whenComplete((HttpHeaders trailers, Throwable t) -> {
             if(t == null) {
                 trailers.map()
                         .entrySet()
                         .forEach(System.out::println);
             } else {
                  t.printStackTrace();
             }
         });

九. 設定請求重定向策略

一個HTTP請求對應的響應,Web伺服器可以返回3XX響應狀態碼,其中X是0到9之間的數字。該狀態碼錶示客戶端需要執行附加操作才能完成請求。 例如,狀態程式碼為301表示URL已被永久移動到新位置。 響應實體包含替代位置。 預設情況下,在收到3XX狀態程式碼後,請求不會重新提交到新位置。 可以將HttpClient.Redirect列舉的以下常量設定為HttpClient執行的策略,以防返回的響應包含3XX響應狀態程式碼:

  • ALWAYS
  • NEVER
  • SAME_PROTOCOL
  • SECURE

ALWAYS指示應始終遵循重定向。 也就是說,請求應該重新提交到新的位置。

NEVER表示重定向不應該被遵循。 這是預設值。

SAME_PROTOCOL表示如果舊位置和新位置使用相同的協議(例如HTTP到HTTP或HTTPS到HTTPS),則可能會發生重定向。

SECURE表示重定向應始終發生,除非舊位置使用HTTPS,而新的位置使用了HTTP。

十. 使用WebSocket協議

WebSocket協議在兩個endpoint(客戶端endpoint和伺服器endpoint)之間提供雙向通訊。 endpoint 是指使用WebSocket協議的連線的兩側中的任何一個。 客戶端endpoint啟動連線,伺服器端點接受連線。 連線是雙向的,這意味著伺服器endpoint可以自己將訊息推送到客戶端端點。 在這種情況下,也會遇到另一個術語,稱為對等體(peer)。 對等體只是連線的另一端。 例如,對於客戶端endpoint,伺服器endpoint是對等體,對於伺服器endpoint,客戶端endpoint是對等體。 WebSocket會話表示endpoint和單個對等體之間的一系列互動。

WebSocket協議可以分為三個部分:

  • 開啟握手
  • 資料交換
  • 關閉握手

客戶端發起與與伺服器的開啟握手。 使用HTTP與WebSocket協議的升級請求進行握手。 伺服器通過升級響應響應開啟握手。 握手成功後,客戶端和伺服器交換訊息。 訊息交換可以由客戶端或伺服器發起。 最後,任一endpoint都可以傳送關閉握手; 對方以關閉握手迴應。 關閉握手成功後,WebSocket關閉。

JDK 9中的HTTP/2 Client API支援建立WebSocket客戶端endpoint。 要擁有使用WebSocket協議的完整示例,需要具有伺服器endpoint和客戶端endpoint。 以下部分涵蓋了建立兩者。

1. 建立伺服器端Endpoint

建立伺服器Endpoint需要使用Java EE。 將簡要介紹如何建立一個伺服器Endpoint示例中使用。 使用Java EE 7註解建立一個WebSocket伺服器Endpoint。

下面包含TimeServerEndPoint類的程式碼。 該類包含在原始碼的webapp目錄中的Web應用程式中。 將Web應用程式部署到Web伺服器時,此類將部署為伺服器Endpoint。

// TimeServerEndPoint.java
package com.jdojo.ws;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.concurrent.TimeUnit;
import javax.websocket.CloseReason;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import static javax.websocket.CloseReason.CloseCodes.NORMAL_CLOSURE;
@ServerEndpoint("/servertime")
public class TimeServerEndPoint {
    @OnOpen
    public void onOpen(Session session) {                
        System.out.println("Client connected. ");
    }
    @OnClose
    public void onClose(Session session) {        
        System.out.println("Connection closed.");
    }
    @OnError
    public void onError(Session session, Throwable t) {
        System.out.println("Error occurred:" + t.getMessage());
    }
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("Client: " + message);                
        // Send messages to the client
        sendMessages(session);
    }
    private void sendMessages(Session session) {
        /* Start a new thread and send 3 messages to the
           client. Each message contains the current date and
           time with zone.
        */
        new Thread(() -> {
            for(int i = 0; i < 3; i++) {
                String currentTime =
                    ZonedDateTime.now().toString();
                try {
                    session.getBasicRemote()
                           .sendText(currentTime, true);
                    TimeUnit.SECONDS.sleep(5);
                } catch(InterruptedException | IOException e) {
                    e.printStackTrace();
                    break;
                }
            }
            try {
                // Let us close the WebSocket
                session.close(new CloseReason(NORMAL_CLOSURE,
                                              "Done"));
            } catch (IOException e) {
                e.printStackTrace();
            }
        })
        .start();
    }
}

TimeServerEndPoint類上使用@ServerEndpoint("/servertime")註解使該類成為伺服器Endpoint,當它部署到Web伺服器時。註解value元素的值為/servertime,這將使Web伺服器在此URL釋出此Endpoint。

該類包含四個方法,它們已經添加了@onOpen@onMessage@onClose@onError註解。 命名這些方法的名字與這些註解相同。 這些方法在伺服器Endpoint的生命週期的不同點被呼叫。 他們以Session物件為引數。 Session物件表示此Endpoint與其對等體的互動,這將是客戶端。

當與對等體進行握手成功時,將呼叫onOpen()方法。 該方法列印客戶端連線的訊息。

當從對等體接收到訊息時,會呼叫onMessage()。 該方法列印它接收的訊息,並呼叫一個名為sendMessages()的私有方法。 sendMessages()方法啟動一個新執行緒,並向對等體傳送三條訊息。 執行緒在傳送每條訊息後休眠五秒鐘。 該訊息包含當前日期和時間與時區。 可以同步或非同步地向對等體傳送訊息。 要傳送訊息,需要獲得表示與對等體的會話的RemoteEndpoint介面的引用。 在Session例項上使用getBasicRemote()getAsyncRemote()方法來獲取可以分別同步和非同步傳送訊息的RemoteEndpoint.BasicRemoteEndpont.Async例項。 一旦得到了對等體(遠端endpoint)的引用,可以呼叫其幾個sendXxx()方法來向對等體傳送不同型別的資料。

// Send a synchronous text message to the peer
session.getBasicRemote()
       .sendText(currentTime, true);

sendText()方法中的第二個引數指示是否是傳送的部分訊息的最後一部分。 如果訊息完成,請使用true。

在所有訊息傳送到對等體後,使用sendClose()方法傳送關閉訊息。 該方法接收封閉了一個關閉程式碼和一個緊密原因的CloseReason類的物件。 當對等體收到一個關閉訊息時,對等體需要響應一個關閉訊息,之後WebSocket連線被關閉。

請注意,在傳送關閉訊息後,伺服器endpoint不應該向對等體傳送更多訊息。

當出現錯誤而不是由WebSocket協議處理時,會呼叫onError()方法。

不能單獨使用此endpoint。 需要建立一個客戶端endpoint,將在下一節中詳細介紹。

2. 建立客戶端Endpoint

開發WebSocket客戶端Endpoint涉及使用WebSocket介面,它是JDK 9中的HTTP/2 Client API的一部分。WebSocket介面包含以下巢狀型別:

  • WebSocket.Builder
  • WebSocket.Listener
  • WebSocket.MessagePart

WebSocket介面的例項表示一個WebSocket客戶端endpoint。 構建器,它是WebSocket.Builder介面的例項,用於建立WebSocket例項。 HttpClient類newWebSocketBuilder(URI uri, WebSocket.Listener listener)方法返回一個WebSocket.Builder介面的例項。

當事件發生在客戶端endpoint時,例如,完成開啟握手,訊息到達,關閉握手等,通知被髮送到一個監聽器,該監聽器是WebSocket.Listener介面的例項。 該介面包含每種通知型別的預設方法。 需要建立一個實現此介面的類。 僅實現與接收通知的事件相對應的那些方法。 建立·WebSocket·例項時,需要指定監聽器。

當向對等體傳送關閉訊息時,可以指定關閉狀態程式碼。 WebSocket介面包含以下可以用作WebSocket關閉訊息狀態程式碼的int型別常量:

  • CLOSED_ABNORMALLY:表示WebSocket關閉訊息狀態程式碼(1006),這意味著連線異常關閉,例如,沒有傳送或接收到關閉訊息。
  • NORMAL_CLOSURE:表示WebSocket關閉訊息狀態程式碼(1000),這意味著連線正常關閉。 這意味著建立連線的目的已經實現了。

伺服器Endpoint可能會發送部分訊息。 訊息被標記為開始,部分,最後或全部,表示其位置。 WebSocket.MessagePart列舉定義了與訊息的位置相對應的四個常量:FIRSTPARTLASTWHOLE。 當監聽器收到已收到訊息的通知時,將這些值作為訊息的一部分。

以下部分將詳細介紹設定客戶端Endpoint的各個步驟。

十一. 建立監聽器

監聽器是WebSocket.Listener介面的例項。 建立監聽器涉及建立實現此介面的類。 該介面包含以下預設方法:

CompletionStage<?> onBinary(WebSocket webSocket, ByteBuffer message, WebSocket.MessagePart part)
CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason)
void onError(WebSocket webSocket, Throwable error)
void onOpen(WebSocket webSocket)
CompletionStage<?> onPing(WebSocket webSocket, ByteBuffer message)
CompletionStage<?> onPong(WebSocket webSocket, ByteBuffer message)
CompletionStage<?> onText(WebSocket webSocket, CharSequence message, WebSocket.MessagePart part)

當客戶端Endpoint連線到引用傳遞給該方法的對等體作為第一個引數時,呼叫onOpen()方法。 預設實現請求一個訊息,這意味著該偵聽器可以再接收一條訊息。 訊息請求是使用WebSocket介面的request(long n)方法進行的:

// Allow one more message to be received
webSocket.request(1);

如果伺服器傳送的訊息多於請求訊息,則訊息在TCP連線上排隊,最終可能強制傳送方通過TCP流控制停止傳送更多訊息。 請在適當的時間呼叫request(long n)方法並使用適當的引數值,這樣監聽器就不會從伺服器一直接收訊息。 在監聽器中重寫onOpen()方法是一個常見的錯誤,而不是呼叫webSocket.request(1)方法,後者會阻止從伺服器接收訊息。

當endpoint收到來自對等體的關閉訊息時,呼叫onClose()方法。 這是監聽器的最後通知。 從此方法丟擲的異常將被忽略。 預設的實現不會做任何事情。 通常,需要向對方傳送一條關閉訊息,以完成關閉握手。

當endpoint從對等體接收到Ping訊息時,呼叫onPing()方法。 Ping訊息可以由客戶端和伺服器endpoint傳送。 預設實現將相同訊息內容的Pong訊息傳送給對等體。

當endpoint從對等體接收到Pong訊息時,呼叫onPong()方法。 通常作為對先前傳送的Ping訊息的響應來接收Pong訊息。 endpoint也可以接收未經請求的Pong訊息。 onPong()方法的預設實現在監聽器上再請求一個訊息,不執行其他操作。

當WebSocket上發生I/O或協議錯誤時,會呼叫onError()方法。 從此方法丟擲的異常將被忽略。 呼叫此方法後,監聽器不再收到通知。 預設實現什麼都不做。

當從對等體接收到二進位制訊息和文字訊息時,會呼叫onBinary()onText()方法。 確保檢查這些方法的最後一個引數,這表示訊息的位置。 如果收到部分訊息,需要組裝它們以獲取整個訊息。 從這些方法返回null表示訊息處理完成。 否則,返回CompletionStage<?>,並在訊息處理完成後完成。

以下程式碼段建立一個可以接收資訊的WebSocket監聽器:

WebSocket.Listener listener =  new WebSocket.Listener() {
    @Override
    public CompletionStage<?> onText(WebSocket webSocket,
                                     CharSequence message,
                                     WebSocket.MessagePart part) {
        // Allow one message to be received by the listener
        webSocket.request(1);
        // Print the message received from the server
        System.out.println("Server: " + message);
        // Return null indicating that we are done processing this message
        return null;
     }
};

十二. 構建Endpoint

需要構建充當客戶端點的WebSocket介面的例項。 該例項用於與伺服器Endpoint連線和交換訊息。 WebSocket例項使用WebSocket.Builder構建。 可以使用HttpClient類的以下方法獲取構建器:

WebSocket.Builder newWebSocketBuilder(URI uri, WebSocket.Listener listener)

用於獲取WebSocket構建器的HttpClient例項提供了WebSocket的連線配置。 指定的uri是伺服器Endpoint的URI。 監聽器是正在構建的Endpoint的監聽器, 擁有構建器後,可以呼叫以下方法來配置endpoint:

WebSocket.Builder connectTimeout(Duration timeout)
WebSocket.Builder header(String name, String value)
WebSocket.Builder subprotocols(String mostPreferred, String... lesserPreferred)

connectTimeout()方法允許指定開啟握手的超時時間。 如果開放握手在指定的持續時間內未完成,則從WebSocket.BuilderbuildAsync()方法完成後返回帶有異常的HttpTimeoutExceptionCompletableFuture。 可以使用header()方法新增任何用於開啟握手的自定義首部。 可以使用subprotocols()方法在開啟握手期間指定給定子協議的請求 —— 只有其中一個將被伺服器選擇。 子協議由應用程式定義。 客戶端和伺服器需要同意處理特定的子協議及其細節。

最後,呼叫WebSocket.Builder介面的buildAsync()方法來構建Endpoint。 它返回CompletableFuture <WebSocket>,當該Endpoint連線到伺服器Endpoint時,正常完成; 當有錯誤時,返回異常。 以下程式碼片段顯示瞭如何構建和連線客戶端Endpoint。 請注意,伺服器的URI以ws開頭,表示WebSocket協議。

URI serverUri = new URI("ws://localhost:8080/webapp/servertime");
// Get a listener
WebSocket.Listener listener = ...;
// Build an endpoint using the default HttpClient
HttpClient.newHttpClient()
          .newWebSocketBuilder(serverUri, listener)
          .buildAsync()
          .whenComplete((WebSocket webSocket, Throwable t) -> {
               // More code goes here
           });

十三. 向對等體傳送訊息

一旦客戶端Endpoint連線到對等體,則交換訊息。 WebSocket介面的例項表示一個客戶端Endpoint,該介面包含以下方法向對等體傳送訊息:

CompletableFuture<WebSocket> sendBinary(ByteBuffer message, boolean isLast)
CompletableFuture<WebSocket> sendClose()
CompletableFuture<WebSocket> sendClose(int statusCode, String reason)
CompletableFuture<WebSocket> sendPing(ByteBuffer message)
CompletableFuture<WebSocket> sendPong(ByteBuffer message)
CompletableFuture<WebSocket> sendText(CharSequence message)
CompletableFuture<WebSocket> sendText(CharSequence message, boolean isLast)

sendText()方法用於向對等體傳送資訊。 如果傳送部分訊息,請使用該方法的兩個引數的版本。 如果第二個引數為false,則表示部分訊息的一部分。 如果第二個引數為true,則表示部分訊息的最後部分。 如果以前沒有傳送部分訊息,則第二個引數中的true表示整個訊息。

endText(CharSequence message)是一種便捷的方法,它使用true作為第二個引數來呼叫該方法的第二個版本。

sendBinary()方法向對等體傳送二進位制資訊。

sendPing()sendPong()方法分別向對等體傳送Ping和Pong訊息。

sendClose()方法向對等體傳送Close訊息。 可以傳送關閉訊息作為由對等方發起的關閉握手的一部分,或者可以傳送它來發起與對等體的閉合握手。

Tips
如果想要突然關閉WebSocket,請使用WebSocket介面的abort()方法。

1. 執行WebSocket程式

現在是檢視WebSocket客戶端endpoint和WebSocket伺服器endpoint交換訊息的時候了。下面包含一個封裝客戶機endpoint的WebSocketClient類的程式碼。 其用途如下:

// Create a client WebSocket
WebSocketClient wsClient = new WebSocketClient(new URI(“<server-uri>”));
// Connect to the server and exchange messages
wsClient.connect();
// WebSocketClient.java
package com.jdojo.http.client;
import java.net.URI;
import java.util.concurrent.CompletionStage;
import jdk.incubator.http.HttpClient;
import jdk.incubator.http.WebSocket;
public class WebSocketClient {
    private WebSocket webSocket;
    private final URI serverUri;
    private boolean inError = false;
    public WebSocketClient(URI serverUri) {
        this.serverUri = serverUri;
    }
    public boolean isClosed() {
        return (webSocket != null && webSocket.isClosed())
               ||
               this.inError;        
    }
    public void connect() {
        HttpClient.newHttpClient()
                  .newWebSocketBuilder(serverUri, this.getListener())
                  .buildAsync()
                  .whenComplete(this::statusChanged);
    }
    private void statusChanged(WebSocket webSocket, Throwable t) {
        this.webSocket = webSocket;
        if (t == null) {        
            this.talkToServer();
        } else {
            this.inError = true;
            System.out.println("Could not connect to the server." +
                               " Error: " + t.getMessage());
        }
    }
    private void talkToServer() {
        // Allow one message to be received by the listener
        webSocket.request(1);
        // Send the server a request for time
        webSocket.sendText("Hello");
    }
    private WebSocket.Listener getListener() {
        return new WebSocket.Listener() {
            @Override
            public void onOpen(WebSocket webSocket) {
                // Allow one more message to be received by the listener
                webSocket.request(1);
                // Notify the user that we are connected
                System.out.println("A WebSocket has been opened.");                
            }
            @Override
            public CompletionStage<?> onClose(WebSocket webSocket,
                             int statusCode, String reason) {
                // Server closed the web socket. Let us respond to
                // the close message from the server
                webSocket.sendClose();
                System.out.println("The WebSocket is closed." +
                                   " Close Code: " + statusCode +
                                   ", Close Reason: " + reason);
                // Return null indicating that this WebSocket
                // can be closed immediately
                return null;
            }
            @Override
            public void onError(WebSocket webSocket, Throwable t) {
                System.out.println("An error occurred: " + t.getMessage());
            }
            @Override
            public CompletionStage<?> onText(WebSocket WebSocket,
                CharSequence message, WebSocket.MessagePart part) {
                // Allow one more message to be received by the listener
                webSocket.request(1);
                // Print the message received from the server
                System.out.println("Server: " + message);
                // Return null indicating that we are done
                // processing this message
                return null;
            }
        };
    }
}

WebSocketClient類的工作原理如下:

  • webSocket例項變數儲存客戶端endpoint的引用。
  • serverUri例項變數儲存伺服器端endpoint的URI。
  • isError例項變數儲存一個指示符,無論該endpoint 是否出錯。
  • isClosed()方法檢查endpoint 是否已經關閉或出錯。
  • 在開啟握手成功之前,webSocket例項變數置為null。 它的值在statusChanged()方法中更新。
  • connect()方法構建一個WebSocket並啟動一個開始握手。 請注意,無論連線狀態如何,它在開始握手完成後呼叫statusChanged()方法。
  • 當開始握手成功時,tatusChanged()方法通過呼叫talkToServer()方法與伺服器通訊。 否則,它會列印一條錯誤訊息,並將isError標誌設定為true。
  • talkToServer()方法允許監聽器再接收一個訊息,並向伺服器endpoint傳送一條資訊。 請注意,伺服器endpoint從客戶端endpoint接收到資訊時,會以五秒的間隔傳送三個訊息。 從talkToServer()方法傳送此訊息將啟動兩個endpoint之間的訊息交換。
  • getListener()方法建立並返回一個WebSocket.Listener例項。 伺服器endpoint將傳送三個訊息,後跟一個關閉訊息。 監聽器中的onClose()方法通過傳送一個空的關閉訊息來響應來自伺服器的關閉訊息,這將結束客戶端endpoint操作。

如下包含執行客戶端endpoint的程式。 如果執行WebSocketClientTest類,請確保具有伺服器endpoint的Web應用程式正在執行。 還需要修改SERVER_URI靜態變數以匹配Web應用程式的伺服器endpoint的URI。 輸出將使用時區列印當前日期和時間,因此可能會得到不同的輸出。

// WebSocketClientTest.java
package com.jdojo.http.client;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.TimeUnit;
public class WebSocketClientTest {    
    // Please change the URI to point to your server endpoint
    static final String SERVER_URI ="ws://localhost:8080/webapp/servertime";
    public static void main(String[] args)
       throws URISyntaxException, InterruptedException {
        // Create a client WebSocket
        WebSocketClient wsClient = new WebSocketClient(new URI(SERVER_URI));
        // Connect to the Server
        wsClient.connect();
        // Wait until the WebSocket is closed
        while(!wsClient.isClosed()) {            
            TimeUnit.SECONDS.sleep(1);
        }
        // Need to exit
        System.exit(0);
    }
}

輸出結果為:

A WebSocket has been opened.
Server: 2016-12-15T14:19:53.311-06:00[America/Chicago]
Server: 2016-12-15T14:19:58.312-06:00[America/Chicago]
Server: 2016-12-15T14:20:03.313-06:00[America/Chicago]
The WebSocket is closed.  Close Code: 1000, Close Reason: Done

2. WebS