天天看點

HttpClient的異步調用,你造嗎?

HttpClient的異步調用,你造嗎?

一、前言

HttpClient提供了兩種I/O模型:經典的java阻塞I/O模型和基于Java NIO的異步非阻塞事件驅動I/O模型。

Java中的阻塞I/O是一種高效、便捷的I/O模型,非常适合并發連接配接數量相對适中的高性能應用程式。隻要并發連接配接的數量在1000個以下并且連接配接大多忙于傳輸資料,阻塞I/O模型就可以提供最佳的資料吞吐量性能。然而,對于連接配接大部分時間保持空閑的應用程式,上下文切換的開銷可能會變得很大,這時非阻塞I/O模型可能會提供更好的替代方案。

異步I/O模型可能更适合于比較看重資源高效利用、系統可伸縮性、以及可以同時支援更多HTTP連接配接的場景。

二、HttpClient中的Future

在HttpClient官網Tutorial的進階話題中,我們可以發現其提供了用于異步執行的FutureRequestExecutionService服務類。

使用FutureRequestExecutionService,允許我們發起http調用後,調用函數馬上傳回(調用線程不會阻塞等到相應結果傳回)一個Future對象,然後調用線程可以在需要響應結果的地方調用Future對象的get方法來阻塞等待結果。

使用FutureRequestExecutionService的優點是,我們可以使用多個線程并發排程請求、設定任務逾時,或者在不再需要響應時取消它們。

FutureRequestExecutionService其實是用一個HttpRequestFutureTask包裝請求,該HttpRequestFutureTask擴充了JDK中的FutureTask。這個類允許我們取消任務、跟蹤各種執行名額,如請求持續時間等。

下面我們看一個例子:

// 1.建立線程池
 private static ExecutorService executorService = Executors.newFixedThreadPool(5);

 // 2.建立http回調函數
 private static final class OkidokiHandler implements ResponseHandler<String> {
  public String handleResponse(final HttpResponse response) throws ClientProtocolException, IOException {
   // 2.1處理響應結果
   return EntityUtils.toString(response.getEntity());
  }
 }

 public static void main(String[] args) throws IOException, InterruptedException, ExecutionException {
  // 3.建立httpclient對象
  CloseableHttpClient httpclient = HttpClients.createDefault();

  // 4.建立FutureRequestExecutionService執行個體
  FutureRequestExecutionService futureRequestExecutionService = new FutureRequestExecutionService(httpclient,
    executorService);

  // 5.發起調用
  try {
   // 5.1請求參數
   HttpGet httpget1 = new HttpGet("http://127.0.0.1:8080/test1");
   HttpGet httpget2 = new HttpGet("http://127.0.0.1:8080/test2");
   // 5.2發起請求,不阻塞,馬上傳回
   HttpRequestFutureTask<String> task1 = futureRequestExecutionService.execute(httpget1,
     HttpClientContext.create(), new OkidokiHandler());

   HttpRequestFutureTask<String> task2 = futureRequestExecutionService.execute(httpget2,
     HttpClientContext.create(), new OkidokiHandler());

   // 5.3 do somthing

   // 5.4阻塞擷取結果
   String str1 = task1.get();
   String str2 = task2.get();
   System.out.println("response:" + str1 + " " + str2);
  } finally {
   httpclient.close();
  }
 }
           

如上代碼1建立了一個線程池用來作為http異步執行的背景線程,代碼2建立了一個http響應結果處理器,用來異步處理http的響應結果。

代碼3建立了一個HttpClient對象,代碼4建立一個FutureRequestExecutionService,參數1為建立的httpclient對象,參數2為建立的線程池。

代碼5則建立2個Get請求參數,然後執行代碼5.2發起兩個http請求,該調用會馬上傳回自己對于的HttpRequestFutureTask對象,調用線程也會馬上傳回,然後調用線程就可以在5.3做其他的事情,最後在需要擷取http響應結果的地方,比如代碼5.4調用兩個future的get()方法來擷取結果。

如上基于Future方式,我們可以并發的發起兩個http請求,而之前阻塞方式,則是順序執行的。

但是基于上面Future方式,我們調用線程還是會在代碼5.4阻塞等待響應結果,這并不是我們想要的,我們想要的是事件通知,http确實也提供了注冊CallBack的方式。

