Tips
做一個終身學習的人。
在此章中,主要介紹以下內容:
- 什么是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。 以下是具體的步驟:
- 源代碼包含與項目名稱相同目錄中的jdk.incubator.httpclient NetBeans項目。
- 安裝JDK 9時,其源代碼將作為src.zip文件復制到安裝目錄中。 將所有內容從src.zip文件中的jdk.incubator.httpclient目錄復制到下載的源代碼中的Java9revealed\jdk.incubator.httpclient\src目錄中。
- 在NetBeans中打開jdk.incubator.httpclient項目。
- 右鍵單擊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服務器。
使用構建器創建HttpClient
,HttpRequest
和WebSocket
的實例。 每個類型都包含一個名為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
類創建HttpClient
。 HttpClient.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)
使用HttpClient
將HttpRequest
發送到服務器。 當構建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請求的處理器,請求特定類型的資源(例如String
,byte []
或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時完成。 表單數據由三個n1
,n2
和op
字段組成。
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表示響應內容實體的類型,例如String
,byte []
或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()
方法返回響應的狀態代碼,類型為int
。 HttpResponse
類的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.Basic
和RemoteEndpont.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
枚舉定義了與消息的位置相對應的四個常量:FIRST
,PART
,LAST
和WHOLE
。 當監聽器收到已收到消息的通知時,將這些值作為消息的一部分。
以下部分將詳細介紹設置客戶端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.Builder
的buildAsync()
方法完成后返回帶有異常的HttpTimeoutException
的CompletableFuture
。 可以使用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) 調用時,會發生這種情況。 服務器正在發送消息,因為不允許更多消息排隊。 在onOpen ,onText 和其他事件中調用request(n) 方法來解決這個問題。 |
十四. 總結
JDK 9添加了一個HTTP/2 Client API,可以在Java應用程序中使用HTTP請求和響應。 API提供類和接口來開發具有身份驗證和TLS的WebSocket客戶端。 API位於jdk.incubator.http包中,該包位於jdk.incubator.httpclient模塊中。
三個抽象類,HttpClient
,HttpRequest
和HttpResponse
,WebSocket
接口是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支持與對等體交換文本以及二進制消息。消息可以部分交換。