HTTP协议(三):连接管理

2019-11-09 21:50  |  分类:理论知识  |  标签: HTTP浏览器原理

摘要:上一篇文章我们细致讲了HTTP的URL、资源、HTTP报文相关的内容。这一篇文章讲下HTTP的连接管理相关的内容

TCP连接

简介

几乎所有HTTP通信都是由TCP/IP承载,TCP/IP是全球计算机及网络设备都在使用的一种常用的分组交换网络分层协议集。客户端可以打开一条TCP/IP连接。连接到可能运行在任意地方的服务器应用程序。一旦客户端和服务器连接建立起来了,那么两者之间交换的报文就永远不会丢失、受损或失序。

比如,你访问某个URL时:

http://fishelly.top/article/list

  • 首先浏览器由解析出主机名(www.fishelly.top)
  • 浏览器根据主机名获取IP地址 (DNS:118.24.35.159 )
  • 浏览器获取端口号(80)
  • 浏览器发起到118.24.35.159端口80的链接
  • 浏览器向服务器发送一条HTTP GET报文
  • 浏览器从服务器读取HTTP 响应报文
  • 浏览器关闭连接

TCP可靠的数据通道

TCP为HTTP提供了一条可靠的比特传输管道。从TCP连接一段填入的字节会从另一端以原有的顺序、正确的传输过来。TCP会按需,无差错的承载HTTP数据。

TCP流是分段的,由IP分组传送

首先,TCP数据是通过IP分组的小数据块发送的。分组包括:

  • 一个IP分组首部(通常为20个字节),包含了源和目的IP地址、长度和其他一些标记
  • 一个TCP段首部(通常为20个字节),TCP端口号、TCP控制标记、以及用于数据排序和完整性检查的一些数字值。
  • 一个TCP数据块(0个或多个字节)

HTTP发送报文时。会以流的形式将报文数据的内容通过打开一条TCP连接按需传输。TCP收到数据流后,会将其分成被称为段的小数据块,将在段封装到IP分组中,通过网络传输。

TCP段图

TCP套接字编程

见图:

TCP套接字编程

TCP性能

HTTP是由TCP承载的,位于其上层,所以HTTP事务的性能在很大程度上取决于其底层TCP通道的性能

HTTP事务的时延

HTTP事务时延主要由以下几个原因

  • 客户端需要根据URL来确定服务器的IP地址和端口号。这里就是一个DNS解析的过程。而DNS解析的时间是不确定的,快则毫秒内,慢则数十秒。
  • 客户端会向服务器发起TCP连接,并等待服务器返回一个接受答应。每条TCP连接都会存在建立时延,如果存在很多的HTTP事务,则这个建立时延就会叠加上去。
  • 连接建立,客户端发送HTTP请求,Web服务器解析请求,并进行响应。这一个过程也需要花费一定的时间。

TCP网络时延大小取决于硬件速度,网络和服务器负载、请求和响应报文的尺寸,以及客户端和服务器的距离。

影响TCP性能的因素

主要包括

  • TCP连接建立握手
  • TCP慢启动拥塞控制
  • 数据聚焦的Nagle算法
  • 用于捎带确认的TCP延迟确认算法
  • TIME_WAIT时延和端口耗尽

TCP握手时延

首先,TCP连接握手,需要经过以下步骤:

  • 请求新的TCP链接时,客户端向服务器发送一个小的TCP分组,这个分组设置了一个SYN的特殊标记,说明是一个连接请求。(下图 a步)
  • 服务器接受此连接,就会对一些连接参数进行计算,并向客户端返回一个TCP分组,这个分组中的SYN和ACK标记都被置位,说明连接请求已被接受(下图 b步)
  • 最后客户端向服务器再次发送一个确认信息,通知她连接成建立(下图 c步)

TCP三次握手

通过上面的步骤我们可以知道,在SYN\SYN+ACK握手中会产生一个可测量的时延。TCP连接的ACK分组通常足够大,可以承载整个HTTP请求报文,而且很多HTTP服务器响应报文都可以放入一个IP分组去。

延迟确认

由于因特网自身无法保证可靠的分组传输(在超载的情况下可以随意丢弃分组),所以TCP实现了自己的确认机制来确保数据的成功传输。

