实践:apache httpclient中网络连接的创建与获取

前言

实践是检验真理的唯一标准,在学习了网络连接的基本知识后,结合具体的实例可以加深网络的理解。我们从apache的httpclient框架开始。

连接池与连接对象

httpclient内容庞杂,涉及请求的处理与响应、连接池与复用、配置体系与一些高级特性等等。本文主要讲解连接池与复用相关内容。

整体架构

上图揭示了框架连接池的基本架构,应用程序通过配置定义连接池管理对象。连接池管理对象中定义了连接池、连接工厂、策略等对象。框架通过对外暴露的对象httpClient与应用程序交互,通过httpClient执行请求、处理响应等。

创建连接池对象
sequenceDiagram MainClientExec -) PoolingHttpClientConnectionManager:requestConnection(HttpRoute route) PoolingHttpClientConnectionManager -) AbstractConnPool:lease(route) AbstractConnPool -) AbstractConnPool:new PoolEntryFuture(){getPoolEntry(){getPoolEntryBlocking()}} PoolingHttpClientConnectionManager --) MainClientExec:闭包:new ConnectionRequest(){get(){leaseConnection()}} MainClientExec -) ConnectionRequest:get() ConnectionRequest --) PoolingHttpClientConnectionManager:对象引用:leaseConnection(future) PoolingHttpClientConnectionManager -) PoolEntryFuture:get() PoolEntryFuture -) PoolEntryFuture:getPoolEntry() PoolEntryFuture -) AbstractConnPool:对象应用:getPoolEntryBlocking() AbstractConnPool -) AbstractConnPool:getPool(route){new RouteSpecificPool{createEntry(Connection)}} AbstractConnPool -) InternalConnectionFactory:create(route) InternalConnectionFactory -) InternalConnectionFactory:new BHttpConnectionBase(){newsocketHolder} InternalConnectionFactory --) AbstractConnPool:return connection AbstractConnPool -) RouteSpecificPool:add() RouteSpecificPool -) RouteSpecificPool:抽象方法实现createEntry(connection) RouteSpecificPool -) CPool:new CPoolEntry{new PoolEntry(connection、update、expiry、validityDeadline)} RouteSpecificPool -) RouteSpecificPool:塞入本路由池的租用列表 AbstractConnPool -) AbstractConnPool:塞入连接池租用列表 RouteSpecificPool --) MainClientExec: managedConn

上述时序图以第一次创建连接对象为例,大体介绍了连接对象的创建过程,其核心流程主要为:创建路由池、创建连接对象BHttpConnectionBase、包装对象为CPoolEntry、将CPoolEntry对象塞到连接池和路由池的租用列表。上述流程省略从已有路由池中获取已有连接对象的流程,如您感兴趣可自行参阅源码。

  • 连接池
    连接池对象cPool中维护了leased和available两个集合保存租借和可用的连接对象
  • 路由池
    在连接池对象cPool中维护了一个key为域名+端口,value为RouteSpecificPool对象的Map。RouteSpecificPool对象维护了leased和available两个集合保存租借和可用的连接对象;在获取连接对象时,实际是根据请求中的域名+端口找到对应的RouteSpecificPool对象并从该对象的avaliable集合中获取连接对象。

请求与响应

建立路由
sequenceDiagram MainClientExec->>MainClientExec: establishRoute MainClientExec-->>PoolingHttpClientConnectionManager: connect() PoolingHttpClientConnectionManager -) PoolEntry:getConnection() PoolEntry --) PoolingHttpClientConnectionManager:conneciont PoolingHttpClientConnectionManager -) DefaultHttpClientConnectionOperator:connect() DefaultHttpClientConnectionOperator -) DefaultHttpClientConnectionOperator:dnsResolver解析域名 DefaultHttpClientConnectionOperator -) DefaultHttpClientConnectionOperator:解析端口 DefaultHttpClientConnectionOperator -) DefaultHttpClientConnectionOperator:new Socket(soKeepAlive=false) DefaultHttpClientConnectionOperator -) PlainConnectionSocketFactory: socket.connect() DefaultHttpClientConnectionOperator -) BHttpConnectionBase:绑定:socketHolder.set(socket);

上述时序图展示了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. 检查 ConnectionProxy-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;  // 头解析失败
    }
}

优先级规则

  1. Connection: close > Connection: keep-alive
    (即使同时存在,close 会强制关闭)
  2. Connection 头不存在,检查 Proxy-Connection(非标准但常见)
3. 默认策略(无明确指令时)
kotlin 复制代码
return !ver.lessEquals(HttpVersion.HTTP_1_0);
  • HTTP/1.1+ :默认保持(true
  • HTTP/1.0 :默认关闭(false

扩展点
  1. 自定义 Token 解析

    可覆盖 createTokenIterator() 方法修改 Connection 头的解析逻辑:

    typescript 复制代码
    @Override
    protected TokenIterator createTokenIterator(HeaderIterator hit) {
        return new CustomTokenIterator(hit);  // 自定义实现
    }
  2. 调整 Body 判断逻辑

    修改 canResponseHaveBody() 可扩展对特殊状态码的支持。

  • 保活策略
    DefaultConnectionKeepAliveStrategy.getKeepAliveDuration是 Apache HttpClient 4.x 中决定 HTTP 连接保活状态的核心方法。它的作用是根据服务器返回的响应头,判断当前连接是否可以保持活跃的时间和请求的次数。

恢复连接

ConnectionHolder对象包装了HttpClientConnection对象和HttpClientConnectionManager对象,在设置重用和保活属性时,ConnectionHolder对象保存这些属性。

恢复连接
sequenceDiagram IOUtils->>ConnectionHolder: releaseConnection() ConnectionHolder->>PoolingHttpClientConnectionManager: releaseConnection() PoolingHttpClientConnectionManager -) PoolEntry: 更新update和expiry属性 PoolingHttpClientConnectionManager -) AbstractConnPool:release() AbstractConnPool -) RouteSpecificPool:塞进avaliable集合,从lease集合移除 AbstractConnPool -) AbstractConnPool:塞进avaliable集合,从lease集合移除 AbstractConnPool -) AbstractConnPool:唤醒等待线程

上述时序图展示了连接对象被设置为可重用与保活后,如何重新进入线程池的逻辑。


创作不易,转载请说明出处

相关推荐
chirrupy_hamal7 小时前
HTTP/1.1 队头堵塞问题
http
DanmF--10 小时前
详解与HTTP服务器相关操作
服务器·网络·网络协议·http·unity·c#
2501_9159090611 小时前
安卓APP-HTTPS抓包Frida Hook教程
websocket·网络协议·tcp/ip·http·网络安全·https·udp
Hello.Reader14 小时前
Mitmproxy 11 发布 —— 完整支持 HTTP/3!
网络·网络协议·http
zczlsy1114 小时前
Axios的使用
http·axios
掘金-我是哪吒2 天前
分布式微服务系统架构第109集:HTTP缓存优化,Nginx 代理配置,蓝绿部署, Jenkins一键切流脚本
分布式·http·缓存·微服务·系统架构
00后程序员张3 天前
iPhone相册导出到电脑的完整指南
websocket·网络协议·tcp/ip·http·网络安全·https·udp
August_._3 天前
【JavaWeb】详细讲解 HTTP 协议
java·网络·网络协议·http
wordbaby3 天前
Vue 图片重试指令 (v-img-retry) 增强:集成 visibility 控制,实现无缝加载过渡
前端·vue.js·http