美文网首页
HTTP 协议 connection、keep-alive

HTTP 协议 connection、keep-alive

作者: 程序员札记 | 来源:发表于2023-10-22 17:52 被阅读0次

    在看 apollo 客户端的时候,里面有一个实现类HttpUtil.java,看到 HttpURLConnection 在创建使用后,并没有调用 disconnect 方法去关闭连接,根据说明,是为了 keep-alive 保持会话。这就比较纳闷了,之前所有的用法,都是会调用 disconnect 的,这不调用 disconnect 就可以 keep-alive 会话保持了么。

    我们知道在服务端(nginx)可以通过设置keepalive_timeout来控制连接保持时间,那么http连接的保持需要浏览器(客户端)支持吗?今天咱们一起来通过java.net.HttpURLConnection源码看看客户端是如何维护这些http连接的。

    测试代码

    package net.mengkang.demo;
    
    import java.io.*;
    import java.net.HttpURLConnection;
    import java.net.URL;
    
    public class Demo {
        public static void main(String[] args) throws IOException {
            test();
            test();
        }
    
        private static void test() throws IOException {
            URL url = new URL("http://static.mengkang.net/upload/image/2019/0921/1569075837628814.jpeg");
    
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestProperty("Charset", "UTF-8");
            connection.setRequestProperty("Connection", "Keep-Alive");
            connection.setRequestMethod("GET");
            connection.connect();
    
            BufferedInputStream bufferedInputStream = new BufferedInputStream(connection.getInputStream());
    
            File file = new File("./xxx.jpeg");
            OutputStream out = new FileOutputStream(file);
            int size;
            byte[] buf = new byte[1024];
            while ((size = bufferedInputStream.read(buf)) != -1) {
                out.write(buf, 0, size);
            }
    
            connection.disconnect();
        }
    }
    

    解析返回的头信息

    当客户端从服务端获取返回的字节流时

    connection.getInputStream()
    

    HttpClient会对返回的头信息进行解析,我简化了摘取了最重要的逻辑代码

    private boolean parseHTTPHeader(MessageHeader var1, ProgressSource var2, HttpURLConnection var3) throws IOException {
        String var15 = var1.findValue("Connection");
        ...
        if (var15 != null && var15.toLowerCase(Locale.US).equals("keep-alive")) {
            HeaderParser var11 = new HeaderParser(var1.findValue("Keep-Alive"));
            this.keepAliveConnections = var11.findInt("max", this.usingProxy ? 50 : 5);
            this.keepAliveTimeout = var11.findInt("timeout", this.usingProxy ? 60 : 5);
        }
        ...
    }
    

    是否需要保持长连接,是客户端申请,服务端决定,所以要以服务端返回的头信息为准。比如客户端发送的请求是Connection: Keep-Alive,服务端返回的是Connection: Close那也得以服务端为准。

    客户端请求完成

    当第一次执行时bufferedInputStream.read(buf)时,HttpClient会执行finished()方法

    public void finished() {
        if (!this.reuse) {
            --this.keepAliveConnections;
            this.poster = null;
            if (this.keepAliveConnections > 0 && this.isKeepingAlive() && !this.serverOutput.checkError()) {
                this.putInKeepAliveCache();
            } else {
                this.closeServer();
            }
    
        }
    }
    

    加入到 http 长连接缓存

    protected static KeepAliveCache kac = new KeepAliveCache();
    
    protected synchronized void putInKeepAliveCache() {
        if (this.inCache) {
            assert false : "Duplicate put to keep alive cache";
    
        } else {
            this.inCache = true;
            kac.put(this.url, (Object)null, this);
        }
    }
    public class KeepAliveCache extends HashMap<KeepAliveKey, ClientVector> implements Runnable {
        ...
        public synchronized void put(URL var1, Object var2, HttpClient var3) {
            KeepAliveKey var5 = new KeepAliveKey(var1, var2); // var2 null
            ClientVector var6 = (ClientVector)super.get(var5);
            if (var6 == null) {
                int var7 = var3.getKeepAliveTimeout();
                var6 = new ClientVector(var7 > 0 ? var7 * 1000 : 5000);
                var6.put(var3);
                super.put(var5, var6);
            } else {
                var6.put(var3);
            }
        }
        ...
    }
    

    这里涉及了KeepAliveKey和ClientVector

    class KeepAliveKey {
        private String protocol = null;
        private String host = null;
        private int port = 0;
        private Object obj = null;
    }
    

    设计这个对象呢,是因为只有protocol+host+port才能确定为同一个连接。所以用KeepAliveKey作为KeepAliveCache的key。
    ClientVector则是一个栈,每次有同一个域下的请求都入栈。

    class ClientVector extends Stack<KeepAliveEntry> {
        private static final long serialVersionUID = -8680532108106489459L;
        int nap;
    
        ClientVector(int var1) {
            this.nap = var1;
        }
    
        synchronized void put(HttpClient var1) {
            if (this.size() >= KeepAliveCache.getMaxConnections()) {
                var1.closeServer();
            } else {
                this.push(new KeepAliveEntry(var1, System.currentTimeMillis()));
            }
        }
        ...
    }
    

    “断开”连接

    connection.disconnect();
    

    如果是保持长连接的,实际只是关闭了一些流,socket 并没有关闭。接开篇的疑问,开盘的类的实现是相当有问题的。这样会导致一个典型错误;java.net.ConnectException: Cannot assign requested address (connect failed)

    如果不关闭流,就意味着当前connection 还没有处理完。对新发的request不得不开辟一个新的connection,此时并不能重用。

    如果一直不关闭流,新的connection 数量不停的增加,不关闭流的connection 会在idle time 之后才关闭,通常是keepalive 的时间(sever 端和client 端都会处理,server 会主动断开连接,client端也会根据idle time 来通知server)。这样,增加的端口的速度远远大于关闭的端口数量,就会导致socket 端口用光

    public void disconnect() {
    ...
          boolean var2 = var1.isKeepingAlive();
          if (var2) {
              var1.closeIdleConnection();
          }
    ...
    }
    public void closeIdleConnection() {
        HttpClient var1 = kac.get(this.url, (Object)null);
        if (var1 != null) {
            var1.closeServer();
        }
    }
    

    连接的复用

    public static HttpClient New(URL var0, Proxy var1, int var2, boolean var3, HttpURLConnection var4) throws IOException {
        ...
        HttpClient var5 = null;
        if (var3) {
            var5 = kac.get(var0, (Object)null);
            ...
        }
    
        if (var5 == null) {
            var5 = new HttpClient(var0, var1, var2);
        } else {
            ...
            var5.url = var0;
        }
    
        return var5;
    }
    public class KeepAliveCache extends HashMap<KeepAliveKey, ClientVector> implements Runnable {
        ...
        public synchronized HttpClient get(URL var1, Object var2) {
            KeepAliveKey var3 = new KeepAliveKey(var1, var2);
            ClientVector var4 = (ClientVector)super.get(var3);
            return var4 == null ? null : var4.get();
        }
        ...
    }
    

    ClientVector取的时候则出栈,出栈过程中如果该连接已经超时,则关闭与服务端的连接,继续执行出栈操作。

    class ClientVector extends Stack<KeepAliveEntry> {
        private static final long serialVersionUID = -8680532108106489459L;
        int nap;
    
        ClientVector(int var1) {
            this.nap = var1;
        }
    
        synchronized HttpClient get() {
            if (this.empty()) {
                return null;
            } else {
                HttpClient var1 = null;
                long var2 = System.currentTimeMillis();
    
                do {
                    KeepAliveEntry var4 = (KeepAliveEntry)this.pop();
                    if (var2 - var4.idleStartTime > (long)this.nap) {
                        var4.hc.closeServer();
                    } else {
                        var1 = var4.hc;
                    }
                } while(var1 == null && !this.empty());
    
                return var1;
            }
        }
        ...
    }
    

    这样就实现了客户端http连接的复用。
    从相关源代码(HttpClient,KeepAliveCache,HttpURLConnection)来看,disconnect 只会释放空余的连接,而 keep-alive 的连接,会放到 KeepAliveCache 的缓存中,并且默认有5秒的缓存失效时间。因此,为了保持一致性,建议还是调用 disconnect,这样如果 keep-alive 不生效时,能及时释放链接 ,keep-alive 生效时,也不影响连接的重用。

    小结

    存储结构如下

    image.png

    复用tcp的连接标准是protocol+host+port,客户端连接与服务端维持的连接数也不宜过多,HttpURLConnection默认只能存5个不同的连接,再多则直接断开连接(见上面HttpClient#finished方法),保持连接数过多对客户端和服务端都会增加不小的压力。
    同时KeepAliveCache也每隔5秒钟扫描检测一次,清除过期的httpClient

    相关文章

      网友评论

          本文标题:HTTP 协议 connection、keep-alive

          本文链接:https://www.haomeiwen.com/subject/affdidtx.html