HTTP协议中的keep-alive

2023-09-26  本文已影响0人  程序员札记

为什么HTTP是短连接

众所周知,HTTP是短连接,client向server发送一个request,得到response后,连接就关闭。之所以这样设计使用,主要是考虑到实际情况。例如,用户通过浏览器访问一个web站点上的某个网页,当网页内容加载完毕之后,用户可能需要花费几分钟甚至更多的时间来浏览网页内容,此时完全没有必要继续维持底层连。当用户需要访问其他网页时,再创建新的连接即可。

因此,HTTP连接的寿命通常都很短。这样做的好处是,可以极大的减轻服务端的压力。一般而言,一个站点能支撑的最大并发连接数也是有限的,面对这么多客户端浏览器,不可能长期维持所有连接。每个客户端取得自己所需的内容后,即关闭连接,更加合理。

为什么要引入keep-alive

通常一个网页可能会有很多组成部分,除了文本内容,还会有诸如:js、css、图片等静态资源,有时还会异步发起AJAX请求。只有所有的资源都加载完毕后,我们看到网页完整的内容。然而,一个网页中,可能引入了几十个js、css文件,上百张图片,如果每请求一个资源,就创建一个连接,然后关闭,代价实在太大了。

基于此背景,我们希望连接能够在短时间内得到复用,在加载同一个网页中的内容时,尽量的复用连接,这就是HTTP协议中keep-alive属性的作用。

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

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


image.png

在 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 也有自己的优缺点,并不是所有场景下都适用

优点

缺点

对于某些低频访问的资源 / 服务,比如一个冷门的图片服务器,一年下不了几次,每下一次连接还保持就比较浪费了(这个场景举的不是很恰当)。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 的原理:


image.png

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

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


image.png

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

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

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

通过 Keep-Alive Timeout 标识

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

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

Keep-Alive:timeout=15

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


image.png

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

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

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

image.png

通过 Connection close 标识

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

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


image.png

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

image.png

通过 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(客户端)

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

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

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

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

server端如何处理keep-alive

对于客户端来说,不论是浏览器,还是手机App,或者我们直接在Java代码中使用HttpUrlConnection,只是负责在请求头中设置Keep-Alive。而具体的连接复用时间的长短,通常是由web服务器控制的。

这里有个典型的误解,经常听到一些同学会说,通过设置http的keep-alive来保证长连接。通常我们所说的长连接,指的是一个连接创建后,除非出现异常情况,否则从应用启动到关闭期间,连接一直是建立的。例如在RPC框架,如dubbo,服务的消费者在启动后,就会一直维护服务提供者的底层TCP连接。

在HTTP协议中,Keep-Alive属性保持连接的时间长短是由服务端决定的,通常配置都是在几十秒左右。 例如,在tomcat中,我们可以server.xml中配置以下属性:

<Connector port="8080" 
    protocol="HTTP/1.1" 
    connectionTimeout="20000" 
    redirectPort="8443" 
    keepAliveTimeout="60000"
    maxKeepAliveRequests="100" />

说明如下:

当然,这不是所有内容,在一些异常情况下,keepalive也会失效。tomcat会根据http响应的状态码,判断是否需要丢弃连接(笔者这里看的是tomcat 9.0.19的源码)。

org.apache.coyote.http11.Http11Processor#statusDropsConnection

image.png

另外,值得一提的是,Tomcat 7版本支持三种运行模式:NIO、BIO、APR,且默认在BIO模式下运行。由于每个请求都要创建一个线程来处理,线程开销较大,因此针对BIO,额外提供了一个disableKeepAlivePercentage参数,根据工作线程池中繁忙线程数动态的对keepalive进行开启或者关闭:


image.png

由于Tomcat 8版本之后,废弃了BIO,默认在NIO模式下运行,对应的也取消了这个参数。

Anyway,我们知道了,在HTTP协议中keep-alive的连接复用机制主要是由服务端来控制的,笔者也不认为其实真正意义上的长连接。

1.4 JDK对keep-alive的支持

前文讲解了HTTP协议中,以tomcat为例说明了server端是如何处理keepalive的。但这并不意味着在client端,除了设置keep-alive请求头之外,就什么也不用考虑了。

在客户端,我们可以通过HttpUrlConnection来进行网络请求。当我们创建一个HttpUrlConnection对象时,其底层实际上会创建一个对应的Socket对象。我们要复用的不是****HttpUrlConnection,而是底层的Socket

