Working with the New HTTP/2 Client


요점 정리

  • Legacy HTTP Client API는 HTTP/2와 WebSocket을 지원하지 않았고 Blocking모드에서만 동작합니다.

  • Java9에서는 HTTP/1과 HTTP/2를 둘 다 제공합니다. 또한 동기 (Blocking모드)와 비동기 (Non Blocking모드) 모두 지원합니다. 비동기 모드에서 WebSocket을 지원합니다. 이 예제에서는 HTTP 기능에 중점을 둡니다

  • HttpURLConnection API를 대체할 수 있습니다. (HTTP 클라이언트는 인큐베이터 모듈로 포함) HttpURLConnection의 부모인 URLConnection은 http, ftp, gopher 등과 같은 여러 프로토콜을 지원하도록 설계되었습니다. 이 중 많은 프로토콜은 더 이상 사용되지 않습니다.

  • 새 클라이언트는 TCP/IP를 통한 HTTP/1.1에서 HTTP/2 업그레이드를 위해 h2c 토큰과 함께 업그레이드 헤더 필드를 포함 할 수 있습니다. 서버가 h2c를 지원하지 않으면 HTTP/1.1에서 작동합니다.

  • 새로운 클라이언트는 TLS 확장을 사용하여 ALPN (Application-Layer Protocol Negotiation)에 대한 지원을 제공 할 수 있으므로 더 적은 왕복으로 HTTP/2를 사용 할 수 있습니다.

    • 서버는 하나의 프로토콜 명을 응답하겠지만, 만약 클라이언트가 요청한 목록에서 지원되는 것이 없다면 연결은 끊어지게 됩니다.
    • TLS handshake가 완료되면 보안 연결이 수립되고 클라이언트와 서버는 어떤 응용프로그램 프로토콜을 사용할지 동의합니다.
    • 협상된 프로토콜을 통해 클라이언트와 서버는 바로 메시지를 교환할 수 있습니다.

JShell을 사용하여 HTTP 클라이언트의 사용법을보다 쉽게 ​​보여줄 수 있습니다.

jshell --add-modules=jdk.incubator.httpclient

import jdk.incubator.http.*;
import java.lang.*;
import java.net.URI;
import java.net.URISyntaxException;

다음은 기존 HttpURLConnection 동기식 코드 입니다.

URL url = new URL("http://soen.kr");

HttpURLConnection conn = (HttpURLConnection) url.openConnection();

conn.setRequestMethod("GET");
conn.setInstanceFollowRedirects(true);

System.out.println(String.format("Status code: %d", conn.getResponseCode()));

InputStream in = conn.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();

try (in; out) {
    byte[] buf = new byte[1024 * 8];
    int length = 0;

    while ((length = in.read(buf)) != -1) {
        out.write(buf, 0, length);
    }

    System.out.println(String.format(String.format("Body length: %d",, 
                           new String(out.toByteArray(), "UTF-8").length()));
}

conn.disconnect();

새 모듈은 request와 response를 분리합니다. 다음은 HTTP 작업을 수행하기 위한 주요 클래스입니다.

  • HttpClient : HTTP 클라이언트를 나타내며 요청을 보내고 응답을 받을 수 있도록 합니다.
  • HttpRequest : HTTP 요청을 나타냅니다.
  • HttpResponse : HTTP 응답을 나타냅니다. API는 인스턴스를 만들고 다른 부분을 구성하는 빌더를 제공합니다. HTTPHeader 클래스로 표시되는 헤더에 대한 빌더를 제공하지 않습니다. 아쉽게도 URI는 여전히 java.net.URI 인스턴스로 지정됩니다.

새로운 HttpClient 동기식 코드 입니다.

HttpClient client = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.ALWAYS)
    .build();

System.out.println(client.version());

HttpRequest request = HttpRequest
    .newBuilder()
    .uri(new URI("http://www.oracle.com"))
    .GET()
    .build();

HttpResponse <String> response = client.send(request, HttpResponse.BodyHandler.asString());

System.out.println(String.format("Status code: %d", response.statusCode()));
System.out.println(String.format("Body length: %d", response.body().length()));

코드 분석

HttpClient client = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.ALWAYS)
    .build();

예제 코드는 새로운 HttpClient를 만드는 HttpClient.newBuilder 메서드를 포함한 여러 메서드 호출을 연결하여 인스턴스를 만듭니다. 예를 들어, HttpClient.Redirect.ALWAYS를 인수로 사용하는 followRedirects 메서드를 호출하면 클라이언트가 항상 리디렉션을 따르도록 지정합니다. 빌드 메소드에 대한 마지막 연결 호출은 빌더 코드를 가져옵니다.

System.out.println(client.version());

HttpClient 인스턴스가 빌드 된 후에 client.version() 메서드를 호출 한 결과를 인쇄합니다.

JShell은 기본 구성을 사용했기 때문에 HTTP_1_1을 표시합니다. 기본값은 HttpClient.Version.HTTP_1_1이며 클라이언트는 HTTP / 1.1에서만 작동합니다.

