3、HttpClient源码解析之池化连接的返回与释放

HttpEntity entity = closeableHttpResponse.getEntity();
//InputStream content = entity.getContent(); //content:EofSensorInputStream
 //释放连接 & 并且关闭流
 EntityUtils.toString(entity);
  • ResponseEntityProxy重写了获取InputStream的方法,返回的是EofSensorInputStream类型的InputStream对象,真正持有响应流数据的是EofSensorInputStream属性wrappedStream。
  • 关闭流其实是对EofSensorInputStream属性wrappedStream流的关闭。

1、EntityUtils

private static CharArrayBuffer toCharArrayBuffer(final InputStream inStream, final long contentLength,
       final Charset charset, final int maxResultLength) throws IOException {
    final Charset actualCharset = charset == null ? DEFAULT_CHARSET : charset;
    final CharArrayBuffer buf = new CharArrayBuffer(
            Math.min(maxResultLength, contentLength > 0 ? (int) contentLength : DEFAULT_CHAR_BUFFER_SIZE));
    final Reader reader = new InputStreamReader(inStream, actualCharset);
    final char[] tmp = new char[DEFAULT_CHAR_BUFFER_SIZE];
    int chReadCount;
    while ((chReadCount = reader.read(tmp)) != -1) {//EofSensorInputStream#read
        buf.append(tmp, 0, chReadCount);
    }
    return buf;
}

2、EofSensorInputStream

读取完响应流字节后,关闭响应式流。

public class EofSensorInputStream extends InputStream {

	private InputStream wrappedStream;//请求响应流

	public int read() throws IOException {
        int b = -1;
        if (isReadAllowed()) {
            try {
                b = wrappedStream.read();
                checkEOF(b);
            } catch (final IOException ex) {
                checkAbort();
                throw ex;
            }
        }
        return b;
    }
	
	private void checkEOF(final int eof) throws IOException {
	    final InputStream toCheckStream = wrappedStream;
	    if ((toCheckStream != null) && (eof < 0)) {// TRUE:表示响应流wrappedStream通道中字节读取完毕
	        try {
	            boolean scws = true; // should close wrapped stream?
	            if (eofWatcher != null) {
	            	// eofWatcher:ResponseEntityProxy
	                scws = eofWatcher.eofDetected(toCheckStream);
	            }
	            if (scws) {
	                toCheckStream.close();//关闭流
	            }
	        } finally {
	            wrappedStream = null;
	        }
	    }
	}
}

2.1、返回连接方式二

public void close() throws IOException {
   // tolerate multiple calls to close()
    selfClosed = true;
    checkClose();//此处会返回连接至连接池
}

3、ResponseEntityProxy

public boolean eofDetected(final InputStream wrapped) throws IOException {
   try {
        if (wrapped != null) {
            wrapped.close();
        }
        releaseConnection();
    } catch (final IOException | RuntimeException ex) {
        discardConnection();
        throw ex;
    } finally {
        cleanup();
    }
    return false;
}

public void releaseConnection() {
    if (this.execRuntime != null) {
    	// InternalExecRuntime
        this.execRuntime.releaseEndpoint();
    }
}

4、InternalExecRuntime

参考 #2 从endpointRef获取Endpoint之InternalConnectionEndpoint。

public void releaseEndpoint() {
    final ConnectionEndpoint endpoint = endpointRef.getAndSet(null);
    if (endpoint != null) {
        if (reusable) {
            // PoolingHttpClientConnectionManager
            manager.release(endpoint, state, validDuration);
        } else {
            discardEndpoint(endpoint);
        }
    }
}

5、PoolingHttpClientConnectionManager

PoolEntry参考 #3.2

通过PoolEntry获取ManagedHttpClientConnection 参考 #3.2