首先我們需要實作FutureCallback接口,用來接收通知:

private static final class MyCallback implements FutureCallback<String> {

  public void failed(final Exception ex) {
   System.out.println(ex.getLocalizedMessage());
  }

  public void completed(final String result) {
   System.out.println(result);
  }

  public void cancelled() {
   System.out.println("cancelled");
  }
 }
           

然後我們隻需要修改代碼5.2,使用三個參數的execute方法發起調用:

// 5.發起調用
  try {
   // 5.1請求參數
   HttpGet httpget1 = new HttpGet("http://127.0.0.1:8080/test1");
   HttpGet httpget2 = new HttpGet("http://127.0.0.1:8080/test2");
   // 5.2發起請求,不阻塞,馬上傳回
   HttpRequestFutureTask<String> task1 = futureRequestExecutionService.execute(httpget1,
     HttpClientContext.create(), new OkidokiHandler(), new MyCallback());

   HttpRequestFutureTask<String> task2 = futureRequestExecutionService.execute(httpget2,
     HttpClientContext.create(), new OkidokiHandler(), new MyCallback());
              //main線程休眠10s,避免請求結束前,關閉了連結
   Thread.sleep(10000);
...
           

如上代碼,使用CallBack後,調用線程就得到了徹底解放,就不必再阻塞擷取結果了,當http傳回結果後,會自動調用我們注冊的CallBack。

三、HttpAsyncClient-真正的異步

上面HttpClient提供的CallBack的方式,雖然解放了調用線程,但是并不是真正意義上的異步調用,因為其異步調用的支援是基于我們建立的executorService線程。即:雖然發起http調用後,調用線程馬上傳回了,但是其内部還是使用executorService中的一個線程阻塞等待響應結果。

HttpAsyncClient則使用Java NIO的異步非阻塞事件驅動I/O模型,實作了真正意義的異步調用,使用HttpAsyncClient我們需要引入其專門的包:

<dependency>
  <groupId>org.apache.httpcomponents</groupId>
  <artifactId>httpasyncclient</artifactId>
  <version>4.1.4</version>
</dependency>
           

然後改造後代碼如下:

// 1.建立CallBack
 private static final class MyCallback implements FutureCallback<HttpResponse> {

  public void failed(final Exception ex) {
   System.out.println(ex.getLocalizedMessage());
  }

  public void completed(final HttpResponse response) {
   try {
    System.out.println(EntityUtils.toString(response.getEntity()));
   } catch (ParseException e) {
    e.printStackTrace();
   } catch (IOException e) {
    e.printStackTrace();
   }
  }

  public void cancelled() {
   System.out.println("cancelled");
  }
 }

 public static void main(String[] args) throws IOException, InterruptedException, ExecutionException {
  // 2.建立異步httpclient對象
  CloseableHttpAsyncClient httpclient = HttpAsyncClients.custom().build();

  // 3.發起調用
  try {

   // 3.0啟動
   httpclient.start();
   // 3.1請求參數
   HttpGet httpget1 = new HttpGet("http://127.0.0.1:8080/test1");
   HttpGet httpget2 = new HttpGet("http://127.0.0.1:8080/test2");
   // 3.2發起請求,不阻塞,馬上傳回
   httpclient.execute(httpget1, new MyCallback());
   httpclient.execute(httpget2, new MyCallback());

   // 3.3休眠10s,避免請求執行完成前,關閉了連結
   Thread.sleep(10000);
  } finally {
   httpclient.close();
  }
 }
           

如上代碼1,建立異步回調實作,用于處理Http響應結果。代碼2建立了異步HttpClient,代碼3.0啟動client,代碼3.2發起請求。

基于Java NIO的異步,當發起請求後,調用方不會使用任何線程同步等待http服務端的響應結果(少量的NIO線程不算哦,因為其個數固定,并且不随并發請求數量變化),而是會使用少量記憶體來記錄請求資訊,以便服務端響應結果回來後,可以找到對應的回調函數進行執行。

四、總結

本文概要講解了Http的異步調用,關于更多Java中異步調用與異步執行的知識,可以參考Java異步程式設計實戰這本書

繼續閱讀