最近,使用了 Java 11内置的java.net.http.HttpRequest请求外部服务,发现日志中出现了很多如下图的错误:
这篇文章,我们就来分析如何排查和解决这种错误,以及分析下HttpRequest的工作原理。
排查过程:
遇到这种问题,首先google搜索下关键字:java.io.IOException: HTTP/1.1 header parser received no bytes
总结下 Google查询的结果,可以得到两个主要原因:
- 服务器返回空响应,导致解析 response异常
- 网络问题
针对第一种情况,到下游服务查看日志发现请求根本没有进来,于是把原因定位到网络问题。经过多次的测试后发现,错误是有规律性的出现,多年工作经验的直觉告诉我,这种http请求,一定会复用连接,会不会复用了一个失效的链接,于是把问题再次缩小。
那么,JDK内置的HttpRequest链接存活的时间是多久呢?
对,找官方资料,如下链接和图片:
官方默认的keepalive是1200s,是不是太大了,于是调整了 keepalive的时间,修改参数的方式:
# 方法1. 启动指令中增加如下参数
-Djdk.httpclient.keepalive.timeout=10
# 方法2. 代码中配置如下参数
System.setProperty("jdk.httpclient.keepalive.timeout", "10s");
很奇怪,为什么JDK没有提供变量来设置这个参数,而是作为JVM 系统属性设置???不管怎样,经过一番验证之后,问题解决。
所以,如果有使用 JDK内置HttpRequest的小伙伴,一定要注意这个坑。
既然讲到了HttpRequest,不如顺道把它的工作原理也分析下。
一、 JDK 内置 HttpRequest 的实现原理
1. 基础架构
JDK 内置的 HTTP 客户端基于异步非阻塞 I/O(NIO)设计,采用了事件驱动的架构。这种设计使其能够高效地处理大量并发连接,同时保持较低的资源消耗。HttpClient 是核心类,负责创建和配置 HTTP 请求,而 HttpRequest 则用于定义具体的请求细节。
2. 异步与同步请求
HttpClient 支持同步和异步两种请求方式:
- 同步请求:调用 send 方法,线程会被阻塞直到服务器响应返回。这种方式适用于简单的请求场景,但在高并发环境下可能导致线程阻塞问题。
- 异步请求:调用 sendAsync 方法,返回一个 CompletableFuture 对象,允许在请求进行时执行其他操作,提升应用的响应性和吞吐量。
3. 支持的协议
内置 HTTP 客户端支持 HTTP/1.1 和 HTTP/2 协议。HTTP/2 的引入带来了多路复用、头部压缩和服务器推送等特性,显著提升了传输效率。客户端会根据服务器支持的协议自动选择最优协议,确保最佳的传输性能。
4. 连接管理
HttpClient 内部维护着连接池,自动管理 HTTP 连接的复用和关闭。通过连接池机制,可以避免频繁建立和关闭连接带来的性能损耗。连接池根据请求的目标主机和协议进行分类管理,确保高效的资源利用。
5. 安全与认证
内置客户端提供丰富的安全特性,包括 SSL/TLS 支持、证书验证和多种认证机制(如 Basic、Digest、Bearer 认证等)。开发者可以通过配置 SSLContext 和相关认证信息,确保请求的安全性。
6. 中间件与过滤器
HttpClient 允许开发者添加自定义的过滤器和拦截器,对请求和响应进行预处理和后处理。这为实现日志记录、请求重试、错误处理等功能提供了灵活的扩展点。
二、优缺点
1. 优点
- 简化的 API:相比于传统的 HttpURLConnection,HttpClient 提供了更现代化和简洁的 API,降低了使用难度和代码复杂度。
- 异步支持:内置的异步请求机制允许更高效地处理并发请求,提升了应用的性能和响应性。
- 协议支持:自动支持 HTTP/2,使得应用能够利用更高效的传输协议,无需额外配置。
- 内置安全特性:丰富的安全配置选项让开发者能够轻松地实现安全的网络通信,包括 SSL/TLS 和多种认证方式。
- 连接池管理:自动的连接池管理减少了资源管理的负担,提升了连接的复用性和整体性能。
- 跨平台一致性:作为 JDK 的一部分,HttpClient 在不同操作系统和环境下表现一致,减少了跨平台开发的难度。
2. 缺点
- 功能限制:虽然 HttpClient 覆盖了大多数常见的 HTTP 功能,但在某些高级用例下,可能缺乏第三方库(如 Apache HttpClient 或 OkHttp)提供的特定功能。
- 版本依赖:HttpClient 是从 Java 11 开始引入的,对于使用更早版本 JDK 的项目,需要依赖外部库来实现相似功能。
- 社区和生态:相比于成熟的第三方 HTTP 客户端,JDK 内置的 HttpClient 在社区支持和生态上仍有待发展,可能缺乏某些特定场景下的最佳实践和解决方案。
- 性能优化:尽管 HttpClient 已经具备良好的性能,但在极端高并发或特定优化需求下,可能无法完全满足专业级别的性能调优需求。
三、核心参数
在使用 HttpRequest 时,开发者需要配置多个参数以定义请求的行为和特性。以下是一些核心参数及其说明:
1. 请求 URI
每个 HTTP 请求都需要一个目标 URI,指定资源的位置。例如:
URI uri = URI.create("https://api.example.com/data");
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.build();
2. HTTP 方法
HttpRequest 支持常见的 HTTP 方法,如 GET、POST、PUT、DELETE 等。可以通过 method 方法或专门的快捷方法设置:
// 使用快捷方法设置 GET 请求
HttpRequest getRequest = HttpRequest.newBuilder()
.uri(uri)
.GET()
.build();
// 使用 method 方法设置 POST 请求
HttpRequest postRequest = HttpRequest.newBuilder()
.uri(uri)
.method("POST", HttpRequest.BodyPublishers.ofString("request body"))
.build();
3. 请求头
可以通过 headers 方法添加一个或多个请求头,或使用 header 方法逐个添加:
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token")
.GET()
.build();
4. 请求体
对于需要发送数据的请求(如 POST、PUT),需要配置请求体。HttpRequest.BodyPublisher 提供多种数据发布方式:
HttpRequest postRequest = HttpRequest.newBuilder()
.uri(uri)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"key\":\"value\"}"))
.build();
支持的 BodyPublisher 包括:
- ofString(String): 发送字符串数据
- ofFile(Path): 发送文件内容
- ofByteArray(byte[]): 发送字节数组
- noBody(): 无请求体(适用于 GET 请求)
5. 超时设置
可以为请求设置超时时间,防止请求长时间挂起:
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.timeout(Duration.ofSeconds(10))
.GET()
.build();
6. 重定向策略
通过 HttpClient 的构建器可以设置重定向的策略,如跟随重定向、禁止重定向等:
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
7. 优先级
可以为请求设置优先级,影响请求的调度顺序:
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.priority(10)
.GET()
.build();
优先级值越高,表示请求越重要。
8. 版本协议
可以指定使用的 HTTP 版本,如 HTTP/1.1 或 HTTP/2:
HttpRequest request = HttpRequest.newBuilder()
.uri(uri)
.version(HttpClient.Version.HTTP_2)
.GET()
.build();
9. 代理设置
HttpClient 支持通过代理服务器发送请求,可以在 HttpClient 构建器中配置:
HttpClient client = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress("proxy.example.com", 8080)))
.build();
10. 身份认证
通过 Authenticator 配置认证信息,以便客户端在需要时自动提供认证凭证:
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("user", "password".toCharArray());
}
};
HttpClient client = HttpClient.newBuilder()
.authenticator(authenticator)
.build();
四、示例分析
为了更好地理解 HttpRequest 的使用,这里提供一个简单的示例:发送一个 POST 请求,并异步处理响应。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
publicclass HttpClientExample {
public static void main(String[] args) {
// 创建 HttpClient 实例,配置超时和重定向策略
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
// 构建 POST 请求,设置 URI、请求头和请求体
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(5))
.POST(HttpRequest.BodyPublishers.ofString("{\"name\":\"John Doe\",\"age\":30}"))
.build();
// 发送异步请求,并处理响应
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(responseBody -> {
System.out.println("Response received:");
System.out.println(responseBody);
})
.exceptionally(e -> {
System.err.println("Request failed: " + e.getMessage());
returnnull;
});
// 防止主线程提前退出
try {
Thread.sleep(6000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
代码解析:
- HttpClient 创建:通过 HttpClient.newBuilder() 创建一个 HttpClient 实例,配置了连接超时和自动跟随标准重定向。
- HttpRequest 构建:定义了一个 POST 请求,目标 URI 为 https://api.example.com/data,设置了 Content-Type 请求头,并通过 BodyPublishers.ofString 发送 JSON 格式的请求体。
- 发送异步请求:调用 sendAsync 方法发送请求,指定响应体处理器为 ofString,即将响应体转换为字符串。
- 处理响应:使用 thenApply 和 thenAccept 链式调用处理响应体,打印到控制台。如果请求失败,通过 exceptionally 捕获并打印错误信息。
- 主线程等待:由于请求是异步发送的,主线程需要等待一段时间以确保响应能够处理。实际应用中,可以使用更优雅的方式管理线程同步。
五、总结
本文,我们从使用 JDK内置的HttpRequest遇到的坑以及如何解决它,到工作原理的分析,HttpRequest为 Java 开发者提供了一个强大且易用的 HTTP 客户端工具。但是,相比于一些成熟的第三方库(比如 Apache HttpClient)还是稍显不足。
因此,在使用一个工具或者框架时,最好能先了解其实现原理、优缺点等,可以做到提前避免出现上面类似的问题,或者出现问题时能快速定位和解决问题。