@Override
public void release(final ConnectionEndpoint endpoint, final Object state, final TimeValue keepAlive) {
    // 从InternalConnectionEndpoint中获取连接实例 PoolEntry
    final PoolEntry<HttpRoute, ManagedHttpClientConnection> entry = cast(endpoint).detach();
    ...
    final ManagedHttpClientConnection conn = entry.getConnection();
    if (conn != null && keepAlive == null) {
        conn.close(CloseMode.GRACEFUL);
    }
    boolean reusable = conn != null && conn.isOpen() && conn.isConsistent();
    if (reusable) {
       entry.updateState(state);
       entry.updateExpiry(keepAlive);
       conn.passivate();
       ...
   }
   ...
   //StrictConnPool
   this.pool.release(entry, reusable);
}

6、StrictConnPool

public class StrictConnPool<T, C extends ModalCloseable> implements ManagedConnPool<T, C> {
	private final Set<PoolEntry<T, C>> leased;
    private final LinkedList<PoolEntry<T, C>> available;
    
	@Override
	public void release(final PoolEntry<T, C> entry, final boolean reusable) {
	    ...
	    if (!reusable) {
	        entry.discardConnection(CloseMode.GRACEFUL);
	    }
	    this.lock.lock();
	    try {
	        if (this.leased.remove(entry)) {// 租借集合中移除PoolEntry【租借时加入的】
	            if (this.connPoolListener != null) {
	                this.connPoolListener.onRelease(entry.getRoute(), this);
	            }
	            final PerRoutePool<T, C> pool = getPool(entry.getRoute());
	            final boolean keepAlive = entry.hasConnection() && reusable;
	            //PerRoutePool:返还连接到池中
	            pool.free(entry, keepAlive);
	            if (keepAlive) {
	                switch (policy) {
	                    case LIFO:
	                    	// PoolEntry可用集合 available 重新添加当前PoolEntry【租借时移出的】
	                        this.available.addFirst(entry);
	                        break;
	                    case FIFO:
	                        this.available.addLast(entry);
	                        break;
	                    default:
	                        throw new IllegalStateException("Unexpected ConnPoolPolicy value: " + policy);
	                }
	            } else {
	                entry.discardConnection(CloseMode.GRACEFUL);
	            }
	            processNextPendingRequest();
	        }
	    } 
	    fireCallbacks();
	}
	
	static class PerRoutePool<T, C extends ModalCloseable> {
		private final Set<PoolEntry<T, C>> leased;
        private final LinkedList<PoolEntry<T, C>> available;
        
		public void free(final PoolEntry<T, C> entry, final boolean reusable) {
            final boolean found = this.leased.remove(entry);// 租借集合中移除PoolEntry【租借时加入的】
            if (reusable) {
                this.available.addFirst(entry);// PoolEntry可用集合 available 重新添加当前PoolEntry【租借时移出的】
            }
        }
	}
}

7、关闭连接&释放连接

7.1、EntityUtils.toString

不会关闭连接、释放连接以及关闭流EofSensorInputStream。

private static String toString(HttpEntity entity,ContentType contentType) {
    final InputStream inStream = entity.getContent();
    try {
        ...
        final Reader reader = new InputStreamReader(inStream, charset);
        final CharArrayBuffer buffer = new CharArrayBuffer(capacity);
        final char[] tmp = new char[1024];
        int l;
        while((l = reader.read(tmp)) != -1) {
            buffer.append(tmp, 0, l);
        }
        return buffer.toString();
    } finally {
        inStream.close();
    }
}

7.2、Abort

在释放链接【 EntityUtils.toString(entity)】前后执行abort其结果是不一样的。

  • 之前执行,则会关闭连接Socket,因此就不会返回到连接池中。
  • 之后执行,不会对连接产生任何效果。或者之后执行与否,没有任何意义。
  • 不过如果发现连接存在问题,可以考虑该种方式关闭连接。

7.3、CloseableHttpResponse

如果存在 #7.1 ,当前CloseableHttpResponse#close没有任何意义。
作用:关闭连接、关闭流EofSensorInputStream。

8、连接属性

参考文章

