• 2.jdk1.8+springboot中http1.1之tcp连接复用实现


    接上篇:https://www.cnblogs.com/Hleaves/p/11284316.html

    环境:jdk1.8 + springboot 2.1.1.RELEASE + feign-hystrix 10.1.0,以下仅为个人理解,如果异议,欢迎指正。

    上篇中,设置tomcat的max-connection=1

      因为之前一直理解的这个参数是同一时刻可以处理的http请求的数量,比如说我用浏览器‘同时’发起2个http请求(可以通过debug在controller层断点之后再发起另一个请求)A、B,此时A请求只要响应之后(忽略tcp连接是否释放),B请求正常来说就可以立即被服务端处理,事实并不是这样的,B请求一直在等待连接,但是再次发起一个请求C,C请求可以立即被处理,B请求还是一直在等待,只能串行执行,这个到底是为什么呢?后来查了一些资料,说是http1.1的请求头中默认加了Connection:keep-alive

    就是这个,使得在一个请求完成之后,不会马上释放tcp连接,发起其他请求(同一个url)时,会复用这个tcp连接(tcp的长连接),而且浏览器对同一个域名的tcp长连接最大数量有限制(具体自行查资料吧,参考:https://www.jianshu.com/p/1d535bd7fefb),所以建议不同服务使用多个域名部署,那tcp复用到底是怎么实现的呢?浏览器没有办法直观的看到如何去选择空闲的长连接的,feign的调用默认使用的也是http1.1,我们可以参考这个调用过程去探寻一下tcp连接和释放,整个生命周期是怎样的,feign使用的默认的Client.Default,请求方式HttpURLConnection,不是ApacheHttpClient,也不是OkHttpClient

    -------------------下面是使用feign实验的结果,均为debug出的结果,可能中间有些理解的不到位的地方--------------------

    先说一下大致流程,从feign.Client.Default#execute开始,

     public Response execute(Request request, Options options) throws IOException {
                HttpURLConnection connection = this.convertAndSend(request, options);
                return this.convertResponse(connection, request);
     }

    1. this.convertAndSend(request, options);准备connection的基本信息,比如连接超时时间,读取超时时间等等,此时并没有建立tcp连接

    2.this.convertResponse(connection, request);

      2.1 先调用HttpClient.New(..)获取一个可用的httpClient,这是一个静态方法,这个方法中会先去KeepAliveCache中查找是否有可用的httpClient,如果有的话直接拿过来用

      2.2 没有的情况下,调用HttpClient的构造,新建一个httpClient对象,这个构造方法的最后一行调用了openServer()方法,这个时候才会去真正的建立tcp连接

      2.3 有了连接,这个时候可以向server端写数据了,这个时候会调用HttpURLConnection的writeRequests,此方法会判断httpClient.isKeepAlive的,默认是true,所以在请求头中加上了Connection:keep-alive

      2.4 写数据完成之后,会调用HttpClient.parseHTTP(..)方法,去解析服务端响应的数据,包括响应头,此时如果响应头中包含了Connection:keep-alive,并且还设置了Keep-Alive:timeout=xx,max=xxx(client端的‘空闲’超时时间,默认5s,最多处理多少个请求,默认5个),会将这个值覆盖掉刚才的httpClient对象的keepAliveTimeout和keepAliveConnections属性

      2.5 读取完数据之后,最终会调用到httpClient.finished方法,划重点 ,这个地方是实现tcp连接复用的关键

    protected static KeepAliveCache kac = new KeepAliveCache();
    private static boolean keepAliveProp = true;

    ......
    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(); } } } 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); } }

     如果条件不成立,则直接close掉当前连接,就不会出现复用的情况了;反之会将当前对象存到KeepAliveCache中,KeepAliveCache继承了HashMap,本质上就是一个map,这里的key是host+port,跟前面说的浏览器是根据域名划分的好像是一致的(这个没有做深入的了解),我们看下KeepAliveCache的put操作都做了什么

       public synchronized void put(URL var1, Object var2, HttpClient var3) {
            boolean var4 = this.keepAliveTimer == null;
            if (!var4 && !this.keepAliveTimer.isAlive()) {
                var4 = true;
            }
    
            if (var4) {
                this.clear();
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        ThreadGroup var1 = Thread.currentThread().getThreadGroup();
    
                        for(ThreadGroup var2 = null; (var2 = var1.getParent()) != null; var1 = var2) {
                        }
    
                        KeepAliveCache.this.keepAliveTimer = new Thread(var1, KeepAliveCache.this, "Keep-Alive-Timer");
                        KeepAliveCache.this.keepAliveTimer.setDaemon(true);
                        KeepAliveCache.this.keepAliveTimer.setPriority(8);
                        KeepAliveCache.this.keepAliveTimer.setContextClassLoader((ClassLoader)null);
                        KeepAliveCache.this.keepAliveTimer.start();
                        return null;
                    }
                });
            }
    
            KeepAliveKey var5 = new KeepAliveKey(var1, var2);
            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);
            }
    
        }
    

    这几个类的关系和功能如下图所示,到此为止,为什么会复用,以及什么条件下可以复用基本上都明了了

    ------------延伸问题--------------------

    feign中默认使用的jdk1.8中的HttpClient,每个请求都会有一个httpClient,一个httpClient都持有一个tcp长连接,所以tcp长连接的复用,其实就是httpClient的复用

    1.首先是为什么feign调用会默认在请求头中加上Connection:keep-alive?

      sun.net.www.http.HttpClient

     static {
            String var0 = (String)AccessController.doPrivileged(new GetPropertyAction("http.keepAlive"));
            String var1 = (String)AccessController.doPrivileged(new GetPropertyAction("sun.net.http.retryPost"));
            String var2 = (String)AccessController.doPrivileged(new GetPropertyAction("jdk.ntlm.cache"));
            String var3 = (String)AccessController.doPrivileged(new GetPropertyAction("jdk.spnego.cache"));
            if (var0 != null) {
                keepAliveProp = Boolean.valueOf(var0);
            } else {
                keepAliveProp = true;
            }
    
            if (var1 != null) {
                retryPostProp = Boolean.valueOf(var1);
            } else {
                retryPostProp = true;
            }
            .......
    }

    可以看到这是取得一个系统配置http.keepAlive,如果没有增加或修改这个配置,默认就是keepAliveProp=true的,这个就是2.3中用来判断的其中一个条件

    2.tcp连接复用的话,到底是谁先close的,server还是client端?

      再上一篇中分析了springboot server 有一个connection-timeout的配置,默认是60s,就是client端请求完成之后,如果server端正常响应200,server端的org.apache.tomcat.util.net.NioEndpoint.Poller#timeout会判断当前的socket是否已超时,判断的依据是 (当前系统时间-最后一次读写的时间>connection-timeout时间),如果超时,就会close掉当前的socket,但是,在未达到超时时间时,通过命令行查看tcp的状态,发现服务端的端口状态是CLOSE_WAIT的,也就是说client已经主动关闭了连接,到底是什么时候在哪里关闭的连接?

    在流程的2.5中,在缓存httpClient时会在KeepAliveEntry中记录一下当前的系统时间,标记为idleStartTime,顾名思义,就是你开始闲着的时间,哈哈,2.5中的Keep-Alive-Timer线程的run方法会去判断此httpClient是不是已经闲够了,闲够了就把它close掉,这个时间默认是5s,可以通过流程2.4,在response中修改这个值,不管是不是用的缓存的httpClient,每次请求完成都会调用2.5的finished方法,所以这个idleStartTime每次都会更新的。所以现在client端的‘超时’时间是5s,server端的超时时间是60s,所以就会出现client端先close掉,然后server端一直等到60s才去close的情况,所以server端的这个60s是不是有点多余了。。。。。。

    ps: server端会判断即将响应的结果,如果是异常的,比如是以下的状态码,则会将scoket的状态标记为error,此时即使client设置了keepAlive,server也会自动close掉当前连接

     return status == 400 /* SC_BAD_REQUEST */ ||
                   status == 408 /* SC_REQUEST_TIMEOUT */ ||
                   status == 411 /* SC_LENGTH_REQUIRED */ ||
                   status == 413 /* SC_REQUEST_ENTITY_TOO_LARGE */ ||
                   status == 414 /* SC_REQUEST_URI_TOO_LONG */ ||
                   status == 500 /* SC_INTERNAL_SERVER_ERROR */ ||
                   status == 503 /* SC_SERVICE_UNAVAILABLE */ ||
                   status == 501 /* SC_NOT_IMPLEMENTED */;
    

    3. tcp连接复用的之http1.1和http2.0

    参考:https://blog.csdn.net/CrankZ/article/details/81239654

    http1.1的复用,是串行的,一个tcp连接,只能等一个请求完成才可以给另一个请求使用

    http2.0的复用,可以并行处理,增加了HttpStream,一个tcp连接中可以同时处理多个HttpStream(同步代码块实现的),但是只支持https,server端和client端都要做改动,只是目前了解到的一丢丢而已,后续再做补充

  • 相关阅读:
    GitHub 和 Gitee 开源免费 10 个超赞后台管理面板,看完惊呆了!
    LeetCode234.回文链表
    LeetCode104.二叉树的最大深度
    LeetCode142.环形链表II(链表中环的入口节点)
    云原生动态周刊:你订阅 GitHub README 播客了吗?
    云原生爱好者周刊:Crossplane 成为 CNCF 孵化项目
    凌晨 12 点突发 istio 生产事故!一顿操作猛如虎解决了
    新东方在有状态服务 In K8s 的实践
    面向无人驾驶 “云端大脑” 可用性的云原生实践
    Qunar 云原生容器化落地实践
  • 原文地址:https://www.cnblogs.com/Hleaves/p/11286173.html
Copyright © 2020-2023  润新知