zl程序教程

您现在的位置是:首页 >  后端

当前栏目

细说Http中的Keep-Alive和Java Http中的Keep-Alive机制

JAVAHTTP 机制 细说 keep alive
2023-09-11 14:19:34 时间

参考:https://blog.csdn.net/qq826654664jx/article/details/100864080

参考:https://blog.csdn.net/weixin_37672169/article/details/80283935

什么是 Keep-Alive

这个词看着有点熟,很多地方好像都见过。

TCP 的 KeepAlive,Http 的 KeepAlive,现在就连一些前端框架都有类似 KeepAlive 的东西了(比如 VUE.js,保持路由)。

本文介绍 HTTP 和 TCP 中的 KeepAlive 机制,其他方面不在本文讨论范围。

Http 中的 Keep-Alive

HTTP 持久连接(HTTP persistent connection,也称作 HTTP keep-alive 或 HTTP connection reuse,翻译过来可以是保持连接或者连接复用)是使用同一个 TCP 连接来发送和接收多个 HTTP 请求 / 应答,而不是为每一个新的请求 / 应答打开新的连接的方式。

HTTP 协议采用 “请求 - 应答” 模式,当使用普通模式,即非 KeepAlive 模式时,每个请求 / 应答客户和服务器都要新建一个连接,完成 之后立即断开连接(HTTP 协议为无连接的协议),每次请求都会经过三次握手四次挥手过程,效率较低;当使用Keep-Alive模式时,客户端到服务器端的连接不会断开,当出现对服务器的后继请求时,客户端就会复用已建立的连接。

下图是每次新建连接和连接复用在通信模型上的区别:

在 Http 1.0 中,Keep-Alive是没有官方支持的,但是也有一些 Server 端支持,这个年代比较久远就不用考虑了。

Http1.1 以后,Keep-Alive已经默认支持并开启。客户端(包括但不限于浏览器)发送请求时会在 Header 中增加一个请求头Connection: Keep-Alive,当服务器收到附带有Connection: Keep-Alive的请求时,也会在响应头中添加 Keep-Alive。这样一来,客户端和服务器之间的 HTTP 连接就会被保持,不会断开(断开方式下面介绍),当客户端发送另外一个请求时,就可以复用已建立的连接。

现在的 Http 协议基本都是 Http 1.1 版本了,不太需要考虑 1.0 的兼容问题

Keep-Alive 真的就这么完美吗

当然不是,Keep-Alive 也有自己的优缺点,并不是所有场景下都适用

优点

  • 节省了服务端 CPU 和内存适用量
  • 降低拥塞控制 (TCP 连接减少)
  • 减少了后续请求的延迟(无需再进行握手)

缺点

对于某些低频访问的资源 / 服务,比如一个冷门的图片服务器,一年下不了几次,每下一次连接还保持就比较浪费了(这个场景举的不是很恰当)。Keep-Alive 可能会非常影响性能,因为它在文件被请求之后还保持了不必要的连接很长时间,额外占用了服务端的连接数。

连接复用后会有什么问题

在没有连接复用时,Http 接收端(注意这里是接收端,并没有特指 Client/Server,因为 Client/Server 都同是发送端和接收端)只需要读取 Socket 中所有的数据就可以了,解决 “拆包” 问题即可;但是连接复用后,无法区分单次 Http 报文的边界,所以还需要额外处理报文边界问题。当然这个通过 Http 中 Header 的长度字段,按需读取即可解决。

粘包拆包的介绍可以参考另一篇文章细说 Netty 中的粘包和拆包

Http 连接复用后包边界问题处理

由于 Http 中 Header 的存在,通过定义一些报文长度的首部字段,可以很方便的处理包边界问题。

在 Http 中,有两种方式处理包边界问题:

Content-Length 处理包边界

这个是最通常的处理方式,接收端处理报文时首先读取完整首部(Header),然后通过 Header 中的Content-Length来确认报文大小,读取报文时按此长度读取即可,超出长度的报文(“粘包”)不读取,不够长度的报文缓存等待继续读取(“拆包”)。

Chunked 处理包边界

对于无法确认总报文大小的情况,可以使用 Chunked 的方式来对报文进行分块传输,每一块内标示报文大小。比如 Nginx,开启 Gzip 压缩后,就会开启 Chunked 的传输方式。