실제로 사용해보면 HTTP/2가 출력됩니다.

HttpRequest request = HttpRequest
    .newBuilder()
    .uri(new URI("http://www.oracle.com"))
    .GET()
    .build();

HTTP GET 요청을 만들 URI를 사용하여 새 URI 인스턴스를 만들고 uri에 저장합니다. 그런 다음 코드는 새로운 HttpRequest 빌더를 작성하는 여러 메소드 호출을 체인화하여 request라는 HttpRequest 인스턴스를 생성합니다.

HttpResponse <String> response = client.send(request, HttpResponse.BodyHandler.asString());

System.out.println(String.format("Status code: %d", response.statusCode()));
System.out.println(String.format("Body length: %d", response.body().length()));

동기적으로 실행되는 client.send 메서드를 호출하고 응답이 검색 될 때까지 실행을 차단합니다. 이 코드는 이전에 생성 된 HttpRequest 인스턴스, 요청 및 HttpResponse.BodyHandler.asString()을 인수로 전달합니다. 두 번째 인수는 사용할 응답 본문 처리기를 지정합니다. 본문 처리기는 응답 상태 코드와 응답 헤더를 가져와서 응답 본문을 String으로 저장하는 본문 프로세서를 반환합니다. HTTP GET 후에 요청이 성공적으로 처리되면, send 메소드는 HttpResponse <String>을 리턴합니다.

HttpClient와 HttpRequest가 비슷한 방법으로 만들어 짐을 알 수 있습니다. 최신 Java 코드에 익숙하다면 API를 매우 쉽게 사용할 수 있습니다. JShell에서 몇 줄의 코드만 사용하여 새로운 HTTP 클라이언트를 매우 기본적인 구성과 동기 API로 사용할 수있었습니다.


Working with Asynchronous Execution

HTTP/2 및 TLS로 작업 할 클라이언트의 사용법을 설명하기 전에 비동기 API에 대해 작업 합니다.

import java.util.concurrent.CompletableFuture;

비동기 실행을 위해 client에서 CompletableFuture를 반환하는 sendAsync메서드를 호출합니다. 다음 코드에서 response.whenComplete를 호출하여 응답에 의해 반환된 결과를 출력합니다.

HttpClient client = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.ALWAYS)
    .build();

HttpRequest request = HttpRequest
    .newBuilder()
    .uri(new URI("http://www.oracle.com"))
    .GET()
    .build();

CompletableFuture<HttpResponse<String>> response =
    client.sendAsync(request,
    HttpResponse.BodyHandler.asString());

response.whenComplete((HttpResponse<String> response, Throwable exception) -> {
    if (exception == null) {
        System.out.println(String.format("Status code: %d", response.statusCode()));
        System.out.println(String.format("Body length: %d", response.body().length()));
    } else {
        System.out.println(String.format("Something went wrong. %s",
            exception.getMessage()));
    }
});

JShell의 이러한 몇 줄의 코드만으로 새로운 HTTP 클라이언트를 매우 기본적인 구성과 비동기 API로 사용할 수있었습니다.


Working with HTTP/2 over TLS

HTTP 클라이언트는 표준 Java TLS 메커니즘을 사용하여 TLS (HTTP)를 통한 작업을 가능하게 합니다.

접속 사이트에서 사설 인증기관을 사용하고 서버와 서버 사이의 연결인 경우에 교신 상대의 사설 인증기관 인증서를 JAVA_HOME/jre/lib/security/cacerts로 임포트하거나, Connection 생성 부분에서 SSLContext에 인증서를 추가해야 합니다.

만약 사설 CA라면 클라이언트에서 생성한 SSL 소켓(SSLContext)의 TrustManager를 확인한 후, 등록이 되어있다면 통과합니다.

  1. 서버 인증서를 발급한 CA를 알 수 없음
  2. 서버 인증서가 CA에 의해 서명되지 않고 자체 서명됨
  3. 서버 구성에서 중간 CA가 누락됨

다음은 SSLContext를 포함한 HTTP Client 예제 입니다.

///////////////////////////////////////////////////
// Source Code
///////////////////////////////////////////////////

SSLContext context = SSLContext.getInstance("TLSv1.2");