每个TCP段都有一个序列号和数据完整性校验和。每个段的接受者在收到完整的段后,都会向发送者回送小的确认分组。如果发送者在指定的窗口时间内没有收到确认分组信息,发送者就认为分组已不完整,并重发数据。

由于确认报文很小,因此TCP运行发往相同方向的输出数据分组中对齐进行“携带”。TCP将返回的确认信息与输出数据分组结合在一起,更有效的利用网络资源。

为了增加确认报文找到同向传输数据分组的可能性,很多TCP栈都实现了一种“延迟确认”算法。延迟确认算法会在一个特定的窗口时间(一般是100 ~ 200毫秒)内将输出确认存放在缓冲区中,以寻找能够携带它的输出数据分组。如果在此个窗口时间内未找到可以携带的输出数据分组,则会将确认信息单独分组发送。

但是,由于HTTP具有双峰特征的请求 - 答应行为降低了携带信息的可能,因此延迟确认算法会引入相当大的时延。

TCP慢启动

TCP数据传输的性能还取决于TCP链接的使用期。TCP连接会随着时间进行自我“调谐”,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度,这种机制被称为TCP的慢启动,用于防止因特网的突然过载和拥塞。

TCP慢启动限制了一个TCP端点在任意时刻可以传输的分组数。换句话说,就是每成功接收一个分组,发送端就会有发送另外2个分组权限。如果某个HTTP事物有大量数据需要传输发送,是不可能一次性发送出去的。必须发送一个分组,等待确认,然后可以发送2个分组,每个分组都必须被确认,这样就可以发送4个分组,以此类推。这样的方式被称为“打开拥塞窗口”。

Nagle算法与TCP_NODELAY

TCP数据分组中即时放入1字节的数据也是可以的。因此如果发送大量包含少量的数据的分组,会造成网络性能严重下降的问题。

Nagle算法试图在发送一个分组之前,将大量TCP数据绑定在一起发送,以提升网络效率。其鼓励发送全尺的段。只有当所有其他分组都被确认之后,Nagle算法才允许发送非全尺寸的段。如果其他分组仍然在传输的过程中,就会将那部分数据缓存起来,只有当挂起的分组被确认了,或者缓存数据累计到足够发送全尺寸的段时,才会将缓存数据发送出去。

很明显,这样强制发送全尺寸端的行为,会造成HTTP性能问题。首先小的HTTP报文可能无法填满一个分组,这时候会等待那些永远不会到来的额外数据而产生一个时延。其次,Nagle算法会阻止数据的发送,直到有确认分组到达,但确认分组自身会被延迟算法延迟确认。

可以在TCP设置中设置TCP_NODELAY来禁止Nagle算法。

TIME_WAUT累积与端口耗尽

当某个TCP端口关闭TCP连接时,会在内存中维护一个小的控制块,用来记录最近所关闭连接的IP地址和端口号。这类信息指挥维持一小段时间,通常为所估计的最大分段时间的2倍左右(称为2MSL,通常是2分钟),以确保在这段时间内不会创建具有相同地址和端口号的新连接。

由于有上面这种机制存在,那么就会存在一个问题:由于客户端每次连接到服务器时,都会获得一个新的端口来保证连接的唯一性,但是由于端口数量是有限的(65535个),且在2MSL时间内,服务器无法重复相同地址的连接,因此连接率被限制在 65535/120≈546次/秒,如果超过这个数就会造成端口耗尽,导致异常产生。

HTTP Connection首部和串行时延

Connection首部

由于HTTP程序和最终服务器之间可以存在一系列的HTTP中间件(如:代理,高速缓存等),HTTP报文会一层层经过最终将报文传送到服务器中。

HTTP的Connection首部有一个以逗号分隔的连接标签列表,这些标签为此连接指定了一些不会传播到其他连接中去的选项。

Connection可以承载3种不同类型的标签

  • HTTP首部字段名,列出了只与此连接有关的首部
  • 任意标签值,用于描述此链接的非标准选项
  • 值为close,表示操作完成后关闭这条持久连接

HTTP应用程序收到带有Connection首部的报文时,接收端会解析发送端的所有选项,然后在将此报文转发到下一层之前,会删除Connection首部以及其列出的所有首部。

串行事务处理时延

串行的HTTP事务会由于TCP连接时延和慢启动时延导致其自身时延慢慢的累加起来。比如,加载嵌有多张图片的web页面。浏览器就需要发起多个请求来显示页面,一个用于顶层的HTML页面,其余的用于显示的图片。如果此时是串行的建立新的连接,这样势必会影响性能,影响用户体验。

