首先恭喜LZ已经毕业快半年了,好久没写博文了,趁着最近要学习一下http连接池,于是来更更文。(LZ是个小白,很多都是照搬网上的)
关于HTTP协议,之前几乎没有接触,也不是很明白。半路直接跳到连接池,可能有点跳,不过应该可以慢慢理解,先跳一下问题也不大。
下图来自上述链接:
我们可以看到串行连接时,客户端每一次发送请求便会打开一个连接,然后响应结束之后关闭连接。这种情况下,连接的建立和销毁都会消耗很多额外资源。而持久连接,则是第一次请求到达时会打开连接,随后连接不会被关闭,等到下一次请求到达。
在http1.0时,采用keep-alive的方式标识这个请求会一直被保持,然而可能一些代理服务器不支持keep-alive。在http1.1之后,采用了一个默认的行为,即所有的请求均是默认keep-alive,除非你配置了connection:close,当然你的持久请求也可能也可能会被关闭。
同线程池概念差不多,连接池也是采用了“池”的概念达到了复用:
- 当有连接第一次使用的时候建立连接
- 结束时对应连接不关闭,归还到池中
- 下次同个目的的连接可从池中获取一个可用连接
- 定期清理过期连接
那讲到这里,我会先停住讲解http连接池,转到restTemplate如何集成http连接池,再回过头来讲http连接池底层复用原理。
首先,LZ搭建了一个server和customer两个项目,一个做为接收请求的服务方,一个做为发送请求的客户端。
基于强大的springboot的注解,我们server端需要做的便是,配置配置文件。
首先查看restTemplate的官方API,我们会发现restTemplate提供了很多调用方法,例如:getForObject、postForObject等等,而由于LZ项目中基本上只涉及到Get和Post方法,并且LZ习惯性是Post居多,所以只以Post举例。
restTemplate.postForEntity的方法内部,核心方法为doExecute方法,该方法内部核心为createRequest()
这个方法含义为:通过url和httpMethod创建一个http请求。
这个getRequestFactory()默认获取到的是SimpleClientHttpRequestFactory。那这个便是用于获取到连接的工厂,之前查到过用
HttpComponentsClientHttpRequestFactory功能更强大,但是找不到文章在哪里了,大家可以找一找告诉LZ一下。
那这个工厂可以通过springboot注解形式注入,
LZ使用的就是HttpComponentsClientHttpRequestFactory,而在factory里面注入了httpClient,这个httpClient()方法内部的配置便是关于httpClient连接池配置的最关键的部分了。httpClient内部配置了关于重试机制、保持连接策略、连接池管理类和请求的一些参数(超时时间等等)。
具体的就不列了,大家可以参照
回到createRequest这个方法,这个方法获取到一个ClientHttpRequest,随后调用核心方法response = request.execute();
这个方法内部核心方法是executeInternal中的execute,
我们在之前配置restTemplate时,往其内注入了httpClient,(CloseableHttpClient)。OK,从这里开始,我们回到起点,开始讲解http连接池的底层调用。
(源码太长,全部截图不了╮(╯▽╰)╭,我就只弄核心的了)
标红处:通过PoolingHttpClientConnectionManager的requestConnection去获得一个连接的请求,注意这里是连接的请求,而非连接。
在配置restTemplate时,LZ用的是连接管理类是PoolingHttpClientConnectionManager,故下面源码来着这个类。emmmm,LZ怕图太多了很尴尬,就不贴了。
PoolingHttpClientConnectionManager类的requestConnection内部的核心方法有两个:
1、 final Future<CPoolEntry> future = this.pool.lease(route, state, null);
2、final HttpClientConnection conn = leaseConnection(future, timeout, tunit);
第一个方法可以字面理解就是从这个池中租一个CPoolEntry,这个方法里面其实可以理解起来LZ觉得可以套用线程池:
核心方法getPoolEntryBlocking:
首先加锁,之后通过你的route(IP+PORT)获取到关于这个路由的一个连接池,所以,http连接池是一个池中池。然后在获取到的这个路由的池中获得一个entry,如若entry不为null,则该entry从可使用的List中移除,加入到被租借的Set的中。随后如若entry为null,则判断当前route池中的最大连接数是否(LRU算法,emmmm这个算法LZ就撤了),即关闭连接,从可使用List中移除,并从pool中移除。
而如若没有超过上限,则根据route创建一个连接,并将这个连接放入池中和大池中。而在上述执行了LRU算法,还是没有获得有效连接,并且想要自建时发现超过了该route连接池的最大值时,则将这个futrue放入等待队列(大池和小池)。最后等待通知看是否超时,如若未超时或被取消,则重新循环。
以上描述,为第一个方法获取到这个route的一个连接,随后将这个获取到的entry通过代理的方式传回用以获得HttpClientConnection。emmm么有贴图了大家将就对着LZ写的看看源码。。。或者参照最开始的那个链接,终极详细。。
那在连接被使用了之后,我们就需要判断这个连接是否需要被复用了。源码如下:
注释英文也解释了,下面的方式是用来判断一个连接是否要关闭还是重用。核心方法是reuseStrategy.keepAlive(response, context),
是否复用规则如下:
- 如果request首部中包含Connection:Close,不复用
- 如果response中Content-Length长度设置不正确,不复用
- 如果response首部包含Connection:Close,不复用
- 如果reponse首部包含Connection:Keep-Alive,复用
- 都没命中的情况下,如果HTTP版本高于1.0则复用
随后, 我们会去清理过期的连接,
- 只有在HttpClientBuilder手动设置后,才会开启清理过期与空闲连接
- 手动设置后,会启动一个线程死循环执行,每次执行sleep一定时间,调用HttpClientConnectionManager的清理方法清理过期与空闲连接。
源码就不贴了,LZ只能对着博客看看,水平有限。
故总结如下:restTemplate底层实现httpClient,封装了关于http连接池的一系列方法,而我们只需要通过配置文件的形式设置超时时间,最大连接数、每路由的最大连接数等等即可。而达到复用是通过连接的route(IP+PORT)从每个route的池中获得连接,如若超过数量,则创建或等到,知道超时。