This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

ESA HttpClient

ESA HttpClient is an asynchronous event-driven http client based on netty.

Features

  • Http1/H2/H2cUpgrade
  • Https
  • Epoll/NIO
  • Interceptor
  • Filter
  • Retry, Redirect, 100-expect-continue
  • Segmented read/write
  • Multipart
  • Metrics
  • more features…

1 - Getting Started

It’s so easy to get start with HttpClient.

Step 1: Add dependency

Note:netty 4.1.52.Final and tcnative 2.0.34.Final are directly dependent on.

Note: Please make sure the version of tcnative matches the version of netty.

<dependency>
    <groupId>io.esastack</groupId>
    <artifactId>httpclient-core</artifactId>
    <version>${esa-httpclient.version}</version>
</dependency>

Step 2: Send a request and handle response

Note:执行请求得到的CompletionStage<HttpResponse>直接由IO线程执行,请勿在该线程内做其他耗时操作,以免阻塞IO线程

final HttpClient client = HttpClient.ofDefault();

final HttpResponse response = client.post("http://127.0.0.1:8081/").body("Hello Server".getBytes()).execute().get();
// handle response here...

2 - Interceptor

HttpRequest处理过程中,有时可能需要执行Retry、Redirect、Cache等操作,使用Interceptor可以实现类似功能。

Filter的区别

Filter不同的是,在Interceptor中 可以同时获取HttpRequest及经过处理后的HttpResponse,甚至可以替换原始的HttpRequest及处理后的HttpResponse。更多与Filter的不同如下:

  1. 执行时机不同:RequestFilter在所有拦截器对HttpRequest的处理之后才会执行,ResponseFilter在响应头接收到之后(响应body处理前)立即执行,此时所有拦截器对HttpResponse的处理均未执行。
  2. 关联性不同:Interceptor中可以获取HttpRequest经过处理后的HttpResponse,通过此种方式可以将请求和响应关联起来,而RequestFilterResponseFilter中均无法同时获取到请求和响应,因此 也不能将两者关联起来,但是同一个请求将在RequestFilterResponseFilter中共享一个FilterContext实例,因此可以通过该对象传递上下文参数。
  3. 通过拦截器可以替换HttpRequestHttpResponse,而通过Filter无法实现。

鉴于InterceptorFilter的区别,下述场景更适合使用Interceptor

  1. 需要同时处理HttpRequestHttpResponse,如重试、重定向等(因为需要获取HttpRequest处理结果后的HttpResponse再决定下一步处理逻辑)。
  2. HttpRequest处理过程中仅需要执行一次的逻辑,同时需要注意该类拦截器的优先级要高于重试、重定向等内置拦截器,否则发生重试、重定向时该拦类拦截器仍会被 多次执行。
  3. 需要替换原始的HttpRequest或处理后的HttpResponse

2.1 - 内置Interceptor

HttpClient中存在一些内置Interceptor,用于实现 100-expect-continue、重试、重定向 等功能。

100-expect-continue

HttpClient内置了对100(“expect-continue”)响应码的支持,使用时可以设置Client或者Request级别的useExpectContinue参数为false来禁用该功能。

重试

HttpClient使用内置的RetryInterceptor实现重试功能。默认情况下,会对所有抛出连接异常的请求进行重试,其中:最大重试次数为3(不包括原始请求),重试间隔时间为0。使用时,可以通过自定义RetryOptions参数更改重试次数、重试条件、重试间隔时间等。

重定向

默认情况下,HttpClient会对响应状态码为301,302,303,307,308的请求重定向,其中:最大重定向次数为5(不包含原始请求)。使用时,可以通过maxRedirects更新重定向次数或者禁用(maxRedirects=0)重定向功能。

覆盖内置拦截器

当内置拦截器的功能不能满足用户需求时,可重写对应的内置拦截器的相关方法并通过Builder配置或者Spi加载的机制传入,此时,对应的内置拦截器将自动失效。

2.2 - 使用方式

HttpClient支持通过builder配置和SPI加载两种方式配置Interceptor

Builder配置

在构造HttpClient时传入自定义的Interceptor实例,如:

final HttpClient client = HttpClient.create().addInterceptor((request, next) -> {
    System.out.println("Interceptor");
    return next.proceed(request);
}).build();