通过 Wireshark 抓包,可以很直观的看初 Chunked 的原理:

注意,这里的 chunk 包,和 tcp segment 不是一回事,chunk 只是应用层的一个分包,而 tcp 的 segment 是对应用层报文再次进行分组

每个 chunk 报文前,会携带当前 chunk 的大小。

Http 连接复用后怎样断开连接

通过 Keep-Alive 已经做到连接复用了,但复用之后什么时候断开连接呢,不然一直保持连接,造成资源的浪费。

Http 协议规定了两种关闭复用连接的方式:

通过 Keep-Alive Timeout 标识

如果服务端 Response Header 设置了Keep-Alive:timeout={timeout},客户端会就会保持此连接 timeout(单位秒)时间,超时之后关闭连接。

现在在服务端设置响应 Header:

Keep-Alive:timeout=5

通过 Wireshark 来看下配置了 timeout 的效果:

从上图可以看出,客户端发送请求后,在 15S 内(图上没有体现时间,就当 15S 吧)保持了连接不销毁,超时后经过了 4 次挥手,断开连接

但是如果在 15S 内再次请求,连接是可以复用的,不会重新 3 次握手。

下图是 15S 内再次请求的效果:

通过 Connection close 标识

还有一种方式是接收端通在 Response Header 中增加Connection close标识,来主动告诉发送端,连接已经断开了,不能再复用了;客户端接收到此标示后,会销毁连接,再次请求时会重新建立连接。

注意:配置 close 配置后,并不是说每次都新建连接,而是约定此连接可以用几次,达到这个最大次数时,接收端就会返回 close 标识(服务端配置方法下面会介绍)

下面来测试下效果,客户端发送两次请求:

通过 wireshark 截图可以发现,配置了 Connection:close 之后 (服务端设置了请求只可以用 1 此,所所以请求完成就销毁连接),两次请求都重新建立了连接。

Nginx 中设置 Keep-Alive(服务端)

Keep-Alive timeout 配置:

Syntax:     keepalive_timeout timeout [header_timeout];
Default:    keepalive_timeout 75s;
Context:    http, server, location

第一个参数设置一个超时,在此期间保持活动的客户机连接将在服务器端保持打开状态。如果为0则禁用保Keep-Alive。第二个可选参数在“Keep-Alive: timeout=time”响应头字段中设置一个值。

“Keep-Alive: timeout=time”报头字段被Mozilla和Konqueror识别。MSIE在大约60秒内自动关闭保持连接。

Keep-Alive requests(连接可用次数)配置:

Syntax:     keepalive_requests number;
Default:    keepalive_requests 100;
Context:    http, server, location

设置通过一个保持活动连接可以服务的请求的最大数量。在发出最大数量的请求之后,连接关闭。

Tomcat 中设置 Keep-Alive(服务端)

<Connector>标签中配置属性:

Keep-Alive timeout 配置:

keepAliveTimeout="超时时间",默认值是使用为 connectionTimeout 属性设置的值 。值为 - 1 表示没有(即无限)超时。

Keep-Alive requests(连接可用次数)配置:

maxKeepAliveRequests="连接可用次数",-1 为永不失效。如果未指定,默认为 100。

例如:

<Connector port="8080" 
    protocol="HTTP/1.1" 
    connectionTimeout="20000" 
    redirectPort="8443" 
    keepAliveTimeout="超时时间(单位秒)"
    maxKeepAliveRequests="连接可用次数" />

Apache HttpClient 设置 Keep-Alive(客户端)

Apache HttpClient 算是 Java 中最强的 HttpClient 了,也是最主流的(后端方向),功能强大。
Apache HttpClient 在处理 KeepAlive 的地方设计的比较灵活,提供了可配置的接口,使用者可以使用 Http 标准的策略,也自定定制策略。

1 HttpClients.custom()
2                 //连接是否复用策略,通过此策略返回是否复用
3                 //DefaultClientConnectionReuseStrategy是默认的Http策略,不设置也可以
4                 .setConnectionReuseStrategy(new DefaultClientConnectionReuseStrategy())
5                 //连接复用后有效期(持久时间)策略,复用后通过此策略判断复用超时时间
6                 //DefaultConnectionKeepAliveStrategy是默认的判断超时时间策略,读取的是Keep-Alive:timeout=超时时间
7                 .setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy())
8                 .build();

