前言
实践是检验真理的唯一标准,在学习了网络连接的基本知识后,结合具体的实例可以加深网络的理解。我们从apache的httpclient框架开始。
连接池与连接对象
httpclient内容庞杂,涉及请求的处理与响应、连接池与复用、配置体系与一些高级特性等等。本文主要讲解连接池与复用相关内容。
整体架构
上图揭示了框架连接池的基本架构,应用程序通过配置定义连接池管理对象。连接池管理对象中定义了连接池、连接工厂、策略等对象。框架通过对外暴露的对象httpClient与应用程序交互,通过httpClient执行请求、处理响应等。
创建连接池对象
上述时序图以第一次创建连接对象为例,大体介绍了连接对象的创建过程,其核心流程主要为:创建路由池、创建连接对象BHttpConnectionBase、包装对象为CPoolEntry、将CPoolEntry对象塞到连接池和路由池的租用列表。上述流程省略从已有路由池中获取已有连接对象的流程,如您感兴趣可自行参阅源码。
- 连接池
连接池对象cPool中维护了leased和available两个集合保存租借和可用的连接对象 - 路由池
在连接池对象cPool中维护了一个key为域名+端口,value为RouteSpecificPool对象的Map。RouteSpecificPool对象维护了leased和available两个集合保存租借和可用的连接对象;在获取连接对象时,实际是根据请求中的域名+端口找到对应的RouteSpecificPool对象并从该对象的avaliable集合中获取连接对象。
请求与响应
建立路由
上述时序图展示了connection对象与socket绑定的过程和socket对象连接到目标服务器的过程。此处,socket对象的KeepAlive属性被设置为false,这表示本条TCP连接将不会启用保活机制。这个属性在本质上支撑了http keep-alive的可行性,即在同一个tcp连接内发送多次http请求,直到一方显式关闭。httpClient框架通过PoolEntry的expiry、update等属性和请求连接对象过程中的判断条件来保证连接对象的可用和对不可用对象的清理.如您感兴趣可自行参阅源码,本文不在赘述。
发送请求
HttpRequestExecutor.doSendRequest() 方法是 Apache HttpClient 4.x 中核心的请求发送逻辑,负责处理 HTTP 请求的发送和 Expect-Continue 握手流程。
java
Args.notNull(request, "HTTP request"); // 校验非空
Args.notNull(conn, "Client connection");
Args.notNull(context, "HTTP context");
context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn); // 存储连接引用
context.setAttribute(HttpCoreContext.HTTP_REQ_SENT, Boolean.FALSE); // 标记请求未发送
conn.sendRequestHeader(request); // 发送请求行和头部
// 处理带请求体的请求(HttpEntityEnclosingRequest)
if (request instanceof HttpEntityEnclosingRequest) {
boolean sendentity = true; // 默认发送请求体
// 检查是否启用 Expect-Continue 机制(HTTP 1.1+)
if (((HttpEntityEnclosingRequest) request).expectContinue()
&& !ver.lessEquals(HttpVersion.HTTP_1_0)) {
conn.flush(); // 强制刷新缓冲区,确保头部已发送
// 等待服务器返回 100 Continue(有超时机制)
if (conn.isResponseAvailable(this.waitForContinue)) {
response = conn.receiveResponseHeader(); // 接收响应头
// 处理可能的响应体(如错误响应)
if (canResponseHaveBody(request, response)) {
conn.receiveResponseEntity(response);
}
int status = response.getStatusLine().getStatusCode();
if (status < 200) {
if (status != HttpStatus.SC_CONTINUE) { // 非100 Continue
throw new ProtocolException("Unexpected response");
}
response = null; // 忽略100 Continue
} else {
sendentity = false; // 服务器拒绝,不发送请求体(如403 Forbidden)
}
}
}
// 根据判断结果发送请求体
if (sendentity) {
conn.sendRequestEntity((HttpEntityEnclosingRequest) request);
}
}
conn.flush(); // 确保所有数据发送完毕
context.setAttribute(HttpCoreContext.HTTP_REQ_SENT, Boolean.TRUE); // 标记请求已发送
return response; // 返回可能的早期响应(如4xx/5xx)
接受响应
HttpRequestExecutor.doReceiveResponse() 方法是 Apache HttpClient 4.x 中处理 HTTP 响应接收的核心逻辑,专门设计用于处理多段响应(如 1xx 信息性响应)并返回最终的有效响应。
java
Args.notNull(request, "HTTP request"); // 校验非空
Args.notNull(conn, "Client connection");
Args.notNull(context, "HTTP context");
HttpResponse response = null;
int statusCode = 0; // 用于跟踪响应状态码
while (response == null || statusCode < HttpStatus.SC_OK) { // SC_OK = 200
// 接收响应头
response = conn.receiveResponseHeader();
// 检查是否需要接收响应体(根据请求方法和状态码)
if (canResponseHaveBody(request, response)) {
conn.receiveResponseEntity(response); // 接收响应体
}
statusCode = response.getStatusLine().getStatusCode();
}
return response; // 返回第一个状态码≥200的响应
重用与保活
框架中定义了重用策略reuse和保活策略KeepAlive
- 重用策略
DefaultConnectionReuseStrategy.keepAlive() 是 Apache HttpClient 4.x 中决定 HTTP 连接是否可复用(Keep-Alive) 的核心方法。它的作用是 根据 HTTP 协议规范 和 服务器返回的响应头,判断当前连接是否可以保持活跃并用于后续请求。
核心逻辑分解
1. 检查响应体是否自终止(Self-Terminating)
kotlin
// 检查 Transfer-Encoding
final Header teh = response.getFirstHeader(HTTP.TRANSFER_ENCODING);
if (teh != null) {
// 若非分块传输(chunked),则不可复用
if (!HTTP.CHUNK_CODING.equalsIgnoreCase(teh.getValue())) {
return false;
}
} else {
// 检查 Content-Length(仅当响应可能有 body 时)
if (canResponseHaveBody(response)) {
final Header[] clhs = response.getHeaders(HTTP.CONTENT_LEN);
if (clhs.length == 1) {
try {
int contentLen = Integer.parseInt(clhs[0].getValue());
if (contentLen < 0) { // 非法长度
return false;
}
} catch (NumberFormatException ex) {
return false; // 格式错误
}
} else {
return false; // 无或重复 Content-Length
}
}
}
关键点:
- 如果响应使用 非分块传输 且 无法确定数据边界 (如缺少
Content-Length
),则关闭连接。 canResponseHaveBody()
排除204 No Content
等不应有响应体的状态码。
2. 检查 Connection
或 Proxy-Connection
头
ini
HeaderIterator hit = response.headerIterator(HTTP.CONN_DIRECTIVE);
if (!hit.hasNext()) {
hit = response.headerIterator("Proxy-Connection"); // 回退到代理头
}
if (hit.hasNext()) {
try {
TokenIterator ti = createTokenIterator(hit);
boolean keepalive = false;
while (ti.hasNext()) {
String token = ti.nextToken();
if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {
return false; // 显式关闭
} else if (HTTP.CONN_KEEP_ALIVE.equalsIgnoreCase(token)) {
keepalive = true; // 标记可能保持
}
}
if (keepalive) {
return true; // 显式要求保持
}
} catch (ParseException px) {
return false; // 头解析失败
}
}
优先级规则:
Connection: close
>Connection: keep-alive
(即使同时存在,close
会强制关闭)- 若
Connection
头不存在,检查Proxy-Connection
(非标准但常见)
3. 默认策略(无明确指令时)
kotlin
return !ver.lessEquals(HttpVersion.HTTP_1_0);
- HTTP/1.1+ :默认保持(
true
) - HTTP/1.0 :默认关闭(
false
)
扩展点
-
自定义 Token 解析
可覆盖
createTokenIterator()
方法修改Connection
头的解析逻辑:typescript@Override protected TokenIterator createTokenIterator(HeaderIterator hit) { return new CustomTokenIterator(hit); // 自定义实现 }
-
调整 Body 判断逻辑
修改
canResponseHaveBody()
可扩展对特殊状态码的支持。
- 保活策略
DefaultConnectionKeepAliveStrategy.getKeepAliveDuration是 Apache HttpClient 4.x 中决定 HTTP 连接保活状态的核心方法。它的作用是根据服务器返回的响应头,判断当前连接是否可以保持活跃的时间和请求的次数。
恢复连接
ConnectionHolder对象包装了HttpClientConnection对象和HttpClientConnectionManager对象,在设置重用和保活属性时,ConnectionHolder对象保存这些属性。
恢复连接
上述时序图展示了连接对象被设置为可重用与保活后,如何重新进入线程池的逻辑。
创作不易,转载请说明出处