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端都要做改动,只是目前了解到的一丢丢而已,后续再做补充
2.jdk1.8+springboot中http1.1之tcp连接复用实现的更多相关文章
- SpringBoot中使用hikariCP
本篇文章主要实现SpringBoot中使用hikariCP: 一 .使用工具 1. JDK1.8 2. springToolSuit(STS) 3. maven 二.创建项目 1.首先创建一个Spri ...
- springboot中json转换LocalDateTime失败的bug解决过程
环境:jdk1.8.maven.springboot 问题:前端通过json传了一个日期:date:2019-03-01(我限制不了前端开发给到后端的日期为固定格式,有些人就是这么不配合), ...
- SpringBoot中BeanValidation数据校验与优雅处理详解
目录 本篇要点 后端参数校验的必要性 不使用Validator的参数处理逻辑 Validator框架提供的便利 SpringBoot自动配置ValidationAutoConfiguration Va ...
- SpringBoot中yaml配置对象
转载请在页首注明作者与出处 一:前言 YAML可以代替传统的xx.properties文件,但是它支持声明map,数组,list,字符串,boolean值,数值,NULL,日期,基本满足开发过程中的所 ...
- 如何在SpringBoot中使用JSP ?但强烈不推荐,果断改Themeleaf吧
做WEB项目,一定都用过JSP这个大牌.Spring MVC里面也可以很方便的将JSP与一个View关联起来,使用还是非常方便的.当你从一个传统的Spring MVC项目转入一个Spring Boot ...
- springboot中swaggerUI的使用
demo地址:demo-swagger-springboot springboot中swaggerUI的使用 1.pom文件中添加swagger依赖 2.从github项目中下载swaggerUI 然 ...
- JAVA帮助文档全系列 JDK1.5 JDK1.6 JDK1.7 官方中英完整版下载
JAVA帮助文档全系列 JDK1.5 JDK1.6 JDK1.7 官方中英完整版下载JDK(Java Development Kit,Java开发包,Java开发工具)是一个写Java的applet和 ...
- spring-boot+mybatis开发实战:如何在spring-boot中使用myabtis持久层框架
前言: 本项目基于maven构建,使用mybatis-spring-boot作为spring-boot项目的持久层框架 spring-boot中使用mybatis持久层框架与原spring项目使用方式 ...
- 由浅入深学习springboot中使用redis
很多时候,我们会在springboot中配置redis,但是就那么几个配置就配好了,没办法知道为什么,这里就详细的讲解一下 这里假设已经成功创建了一个springboot项目. redis连接工厂类 ...
随机推荐
- Oracle 调试存储过程
调试过程对找到一个存过的bug或错误是非常重要的,Oracle作为一款强大的商业数据库,其上面的存过少则10几行,多则上千行,免不了bug的存在,存过上千行的话,找bug也很费力,通过调试可以大大减轻 ...
- IEAD工具教你创建maven项目
之前一直用的是其他的开发工具,maven到目前为止也就用了3个月,今天又时间整理一些初期的使用方法,仅供参照. 为什么要用maven 原因很简单,因为使用maven,会使得项目非常容易管理. 举个例子 ...
- python 中startswith()和endswith() 方法
startswith()方法 Python startswith() 方法用于检查字符串是否是以指定子字符串开头如果是则返回 True,否则返回 False.如果参数 beg 和 end 指定值,则在 ...
- 为什么有了uwsgi还要nginx这个“前端”服务器
相信每一个使用nginx+uwsgi+django部署过的人,都感到非常复杂.到底为什么一个项目的发布要经过这么多层级,他们每一层有什么理由存在?这就带大家宏观地看待一下 首先nginx 是对外的服务 ...
- miguowangluozhan
加紧备战 美国欲将全球拖入网络战争 人民日报 06-1405:01 去年,美国国防部发布的网络空间战略强调了“前沿防御(Defense forward)”理念.这被外界解读为美国军方将在他国而非美国本 ...
- 地沟油jiance
与地沟油较劲九年(我和我的祖国) 任 飞 2019年06月13日05:00 来源:人民网-人民日报 分享到: 地沟油中含有多种有毒有害物质,但地沟油的检测却是一大难题.单靠眼睛谁也无法判断 ...
- macos下简单的socket服务器+客户端
TCP客户端服务器编程模型: 服务器: 调用socket函数创建套接字 调用bind绑定本地IP和端口 调用listen启动监听(准备好接收客户端链接的队列) 调用accept从已连接队列中提取第一个 ...
- PAT Advanced 1152 Google Recruitment (20 分)
In July 2004, Google posted on a giant billboard along Highway 101 in Silicon Valley (shown in the p ...
- 使用PyInstaller将.py文件打包并生成Windows下可执行的.exe文件
最近要使用Qt写一个简单的GUI程序,因此使用了PyQt5来加快开发,使用PyQt5生成可执行的程序时,在Windows操作系统下可以使用pyinstaller库将.py文件及其相关依赖生成为.exe ...
- JavaScript教程——函数(arguments 对象)
arguments 对象 定义 由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数.这就是arguments对象的由来. arguments对象包含了 ...