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

Tips
作一個終身學習的人。html

Java 9

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

  • 什麼是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中孵化器模塊的更多信息。web

孵化器模塊在編譯時或運行時未被默認解析,所以須要使用--add-modules選項將jdk.incubator.httpclient模塊添加到默認的根模塊中,以下所示:api

<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。websocket

由於jdk.incubator.httpclient模塊不在Java SE中,因此將不會爲此模塊找到Javadoc。 爲了生成此模塊的Javadoc,並將其包含在本書的源代碼中。 可使用下載的源代碼中的Java9Revealed/jdk.incubator.httpclient/dist/javadoc/index.html文件訪問Javadoc。 使用JDK 9早期訪問構建158的JDK版原本生成Javadoc。 API可能會改變,可能須要從新生成Javadoc。 如下是具體的步驟:cookie

  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有如下問題:session

  • 它被設計爲支持多個協議,如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. WebSocket應用程序疑難解答

當測試WebSocket應用程序時,會出現一些問題。 下表列出了一些這些問題及其解決方案。

錯誤信息 解決方案
Could not connect to the server. Error: java.net.ConnectException: Connection refused: no further information 表示Web服務器未運行或服務器URI不正確。 嘗試運行Web服務器並檢查在WebSocketClientTest類中其SERVER_URI靜態變量的指定的服務器URI。
Could not connect to the server. Error: java.net.http.WebSocketHandshakeException: 404: RFC 6455 1.3. Unable to complete handshake; HTTP response status code 404 表示服務器URI未指向服務器上的正確endpoint 。 驗證WebSocketClientTest類中SERVER_URI靜態變量的值是否正確。
A WebSocket has been opened. Dec 15, 2016 2:58:03 PM java.net.http.WS$1 onError WARNING: Failing connection java.net.http.WS@162532d6[CONNECTED], reason: 'RFC 6455 7.2.1. Stream ended before a Close frame has been received' An error occurred: null 表示開啓握手後,服務器將自動關閉服務器endpoint。 這一般由計算機上運行的防病毒程序執行的。 須要配置防病毒程序以容許指定端口上的HTTP鏈接,或者在另外一個未被防病毒程序阻止的端口上使用HTTP監聽器運行Web服務器。
A WebSocket has been opened. Server: 2016-12-16T07:15:04.586-06:00[America/Chicago] 在這種狀況下,應用程序會打印一行或兩行輸出並一直等待。 當在客戶端endpoint邏輯中沒有webSocket.request(1)調用時,會發生這種狀況。 服務器正在發送消息,由於不容許更多消息排隊。 在onOpenonText和其餘事件中調用request(n)方法來解決這個問題。

十四. 總結

JDK 9添加了一個HTTP/2 Client API,能夠在Java應用程序中使用HTTP請求和響應。 API提供類和接口來開發具備身份驗證和TLS的WebSocket客戶端。 API位於jdk.incubator.http包中,該包位於jdk.incubator.httpclient模塊中。

三個抽象類,HttpClientHttpRequestHttpResponseWebSocket接口是HTTP/2 Client API的核心。這些類型的實例使用構建器建立。 HttpClient類是不可變的。HttpClient類的實例保存能夠重複用於多個HTTP請求的HTTP鏈接配置。 HttpRequest類實例表示HTTP請求。 HttpResponse類的實例表示從服務器接收的HTTP響應。能夠同步或異步地發送和接收HTTP請求和響應。

WebSocket接口的實例表示一個WebSocket客戶端endpoint。與WebSocket服務器端endpoint的通訊是異步完成的。 WebSocket API是基於事件的。須要爲WebSocket客戶端endpoint指定一個監聽器,它是WebSocket.Listener接口的一個實例。監聽器經過調用其適當的方法 —— 當事件發生在endpoint上時,例如,當經過調用監聽器的onOpen()方法成功完成與對等體的打開握手時,通知監聽器。 API支持與對等體交換文本以及二進制消息。消息能夠部分交換。

相關文章
相關標籤/搜索