使用 Java 11 HTTP Client API 實現 HTTP/2 伺服器推送

專注的阿熊發表於2019-10-31

對 HttpUrlConnection 你還有印象嗎?JDK 11為 HttpUrlConnection 重新設計了 HTTP Client API。HTTP Client API 使用簡單,支援 HTTP/2(預設)和 HTTP/1.1。為了向後相容,當伺服器不支援 HTTP/2時,HTTP Client API 會自動從 HTTP/2 降到 HTTP1.1。 

此外,HTTP Client API 支援同步和非同步程式設計模型,並依靠 stream 傳輸資料(reactive stream)。它還支援 WebSocket 協議,用於實時 Web 應用程式,降低客戶端與伺服器間通訊開銷。

除了多路複用(Multiplexing),HTTP/2 另一個強大的功能是 伺服器推送 。傳統方法(HTTP/1.1)中,主要透過瀏覽器發起請求 HTML 頁面,解析接收的標記(Markup)並標識引用的資源(例如JS、CSS、影像等)。 

為了獲取資源,瀏覽器會繼續傳送資源請求(每個資源一個請求)。相反,HTTP/2 會傳送 HTML 頁面和引用的資源,不需要瀏覽器主動請求。因此,瀏覽器請求 HTML 頁面後,就能收到頁面以及顯示所需的所有其他資訊。HTTP Client API 透過 PushPromiseHandler 介面支援 HTTP/2 功能。 

介面實現必須作為 send() 或 sendAsync() 方法的第三個引數填入。PushPromiseHandler 依賴下面三項協同:

  • 客戶端發起的 send request(initiatingRequest)

  • 合成 push request(pushPromiseRequest)

  • acceptor 函式,必須成功呼叫該函式才能接受 push promise(acceptor)

呼叫特定 acceptor 函式接受 push promise。acceptor 函式必須傳入一個 BodyHandler(不能為 null)用來處理 Promise 的 request body。acceptor 函式會返回一個 CompletableFuture 例項,完成 promise response。

基於以上資訊,看一下 PushPromiseHandler 實現:


private 
static final List<CompletableFuture<Void>>

  asyncPushRequests = new CopyOnWriteArrayList<>();
...
private static HttpResponse.PushPromiseHandler<String> pushPromiseHandler() {
  return (HttpRequest initiatingRequest,
     HttpRequest pushPromiseRequest,
     Function<HttpResponse.BodyHandler<String> ,
     CompletableFuture<HttpResponse<String>>> acceptor) -> {
     CompletableFuture<Void> pushcf =
        acceptor.apply(HttpResponse.BodyHandlers.ofString())
        .thenApply(HttpResponse::body)
        .thenAccept((b) -> System.out.println(
            "\nPushed resource body:\n " + b));
        asyncPushRequests.add(pushcf);
        System.out.println( "\nJust got promise push number: " +
           asyncPushRequests.size());
        System.out.println( "\nInitial push request: " +
           initiatingRequest.uri());
        System.out.println( "Initial push headers: " +
           initiatingRequest.headers());
        System.out.println( "Promise push request: " +
           pushPromiseRequest.uri());
        System.out.println( "Promise push headers: " +
           pushPromiseRequest.headers());
  };
}

現在,觸發一個 request 把 PushPromiseHandler 傳給 sendAsync():

HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create( "))
  .build();
client.sendAsync(request,
  HttpResponse.BodyHandlers.ofString(), pushPromiseHandler())
     .thenApply(HttpResponse::body)
     .thenAccept((b) -> System.out.println( "\nMain resource:\n" + b))
     .join();
asyncPushRequests.forEach(CompletableFuture::join);
System.out.println( "\nFetched a total of " +
  asyncPushRequests.size() + " push requests");

完整原始碼可在 GitHub 上找到。

github.com/PacktPublishing/Java-Coding-Problems/tree/master/Chapter13/P268_ServerPush

如果要把所有 push promise 及 response 彙總到指定的 map 中,可以使用 PushPromiseHandler.of() 方法,如下所示:


private

 
static

 final ConcurrentMap<HttpRequest,

  CompletableFuture<HttpResponse<String>>> promisesMap
     =
new ConcurrentHashMap<>();
private static final Function<HttpRequest,
  HttpResponse.BodyHandler<String>> promiseHandler
     = (HttpRequest req) -> HttpResponse.BodyHandlers.ofString();
public static void main (String[] args)
        throws IOException, InterruptedException
{
  HttpClient client = HttpClient.newHttpClient();
  HttpRequest request = HttpRequest.newBuilder()
     .uri(URI.create(
" ))
     .build();
  client.sendAsync(request,
     HttpResponse.BodyHandlers.ofString(), pushPromiseHandler())
        .thenApply(HttpResponse::body)
        .thenAccept((b) -> System.out.println(
"\nMain resource:\n" + b))
        .join();
function(){   //外匯跟單   System.out.println(
"\nPush promises map size: " +
     promisesMap.size() +
"\n" );
  promisesMap.entrySet().forEach((entry) -> {
     System.out.println(
"Request = " + entry.getKey() +
         
", \nResponse = " + entry.getValue().join().body());
  });
}
private static HttpResponse.PushPromiseHandler<String> pushPromiseHandler() {
 
return HttpResponse.PushPromiseHandler.of(promiseHandler, promisesMap);
}

完整原始碼可在 GitHub 上找到。

github.com/PacktPublishing/Java-Coding-Problems/tree/master/Chapter13/P268_ServerPushToMap

前面兩個解決方案中 BodyHandler 都用到了 String 型別的 ofString()。如果伺服器還需要推送二進位制資料(比如影像),就不是很適用。因此,如果要處理二進位制資料,則需要用 ofByteArray() 切換到byte[] 型別的 BodyHandler。也可以用 ofFile() 把 push 資源儲存到磁碟,下面的解決方案是之前方案的改進版:


private 
static final ConcurrentMap<HttpRequest,

  CompletableFuture<HttpResponse<Path>>>
     promisesMap = new ConcurrentHashMap<>();
private static final Function<HttpRequest,
  HttpResponse.BodyHandler<Path>> promiseHandler
     = (HttpRequest req) -> HttpResponse.BodyHandlers.ofFile(
       Paths.get(req.uri().getPath()).getFileName());
public static void main (String[] args)
                 throws IOException, InterruptedException
{
  HttpClient client = HttpClient.newHttpClient();
  HttpRequest request = HttpRequest.newBuilder()
     .uri(URI.create( "))
     .build();
  client.sendAsync(request, HttpResponse.BodyHandlers.ofFile(
     Path.of( "index.html")), pushPromiseHandler())
        .thenApply(HttpResponse::body)
        .thenAccept((b) -> System.out.println( "\nMain resource:\n" + b))
        .join();
  System.out.println( "\nPush promises map size: " +
     promisesMap.size() + "\n");
  promisesMap.entrySet().forEach((entry) -> {
     System.out.println( "Request = " + entry.getKey() +
        ", \nResponse = " + entry.getValue().join().body());
  });
}
private static HttpResponse.PushPromiseHandler<Path> pushPromiseHandler() {
  return HttpResponse.PushPromiseHandler.of(promiseHandler, promisesMap);
}

上面的程式碼把 push 資源儲存到應用程式 classpath 中, 完整原始碼可在 GitHub 上找到。

github.com/PacktPublishing/Java-Coding-Problems/tree/master/Chapter13/P268_ServerPushToDisk

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946337/viewspace-2662056/,如需轉載,請註明出處,否則將追究法律責任。

相關文章