深入解析 HttpClient:ConnectionReuseStrategy 与 ConnectionKeepAliveStrategy
1. 基础介绍
1.1 Keep-Alive 解析
HTTP 协议作为应用层协议,构建于 TCP/IP 或 UDP 等传输层协议之上,通过 Socket 实现客户端与服务端的通信。HTTP/1.0 和 HTTP/1.1 是两个主要版本,它们都是无状态协议。
- HTTP/1.0: 每一次请求-响应周期结束后,TCP 连接会断开,下一次请求需要重新建立连接。
- HTTP/1.1: 引入了 Keep-Alive 机制,允许在一次 TCP 连接中持续发送多份数据而无需断开。
Keep-Alive 机制通过减少 TCP 连接的建立次数,降低了 TIME_WAIT 状态的连接数量,从而提高了服务器的性能和吞吐量(更少的 TCP 连接意味着更少的系统内核调用,如 accept() 和 close())。
连接管理
- HTTP/1.0 默认关闭 Keep-Alive。要启用它,需要在 HTTP 请求头中加入
Connection: Keep-Alive。服务器收到该请求后,也会在响应头中添加相同字段以确认使用 Keep-Alive。 - HTTP/1.1 默认启用 Keep-Alive。若要关闭,需明确在请求头中加入
Connection: close。
Keep-Alive 连接并非永久保持,它有一个可配置的保持时间。一个 HTTP 请求能否保持长连接,也取决于服务端是否支持并开启了 Keep-Alive 功能。
1.2 如何判断数据接收完整
在传统的 Socket 编程中,我们通常通过检查 read() 方法是否返回 EOF(-1)来判断数据流是否结束。
1
2
3
4
5
6
7
Socket socket = new Socket("localhost", 10086);
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String info = null;
while ((info = br.readLine()) != -1) {
// process data
}
这种方式在非 Keep-Alive 的 HTTP 请求中是有效的,因为服务器在响应后会关闭连接,客户端自然会收到 EOF。
然而,在 Keep-Alive 模式下,连接在一次请求后保持打开状态,因此客户端无法通过 EOF 来判断响应是否结束。为了解决这个问题,HTTP 协议引入了 Content-Length 和 Transfer-Encoding: chunked。
- HTTP/1.0:
Content-Length字段是可选的。 - HTTP/1.1:
- 如果启用了 Keep-Alive,则响应头中必须包含
Content-Length或Transfer-Encoding: chunked之一。 - 如果未启用 Keep-Alive,则
Content-Length行为与 HTTP/1.0 相同,为可选字段。
- 如果启用了 Keep-Alive,则响应头中必须包含
2. ConnectionReuseStrategy 解析
在 Apache HttpClient 的 DefaultRequestDirector.execute 方法中,有如下逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// The connection is in or can be brought to a re-usable state.
reuse = reuseStrategy.keepAlive(response, context);
if (reuse) {
// Set the idle duration of this connection
final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
if (this.log.isDebugEnabled()) {
final String s;
if (duration > 0) {
s = "for " + duration + " " + TimeUnit.MILLISECONDS;
} else {
s = "indefinitely";
}
this.log.debug("Connection can be kept alive " + s);
}
managedConn.setIdleDuration(duration, TimeUnit.MILLISECONDS);
}
这段代码表明,在一次 HTTP 请求完成后,连接不会立即关闭,而是通过一个可重用策略(ConnectionReuseStrategy)来判断连接是否可以保持以及可以保持多长时间。
reuseStrategy 的默认实现是 DefaultConnectionReuseStrategy。其核心方法 keepAlive 的逻辑如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Override
public boolean keepAlive(final HttpResponse response, final HttpContext context) {
// ... 参数校验 ...
// 1. 对于 HTTP 204 No Content 响应,如果包含 Content-Length > 0 或 Transfer-Encoding,则不重用连接。
// 这是为了防止行为异常的服务器在 204 响应中返回内容体,导致连接状态不同步。
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NO_CONTENT) {
Header clh = response.getFirstHeader(HTTP.CONTENT_LEN);
if (clh != null) {
// ... 检查 Content-Length 是否大于 0 ...
}
Header teh = response.getFirstHeader(HTTP.TRANSFER_ENCODING);
if (teh != null) {
return false;
}
}
// 2. 检查请求头中是否包含 "Connection: close",如果包含,则不重用。
HttpRequest request = (HttpRequest) context.getAttribute(HttpCoreContext.HTTP_REQUEST);
if (request != null) {
// ... 遍历 Connection 头 ...
if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {
return false;
}
}
// 3. 检查响应实体是否是自终止的。如果实体结束的标志是关闭连接,则无法保持 Keep-Alive。
// - Transfer-Encoding 存在但值不是 "chunked",则不重用。
// - 如果响应可以有实体,但 Content-Length 头不合法(不存在、多于一个或值为负),则不重用。
final ProtocolVersion ver = response.getStatusLine().getProtocolVersion();
final Header teh = response.getFirstHeader(HTTP.TRANSFER_ENCODING);
if (teh != null) {
if (!HTTP.CHUNK_CODING.equalsIgnoreCase(teh.getValue())) {
return false;
}
} else {
if (canResponseHaveBody(request, response)) {
// ... 校验 Content-Length ...
}
}
// 4. 检查响应头中的 "Connection" 或 "Proxy-Connection"。
HeaderIterator headerIterator = response.headerIterator(HTTP.CONN_DIRECTIVE);
if (!headerIterator.hasNext()) {
headerIterator = response.headerIterator("Proxy-Connection");
}
if (headerIterator.hasNext()) {
// ... 遍历头信息 ...
// 如果存在 "close",则返回 false。
// 如果存在 "keep-alive",则标记为 true。
}
// 5. 默认策略:HTTP/1.1 及以上版本默认为持久连接,HTTP/1.0 及以下版本默认为非持久连接。
return !ver.lessEquals(HttpVersion.HTTP_1_0);
}
Proxy-Connection 说明
Proxy-Connection 是 HTTP/1.0 时代的产物。当时一些老旧的代理服务器不完全支持 Connection: keep-alive。如果客户端直接发送此头,代理会原样转发给服务器,服务器会误认为要建立长连接,但代理本身不支持,导致问题。
为了解决这个问题,客户端转而发送 Proxy-Connection: keep-alive。
- 新代理:识别此头,并将其转换为
Connection: keep-alive后再转发给服务器,成功建立持久连接。 - 老代理:不识别此头,原样转发,服务器也不认识,因此不会建立持久连接,避免了错误。
总结
keepAlive 方法的核心判断逻辑:
- 返回
false(关闭连接) 的情况:- 响应状态码为
204但包含Content-Length > 0或Transfer-Encoding。 - 请求或响应头中明确包含
Connection: close。 - 响应体长度无法确定(例如,没有
Content-Length或Transfer-Encoding: chunked)。 - HTTP 协议版本低于
1.1且未明确指定Keep-Alive。
- 响应状态码为
- 返回
true(保持连接) 的情况:- 响应头中明确包含
Connection: keep-alive。 - HTTP/1.1 及以上版本,且没有明确的关闭指令。
- 响应头中明确包含
3. ConnectionKeepAliveStrategy 解析
当一个连接被 ConnectionReuseStrategy 判断为可重用后,它的存活时间由 ConnectionKeepAliveStrategy 决定。默认实现是 DefaultConnectionKeepAliveStrategy。
其核心方法 getKeepAliveDuration 用于获取连接的存活时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public long getKeepAliveDuration(final HttpResponse response, final HttpContext context) {
Args.notNull(response, "HTTP response");
final HeaderElementIterator it = new BasicHeaderElementIterator(
response.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
final HeaderElement he = it.nextElement();
final String param = he.getName();
final String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
try {
return Long.parseLong(value) * 1000;
} catch(final NumberFormatException ignore) {
}
}
}
return -1;
}
该方法解析响应头中的 Keep-Alive 字段。例如,如果响应头为 Keep-Alive: timeout=5, max=100,该方法会提取 timeout 的值 5,并将其转换为毫秒(5000ms)作为连接的空闲超时时间。
如果响应头中没有 Keep-Alive 字段或该字段中没有 timeout 参数,则此方法返回 -1,表示连接可以一直保持活动状态(除非被连接池的其他策略管理)。