SPI

HttpClient支持通过SPI的方式加载Interceptor接口的实现类,使用时只需要按照SPI的加载规则将自定义的Interceptor放入指定的目录下即可。

3 - Filter

Filter分为RequestFilterResponseFilter两种,前者主要用于处理HttpRequest,在所有InterceptorHttpRequest处理之后执行, 后者主要用于处理HttpResponse,在响应headers到达之后立即执行,此时所有拦截器对HttpResponse的处理均未开始执行。

Interceptor的区别

  1. 执行顺序不同:RequestFilter在所有拦截器对HttpRequest的处理之后才会执行,ResponseFilter在响应头接收到之后(响应body处理前)立即执行,此时所有拦截器对HttpResponse的处理均未执行。
  2. 关联性不同:RequestFilterResponseFilter中均无法同时获取到请求和响应,因此也不能将两者关联起来,但是同一个请求将在RequestFilterResponseFilter中共享一个FilterContext实例, 因此可以通过该对象传递上下文参数。而Interceptor中可以获取HttpRequest经过处理后的HttpResponse,因此可以通过此种方式将请求和响应关联起来。
  3. 通过RequestFilterResponseFilter无法替换原始的HttpRequestHttpResponse,而通过Interceptor可以实现。

鉴于FilterInterceptor的区别,下述场景更适合使用Filter

  1. 只需要单独处理HttpRequestHttpResponse,如:对每个HttpRequestHttpResponse添加固定的请求header。
  2. HttpRequest处理过程中可能需要多次执行的逻辑,比如在发生重试、重定向时会发出多个网络请求,而这些请求均需要执行的逻辑。

3.1 - 使用方式

HttpClient支持通过builder配置和SPI加载两种方式配置Filter

Builder配置

final HttpClient client = HttpClient.create().addRequestFilter((request, ctx) -> {  // 仅处理Request
    System.out.println("Request Filter");
    return CompletableFuture.completedFuture(null);
}).addResponseFilter((request, response, ctx) -> {   // 仅处理Response
    System.out.println("Response Filter");
    return CompletableFuture.completedFuture(null);
}).addFilter(new DuplexFilter() {   // 同时处理Request和Response
    @Override
    public CompletableFuture<Void> doFilter(HttpRequest request, FilterContext ctx) {
        System.out.println("Request Filter(Duplex)");
        return CompletableFuture.completedFuture(null);
    }

    @Override
    public CompletableFuture<Void> doFilter(HttpRequest request, HttpResponse response, FilterContext ctx) {
        System.out.println("Response Filter(Duplex)");
        return CompletableFuture.completedFuture(null);
    }
}).build();

SPI

HttpClient支持通过SPI的方式加载Filter,使用时,只需要按照Spi的加载规则将自定义的Filter放入指定的目录下即可。

4 - DNS扩展

在每次建立连接前,client可能需要将域名解析成IP地址,HttpClient通过适配netty原生的AddressResolverGroup提供了一种更加简单、 灵活的NameResolver扩展,用于将url地址中的域名解析成IP地址。

使用方式

final HttpClient client = HttpClient.create().resolver(new HostResolver() {
    @Override
    public CompletableFuture<InetAddress> resolve(String inetHost) {
        // resolve inetHost
        return null;
    }
}).build();

在构造HttpClient时传入自定义的HostResolver,后续建立连接时会调用resolve()方法进行Host地址解析。默认情况下,将使用系统默认的命名服务进行Host解析,详情请查看SystemDefaultResolver

5 - 自定义响应处理

默认情况下,HttpClient需要将整个响应body内容聚合后存放在内存中返回给业务处理,对于响应body内容较大的请求,此种方式可能会导致OOM。HttpClient 开放了底层的接口,支持用户自定义响应信息处理Handler,用于处理接收到的响应headers、body(分块的)、trailers等数据。通过这种方式,可以 灵活的处理响应数据,避免响应body堆积在内存中产生OOM的风险。

使用方式

HttpClient提供了两种不同形式的用法来实现自定义Handle—-接口实现和流式写法,前者可以方便的共享对象全局属性,后者 使用方式更简洁,使用时可根据需要选择其一。具体使用方式如下:

自定义Handler

public class FileHandler extends Handler {
    private static final String PATH = "xxxx";

    private RandomAccessFile file;

    @Override
    public void onStart() {
        String fileName = response().headers().get("fileName");
        try {
            file = new RandomAccessFile(new File(PATH, fileName), "rw");
        } catch (FileNotFoundException e) {
            // Handle execption
        }
    }

    @Override
    public void onData(Buffer content) {
        if (file != null) {
            byte[] data = new byte[content.readableBytes()];
            content.readBytes(data);
            try {
                file.write(data);
            } catch (IOException e) {
                // Handle exception
            }
        } else {
            throw new IllegalStateException("file is null");
        }
    }

    @Override
    public void onEnd() {
        IOUtils.closeQuietly(file);
    }

    @Override
    public void onError(Throwable cause) {
        IOUtils.closeQuietly(file);
    }
}

如上所示,自定义一个用于文件下载的Handler,并在构建HttpClient时传入该FileHandler。如下:

final HttpClient client = HttpClient.create()
        .readTimeout(5000).build();
final HttpRequestFacade request = client.get("http://127.0.0.1:8080/abc").handler(new FileHandler());

final CompletableFuture<HttpResponse> response =  request.execute();

response.whenComplete((rsp, th) -> System.out.println(rsp.status()));

// Wait until complete
System.in.read();

自定义Handle

除了自定义上述Handler之外,HttpClient提供了一种更优雅的流式写法来处理响应数据,使用示例如下:

final HttpClient client = HttpClient.create().readTimeout(5000).build();

CompletableFuture<HttpResponse> response =  client.get("http://127.0.0.1:8080/abc")
        .handle((Handle h) -> {
            h.onData((Buffer buf) -> {
                System.out.println("Received response data: " + buf.readableBytes());
            })
             .onTrailer((HttpHeaders trailers) -> h.trailers().add(trailers))
             .onEnd((Void v) -> {
                 System.out.println("Response end");
             })
             .onError((Throwable t) -> System.out.println("Unexpected error: " + t.getMessage()));
        }).execute();

response.whenComplete((rsp, th) -> System.out.println(rsp.status()));

// Wait until complete
System.in.read();

如上所示,当接收到请求数据时会调用用户自定义的Handle来处理。

6 - 分块读写

HttpClient支持分块写请求数据及分块处理响应数据,分块读功能请参考自定义响应处理,此处不再赘述。本文仅介绍分块写请求body相关功能。

使用方式

final HttpClient client = HttpClient.ofDefault();

final SegmentRequest request = client.post("http://127.0.0.1:8080/").segment();

for (int i = 0; i < 100; i++) {
    // request.isWritable()判断适用于较大内容的分块写
    if (request.isWritable()) {
        request.write("It's body".getBytes());
    } else {
        throw new IllegalStateException("Channel is unwritable");
    }
}

HttpResponse response = request.end("It's end".getBytes()).get();
System.out.println(response.status());
System.out.println(response.body().string(StandardCharsets.UTF_8));

如上所示,使用时通过HttpClient构造一个可分块写的SegmentRequest并在有可写数据时直接写入,最后结束请求。

7 - 文件上传及下载

HttpClient支持文件上传和下载功能。需要说明地是,对于内容较小的文件,可通过直接将文件内容写入请求body中或者直接从响应body中读取。 本文只讨论当文件内容过大,直接读取或者写入有OOM风险时的大文件上传和下载功能。

大文件上传

不使用Multipart编码

final HttpClient client = HttpClient.ofDefault();
HttpResponse response = client.post("http://127.0.0.1:8080/abc")
        .body(new File("xxxxx"))
        .execute()
        .get();
System.out.println(response.status());

如上所示,HttpClient将分块读取文件内容并将其写入请求body中,对应请求的Content-Type为application/octet-stream。该情形适用于单个大文件内容作为原始body内容上传的情况。

使用Multipart编码

final HttpClient client = HttpClient.ofDefault();

File file = new File("xxxxx");
final MultipartRequest request = client.post("http://127.0.0.1:9997/file/upload")
        .multipart()
        .file("file", file)
        .attr("name", "Bob")
        .attr("address", "China");