HTTP并行连接

HTTP客户端运行打开多条连接,并行地执行多个HTTP事务。回到上文的显示多个图片的web页面的例子,如果我们不串行执行,而是并行执行,这样就能大大提高HTTP事务的性能。

并行连接的缺点

由于网络的带宽是有限的,而HTTP事务大部分的时间都在传送数据,在这种情况下,如果一个链接到速度较快服务器上的HTTP事务就会容易耗尽可用的带宽。如果并行加载多个对象,每个对象都会去竞争这有限的带宽,每个对象都会以较慢的速度按比例加载,这样带来的性能提示可能就不尽人意了。

而且,打开大量的连接会消耗很对内存资源,从而引发自身的性能问题,造成服务器自身性能呢严重下降。

持久连接

初始化了对某服务器HTTP请求的应用程序很可能会在不久的将来对那台服务器发起更多的请求,为了保证这一情况的性能,我们可能需要在建立TCP连接的时候不立即关闭它,而是在处理结束后保持TCP连接的打开状态,以便未来同站点的HTTP请求重用现存的链接,而这样的连接称之为持久连接(HTTP/1.1版本提供 keep-alive连接)。

非持久连接会在事务结束之后立即关闭,持久连接会在不同事务之间保持打开状态,直到服务器关闭。

与并行连接的比较

并行连接存在以下缺点:

  • 每个事务都会打开/关闭一条连接,耗费时间和带宽
  • 由于TCP慢启动特性,每条新连接的性能都会有所降低
  • 可打开并行连接的数量是有限的

持久连接比并行连接更好的地方在于,持久连接有效的降低了时延和连接建立的开销,将连接保持在已调谐的状态,且减少了打开连接的数量。但是管理持久连接的时候需要注意避免出现大量空闲连接的情况。

持久连接和并行连接一起使用可能是提升性能最有效的方式。持久连接有2种类型:HTTP/1.0+ “keep-alive” 和 HTTP/1.1 “persistent”

keep-alive

实现方式是HTTP首部:Connection:keep-alive

如果服务器愿意为下一条请求连接保持在打开状态,就在响应中设置Connection:keep-alive。如果没有,则客户端认为不支持持久连接,会在发回响应后立即关闭连接

keep-alive首部的选项

  • timeout:估计服务器希望将连接保持在活跃状态的时间。
  • max:估计服务器还希望为多少个事务保持此连接的活跃状态

注意:发起keep-alive之后客户端和服务器不一定同意keep-alive会话。它们可以在任意时刻关闭空闲的keep-alive连接,并可以随意限制keep-alive连接所处理事务的数量。

这里又涉及到另一个问题:哑代理。

其原因在支持持久连接的服务器和客户端之间,存在一个不支持持久连接的代理服务器。这样就会导致:客户端发起了一个keep-alive连接,中间代理服务器由于不支持keep-alive,就原样转发了报文,导致服务器接收到了keep-alive,这样服务器误以为要进行持久连接,那么此时服务器的这条连接就不会被关闭,同时客户端又接收到了服务器同意keep-alive的响应,那么就会又导致客户度的链接被挂起。

Proxy-Connection

为了解决上面哑代理盲目转发Connection首部的问题,网景公司引入了一个非标准首部Proxy-Connection。

浏览器会向代理发送非标准的Proxy-Connection扩展首部,而不是官方支持的Connection首部,如果代理是哑代理,它会将无意义的Proxy-Connection发送给服务器,服务器此时就会忽略并正常返回。如果代理能够识别Proxy-Connection,就会用一个新的Connection来替代Proxy-Connection转发给服务器。

HTTP/1.1 持久连接

HTTP/1.1逐渐停止了对keep-alive的支持。用一种名为“persistent-connection”的持久连接的改进型设计取代了它。

与HTTP/1.0+的keep-alive连接不同,HTTP/1.1的持久连接在默认情况下激活的,除非特别指明,否则,HTTP/1.1假定所有连接都是持久的。在,HTTP/1.1中如果要在事务结束后立即关闭连接,需要在Connection首部复制为close。但是不发送Connection:close也不意味着服务器承诺永远将连接保持在打开状态。

