JDK对Http协议的Keep-Alive的支持,以JDK8为例

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

上一篇:Android学习之使用HTTP协议访问网络之HttpURLConnection


下一篇:java原生 HttpUrlConnection 实现post请求提交文件