從JDK11新增HttpClient談談非阻塞模型

資料價值發表於2018-12-01

北京時間 9 月 26 日,Oracle 官方宣佈 Java 11 正式釋出

一、JDK HTTP Client介紹

JDK11中的17個新特性

JDK11中的17個新特性

JDK11中引入HTTP Client的動機

既有的HttpURLConnection存在許多問題

  • 其基類URLConnection當初是設計為支援多協議,但其中大多已經成為非主流(ftp, gopher…)
  • API的設計早於HTTP/1.1,過度抽象
  • 難以使用,存在許多沒有文件化的行為
  • 它只支援阻塞模式(每個請求/響應占用一個執行緒)

HTTP Client發展史

HTTP Client發展史

在JDK11 HTTP Client出現之前

在此之前,可以使用以下工具作為Http客戶端

  • JDK HttpURLConnection
  • Apache HttpClient
  • Okhttp
  • Spring Rest Template
  • Spring Cloud Feign
  • 將Jetty用作客戶端
  • 使用Netty庫。還

初探JDK HTTP Client

我們來看一段HTTP Client的常規用法的樣例 ——
執行GET請求,然後輸出響應體(Response Body)。

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
      .uri(URI.create("http://openjdk.java.net/"))
      .build();
client.sendAsync(request, asString())
      .thenApply(HttpResponse::body)
      .thenAccept(System.out::println)
      .join();

第一步:建立HttpClient

一般使用JDK 11中的HttpClient的第一步是建立HttpClient物件並進行配置。

  • 指定協議(http/1.1或者http/2)
  • 轉發(redirect)
  • 代理(proxy)
  • 認證(authenticator)
HttpClient client = HttpClient.newBuilder()
      .version(Version.HTTP_2)
      .followRedirects(Redirect.SAME_PROTOCOL)
      .proxy(ProxySelector.of(new InetSocketAddress("www-proxy.com", 8080)))
      .authenticator(Authenticator.getDefault())
      .build();

第二步:建立HttpRequest

從HttpRequest的builder組建request

  • 請求URI
  • 請求method(GET, PUT, POST)
  • 請求體(request body)
  • Timeout
  • 請求頭(request header)
HttpRequest request = HttpRequest.newBuilder()
      .uri(URI.create("http://openjdk.java.net/"))
      .timeout(Duration.ofMinutes(1))
      .header("Content-Type", "application/json")
      .POST(BodyPublisher.fromFile(Paths.get("file.json")))
      .build()

第三步:send

  • http client可以用來傳送多個http request
  • 請求可以被以同步或非同步方式傳送

1. 同步傳送

同步傳送API阻塞直到HttpResponse返回

HttpResponse<String> response =
      client.send(request, BodyHandler.asString());
System.out.println(response.statusCode());
System.out.println(response.body());

2. 非同步傳送

  • 非同步傳送API立即返回一個CompletableFuture
  • 當它完成的時候會獲得一個HttpResponse
client.sendAsync(request, BodyHandler.asString())
      .thenApply(response -> { System.out.println(response.statusCode());
                               return response; } )
      .thenApply(HttpResponse::body)
      .thenAccept(System.out::println);

※CompletableFuture是在java8中加入的,支援組合式非同步程式設計

二、從提升單機併發處理能力的技術來看HttpClient的實現細節

提升單機併發處理能力的技術

  • 多CPU/多核
  • 多執行緒
  • 非阻塞(non-blocking)

Java NIO

Java NIO為Java帶來了非阻塞模型。
NIO

Lambda表示式

  • Lambda表示式可以方便地利用多CPU。
  • Lambda表示式讓程式碼更加具有可讀性

回撥

假設以下程式碼是一個聊天應用伺服器的一部分,該應用以Vert.x框架實現。
(Eclipse Vert.x is a tool-kit for building reactive applications on the JVM.)
向connectHandler方法輸入一個Lambda表示式,每當有使用者連線到聊天應用時,都會呼叫該Lambda表示式。這就是一個回撥。
這種方式的好處是,應用不必控制執行緒模型——Vert.x框架為我們管理執行緒,打理好一切相關複雜性,程式設計師只考慮和回撥就夠了。

vertx.createServer()
    .connectHandler(socket -> {
        socket.dataHandler(new User(socket, this));
    }).listen(10_000);

注意,這種設計裡,不共享任何狀態。物件之間通過向事件匯流排傳送訊息通訊,根本不需要在程式碼中新增鎖或使用synchronized關鍵字。併發程式設計變得更加簡單。

Future

大量的回撥會怎樣?請看以下虛擬碼

(1)->{
    (2)->{
        (3)->{
            (4)->{}
        }
    }
}

大量回撥會形成“末日金字塔”。

如何破解? 使用Future

Future1=(1)->{}
Future2=(Future1.get())->{}
Future3=(Future2.get())->{}
Future4=(Future3.get())->{}

但這會造成原本期望的並行處理,變成了序列處理,帶來了效能問題。
我們真正需要的是將Future和回撥聯合起來使用。下面將要講的CompletableFuture就是結合了Future和回撥,其要點是組合不同例項而無需擔心末日金字塔問題。

CompletableFuture

(new CompletableFuture()).thenCompose((1)->{})
    .thenCompose((2)->{})
    .thenCompose((3)->{})
    .thenCompose((4)->{})
    .join()

Reactive Streams

Reactive Streams是一個倡議,它提倡提供一種帶有非阻塞背壓的非同步流處理的標準(Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure)。

JDK 9中的java.util.concurrent.Flow中的概念,與Reactive Streams是一對一對等的。java.util.concurrent.Flow是Reactive Streams標準的實現之一。

多CPU的並行機制讓處理海量資料的速度更快,訊息傳遞和響應式程式設計讓有限的並行執行的執行緒執行更多的I/O操作。

HttpClient的實現細節

請求響應的body與reactive streams

  • 請求響應的body暴露為reactive streams
  • http client是請求的body的消費者
  • http client是響應的body的生產者
    請求響應的body與reactive streams

HttpRequest內部

public abstract class HttpRequest {
    ...
    public interface BodyPublisher
                extends Flow.Publisher<ByteBuffer> { ... }
}

HttpResponse的內部

public abstract class HttpResponse<T> {
    ...
    public interface BodyHandler<T> {
        BodySubscriber<T> apply(int statusCode, HttpHeaders responseHeaders);
    }

    public interface BodySubscriber<T>
        extends Flow.Subscriber<List<ByteBuffer>> { ... }
}


相關文章