JDK对Http协议的Keep-Alive的支持,以JDK8为例
Http协议对keep-alive的支持
keep-alive顾名思义就是保持连接的意思,在早期的HTTP/1.0中,每次http请求都要创建一个连接,而创建连接的过程需要消耗资源和时间,为了减少资源消耗,缩短响应时间,就需要重用连接。
在后来的HTTP/1.0中以及HTTP/1.1中,引入了重用连接的机制,就是在http请求头中加入Connection: keep-alive来告诉对方这个请求响应完成后不要关闭,下一次咱们还用这个请求继续交流。
协议规定HTTP/1.0如果想要保持长连接,需要在请求头中加上Connection: keep-alive,而HTTP/1.1默认是支持长连接的,有没有这个请求头都行。另外,一般服务端都会设置keep-alive超时时间。超过指定的时间间隔,服务端就会主动关闭连接。同时服务端还会设置一个参数叫最大请求数,比如当最大请求数是300时,只要请求次数超过300次,即使还没到超时时间,服务端也会主动关闭连接。如下图所示为服务器返回的Responser Headers的值。
Reponse Headers
Connection: Keep-Alive
Content-length: 35
content-type: application/json;charset=UTF-8
Keep-Alive: timeout=60,max=5
注意,当Connection:keep-alive存在时,下面的Keep-Alive: timeout=70, max=10才会生效。
Http协议对keep-alive的支持是基于TCP连接的成功建立,而TCP协议是对Http透明的,即TCP协议的Keep-Alive与Http的Keep-Alive是无关的。
TCP协议中的keep-Alive
TCP keepalive指的是TCP保活计时器(keepalive timer)。设想有这样的情况:客户已主动与服务器建立了TCP连接。但后来客户端的主机突然出故障。显然,服务器以后就不能再收到客户发来的数据。因此,应当有措施使服务器不要再白白等待下去。这就是使用保活计时器。服务器每收到一次客户的数据,就重新设置保活计时器,时间的设置通常是两小时。若两小时没有收到客户的数据,服务器就发送一个探测报文段,以后则每隔75秒发送一次。若一连发送10个探测报文段后仍无客户的响应,服务器就认为客户端出了故障,接着就关闭这个连接。
——摘自谢希仁《计算机网络》
JDK对Http协议的keep-alive的支持
JDK对Http协议的Keep-Alive的支持参考oracel官方说明https://docs.oracle.com/javase/8/docs/technotes/guides/net/http-keepalive.html。接下来结合jdk源码分析:
1、HttpURLConnection(java.net.HttpURLConnection)使用长连接
JDK自带的HttpURLConnection,默认启用keepAlive,支持HTTP / 1.1和HTTP / 1.0持久连接, 使用后的HttpURLConnection会放入缓存*以后的同host:port的请求重用,底层的socket在keepAlive超时之前不会关闭。
HttpURLConnection受以下system properties控制:
http.keepAlive=(默认值:true),是否启用keepAlive,如果设置为false,则HttpURLConnection不会缓存,使用完后会关闭socket连接。(可设置)
http.maxConnections=(默认值:5),每个目标host缓存socket连接的最大数。(当http.keepAlive=false时为1,否则为5,下面结合源码分析)
2、sun.net.www.http包下的HttpURLConnection和HttpClient长连接缓存
sun.net.www.http.HttpURLConnection是java.net.HttpURLConnection的实现类,JDK自带的HttpURLConnection底层使用JDK自带的HttpClient发送http请求,java8的HttpClient(sun.net.www.http.HttpClient)存在诸多限制,在Jdk11中,java.net.HttpClient吸收第三方HttpClient的优点,性能肩比第三方HttpClient(okHttptClitn,apache Httpclient)。
下面为sun.net.www.http.HttpURLConnection.HttpURLConnection类,有如下两个方法返回HttpClient的实例和直接关闭socket的disconnect()方法。
public class HttpURLConnection extends java.net.HttpURLConnection {
// subclass HttpsClient will overwrite & return an instance of HttpsClient
protected HttpClient getNewHttpClient(URL url, Proxy p, int connectTimeout)
throws IOException {
return HttpClient.New(url, p, connectTimeout, this);
}
// subclass HttpsClient will overwrite & return an instance of HttpsClient
protected HttpClient getNewHttpClient(URL url, Proxy p, int connectTimeout, boolean useCache)
throws IOException {
return HttpClient.New(url, p, connectTimeout, useCache, this);
}
/**
* Disconnect from the server (public API)
*/
public void disconnect() {
}
}
下面为HttpClient类,parseHTTPHeader会解析header参数,判断HttpURLConnection是否是长连接,如果是长连接会读取keepAliveTimeout参数作为KeepAliveCache类里的长连接的缓存有效时间(默认为0)。并设置keepAliveConnections的值,如过为长连接值为5,否则为1。
完成获取请求返回结果后或调用getInputStream().close(),JDK会清理连接并作为以后使用的连接缓存。具体逻辑为执行finished()方法,如果是短连接,直接关闭套接字(调用closeServer()方法),如果是长连接,则将这个长连接加到KeepAliveCache的缓存线程中(putInKeepAliveCache()方法)。
其中,protected static KeepAliveCache kac = new KeepAliveCache();代码表面运行时只有全局只有一个缓存的线程类。
public class HttpClient extends NetworkClient {
// whether this httpclient comes from the cache
protected boolean cachedHttpClient = false;
protected boolean inCache;
/* where we cache currently open, persistent connections */
protected static KeepAliveCache kac = new KeepAliveCache();
private static boolean keepAliveProp = true;
private boolean parseHTTPHeader(MessageHeader responses, ProgressSource pi, HttpURLConnection httpuc){
keepAliveConnections = -1;
keepAliveTimeout = 0;
}
public void finished() {
if (reuse) /* will be reused */
return;
keepAliveConnections--;
poster = null;
if (keepAliveConnections > 0 && isKeepingAlive() &&
!(serverOutput.checkError())) {
/* This connection is keepingAlive && still valid.
* Return it to the cache.
*/
putInKeepAliveCache();
} else {
closeServer();
}
}
protected synchronized void putInKeepAliveCache() {
if (inCache) {
assert false : "Duplicate put to keep alive cache";
return;
}
inCache = true;
kac.put(url, null, this);
}
}
KeepAliveCache继承了HashMap,实现了Runnable接口。
它本身可以存储不同的KeepAliveKey-ClientVector,KeepAliveKey是关于协议(一般为http),ip,port类,对应了一个socket连接。ClientVector实现了双端队列,存储了HttpClient实例和idleStartTime(该HttpClient开始空闲的时间),最多可以存5个,对应HttpClient类的keepAliveConnections。ClientVector还有一个属性nap,即缓存过期时间,在put()方法实例化时设置,new ClientVector(keepAliveTimeout > 0 ? keepAliveTimeout * 1000 : LIFETIME)
KeepAliveCache也是一个线程类,run方法的逻辑是do-while循环检测HashMap中缓存的长连接是否timeout,如果超时就清理,当HashMap为空时,线程完成自己的逻辑,执行完毕。
KeepAliveCache提供put()方法,运行时KeepAliveCache线程类全局只有一个,没有缓存的线程类时,在put()方法中创建该缓存类keepAliveTimer,并设置长连接的缓存时间。
public class KeepAliveCache
extends HashMap<KeepAliveKey, ClientVector>
implements Runnable {
/* Sleeps for an alloted timeout, then checks for timed out connections.
* Errs on the side of caution (leave connections idle for a relatively
* short time).
*/
@Override
public void run() {
do {
try {
Thread.sleep(LIFETIME);
} catch (InterruptedException e) {}
// Remove all outdated HttpClients.
synchronized (this) {
long currentTime = System.currentTimeMillis();
List<KeepAliveKey> keysToRemove = new ArrayList<>();
for (KeepAliveKey key : keySet()) {
ClientVector v = get(key);
synchronized (v) {
KeepAliveEntry e = v.peek();
while (e != null) {
if ((currentTime - e.idleStartTime) > v.nap) {
v.poll();
e.hc.closeServer();
} else {
break;
}
e = v.peek();
}
if (v.isEmpty()) {
keysToRemove.add(key);
}
}
}
for (KeepAliveKey key : keysToRemove) {
removeVector(key);
}
}
} while (!isEmpty());
}
/**
* Register this URL and HttpClient (that supports keep-alive) with the cache
* @param url The URL contains info about the host and port
* @param http The HttpClient to be cached
*/
public synchronized void put(final URL url, Object obj, HttpClient http) {
boolean startThread = (keepAliveTimer == null);
if (!startThread) {
if (!keepAliveTimer.isAlive()) {
startThread = true;
}
}
if (startThread) {
clear();
/* Unfortunately, we can't always believe the keep-alive timeout we got
* back from the server. If I'm connected through a Netscape proxy
* to a server that sent me a keep-alive
* time of 15 sec, the proxy unilaterally terminates my connection
* The robustness to get around this is in HttpClient.parseHTTP()
*/
final KeepAliveCache cache = this;
AccessController.doPrivileged(new PrivilegedAction<>() {
public Void run() {
keepAliveTimer = InnocuousThread.newSystemThread("Keep-Alive-Timer", cache);
keepAliveTimer.setDaemon(true);
keepAliveTimer.setPriority(Thread.MAX_PRIORITY - 2);
keepAliveTimer.start();
return null;
}
});
}
KeepAliveKey key = new KeepAliveKey(url, obj);
ClientVector v = super.get(key);
if (v == null) {
int keepAliveTimeout = http.getKeepAliveTimeout();
v = new ClientVector(keepAliveTimeout > 0 ?
keepAliveTimeout * 1000 : LIFETIME);
v.put(http);
super.put(key, v);
} else {
v.put(http);
}
}
class ClientVector extends ArrayDeque<KeepAliveEntry> {
// sleep time in milliseconds, before cache clear
int nap;
ClientVector(int nap) {
this.nap = nap;
}
}
class KeepAliveKey {
private String protocol = null;
private String host = null;
private int port = 0;
private Object obj = null; // additional key, such as socketfactory
/**
* Constructor
* @param url the URL containing the protocol, host and port information
*/
public KeepAliveKey(URL url, Object obj) {
this.protocol = url.getProtocol();
this.host = url.getHost();
this.port = url.getPort();
this.obj = obj;
}
}
class KeepAliveEntry {
HttpClient hc;
long idleStartTime;
KeepAliveEntry(HttpClient hc, long idleStartTime) {
this.hc = hc;
this.idleStartTime = idleStartTime;
}
}
}
3、HttpURLConnection使用短连接
3.1)使用短连接,不缓存长连接
设置System.setProperty(“http.keepAlive”, ”false”);将整个 APP 的 http 长连接支持关掉。不会启动新的线程去缓存HttpClient连接,在HttpClient类的finished方法里不执行缓存连接的操作,直接关闭socket连接。
3.2)客户端不使用keep-alive功能
HttpURLConnection的实例获取到服务方的数据后直接关闭HttClient(关闭socket),不缓存如下所示:
//第一种,Header指定短连接
httpConn.setRequestProperty("Connection", "close");
//第二种,请求完后直接关闭socket
httpURLConnection.disconnect();
3.3)服务端长连接关闭
返回的Response Header中包含Connection:close即可。
参考另外几位大佬们的文章:
http://www.itersblog.com/archives/3.html
https://blog.csdn.net/u012216753/article/details/78084327
https://blog.csdn.net/tianshouzhi/article/details/103922842?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-5&spm=1001.2101.3001.4242