下面这个案例,演示了同时创建5个HttpUrlConnection,然后通过netstat命令观察socket连接信息

image.png

运行这段代码,然后通过netstat命令观察tcp的socket连接信息

image.png

可以看到,当我们创建5个HttpUrlConnection后,底层的确创建了对应数量的TCP socket连接。 其中,192.168.1.3是本机ip,220.181.57.216是服务端ip。

当然,我们的重点是Java如何帮我们实现底层socket链接的复用。JDK对keepalive的支持是透明的,keepAlive默认就是开启的。我们需要做的是,学会正确的使用姿势。

参见:https://docs.oracle.com/javase/8/docs/technotes/guides/net/http-keepalive.html

1.  When the application finishes reading the response body or when the application calls close() 
2.  on the InputStream returned by URLConnection.getInputStream(), 
3.  the JDK's HTTP protocol handler will try to clean up the connection and if successful, 
4.  put the connection into a connection cache for reuse by future HTTP requests.

这段话的含义是:当通过URLConnection.getInputStream()读取响应数据之后(在这里是HttpUrlConnection),应该调用InputStream的close方法关闭输入流,JDK http协议处理器会将这个连接放到一个连接缓存中,以便之后的HTTP请求进行复用。

翻译成代码,当发送一次请求,得到响应之后,不是调用HttpURLConnection.disconnect方法关闭,这回导致底层的socket连接被关闭。我们应该通过如下方式关闭,才能进行复用:

1.  InputStream in=HttpURLConnection.getInputStream();
2.  //处理
3.  in.close()

这里并不打算提供完整的代码,官方已经给出的了代码示例,可参考上述链接。在实际开发中,通常是一些第三方sdk,如http-client、ok-http、RestTemplate等。

需要说明的是,只要我们的使用姿势正确。JDK对keepalive的支持对于我们来说是透明的,不过jdk也提供了相关系统属性配置来控制keeplive的默认行为,如下:

image.png

说明:

最后,尽管你可能不直接使用HttpUrlConnection,习惯于使用http-client、ok-http或者其他第三方类库。但是了解JDK原生对keep-alive的支持,也是很重要的。首先,你在看第三方类库的源码时,可能就利用到了这些特性。另外,也许你可以干翻面试官。

2 TCP协议中的keep-alive

首先介绍一下HTTP协议中keep-alive与TCP中keep-alive的区别:

回到TCP keep-alive探针,对于一方发起的keepalive探针,另一方必须响应。响应可能是以下三种形式之一:

用man命令,可以查看linux的tcp的参数:

  1. man 7 tcp

</pre>

其中keep-alive相关的配置参数有三个:

image.png

其中:

这些的默认配置值在/proc/sys/net/ipv4 目录下可以找到,文件中的值,就是默认值,可以直接用cat来查看文件的内容 。


1.  $ ls /proc/sys/net/ipv4 | grep tcp_keepalive
2.  tcp_keepalive_intvl
3.  tcp_keepalive_probes
4.  tcp_keepalive_time

可以通过sysctl命令来查看和修改:


1.  # 查询
2.  cat /proc/sys/net/ipv4/tcp_keepalive_time
3.  #修改
4.  sysctl net.ipv4.tcp_keepalive_time=3600

可以看到,TCP中的SO_KEEPALIVE是一个开关选项,默认关闭,需要在应用程序需要代码中显式的开启。当开启之后,在通信双方没有数据传输时,操作系统底层会定时发送keepalive探测包,以保证连接的存活。

一些编程语言支持在代码层面覆盖默认的配置。在使用Java 中,我们可以通过Socket设置keepAlive为true:


1.  Socket socket=new Socket();
2.  socket.setKeepAlive(true);//开启keep alive
3.  socket.connect(new InetSocketAddress("127.0.0.1",8080));

然而,tcp的keep-alive机制,说实话,有一些鸡肋:

基于此,我们需要加上应用层的心跳。应用层的心跳的作用,取决于你想干啥。笔者理解:

Netty中也提供了IdleSateHandler,来支持心跳机制。笔者的建议是,如果仅仅只是配置了IdleSateHandler,保证连接可用。有精力的话,server端也加上一个连接监控信息可视化的功能。

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

Netty 中设置 Keep-Alive

bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);

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

channel.setOption(StandardSocketOptions.SO_KEEPALIVE,true);

BIO 中设置 Keep-Alive

 Socket socket = serverSocket.accept();
 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分钟足够了。

上一篇下一篇

猜你喜欢

热点阅读