持久连接的限制和规则

  • 发送了Connection:close请求首部后,客户端就无法在那条连接上发送更多的请求。
  • 如果客户端不想在链接上发送请求了,就应该在最后一条请求上发送 Connection:close以关闭连接。
  • 只有连接上所有的报文都有正确的、自定义报文长度时,也就是说实体主体部分的长度和Content-Length一直,或者采用分块传输编码方式编码的链接才能持久保持。
  • HTTP/1.1的代理必须能够分别管理与客户端和服务器的持久连接
  • HTTP/1.1的代理服务器不应该与HTTP/1.0的客户端建立持久连接。
  • 不管Connection值为什么,HTTP/1.1的设备都可以在任意时刻关闭连接。
  • HTTP/1.1应用程序必须能够从异步的关闭恢复出来。只要不存在可能会累积起来的副作用,客户端都应该重试这条请求。
  • 除非重复发起请求会产生副租用,否则如果在客户端收到整条响应之前连接关闭了,客户端就必须重新发起请求
  • 一个客户端对任何服务器或代理最多只能维护2条持久连接,以防止服务器过载。代理服务器可能需要更多的连接来支持用户并发通信,因此有N个用户试图访问服务器的话,代理最多能维持2N条到任意武器的连接。

管道化连接

HTTP/1.1允许在持久连接上可选地使用请求管道。这是相对于keep-alive连接的一个性能优化。在响应到达之前可以将多条请求放入队列中,当第一条请求通过网络流发送到服务器时,第二条和第三条请求也可以发送了。

限制:

  • 如果连接不确定是否能够使用持久连接,则不应该使用管道
  • 必须按照与请求相同的顺序发送HTTP响应。HTTP报文中没有序列号标签,因此如果收到响应失序了,就没办法与对应的请求匹配起来
  • HTTP客户端必须做好连接会在任意时刻被关闭的准备。还要准备好重发所有未完成的管道化请求。
  • HTTP客户端不应该用管道化的方式发送会产生副作用的请求(如POST请求)。总之,出错的时候,管道化方式会阻碍客户端了解服务器执行的是一系列管道化请求中的哪一些。由于无法安全地重试POST这样的非幂等请求,所以出错时,就存在某些方法永远不会被执行的风险。

关闭连接

所有HTTP客户端,服务器,代理服务器都可以在任意时刻关闭一条TCP连接。通常情况下会在一条报文结束时关闭连接的,但是在出错的情况,可以在任意时刻,任意位置关闭连接。

Content-Length及截尾操作

每条HTTP响应都应该有精确的Content-Length首部,但是一些老的服务器会忽略这个首部,或者用错误值去指示,这样就要依赖服务器发出的连接关闭来说明数据的真实末尾。

客户端或代理收到一条随连接关闭而结束的HTTP响应,且实际传输的实体长度与Content-Length不一致,这时候应该质疑长度的正确性。代理遇到此问题时,应该原封不动的转发报文,而不应该试图去纠正Content-Length的正确值。

连接关闭容限、重试以及幂等性

如果传输连接关闭了,那么除非事务处理会带来一些副作用,否则客户端就应该重新打开连接,并重试一次。

如果一个事务不管执行多次得到的结果都是一致的,我们就称这个事务是幂等性。如:GET、HEAD、PUT、DELETE、TRACE、OPTIONS方法都共享这个特性。客户端不应该以管道化方式传输非幂等请求。

正常关闭连接

TCP连接是双向的。每一端都有一个输入队列和一个输出队列,用于数据的读写。

TCP连接在关闭时可以关闭输入和输出信道的任意一个(半关闭),或者两个都关闭了(完全关闭)。

关闭输出信道是安全,连接另一端对等实体会从其缓冲区中读出所有数据后收到一条通知,说明流关闭了,这样就知道连接被关闭了

关闭输入信道比较危险,除非你知道另一端不打算再发送数据了。如果另一端向已关闭的输入信道发数据,操作系统就会想另一端的机器回送一条TCP“连接被对端重置”的报文。这种情况,大部分操作系统都会作为严重错误来处理,删除对端还未读取的所有缓存数据。

正常关闭的应用程序首先应该关闭它们的输出信道,然后等待另一端的对等实体关闭它的输出信道。当两端都告诉对方它们不会再发送任何数据之后,连接就会被完全关闭,不会有重置的危险。

致谢

HTTP权威指南