这里顺带说一下 Apache HttpClient 的使用,希望能帮助到有需要的人。(版本 Apache HttpClient 4.x)

 1 //创建客户端,此客户端最好保持单例,这是个线程安全的类,并发下也没有问题。
 2 //HttpClient中的连接池等组件都包含在内,如果每次都新建的话,
 3 //效率低,占用资源大,连接复用当然也不会生效了。
 4 HttpClients.custom()
 5                 //禁用自动重试,默认有3次的重试策略
 6                 .disableAutomaticRetries()
 7                 //不用默认的重试策略,自定义
 8                 .setRetryHandler()
 9                 //设置默认请求配置,这里可以配置一些超时时间等参数    
10                 .setDefaultRequestConfig(requestConfig())
11                 //全局Header,每次请求都会携带
12                 .setDefaultHeaders()
13                 //当Https证书不受信任的时候,记得自定义此项
14                 .setSSLHostnameVerifier()
15                 //设置UA
16                 .setUserAgent()
17                 //设置代理
18                 .setProxy()
19                 //...还有很多配置,可以自行查阅文档
20                 .build();

TCP 中的 Keep-Alive

TCP 中的 KeepAlive 和 Http 的 Keep-Alive 可不是一回事,HTTP 中是做连接复用的,而 TCP 中的 KeepAlive 是 “心跳监测”,定时发送一个空的 TCP Segment,来监测连接是否存活。下面介绍下 Java 中设置 TCP KeepAive 的一些方式。

Netty 中设置 Keep-Alive

1 bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);

NIO(New NetWorking IO Lib)中设置 Keep-Alive

1 channel.setOption(StandardSocketOptions.SO_KEEPALIVE,true);

BIO 中设置 Keep-Alive

1 Socket socket = serverSocket.accept();
2 socket.setKeepAlive(true);

HTTP和TCP的Keep-Alive timeout区别

Keep-Alive timeout:

Httpd守护进程,一般都提供了keep-alive timeout时间设置参数。比如nginx的keepalive_timeout,和Apache的KeepAliveTimeout。这个keepalive_timout时间值意味着:一个http产生的tcp连接在传送完最后一个响应后,还需要hold住keepalive_timeout秒后,才开始关闭这个连接。
当httpd守护进程发送完一个响应后,理应马上主动关闭相应的tcp连接,设置 keepalive_timeout后,httpd守护进程会想说:”再等等吧,看看浏览器还有没有请求过来”,这一等,便是keepalive_timeout时间。如果守护进程在这个等待的时间里,一直没有收到浏览器发过来http请求,则关闭这个http连接。

Tcp的Keepalive:

连接建立之后,如果客户端一直不发送数据,或者隔很长时间才发送一次数据,当连接很久没有数据报文传输时如何去确定对方还在线,到底是掉线了还是确实没有数据传输,连接还需不需要保持,这种情况在TCP协议设计中是需要考虑到的。
TCP协议通过一种巧妙的方式去解决这个问题,当超过一段时间之后,TCP自动发送一个数据为空的报文(侦测包)给对方,如果对方回应了这个报文,说明对方还在线,连接可以继续保持,如果对方没有报文返回,并且重试了多次之后则认为链接丢失,没有必要保持连接。

tcp keep-alive是TCP的一种检测TCP连接状况的保鲜机制。tcp keep-alive保鲜定时器,支持三个系统内核配置参数:
net.ipv4.tcp_keepalive_intvl = 15
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_time = 1800
keepalive是TCP保鲜定时器,当网络两端建立了TCP连接之后,闲置(双方没有任何数据流发送往来)了tcp_keepalive_time后,服务器就会尝试向客户端发送侦测包,来判断TCP连接状况(有可能客户端崩溃、强制关闭了应用、主机不可达等等)。如果没有收到对方的回答(ack包),则会在 tcp_keepalive_intvl后再次尝试发送侦测包,直到收到对方的ack,如果一直没有收到对方的ack,一共会尝试 tcp_keepalive_probes次,每次的间隔时间在这里分别是15s, 30s, 45s, 60s, 75s。如果尝试tcp_keepalive_probes,依然没有收到对方的ack包,则会丢弃该TCP连接。TCP连接默认闲置时间是2小时,一般设置为30分钟足够了。