Features
- Http1/H2/H2cUpgrade
- Https
- Epoll/NIO
- Interceptor
- Filter
- Retry, Redirect, 100-expect-continue
- Segmented read/write
- Multipart
- Metrics
- more features…
This is the multi-page printable view of this section. Click here to print.
HttpClient
.Note:
netty
4.1.52.Final andtcnative
2.0.34.Final are directly dependent on.
Note: Please make sure the version of
tcnative
matches the version ofnetty
.
<dependency>
<groupId>io.esastack</groupId>
<artifactId>httpclient-core</artifactId>
<version>${esa-httpclient.version}</version>
</dependency>
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...
HttpRequest
处理过程中,有时可能需要执行Retry、Redirect、Cache等操作,使用Interceptor
可以实现类似功能。Filter
的区别与Filter
不同的是,在Interceptor
中
可以同时获取HttpRequest
及经过处理后的HttpResponse
,甚至可以替换原始的HttpRequest
及处理后的HttpResponse
。更多与Filter
的不同如下:
RequestFilter
在所有拦截器对HttpRequest
的处理之后才会执行,ResponseFilter
在响应头接收到之后(响应body处理前)立即执行,此时所有拦截器对HttpResponse
的处理均未执行。Interceptor
中可以获取HttpRequest
经过处理后的HttpResponse
,通过此种方式可以将请求和响应关联起来,而RequestFilter
和ResponseFilter
中均无法同时获取到请求和响应,因此
也不能将两者关联起来,但是同一个请求将在RequestFilter
和ResponseFilter
中共享一个FilterContext
实例,因此可以通过该对象传递上下文参数。HttpRequest
或HttpResponse
,而通过Filter
无法实现。鉴于Interceptor
和Filter
的区别,下述场景更适合使用Interceptor
:
HttpRequest
及HttpResponse
,如重试、重定向等(因为需要获取HttpRequest
处理结果后的HttpResponse
再决定下一步处理逻辑)。HttpRequest
处理过程中仅需要执行一次的逻辑,同时需要注意该类拦截器的优先级要高于重试、重定向等内置拦截器,否则发生重试、重定向时该拦类拦截器仍会被
多次执行。HttpRequest
或处理后的HttpResponse
。HttpClient
中存在一些内置Interceptor
,用于实现 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加载的机制传入,此时,对应的内置拦截器将自动失效。
HttpClient
支持通过builder配置和SPI加载两种方式配置Interceptor
。在构造HttpClient
时传入自定义的Interceptor
实例,如:
final HttpClient client = HttpClient.create().addInterceptor((request, next) -> {
System.out.println("Interceptor");
return next.proceed(request);
}).build();
HttpClient
支持通过SPI的方式加载Interceptor
接口的实现类,使用时只需要按照SPI的加载规则将自定义的Interceptor
放入指定的目录下即可。
使用时可以通过Interceptor
替换原始的HttpRequest
和HttpResponse
; 2.HttpClient
内置的重试、重定向、100-expect-continue协商等功能通过Interceptor
实现。
多个拦截器之间通过getOrder()
方法返回值区分执行顺序,值越小,优先级越高。
Filter
分为RequestFilter
和ResponseFilter
两种,前者主要用于处理HttpRequest
,在所有Interceptor
对HttpRequest
处理之后执行, 后者主要用于处理HttpResponse
,在响应headers到达之后立即执行,此时所有拦截器对HttpResponse
的处理均未开始执行。Interceptor
的区别RequestFilter
在所有拦截器对HttpRequest
的处理之后才会执行,ResponseFilter
在响应头接收到之后(响应body处理前)立即执行,此时所有拦截器对HttpResponse
的处理均未执行。RequestFilter
和ResponseFilter
中均无法同时获取到请求和响应,因此也不能将两者关联起来,但是同一个请求将在RequestFilter
和ResponseFilter
中共享一个FilterContext
实例,
因此可以通过该对象传递上下文参数。而Interceptor
中可以获取HttpRequest
经过处理后的HttpResponse
,因此可以通过此种方式将请求和响应关联起来。RequestFilter
或ResponseFilter
无法替换原始的HttpRequest
或HttpResponse
,而通过Interceptor
可以实现。鉴于Filter
和Interceptor
的区别,下述场景更适合使用Filter
:
HttpRequest
或HttpResponse
,如:对每个HttpRequest
或HttpResponse
添加固定的请求header。HttpRequest
处理过程中可能需要多次执行的逻辑,比如在发生重试、重定向时会发出多个网络请求,而这些请求均需要执行的逻辑。Filter
之间通过getOrder()
方法返回值区分执行顺序,值越小,优先级越高HttpRequest
可以通过共享一个FitlerContext
实例在多个RequestFilter
和ResponseFilter
实例间传递上下文参数HttpClient
支持通过builder配置和SPI加载两种方式配置Filter
。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();
HttpClient
支持通过SPI的方式加载Filter
,使用时,只需要按照Spi的加载规则将自定义的Filter
放入指定的目录下即可。
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
。
HttpClient
需要将整个响应body内容聚合后存放在内存中返回给业务处理,对于响应body内容较大的请求,此种方式可能会导致OOM。HttpClient
开放了底层的接口,支持用户自定义响应信息处理Handler,用于处理接收到的响应headers、body(分块的)、trailers等数据。通过这种方式,可以 灵活的处理响应数据,避免响应body堆积在内存中产生OOM的风险。HttpClient
提供了两种不同形式的用法来实现自定义Handle—-接口实现和流式写法,前者可以方便的共享对象全局属性,后者
使用方式更简洁,使用时可根据需要选择其一。具体使用方式如下:
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();
除了自定义上述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
来处理。
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
并在有可写数据时直接写入,最后结束请求。
HttpClient
支持文件上传和下载功能。需要说明地是,对于内容较小的文件,可通过直接将文件内容写入请求body中或者直接从响应body中读取。 本文只讨论当文件内容过大,直接读取或者写入有OOM风险时的大文件上传和下载功能。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内容上传的情况。
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。
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连接池未被使用时该连接池将被关闭。使用时,可以通过系统属性更新上述参数,具体为:
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();
}