public class HttpClientBuilder {
	public CloseableHttpClient build() {
		...
		ConnectionReuseStrategy reuseStrategyCopy = this.reuseStrategy;
		if (reuseStrategyCopy == null) {
		    if (systemProperties) {//默认为false
		        final String s = System.getProperty("http.keepAlive", "true");//默认为true
		        if ("true".equalsIgnoreCase(s)) {
		        	//DefaultConnectionReuseStrategy INSTANCE = new DefaultConnectionReuseStrategy();
		            reuseStrategyCopy = DefaultConnectionReuseStrategy.INSTANCE;
		        } else {
		            reuseStrategyCopy = new ConnectionReuseStrategy() {//执行此处表明所有连接无法释放,即使是长连接
		                @Override
		                public boolean keepAlive(
		                        final HttpRequest request, final HttpResponse response, final HttpContext context) {
		                    return false;
		                }
		            };
		        }
		    } else {
		        reuseStrategyCopy = DefaultConnectionReuseStrategy.INSTANCE;
		    }
		}
	}
}
public class DefaultConnectionReuseStrategy implements ConnectionReuseStrategy {

	public boolean keepAlive(
            final HttpRequest request, final HttpResponse response, final HttpContext context) {

        if (request != null) {
            final Iterator<String> ti = new BasicTokenIterator(request.headerIterator(HttpHeaders.CONNECTION));
            while (ti.hasNext()) {
                final String token = ti.next();
                if (HeaderElements.CLOSE.equalsIgnoreCase(token)) {
                    return false;
                }
            }
        }
		...
		//从响应头获取Connection属性
        Iterator<Header> headerIterator = response.headerIterator(HttpHeaders.CONNECTION);
        if (!headerIterator.hasNext()) {
            headerIterator = response.headerIterator("Proxy-Connection");
        }

        final ProtocolVersion ver = context.getProtocolVersion();
        if (headerIterator.hasNext()) {//false表示响应头没有相关属性设置
            if (ver.greaterEquals(HttpVersion.HTTP_1_1)) {
                final Iterator<String> it = new BasicTokenIterator(headerIterator);
                while (it.hasNext()) {
                    final String token = it.next();
                    if (HeaderElements.CLOSE.equalsIgnoreCase(token)) {
                        return false;
                    }
                }
                return true;
            }
            final Iterator<String> it = new BasicTokenIterator(headerIterator);
            while (it.hasNext()) {
                final String token = it.next();
                if (HeaderElements.KEEP_ALIVE.equalsIgnoreCase(token)) {
                    return true;
                }
            }
            return false;
        }
        // 此处执行表明上述在请求头、响应头均没有找到短连接Connection:close的标识,则直接通过http版本号判断
        //目前涉及的HTTP版本均为长链接http/1.1
        return ver.greaterEquals(HttpVersion.HTTP_1_1);
    }
}

DefaultConnectionReuseStrategy#keepAlive方法的主要作用就是在响应完成后判断当前连接是否需要释放。如果返回true表明连接是长连接,可以直接释放即归还到连接池,否则就不会释放。

知识点一:http连接的释放&关闭是两个不同的概念:

  • 释放是指将连接还回到连接池。
  • 关闭是指通过四次挥手关闭这个tcp连接了。【Socket套接字无法继续使用】

知识点二:HTTP连接中time_wait & close_wait状态:

挥手阶段只需要关注控制字段。
目的:断开连接。
连接断开的原因:释放端口(端口是有限的)、释放占用的线程等资源。
终止标志位FIN:用来释放TCP连接。
在这里插入图片描述
参考文章

  1. 主动方关闭连接后【调用close】,发送FIN包至被关闭方。主动方其应用层不会发送数据包,进入FIN-WAIT-1(终止等待1)状态。
  2. 被动关闭的一方收到FIN包后,协议层回复ACK。此时被关闭方进入close-wait状态。
    1)、主动关闭方接收到ACK之后,进入FIN-WAIT-2(终止等待2)状态。
    2)、被动关闭方应用层如果此时还存在数据包则继续发送,主动关闭方此时还可以继续处理被关闭方发送的数据。
  3. 被关闭方应用层没有待发送的数据,则调用close方法。协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态。此时被关闭方停止了close-wait状态。
  4. 主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭连接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态。等待2MSL时间,主动关闭的一方,结束TIME_WAIT,进入CLOSED状态。