HttpResponse response = request.execute().get();
System.out.println(response.status());
System.out.println(response.body().string(StandardCharsets.UTF_8));

如上所示,HttpClient将添加的文件和表单参数进行Multipart Encode的结果作为请求的body内容,对应的Content-Type为multipart/form-data。 该情形适用于需要进行multipart encode或者存在表单参数的情形。特别地,如果只上传表单参数,不存在文件时,可以设置multipart值为false,后续上传时请求的Content-Type将设置为application/x-www-form-urlencoded。

8 - 连接池配置

连接的创建和销毁通常比较消耗资源,为了提升高并发下场景下的通信效率,HttpClient会自动使用连接池来管理与服务端的长连接

配置

默认情况下,单个域名的连接池配置如下:

Parameter Description Default
connectionPoolSize 连接池最大值 512
connectionPoolWaitingQueueLength 等待获取连接队列大小 256
connectTimeout 连接超时时间 3000(ms)
readTimeout 读超时时间 6000(ms)

全局配置

在buildHttpClient实例时,可以直接设置全局连接池参数,通过该方式设置的参数对构造出的client实例全局生效。具体使用方式如下:

final HttpClient client = HttpClient.create()
        .connectionPoolSize(512)
        .connectionPoolWaitingQueueLength(256)
        .readTimeout(6000)
        .connectTimeout(3000)
        .build();

域名级别配置

HttpClient支持对不同域名设置不同的连接池参数,如果需要使用该功能,只需要在构造HttpClient实例时传入ChannelPoolOptionsProvider即可。示例如下:

final HttpClient client = HttpClient.create().channelPoolOptionsProvider(new ChannelPoolOptionsProvider() {
    @Override
    public ChannelPoolOptions get(SocketAddress key) {
        // customize options
	return null;
    }
}).build();

如上所示,HttpClient将请求url中的地址解析成SocketAddress,然后以该地址为key获取对应的连接池参数,如果结果不为null则以获取到的 值为准,否则将使用连接池全局配置。

连接池缓存

连接的保持同样需要消耗一定的系统资源,因此及时关闭一些不再需要的连接池是必要的。HttpClient默认连接池缓存参数如下:

Parameter Description Default
initialCapacity 缓存池初始化大小 16
maximumSize 缓存池最大值 512
expireSeconds 访问过期时间 600(s)

如上参数表示:连接池初始容量为16,最大容量为512,当连续10min连接池未被使用时该连接池将被关闭。使用时,可以通过系统属性更新上述参数,具体为:

  • 通过name为"io.esastack.httpclient.caching-connectionPools.initialCapacity"的系统属性设置连接池初始化大小
  • 通过name为"io.esastack.httpclient.caching-connectionPools.maximumSize"的系统属性设置连接池最大值
  • 通过name为"io.esastack.httpclient.caching-connectionPools.expireAfterAccess"的系统属性设置访问过期时间

9 - 指标统计

HttpClient提供了IO线程池及连接池的Metric指标统计,使用时通过HttpClient实例便可直接获取。

使用方式

具体使用如下:

final HttpClient client = HttpClient.ofDefault();

ConnectionPoolMetricProvider connectionPoolMetricProvider = client.connectionPoolMetric();
ConnectionPoolMetric connectionPoolMetric = connectionPoolMetricProvider.get(InetSocketAddress.createUnresolved("127.0.0.1", 8080));

// 连接池配置
connectionPoolMetric.options();

// 等待获取连接的请求个数
connectionPoolMetric.pendingAcquireCount();

// 活跃连接个数
connectionPoolMetric.active();

// 等待获取连接队列最大值
connectionPoolMetric.maxPendingAcquires();

// 连接池最大值
connectionPoolMetric.maxSize();

IoThreadGroupMetric ioThreadGroupMetric = client.ioThreadsMetric();

for (IoThreadMetric ioThreadMetric : ioThreadGroupMetric.childExecutors()) {
    // 任务队列大小
    ioThreadMetric.pendingTasks();

    // 任务队列最大值
    ioThreadMetric.maxPendingTasks();

    // 线程状态
    ioThreadMetric.state();

    // 线程名称
    ioThreadMetric.name();
}