Original: https://www.jb51.net/article/141015.htm
Índice
3. Conexão persistente de HTTP/1.1
4. Como o HttpClient gera uma conexão persistente
4.1 Implementação do pool de conexões HttpClient
5. Como HttpClient reutiliza conexões persistentes?
6. Como o HttpClient limpa conexões expiradas
Oito, configurações importantes do pool de threads httpcleint
1. Fundo
O protocolo HTTP é um protocolo sem estado, ou seja, cada requisição é independente uma da outra. Portanto, sua implementação inicial é que cada solicitação http abrirá uma conexão de soquete tcp e a conexão será encerrada quando a interação for concluída.
O protocolo HTTP é um protocolo full-duplex, portanto, estabelecer e desconectar requer três apertos de mão e quatro acenos de mãos. Obviamente, neste projeto, toda vez que uma requisição Http for enviada, muitos recursos adicionais serão consumidos, ou seja, estabelecimento e destruição da conexão.
Portanto, o protocolo HTTP também foi desenvolvido, e a multiplexação da conexão socket é realizada através do método de conexão persistente.
Como pode ser visto na figura:
- Em uma conexão serial, cada interação abre e fecha a conexão
- Em uma conexão persistente, a primeira interação abrirá a conexão, e a conexão não será fechada após a interação. A próxima interação salvará o processo de estabelecimento de uma conexão.
Há duas implementações de conexões persistentes: conexões persistentes HTTP/1.0+ keep-alive e HTTP/1.1.
2. Keep-Alive de HTTP/1.0+
Desde 1996, muitos navegadores e servidores HTTP/1.0 estenderam o protocolo, que é o protocolo de extensão "keep-alive".
Observe que esse protocolo de extensão aparece como um suplemento de "conexão persistente experimental" para 1.0. keep-alive não é mais usado e não é explicado na especificação HTTP/1.1 mais recente, mas muitos aplicativos continuam.
Os clientes que usam HTTP/1.0 adicionam "Connection: Keep-Alive" ao cabeçalho, solicitando ao servidor que mantenha uma conexão aberta. O servidor incluirá o mesmo cabeçalho na resposta se desejar manter a conexão aberta. Se a resposta não contiver o cabeçalho "Connection: Keep-Alive", o cliente pensará que o servidor não oferece suporte a keep-alive e fechará a conexão atual após enviar a mensagem de resposta.
Através do protocolo suplementar keep-alive, a conexão persistente entre o cliente e o servidor é concluída, mas ainda existem alguns problemas:
- Keep-alive não é um protocolo padrão em HTTP/1.0, e o cliente deve enviar Connection:Keep-Alive para ativar a conexão keep-alive.
- Os servidores proxy podem não suportar keep-alive, porque alguns proxies são "retransmissores cegos", incapazes de entender o significado do cabeçalho e apenas encaminham o cabeçalho salto a salto. Portanto, pode causar a conexão entre o cliente e o servidor, mas o proxy não aceita os dados na conexão.
3. Conexão persistente de HTTP/1.1
HTTP/1.1 substitui Keep-Alive por uma conexão persistente.
As conexões HTTP/1.1 são persistentes por padrão. Se você quiser fechá-lo explicitamente, precisará adicionar o cabeçalho Connection:Close à mensagem. Ou seja, no HTTP/1.1, todas as conexões são multiplexadas.
No entanto, como o Keep-Alive, as conexões persistentes ociosas também podem ser fechadas pelo cliente e pelo servidor a qualquer momento. Não enviar Connection:Close não significa que o servidor promete manter a conexão aberta para sempre.
4. Como o HttpClient gera uma conexão persistente
HttpClien usa um pool de conexões para gerenciar conexões retidas e as conexões podem ser reutilizadas no mesmo link TCP. HttpClient implementa a persistência de conexão por meio do pool de conexões.
Na verdade, a tecnologia "pool" é um design geral e sua ideia de design não é complicada:
- Estabelecer uma conexão quando uma conexão é usada pela primeira vez
- No final, a conexão correspondente não é fechada e retorna ao pool
- A próxima conexão para o mesmo propósito pode obter uma conexão disponível do pool
- Limpe periodicamente as conexões expiradas
Todos os pools de conexão são baseados nessa ideia, mas quando olhamos para o código-fonte do HttpClient, focamos principalmente em dois pontos:
- O esquema de design específico do pool de conexão, para referência na personalização do pool de conexão no futuro
- Como corresponder ao protocolo HTTP, ou seja, a realização da abstração teórica em código
4.1 Implementação do pool de conexões HttpClient
A manipulação do HttpClient de conexões persistentes pode ser concentrada no código a seguir. A parte relacionada ao pool de conexões é extraída de MainClientExec e outras partes são removidas:
public class MainClientExec implements ClientExecChain {
@Override
public CloseableHttpResponse execute(
final HttpRoute route,
final HttpRequestWrapper request,
final HttpClientContext context,
final HttpExecutionAware execAware) throws IOException, HttpException {
//从连接管理器HttpClientConnectionManager中获取一个连接请求ConnectionRequest
final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);final HttpClientConnection managedConn;
final int timeout = config.getConnectionRequestTimeout(); //从连接请求ConnectionRequest中获取一个被管理的连接HttpClientConnection
managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
//将连接管理器HttpClientConnectionManager与被管理的连接HttpClientConnection交给一个ConnectionHolder持有
final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);
try {
HttpResponse response;
if (!managedConn.isOpen()) { //如果当前被管理的连接不是出于打开状态,需要重新建立连接
establishRoute(proxyAuthState, managedConn, route, request, context);
}
//通过连接HttpClientConnection发送请求
response = requestExecutor.execute(request, managedConn, context);
//通过连接重用策略判断是否连接可重用
if (reuseStrategy.keepAlive(response, context)) {
//获得连接有效期
final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
//设置连接有效期
connHolder.setValidFor(duration, TimeUnit.MILLISECONDS); //将当前连接标记为可重用状态
connHolder.markReusable();
} else {
connHolder.markNonReusable();
}
}
final HttpEntity entity = response.getEntity();
if (entity == null || !entity.isStreaming()) {
//将当前连接释放到池中,供下次调用
connHolder.releaseConnection();
return new HttpResponseProxy(response, null);
} else {
return new HttpResponseProxy(response, connHolder);
}
}
Aqui vemos que o processamento da conexão durante o processo de solicitação Http é consistente com a especificação do protocolo e aqui vamos expandir a implementação específica.
PoolingHttpClientConnectionManager é o gerenciador de conexões padrão de HttpClient. Primeiro, obtenha uma solicitação de conexão por meio de requestConnection(). Observe que isso não é uma conexão.
public ConnectionRequest requestConnection(
final HttpRoute route,
final Object state) {final Future<CPoolEntry> future = this.pool.lease(route, state, null);
return new ConnectionRequest() {
@Override
public boolean cancel() {
return future.cancel(true);
}
@Override
public HttpClientConnection get(
final long timeout,
final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {
final HttpClientConnection conn = leaseConnection(future, timeout, tunit);
if (conn.isOpen()) {
final HttpHost host;
if (route.getProxyHost() != null) {
host = route.getProxyHost();
} else {
host = route.getTargetHost();
}
final SocketConfig socketConfig = resolveSocketConfig(host);
conn.setSocketTimeout(socketConfig.getSoTimeout());
}
return conn;
}
};
}
Pode-se ver que o objeto ConnectionRequest retornado é, na verdade, um Future<CPoolEntry> de retenção e CPoolEntry é a instância de conexão real gerenciada pelo pool de conexão.
Do código acima, devemos nos concentrar em:
Future<CPoolEntry> future = this.pool.lease(route, state, null)
Como obter uma conexão assíncrona do pool de conexão CPool, Future<CPoolEntry>
HttpClientConnection conn = leaseConnection(future, timeout, tunit)
Como obter uma conexão real HttpClientConnection através da conexão assíncrona Future<CPoolEntry>
4.2 Futuro<CPoolEntry>
Dê uma olhada em como o CPool libera um Future<CPoolEntry>, o código principal do AbstractConnPool é o seguinte:
private E getPoolEntryBlocking(
final T route, final Object state,
final long timeout, final TimeUnit tunit,
final Future<E> future) throws IOException, InterruptedException, TimeoutException {
//首先对当前连接池加锁,当前锁是可重入锁ReentrantLockthis.lock.lock();
try { //获得一个当前HttpRoute对应的连接池,对于HttpClient的连接池而言,总池有个大小,每个route对应的连接也是个池,所以是“池中池”
final RouteSpecificPool<T, C, E> pool = getPool(route);
E entry;
for (;;) {
Asserts.check(!this.isShutDown, "Connection pool shut down"); //死循环获得连接
for (;;) { //从route对应的池中拿连接,可能是null,也可能是有效连接
entry = pool.getFree(state); //如果拿到null,就退出循环
if (entry == null) {
break;
} //如果拿到过期连接或者已关闭连接,就释放资源,继续循环获取
if (entry.isExpired(System.currentTimeMillis())) {
entry.close();
}
if (entry.isClosed()) {
this.available.remove(entry);
pool.free(entry, false);
} else { //如果拿到有效连接就退出循环
break;
}
} //拿到有效连接就退出
if (entry != null) {
this.available.remove(entry);
this.leased.add(entry);
onReuse(entry);
return entry;
}
//到这里证明没有拿到有效连接,需要自己生成一个
final int maxPerRoute = getMax(route);
//每个route对应的连接最大数量是可配置的,如果超过了,就需要通过LRU清理掉一些连接
final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);
if (excess > 0) {
for (int i = 0; i < excess; i++) {
final E lastUsed = pool.getLastUsed();
if (lastUsed == null) {
break;
}
lastUsed.close();
this.available.remove(lastUsed);
pool.remove(lastUsed);
}
}
//当前route池中的连接数,没有达到上线
if (pool.getAllocatedCount() < maxPerRoute) {
final int totalUsed = this.leased.size();
final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0); //判断连接池是否超过上线,如果超过了,需要通过LRU清理掉一些连接
if (freeCapacity > 0) {
final int totalAvailable = this.available.size(); //如果空闲连接数已经大于剩余可用空间,则需要清理下空闲连接
if (totalAvailable > freeCapacity - 1) {
if (!this.available.isEmpty()) {
final E lastUsed = this.available.removeLast();
lastUsed.close();
final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());
otherpool.remove(lastUsed);
}
} //根据route建立一个连接
final C conn = this.connFactory.create(route); //将这个连接放入route对应的“小池”中
entry = pool.add(conn); //将这个连接放入“大池”中
this.leased.add(entry);
return entry;
}
}
//到这里证明没有从获得route池中获得有效连接,并且想要自己建立连接时当前route连接池已经到达最大值,即已经有连接在使用,但是对当前线程不可用
boolean success = false;
try {
if (future.isCancelled()) {
throw new InterruptedException("Operation interrupted");
} //将future放入route池中等待
pool.queue(future); //将future放入大连接池中等待
this.pending.add(future); //如果等待到了信号量的通知,success为true
if (deadline != null) {
success = this.condition.awaitUntil(deadline);
} else {
this.condition.await();
success = true;
}
if (future.isCancelled()) {
throw new InterruptedException("Operation interrupted");
}
} finally {
//从等待队列中移除
pool.unqueue(future);
this.pending.remove(future);
}
//如果没有等到信号量通知并且当前时间已经超时,则退出循环
if (!success && (deadline != null && deadline.getTime() <= System.currentTimeMillis())) {
break;
}
} //最终也没有等到信号量通知,没有拿到可用连接,则抛异常
throw new TimeoutException("Timeout waiting for connection");
} finally { //释放对大连接池的锁
this.lock.unlock();
}
}
Existem vários pontos importantes na lógica do código acima:
- O pool de conexões possui um número máximo de conexões, cada rota corresponde a um pequeno pool de conexões e também há um número máximo de conexões
- Seja um pool de conexão grande ou um pool de conexão pequeno, quando o número excede, algumas conexões devem ser liberadas por meio de LRU
- Se você obtiver uma conexão disponível, retorne-a para a camada superior para uso
- Se nenhuma conexão disponível for obtida, o HttpClient julgará se o pool de conexão de rota atual excedeu o número máximo e, se o limite superior não for atingido, uma nova conexão será criada e colocada no pool
- Se o limite superior for atingido, aguarde na fila, aguarde o semáforo, obtenha-o novamente e lance uma exceção de timeout se a espera for menor que
- A obtenção de uma conexão por meio do pool de threads precisa ser bloqueada por meio do ReetrantLock para garantir a segurança do thread
Até agora, o programa obteve uma instância CPoolEntry disponível ou o programa foi encerrado lançando uma exceção.
4.3 HttpClientConnection
protected HttpClientConnection leaseConnection(
final Future<CPoolEntry> future,
final long timeout,
final TimeUnit tunit) throws InterruptedException, ExecutionException, ConnectionPoolTimeoutException {
final CPoolEntry entry;
try { //从异步操作Future<CPoolEntry>中获得CPoolEntry
entry = future.get(timeout, tunit);
if (entry == null || future.isCancelled()) {
throw new InterruptedException();
}
Asserts.check(entry.getConnection() != null, "Pool entry with no connection");
if (this.log.isDebugEnabled()) {
this.log.debug("Connection leased: " + format(entry) + formatStats(entry.getRoute()));
} //获得一个CPoolEntry的代理对象,对其操作都是使用同一个底层的HttpClientConnection
return CPoolProxy.newProxy(entry);
} catch (final TimeoutException ex) {
throw new ConnectionPoolTimeoutException("Timeout waiting for connection from pool");
}
}
5. Como HttpClient reutiliza conexões persistentes?
No capítulo anterior, vimos que o HttpClient obtém uma conexão por meio de um pool de conexões e a obtém do pool quando precisa usar uma conexão.
Correspondendo às perguntas do Capítulo 3:
- Estabelecer uma conexão quando uma conexão é usada pela primeira vez
- No final, a conexão correspondente não é fechada e retorna ao pool
- A próxima conexão para o mesmo propósito pode obter uma conexão disponível do pool
- Limpe periodicamente as conexões expiradas
Vimos no Capítulo 4 como o HttpClient lida com os problemas 1 e 3, então como ele lida com o segundo problema?
Ou seja, como HttpClient determina se uma conexão deve ser fechada após o uso ou deve ser colocada no pool para reutilização por outros? Olhe o código de MainClientExec novamente
//发送Http连接 response = requestExecutor.execute(request, managedConn, context);
//根据重用策略判断当前连接是否要复用
if (reuseStrategy.keepAlive(response, context)) {
//需要复用的连接,获取连接超时时间,以response中的timeout为准
final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
if (this.log.isDebugEnabled()) {
final String s; //timeout的是毫秒数,如果没有设置则为-1,即没有超时时间
if (duration > 0) {
s = "for " + duration + " " + TimeUnit.MILLISECONDS;
} else {
s = "indefinitely";
}
this.log.debug("Connection can be kept alive " + s);
} //设置超时时间,当请求结束时连接管理器会根据超时时间决定是关闭还是放回到池中
connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
//将连接标记为可重用 connHolder.markReusable();
} else { //将连接标记为不可重用
connHolder.markNonReusable();
}
Pode-se ver que quando ocorre uma solicitação usando uma conexão, há uma estratégia de nova tentativa de conexão para determinar se a conexão deve ser reutilizada. Se for para ser reutilizada, ela será entregue ao HttpClientConnectionManager e colocada no pool após o término .
Então, qual é a lógica da estratégia de reutilização de conexão?
public class DefaultClientConnectionReuseStrategy extends DefaultConnectionReuseStrategy {
public static final DefaultClientConnectionReuseStrategy INSTANCE = new DefaultClientConnectionReuseStrategy();
@Override
public boolean keepAlive(final HttpResponse response, final HttpContext context) {
//从上下文中拿到request
final HttpRequest request = (HttpRequest) context.getAttribute(HttpCoreContext.HTTP_REQUEST);
if (request != null) { //获得Connection的Header
final Header[] connHeaders = request.getHeaders(HttpHeaders.CONNECTION);
if (connHeaders.length != 0) {
final TokenIterator ti = new BasicTokenIterator(new BasicHeaderIterator(connHeaders, null));
while (ti.hasNext()) {
final String token = ti.nextToken(); //如果包含Connection:Close首部,则代表请求不打算保持连接,会忽略response的意愿,该头部这是HTTP/1.1的规范
if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {
return false;
}
}
}
} //使用父类的的复用策略
return super.keepAlive(response, context);
}
}
Veja a estratégia de reutilização da classe pai
if (canResponseHaveBody(request, response)) {
final Header[] clhs = response.getHeaders(HTTP.CONTENT_LEN);
//如果reponse的Content-Length没有正确设置,则不复用连接 //因为对于持久化连接,两次传输之间不需要重新建立连接,则需要根据Content-Length确认内容属于哪次请求,以正确处理“粘包”现象 //所以,没有正确设置Content-Length的response连接不能复用
if (clhs.length == 1) {
final Header clh = clhs[0];
try {
final int contentLen = Integer.parseInt(clh.getValue());
if (contentLen < 0) {
return false;
}
} catch (final NumberFormatException ex) {
return false;
}
} else {
return false;
}
}
if (headerIterator.hasNext()) {
try {
final TokenIterator ti = new BasicTokenIterator(headerIterator);
boolean keepalive = false;
while (ti.hasNext()) {
final String token = ti.nextToken(); //如果response有Connection:Close首部,则明确表示要关闭,则不复用
if (HTTP.CONN_CLOSE.equalsIgnoreCase(token)) {
return false; //如果response有Connection:Keep-Alive首部,则明确表示要持久化,则复用
} else if (HTTP.CONN_KEEP_ALIVE.equalsIgnoreCase(token)) {
keepalive = true;
}
}
if (keepalive) {
return true;
}
} catch (final ParseException px) {
return false;
}
}
//如果response中没有相关的Connection首部说明,则高于HTTP/1.0版本的都复用连接
return !ver.lessEquals(HttpVersion.HTTP_1_0);
para concluir:
- Se o cabeçalho da solicitação contiver Connection:Close, ele não será reutilizado
- Se o Content-Length na resposta for definido incorretamente, ele não será reutilizado
- Se o cabeçalho de resposta contiver Connection:Close, ele não será reutilizado
- Se o cabeçalho de resposta contiver Connection: Keep-Alive, reutilize
- No caso de nenhum acerto, se a versão do HTTP for superior a 1.0, ela será reutilizada
Como pode ser visto no código, sua estratégia de implementação é consistente com as restrições de nossa camada de protocolo nos Capítulos 2 e 3.
6. Como o HttpClient limpa conexões expiradas
Antes da versão HttpClient4.4, ao reutilizar a conexão do pool de conexões, ele verificará se expirou e será limpo quando expirar.
A versão posterior é diferente, haverá um thread separado para verificar a conexão no pool de conexões e será limpo quando descobrir que o tempo desde o último uso excede o tempo definido. O tempo limite padrão é de 2 segundos.
public CloseableHttpClient build() { //如果指定了要清理过期连接与空闲连接,才会启动清理线程,默认是不启动的
if (evictExpiredConnections || evictIdleConnections) { //创造一个连接池的清理线程
final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,
maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS,
maxIdleTime, maxIdleTimeUnit);
closeablesCopy.add(new Closeable() {
@Override
public void close() throws IOException {
connectionEvictor.shutdown();
try {
connectionEvictor.awaitTermination(1L, TimeUnit.SECONDS);
} catch (final InterruptedException interrupted) {
Thread.currentThread().interrupt();
}
}
}); //执行该清理线程
connectionEvictor.start();
}
Pode-se ver que quando o HttpClientBuilder está sendo construído, se a função de limpeza for especificada, um encadeamento de limpeza do pool de conexões será criado e executado.
public IdleConnectionEvictor(
final HttpClientConnectionManager connectionManager,
final ThreadFactory threadFactory,
final long sleepTime, final TimeUnit sleepTimeUnit,
final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
this.connectionManager = Args.notNull(connectionManager, "Connection manager");
this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory();
this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime;
this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime;
this.thread = this.threadFactory.newThread(new Runnable() {
@Override
public void run() {
try { //死循环,线程一直执行
while (!Thread.currentThread().isInterrupted()) { //休息若干秒后执行,默认10秒
Thread.sleep(sleepTimeMs); //清理过期连接
connectionManager.closeExpiredConnections(); //如果指定了最大空闲时间,则清理空闲连接
if (maxIdleTimeMs > 0) {
connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS);
}
}
} catch (final Exception ex) {
exception = ex;
}
}
});
}
para concluir:
- Somente depois que o HttpClientBuilder for configurado manualmente, ele será habilitado para limpar conexões expiradas e ociosas
- Após a configuração manual, ele iniciará um thread para executar em um loop infinito. Cada vez que executar a suspensão por um determinado período de tempo, ele chamará o método de limpeza do HttpClientConnectionManager para limpar conexões expiradas e ociosas.
7. Resumo deste artigo
- O protocolo HTTP alivia o problema de muitas conexões no projeto inicial por meio de conexões persistentes
- Existem duas formas de conexão persistente: Keep-Avlive do HTTP/1.0+ e conexão persistente padrão do HTTP/1.1
- O HttpClient gerencia conexões persistentes por meio do pool de conexões. O pool de conexões é dividido em dois, um é o pool de conexões total e o outro é o pool de conexões correspondente a cada rota.
- HttpClient obtém uma conexão em pool por meio do assíncrono Future<CPoolEntry>
- A estratégia de reutilização de conexão padrão é consistente com as restrições do protocolo HTTP. De acordo com a resposta, Connection:Close é considerado primeiro como fechado e Connection:Keep-Alive é considerado ativado e a última versão é maior de 1,0 para ser ativado.
- As conexões no pool de conexões serão limpas apenas se a opção para limpar conexões expiradas e ociosas estiver habilitada manualmente no HttpClientBuilder
- Versões posteriores ao HttpClient 4.4 limpam conexões expiradas e ociosas por meio de um thread de loop infinito, que fica inativo por um tempo toda vez que é executado, para obter o efeito de execução regular
Oito, configurações importantes do pool de threads httpcleint
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create().register("http", plainsf).register("https", getSslFactory()).build();
//创建连接池管理器
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(registry);
// 最大连接数
cm.setMaxTotal(maxTotal);
// 默认的每个路由的最大连接数(也就是相同主机最大连接数)
cm.setDefaultMaxPerRoute(maxPerRoute);
// HttpHost httpHost = new HttpHost(hostname, port);
// // 设置到某个路由的最大连接数,会覆盖defaultMaxPerRoute
// cm.setMaxPerRoute(new HttpRoute(httpHost), maxRoute);
setMaxTotal e setDefaultMaxPerRoute