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获取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连接。
参考文章
- 主动方关闭连接后【调用close】,发送FIN包至被关闭方。主动方其应用层不会发送数据包,进入FIN-WAIT-1(终止等待1)状态。
- 被动关闭的一方收到FIN包后,协议层回复ACK。此时被关闭方进入close-wait状态。
1)、主动关闭方接收到ACK之后,进入FIN-WAIT-2(终止等待2)状态。
2)、被动关闭方应用层如果此时还存在数据包则继续发送,主动关闭方此时还可以继续处理被关闭方发送的数据。 - 被关闭方应用层没有待发送的数据,则调用close方法。协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态。此时被关闭方停止了close-wait状态。
- 主动关闭的一方收到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】错误的接收(异常:数据丢了,或者传输太慢了),参见下图:
- SEQ=3的数据包丢失,重传第一次,没有得到ACK确认。
- 如果没有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的数据包。
- 此时,前面的连接上的SEQ=3的数据包再次重传,同时,seq的序号刚好也是3(这个很重要,不然,SEQ的序号对不上,就会RST掉),此时,前面一个连接上的数据被后面的一个连接错误的接收。
第二,确保连接方能在时间范围内,关闭自己的连接。其实,也是因为丢包造成的,参见下图:
- 主动关闭方关闭了连接,发送了FIN。
- 被动关闭方回复ACK同时也执行关闭动作,发送FIN包;此时,被动关闭的一方进入LAST_ACK状态。
- 主动关闭的一方回去了ACK,主动关闭一方进入TIME_WAIT状态。
- 但是最后的ACK丢失,被动关闭的一方还继续停留在LAST_ACK状态。
- 此时,如果没有TIME_WAIT的存在,或者说,停留在TIME_WAIT上的时间很短,则主动关闭的一方很快就进入了CLOSED状态,也即是说,如果此时新建一个连接,源随机端口如果被复用,在connect发送SYN包后,由于被动方仍认为这条连接【五元组】还在等待ACK,但是却收到了SYN,则被动方会回复RST。
- 造成主动创建连接的一方,由于收到了RST,则连接无法成功。
所以,你看到了,TIME_WAIT的存在是很重要的,如果强制忽略TIME_WAIT,还是有很高的机率,造成数据粗乱,或者短暂性的连接失败。