// null, null, null을 입력하면 서버 인증 절차 없이 바로 정보를 받아옴.
// SSLContext.init() 함수의 두번째 인자에 X509TrustManager 를 선언하여 집어 넣었다.
// JRE 를 설치하면 기본적으로 [JRE 경로]/lib/security/cacerts 라는 파일명의 공인 인증된 인증서 저장소 파일이 있다.
// 해당 인증서 저장소를 이용하여 서버의 인증서를 검사하게 하였다.
context.init(null, new TrustManager[] { new X509TrustManager() {
    @Override
    public X509Certificate[] getAcceptedIssuers() { return null; }

    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) 
                                  throws CertificateException { }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) 
                                  throws CertificateException {
        try { // Get trust store
            KeyStore trustStore = KeyStore.getInstance("JKS");
            String cacertPath = System.getProperty("java.home") + "/lib/security/cacerts";
            trustStore.load(new FileInputStream(cacertPath), "changeit".toCharArray());

            // Get Trust Manager
            TrustManagerFactory tmf = TrustManagerFactory
                    .getInstance(TrustManagerFactory.getDefaultAlgorithm());
            tmf.init(trustStore);
            TrustManager[] tms = tmf.getTrustManagers();
            ((X509TrustManager) tms[0]).checkServerTrusted(chain, authType);
        } catch (KeyStoreException e) { e.printStackTrace();
        } catch (NoSuchAlgorithmException e) { e.printStackTrace();
        } catch (IOException e) { e.printStackTrace(); }
    }
}}, null);

System.out.println("Default Client SSL Context Protocol : "
                + context.getProtocol());

System.out.println("Supported SSL Protocols : ");
for (String Protocol : context.getSupportedSSLParameters().getProtocols())
    System.out.println(Protocol);

HttpClient client = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.ALWAYS)
    .sslContext(context)
    .version(HttpClient.Version.HTTP_2)
    .build();

HttpRequest request = HttpRequest
    .newBuilder()
    .uri(new URI("https://dev-gw.talk.naver.com/chatbot/v1/event"))
    .setHeader("Content-Type", "application/json")
    .setHeader("charset", "UTF-8")
    .setHeader("Authorization", "ct_wc8b1i_Pb1AXDQ0RZWuCccpzdNL")
    .POST(HttpRequest.BodyProcessor.fromString(
        "{ \"event\": \"send\", \"user\": \"al-2eGuGr5WQOnco1_V-FQ\", " +
        "\"textContent\": { \"text\": \"hello world\" } }"))
    .build();

CompletableFuture<HttpResponse<String>> response =
     client.sendAsync(request, HttpResponse.BodyHandler.asString());

response.whenComplete((HttpResponse<String> res, Throwable exception) -> {
    if (exception == null) {
        System.out.println("\n[response]");
        System.out.println("headers : ");

        for (String key : res.headers().map().keySet())
            System.out.println(String.format("   %s - %s", key, res.headers().map().get(key)));

        System.out.println("SSL : "     + res.sslParameters().getProtocols()[0]);
        System.out.println("version : " + res.version());
        System.out.println("body : "    + res.body());

        end = true;
    } else {
        System.out.println(String.format("Something went wrong. %s",
                exception.getMessage()));
    }
});

///////////////////////////////////////////////////
// Output
///////////////////////////////////////////////////

[SSLContext]
Default Client SSL Context Protocol : TLSv1.2
Supported SSL Protocols : 
    SSLv2Hello
    SSLv3
    TLSv1
    TLSv1.1
    TLSv1.2

[request]
method  : POST
version : HTTP_2
header  : Content-Type  : application/json 
        : charset       : UTF-8
        : Authorization : ct_wc8b1i_Pb1AXDQ0RZWuCccpzdNL
uri     : "https://dev-gw.talk.naver.com/chatbot/v1/event"
body    : "{ \"event\": \"send\", \"user\": \"al-2eGuGr5WQOnco1_V-FQ\", " +
          "\"textContent\": { \"text\": \"hello world\" } }"

[response]
headers : :status      - [400]
          content-type - [application/json;charset=UTF-8]
          date         - [Thu, 10 Aug 2017 04:52:30 GMT]
          server       - [nginx]
SSL     : TLSv1.2
version : HTTP_2
body    : {{"success":true,"resultCode":"00"}

javax.net.ssl.SSLContext 인스턴스를 만들고 초기화 한 다음 HttpClient 빌더에 연결된 sslContext 메소드에 인수로 전달해야합니다. (sslContext 메소드는 SSL 컨텍스트가 아닌 TLS 컨텍스트를 구성합니다. 예제: TLSv1.2)

다음 행은 호출한 후에 많은 메소드 호출을 체인화하여 HttpClient 인스턴스를 작성합니다. 이 경우 인수로 sslContext 메소드를 호출하면 TLSv1.2에서 HTTP/2로 작업 할 수 있습니다. REST API가 TLSv1.2에서 HTTP/2와 호환되지 않으면 HttpClient는 HTTP/1.1 프로토콜로 작동합니다.


예제는 JShell에서 HTTP/2 over TLS를 사용하여 새로운 인큐베이터 모듈 HTTP/2 클라이언트를 사용하는 방법을 보여주었습니다. 소프트웨어 개발 작업의 경우, 특히 JShell과 같은 대화식 REPL로 작업해야 하는 경우 새로운 HTTP/2 클라이언트를 매우 유용하게 사용할 수 있습니다. 그러나 단점으로 모듈이 인큐베이터로 포함되어 있기 때문에 차후에 변경 될 수 있습니다.

results matching ""

    No results matching ""