- HTTP 是如何使用TCP连接的
- TCP连接的时延,瓶颈以及存在的故障
- HTTP 的优化,包括并行连接,keep-alive(持续连接) 和管道化连接
- 管理连接时应该以及不应该做的事情
世界上几乎所有的HTTP通信都是有TCP/IP承载的,TCP/IP是全世界计算机及网络设备都在使用的一种常用的分组交换网络分层协议集。客户端应用程序可以打开一条TCP/IP协议连接,连接到可能运行的世界上任何地方的服务器应用程序。一旦连接建立起来,在客户端和服务器的计算机之间交换的报文就永久不会丢失、受损或失序
TCP可靠的数据通道
浏览器通过TCP链接与服务器进行交互
HTTP连接可以理解为TCP连接及其使用规则,首先来看看HTTP是怎么来和服务器建立连接的,以及
- 浏览器解析出主机名
- 浏览器通过主机名查询IP地址(DNS)
- 浏览器获取端口号
- 浏览器发起到IP端口的链接
- 浏览器向服务器发起一条HTTP GET报文
- 浏览器从服务器读取HTTP 响应报文
- 浏览器关闭链接
TCP链接
建立一条新的TCP链接时,甚至是在发送任意数据之前,TCP对连接的有关参数进行沟通,这就是常见的三次握手,但是在交互过程中会严重降低HTTP的性能
请求新的TCP连接时,客户端要向服务器发送一个小的TCP分组(通常是40 -- 60个字节)。这个分组中设置了一个特殊的SYN标记,说明这是一个链接请求
如果服务器接受了连接,就会对一些连接参数进行计算,并向客户端返回一个TCP分组,这个分组中的SYN和ACK标记都被置位,说明连接请求已被接受
最后,客户端向服务器再次发送一条确定信息,通知它已经成功连接,现代的TCP栈都允许客户端在这个确定分组中发送数据
PS 现在很多的面试会问到,TCP建立安全可靠的链接,为什么是三次握手,而不是四次握手,下面来举例
在中国,我们见面都喜欢打招呼,表示双方的礼貌,比方说在吃饭时间段
A:吃饭没
B:吃了,你呢?
A:我也吃了
这就类似TCP的三次握手,如果是四次握手那么就是下面的对话
A:吃饭没?
B:吃了
B:你呢?
A:我也吃了
很简单,第二种法案的第二次和第三次对话可以合并,在TCP建立连接时是需要耗时以及资源的,前面已经说了 交互过程中会严重降低HTTP的性能所以,从HTTP的性能出发,三次握手比四次握手更有优势。但是四次握手可以吗?完全没有问题
TCP性能
HTTP紧挨着TCP,位于其上层,所以HTTP事务的性能在很大程度上取决于底层的TCP通道的性能。
HTTP事务延时
HTTP事务延时有一下主要原因
客户端需要根据URL确定Web服务器的IP地址和端口号。如果最近没有对URI中的主机名进行访问,通过DNS解析系统将URI的主机名转换成一个IP地址可能要花费数十秒的时间
接下来,客户端此昂服务器发送一条TCP连接请求,并等待服务器回送一个请求接受回应。每条新的TCP链接都会有链接建立时延。这个值通常最多只有一到两秒钟,但如果有数百个HTTP事务的话,这个值就会快速叠加上去
一旦连接建立起来,客户端就会通过新建立的TCP管道来发送HTTP请求,数据到达时,Web服务器会从TCP连接中读取请求报文,并对秦秋进行处理。因特网传输请求报文,以及服务器处理请求报文都需要时间
Web服务器回送HTTP响应,这也需要花费时间
这些 TCP 网络时延的大小取决于硬件速度、网络和服务器的负载,请求和响应报文 的尺寸,以及客户端和服务器之间的距离。TCP 协议的技术复杂性也会对时延产生 巨大的影响。
性能聚焦区域
最常见的TCP相关延时
- TCP连接建立握手
- TCP慢启动拥塞控制
- 数据聚集的Nagle算法
- 用于捎带确定的TCP延迟确定算法
- TIME_WAIT时延时和端口耗尽
TCP连接的握手时延
TCP 连接握手需要经过以下几个步骤
请求新的 TCP 连接时,客户端要向服务器发送一个小的 TCP 分组(通常是 40 ~ 60 个字节)。这个分组中设置了一个特殊的 SYN 标记,说明这是一个连接请求。
如果服务器接受了连接,就会对一些连接参数进行计算,并向客户端回送一个 TCP 分组,这个分组中的 SYN 和 ACK 标记都被置位,说明连接请求已被接受
最后,客户端向服务器回送一条确认信息,通知它连接已成功建立
现代的 TCP 栈都允许客户端在这个确认分组中发送数据
最后的结果是,小的 HTTP 事务可能会在 TCP 建立上花费 50%,或更多的时间
延时确定
由于因特网自身无法确保可靠的分组传输(因特网路由器超负荷的话,可以随意丢弃分组),所以 TCP 实现了自己的确认机制来确保数据的成功传输。
每个 TCP 段都有一个序列号和数据完整性校验和。每个段的接收者收到完好的段 时,都会向发送者回送小的确认分组。如果发送者没有在指定的窗口时间内收到确 认信息,发送者就认为分组已被破坏或损毁,并重发数据。
由于确认报文很小,所以 TCP 允许在发往相同方向的输出数据分组中对其进行“捎 带”。TCP 将返回的确认信息与输出的数据分组结合在一起,可以更有效地利用网 络。为了增加确认报文找到同向传输数据分组的可能性,很多 TCP 栈都实现了一种“延迟确认”算法。延迟确认算法会在一个特定的窗口时间(通常是 100 ~ 200 毫 秒)内将输出确认存放在缓冲区中,以寻找能够捎带它的输出数据分组。如果在那 个时间段内没有输出数据分组,就将确认信息放在单独的分组中传送。
但是,HTTP 具有双峰特征的请求 - 应答行为降低了捎带信息的可能。当希望有相 反方向回传分组的时候,偏偏没有那么多。通常,延迟确认算法会引入相当大的时 延。根据所使用操作系统的不同,可以调整或禁止延迟确认算法。
在对 TCP 栈的任何参数进行修改之前,一定要对自己在做什么有清醒的认识。TCP 中引入这些算法的目的是防止设计欠佳的应用程序对因特网造成破坏。对 TCP 配置 进行的任意修改,都要绝对确保应用程序不会引发这些算法所要避免的问题。
TCP启动慢
- TCP 数据传输的性能还取决于 TCP 连接的使用期(age)。TCP 连接会随着时间进行 自我“调谐”,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移 提高传输的速度。这种调谐被称为 TCP 慢启动(slow start),用于防止因特网的突 然过载和拥塞。
- TCP 慢启动限制了一个 TCP 端点在任意时刻可以传输的分组数。简单来说,每成功 接收一个分组,发送端就有了发送另外两个分组的权限。如果某个 HTTP 事务有大 量数据要发送,是不能一次将所有分组都发送出去的。必须发送一个分组,等待确 认;然后可以发送两个分组,每个分组都必须被确认,这样就可以发送四个分组了, 以此类推。这种方式被称为“打开拥塞窗口”。
- 由于存在这种拥塞控制特性,所以新连接的传输速度会比已经交换过一定量数据的、 “已调谐”连接慢一些。由于已调谐连接要更快一些,所以 HTTP 中有一些可以重用现存连接的工具。本章稍后会介绍这些 HTTP“持久连接”。
Nagle算法与TCP_NODELAY
- TCP 有一个数据流接口,应用程序可以通过它将任意尺寸的数据放入 TCP 栈中—— 即使一次只放一个字节也可以!但是,每个 TCP 段中都至少装载了 40 个字节的标 记和首部,所以如果 TCP 发送了大量包含少量数据的分组,网络的性能就会严重 下降Nagle 算法(根据其发明者 John Nagle 命名)试图在发送一个分组之前,将大量 TCP 数据绑定在一起,以提高网络效率。RFC 896“IP/TCP 互连网络中的拥塞控 制”对此算法进行了描述。
- Nagle 算法鼓励发送全尺寸(LAN 上最大尺寸的分组大约是 1500 字节,在因特网 上是几百字节)的段。只有当所有其他分组都被确认之后,Nagle 算法才允许发送 非全尺寸的分组。如果其他分组仍然在传输过程中,就将那部分数据缓存起来。只 有当挂起分组被确认,或者缓存中积累了足够发送一个全尺寸分组的数据时,才会 将缓存的数据发送出去。
- Nagle 算法会引发几种 HTTP 性能问题。首先,小的 HTTP 报文可能无法填满一个 分组,可能会因为等待那些永远不会到来的额外数据而产生时延。其次,Nagle 算 法与延迟确认之间的交互存在问题——Nagle 算法会阻止数据的发送,直到有确认 分组抵达为止,但确认分组自身会被延迟确认算法延迟 100 ~ 200 毫秒。
- HTTP 应用程序常常会在自己的栈中设置参数 TCP_NODELAY,禁用 Nagle 算法, 提高性能。如果要这么做的话,一定要确保会向 TCP 写入大块的数据,这样就不会 产生一堆小分组了。
TIME_WAIT累积与端口耗尽
- TIME_WAIT 端口耗尽是很严重的性能问题,会影响到性能基准,但在现实中相对 较少出现。大多数遇到性能基准问题的人最终都会碰到这个问题,而且性能都会变 得出乎意料地差,所以这个问题值得特别关注。
- 当某个 TCP 端点关闭 TCP 连接时,会在内存中维护一个小的控制块,用来记录最 近所关闭连接的 IP 地址和端口号。这类信息只会维持一小段时间,通常是所估计的 最大分段使用期的两倍(称为 2MSL,通常为 2 分钟 8)左右,以确保在这段时间内 不会创建具有相同地址和端口号的新连接。实际上,这个算法可以防止在两分钟内 创建、关闭并重新创建两个具有相同 IP 地址和端口号的连接。
- 现在高速路由器的使用,使得重复分组几乎不可能在连接关闭的几分钟之后,出现 在服务器上。有些操作系统会将 2MSL 设置为一个较小的值,但超过此值时要特别小心。分组确实会被复制,如果来自之前连接的复制分组插入了具有相同连接值的 新 TCP 流,会破坏 TCP 数据。
- 2MSL 的连接关闭延迟通常不是什么问题,但在性能基准环境下就可能会成为一个 问题。进行性能基准测试时,通常只有一台或几台用来产生流量的计算机连接到某 系统中去,这样就限制了连接到服务器的客户端 IP 地址数。而且,服务器通常会在 HTTP 的默认 TCP 端口 80 上进行监听。用 TIME_WAIT 防止端口号重用时,这些 情况也限制了可用的连接值组合
在只有一个客户端和一台 Web 服务器的异常情况下,构建一条 TCP 连接的 4 个值: <source-IP-address, source-port, destination-IP-address, destination-port>
其中的 3 个都是固定的——只有源端口号可以随意改变: <client-IP, source-port, server-IP, 80> - 客户端每次连接到服务器上去时,都会获得一个新的源端口,以实现连接的唯一性。 但由于可用源端口的数量有限(比如,60 000 个),而且在 2MSL 秒(比如,120 秒)内连接是无法重用的,连接率就被限制在了 60 000/120=500 次 / 秒。如果再不 断进行优化,并且服务器的连接率不高于 500 次 / 秒,就可确保不会遇到 TIME_ WAIT 端口耗尽问题。要修正这个问题,可以增加客户端负载生成机器的数量,或 者确保客户端和服务器在循环使用几个虚拟 IP 地址以增加更多的连接组合。
- 即使没有遇到端口耗尽问题,也要特别小心有大量连接处于打开状态的情况,或为 处于等待状态的连接分配了大量控制块的情况。在有大量打开连接或控制块的情况 下,有些操作系统的速度会严重减缓
持久连接
- Web客户端经常会打开到同一个站点的连接。比如,一个Web页面上的大部分内嵌图片通常来自同一个Web站点,而且相当一部分指向其他对象的超链接通常都指向同一个站点。因此,初始化了某个服务器HTTP请求的应用程序很有可能会再不久的将来对同一台服务器发起更多的请求,这种性质被称为站点的局部性
- 因此,HTTP/1.1允许HTTP设备在事务处理结束之后将TCP连接保持在打开的状态,以便为未来的HTTP请求重用现存的连接。在事务处理结束之后然然保持在打开状态的TCP连接被称之为持久连接。
HTTP/1.0 + Keep-alive连接
大约从1996年开始,很多HTTP/1.0浏览器和服务器都进行了扩展,以支持一种被称为keep-alive连接的早期实验型久连接。这些早期的持久连接接受到了一些互操作性设计方面的困扰,这些方面在HTTP/1.1版本中都得到修正,但很多的客户端和服务器仍然在使用这种早期的keep-alive连接。
- 实现HTTP/1.0 keep-alive 连接的客户端可以通过包含Connection:Keep-alive首部请求将一条连接保持在打开状态
- 如果服务器愿意为下一条请求将连接保持在打开状态,就在响应中包含相同的首部。如果响应中没有Connection:keep-alive首部,客户端就认为服务器不支持keep-alive,会再发回响应报文之后关闭连接
Keep-alive连接的限制和规则
HTTP/1.0中,Keep-Alive不是默认使用的,客户端必须发送一个Connection: Keep-Alive请求来激活Keep-Alive连接
Connection: Keep-Alive必须随所有希望持久连接的报文一起发送,客户端没有发送Connection: Keep-Alive首部,则服务器会在发出响应后关闭持久连接;
通过检测响应中是否包含Connection: Keep-Alive响应首部,客户端可以判断服务器是否毁在发出响应之后关闭连接
只有在无需检测到连接的关闭即可确定报文实体主体部分长度的情况下,才能将连接保持在打开状态实体部分必须有正确的Content-Length,否则无法精确一条报文的结束和另一条报文的开始,不能持久连接;
代理或网关必须在将报文转发出去或将其高速缓存之前,删除Connection首部中命名的所有首部字段以及Connection首部;
不应该与无法确定是否支持Connection首部的代理服务器建立Keep-Alive连接。
从技术上来讲,应该忽略所有来源HTTP/1.0设备的Connection首部字段,因为它们可能是由比较老的代理服务器误转发的
除非重复发送请求会产生其他的一些副作用,否则如果在客户端收到完整的响应之前连接就关闭了,客户端就一定要做好重试请求的准备
HTTP/1.1持久连接
HTTP/1.1用了持久连接(persistent connection)取代了Keep-Alive,目的与Keep-Alive一样;
HTTP/1.1默认情况下激活持久连接,要在事务处理结束之后关闭连接,要在响应报文包含Connection: close
响应报文不包含Connection: close,并不意味着服务器承诺永远将连接保持打开,客户端和服务器仍然可以随时关闭空闲的连接。
持久连接的限制和规则
发送了 Connection: Keep-Alive请求首部之后,客户端就无法在那条连接上发送更多的请求了
如果客户端不想在连接上发送其他的请求了,就应该在最后一条请求中发送一个Connection: close
实体主体部分的长度都和相应的Content-Length一致,或者是用分块传输编码方式编码的--连接餐能持久保持
HTTP/1.1的代理必须能够分别管理与客户端和服务器的持久连接--每个持久连接都只适用于一跳传输
HTTP/1.1的代理服务器不应该与HTTP/1.0客户端建立长久连接,除非他们了解客户端的处理能力
不管Connection首部取了什么值,HTTP/1.1S设备都可以在任意时刻关闭连接