http client 实现 keep-alive 源码探究-阿里云开发者社区

开发者社区> 周梦康> 正文

http client 实现 keep-alive 源码探究

简介: 前几天在分享"实现自己的wget"的时候,因为我们的请求是一次性的,http 头里设置的`Connection: Close`。在`HTTP/1.1`为了提升`HTTP 1.0`的网络性能,增加了`keepalive`的特性。那么浏览器在请求的时候都会加上...
+关注继续查看

前几天在分享"实现自己的wget"的时候,因为我们的请求是一次性的,http 头里设置的Connection: Close。在HTTP/1.1为了提升HTTP 1.0的网络性能,增加了keepalive的特性。那么浏览器在请求的时候都会加上Connection: 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);
        }
    }
    ...
}

这里涉及了KeepAliveKeyClientVector

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

设计这个对象呢,是因为只有protocol+host+port才能确定为同一个连接。所以用KeepAliveKey作为KeepAliveCachekey
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 并没有关闭。

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连接的复用。

小结

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

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
OPC Client 核心源码
好像技术一沾上工业,便有了很高的价值,大家三缄其口,谁都不点破这层窗户纸,好多的思路和源码都要从国外网站获得,国内总是有条件,有限制--就是不告诉你,怕教会徒弟,饿死师父吧。
702 0
追本溯源 - Eclipse源码窥探
        最近花时间解决了一些以前积攒的,未曾深入研究的问题。其中一个就是eclipse maven 关联jar包源代码出现乱码的问题。问题的解决方案不算太麻烦,但是追本溯源确是件很有意思的事情(一直追踪到eclipse的源代码,探查到一些开源项目,如:伊利诺斯大学的开源项目CodingSpectator)。当然了,RCP开发本身就挺吸引我的,后面有机会,会和大家分享更多的心得。好了
914 0
OKHTTP3源码和设计模式(上篇)
本文来探究一下 OkHttp3 的源码和其中的设计思想。 关于 OkHttp3 的源码分析的文章挺多,不过大多还是在为了源码而源码。
1648 0
[动态代理三部曲:下] - 从动态代理,看Retrofit的源码实现
前言 关于动态代理的系列文章,到此便进入了最后的“一出好戏”。前俩篇内容分别展开了:从源码上,了解JDK实现动态代理的原理;以及从动态代理切入,学会看class文件结构的含义。
1118 0
hbase源码系列(三)Client如何找到正确的Region Server
Client如何找到正确的Region Server ?
2014 0
Okhttp3源码解析(1)-OkHttpClient分析
前言 上篇文章我们讲了Okhttp的基本用法,今天根据上节讲到请求流程来分析源码,那么第一步就是实例化OkHttpClient对象,所以我们今天主要分析下OkHttpClient源码! 初始化-构造方式 创建 OkHttpClient实例的两种方式 1.
2570 0
+关注
周梦康
十年前从 LNMP 开始个人站长 mengkang.net 生涯。 分享各种线上故障复盘笔记,关注我,防止采坑。
115
文章
70
问答
来源圈子
更多
PHP学习资料大全
+ 订阅
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载