通过上面的一次socket关闭操作,你可以得出以下几点:

  • 主动关闭连接的一方 - 也就是主动调用socket的close操作的一方,最终会进入TIME_WAIT状态。
  • 被动关闭连接的一方,有一个中间状态,即CLOSE_WAIT,因为协议层在等待上层的应用程序,主动调用close操作后才主动关闭这条连接。
  • TIME_WAIT会默认等待2MSL时间后,才最终进入CLOSED状态。
  • 在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的。

所以,TIME_WAIT并不可怕,CLOSE_WAIT才可怕,因为CLOSE_WAIT很多,表示说要么是你的应用程序写的有问题,没有合适的关闭socket;要么是说,你的服务器CPU处理不过来(CPU太忙)或者你的应用程序一直睡眠到其它地方(锁,或者文件I/O等等),你的应用程序获得不到合适的调度时间,造成你的程序没法真正的执行close操作。

知识点三:连接池:

  • 连接池管理的链接都是长链接【persistent connection】,也即意味着客户端、服务端的连接【核心套接字socket】必须显示设置连接为长连接。
  • 在请求头、响应头通过Connection:close选项可以显示标识当前连接为短连接。当前默认情况下Tomcat、HttpClient的连接均为长链接即Connection:keep-alive
  • 持久连接有两种方式:HTTP/1.0+的Keep-Avlive与HTTP/1.1的默认持久连接。

mysql、redis、dubbo通常建议都是使用长连接,通过连接池的方式复用连接。

知识点四:TIME_WAIT的作用

如果我们来做个类比的话,TIME_WAIT的出现,对应的是你的程序里的异常处理,它的出现,就是为了解决网络的丢包和网络不稳定所带来的其他问题:

第一,防止前一个连接【五元组,我们继续以 180.172.35.150:45678, tcp, 180.97.33.108:80 为例】上延迟的数据包或者丢失重传的数据包,被后面复用的连接【前一个连接关闭后,此时你再次访问百度,新的连接可能还是由180.172.35.150:45678, tcp, 180.97.33.108:80 这个五元组来表示,也就是源端口凑巧还是45678】错误的接收(异常:数据丢了,或者传输太慢了),参见下图:

  1. SEQ=3的数据包丢失,重传第一次,没有得到ACK确认。
  2. 如果没有TIME_WAIT,或者TIME_WAIT时间非常端,那么关闭的连接【180.172.35.150:45678, tcp,180.97.33.108:80 的状态变为了CLOSED,源端口可被再次利用】,马上被重用【对180.97.33.108:80新建的连接,复用了之前的随机端口45678】,并连续发送SEQ=1,2的数据包。
  3. 此时,前面的连接上的SEQ=3的数据包再次重传,同时,seq的序号刚好也是3(这个很重要,不然,SEQ的序号对不上,就会RST掉),此时,前面一个连接上的数据被后面的一个连接错误的接收。

第二,确保连接方能在时间范围内,关闭自己的连接。其实,也是因为丢包造成的,参见下图:

在这里插入图片描述

  1. 主动关闭方关闭了连接,发送了FIN。
  2. 被动关闭方回复ACK同时也执行关闭动作,发送FIN包;此时,被动关闭的一方进入LAST_ACK状态。
  3. 主动关闭的一方回去了ACK,主动关闭一方进入TIME_WAIT状态。
  4. 但是最后的ACK丢失,被动关闭的一方还继续停留在LAST_ACK状态。
  5. 此时,如果没有TIME_WAIT的存在,或者说,停留在TIME_WAIT上的时间很短,则主动关闭的一方很快就进入了CLOSED状态,也即是说,如果此时新建一个连接,源随机端口如果被复用,在connect发送SYN包后,由于被动方仍认为这条连接【五元组】还在等待ACK,但是却收到了SYN,则被动方会回复RST。
  6. 造成主动创建连接的一方,由于收到了RST,则连接无法成功。

所以,你看到了,TIME_WAIT的存在是很重要的,如果强制忽略TIME_WAIT,还是有很高的机率,造成数据粗乱,或者短暂性的连接失败。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值