一.TCP连接
TCP
为 HTTP
提供了一条可靠的比特传输管道。从
TCP
连接一端填入的字节会从另一端以原有的顺序、正确地传送出来。
TCP
的数据是通过名为
IP
分组(或
IP
数据报)的小数据块来发送的。
HTTP就是“HTTP
over TCP over IP”这个“协议栈”中的最顶层了。其安全版本
HTTPS
就是在 HTTP
和 TCP
之间插入了一个(称为
TLS
或 SSL的)密码加密层。
HTTP
要传送一条报文时,会以流的形式将报文数据的内容通过一条打开的
TCP
连接按序传输。TCP
收到数据流之后,会将数据流砍成被称作段的小数据块,并将段封装在
IP
分组中,通过因特网进行传输 。
每个 TCP
段都是由 IP
分组承载,从一个
IP
地址发送到另一个
IP
地址的。每个 IP分组中都包括:
?
一个IP分组首部(通常为20字节);
?
一个TCP段首部(通常为20字节);
?
一个TCP数据块(0个或多个字节)。
IP
首部包含了源和目的
IP
地址、长度和其他一些标记。TCP
段的首部包含了 TCP端口号、TCP
控制标记,以及用于数据排序和完整性检查的一些数字值。
二、对TCP性能的考虑
最常见的 TCP
相关时延:
- TCP 连接建立握手;
- TCP 慢启动拥塞控制;
- 数据聚集的Nagle算法;
- 用于捎带确认的TCP延迟确认算法;
- TIME_WAIT 时延和端口耗尽。
1.TIME_WAIT和端口耗尽
当某个 TCP
端点关闭 TCP
连接时,会在内存中维护一个小的控制块,用来记录最近所关闭连接的
IP
地址和端口号。这类信息只会维持一小段时间,通常是所估计的最大分段使用期的两倍(称为
2MSL,通常为
2
分钟 8)左右,以确保在这段时间内不会创建具有相同地址和端口号的新连接。实际上,这个算法可以防止在两分钟内创建、关闭并重新创建两个具有相同
IP
地址和端口号的连接。 (TCP分节可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个迟到的迷途分节到达时可能会引起问题。在关闭“前一个连接”之后,马上又重新建立起一个相同的IP和端口之间的“新连接”,“前一个连接”的迷途重复分组在“前一个连接”终止后到达,而被“新连接”收到了。为了避免这个情况,TCP协议不允许处于TIME_WAIT状态的连接启动一个新的可用连接,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个新TCP连接的时候,来自旧连接重复分组已经在网络中消逝。)
在只有一个客户端和一台 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_W AIT
端口耗尽问题。
2.延迟确认算法
由于因特网自身无法确保可靠的分组传输(因特网路由器超负荷的话,可以随意丢弃分组),所以
TCP
实现了自己的确认机制来确保数据的成功传输。
每个 TCP
段都有一个序列号和数据完整性校验和。每个段的接收者收到完好的段时,都会向发送者回送小的确认分组。如果发送者没有在指定的窗口时间内收到确认信息,发送者就认为分组已被破坏或损毁,并重发数据。
由于确认报文很小,所以 TCP
允许在发往相同方向的输出数据分组中对其进行“捎带”。TCP
将返回的确认信息与输出的数据分组结合在一起,可以更有效地利用网络。为了增加确认报文找到同向传输数据分组的可能性,很多
TCP
栈都实现了一种 “延迟确认”算法。延迟确认算法会在一个特定的窗口时间(通常是
100
~ 200
毫秒)内将输出确认存放在缓冲区中,以寻找能够捎带它的输出数据分组。如果在那个时间段内没有输出数据分组,就将确认信息放在单独的分组中传送。
但是,HTTP
具有双峰特征的请求
- 应答行为降低了捎带信息的可能。当希望有相反方向回传分组的时候,偏偏没有那么多。通常,延迟确认算法会引入相当大的时延。根据所使用操作系统的不同,可以调整或禁止延迟确认算法。
3.TCP慢启动
TCP
数据传输的性能还取决于
TCP
连接的使用期(age)。TCP
连接会随着时间进行自我“调谐”,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度。这种调谐被称为TCP慢启动(slow
start),用于防止因特网的突然过载和拥塞。
TCP
慢启动限制了一个
TCP
端点在任意时刻可以传输的分组数。简单来说,每成功接收一个分组,发送端就有了发送另外两个分组的权限。如果某个
HTTP
事务有大量数据要发送,是不能一次将所有分组都发送出去的。必须发送一个分组,等待确认;然后可以发送两个分组,每个分组都必须被确认,这样就可以发送四个分组了,以此类推。这种方式被称为“打开拥塞窗口”。
由于存在这种拥塞控制特性,所以新连接的传输速度会比已经交换过一定量数据的、“已调谐”连接慢一些。
4.Nagle算法与TCP_NODELAY
TCP
有一个数据流接口,应用程序可以通过它将任意尺寸的数据放入
TCP
栈中——即使一次只放一个字节也可以!但是,每个
TCP
段中都至少装载了
40
个字节的标记和首部,所以如果
TCP
发送了大量包含少量数据的分组,网络的性能就会严重下降。
Nagle算法(根据其发明者John
Nagle命名)试图在发送一个分组之前,将大量TCP数据绑定在一起,以提高网络效率。
Nagle
算法鼓励发送全尺寸(LAN
上最大尺寸的分组大约是
1500
字节,在因特网上是几百字节)的段。只有当所有其他分组都被确认之后,Nagle
算法才允许发送非全尺寸的分组。如果其他分组仍然在传输过程中,就将那部分数据缓存起来。只有当挂起分组被确认,或者缓存中积累了足够发送一个全尺寸分组的数据时,才会将缓存的数据发送出去。6
Nagle
算法会引发几种 HTTP
性能问题。首先,小的
HTTP
报文可能无法填满一个分组,可能会因为等待那些永远不会到来的额外数据而产生时延。其次,Nagle
算法与延迟确认之间的交互存在问题——Nagle
算法会阻止数据的发送,直到有确认分组抵达为止,但确认分组自身会被延迟确认算法延迟
100
~ 200
毫秒。
HTTP
应用程序常常会在自己的栈中设置参数
TCP_NODELA Y,禁用
Nagle
算法,提高性能。如果要这么做的话,一定要确保会向
TCP
写入大块的数据,这样就不会产生一堆小分组了。
三、提高HTTP连接性能的几种方法
? 并行连接
通过多条 TCP
连接发起并发的 HTTP
请求。
? 持久连接
重用 TCP
连接,以消除连接及关闭时延。
? 管道化连接
通过共享的 TCP
连接发起并发的 HTTP
请求。
? 复用的连接交替传送请求和响应报文(实验阶段)。
1.并行连接
HTTP
允许客户端打开多条连接,并行地执行多个
HTTP
事务。在这个例子中,并行加载了四幅嵌入式图片,每个事务都有自己的
TCP
连接。
2.持久连接
Web
客户端经常会打开到同一个站点的连接。比如,一个
Web
页面上的大部分内嵌图片通常都来自同一个
Web
站点,而且相当一部分指向其他对象的超链通常都指向同一个站点。因此,初始化了对某服务器
HTTP
请求的应用程序很可能会在不久的将来对那台服务器发起更多的请求(比如,获取在线图片)。这种性质被称为站点局部性(site
locality)。
因此,HTTP/1.1(以及
HTTP/1.0
的各种增强版本)允许
HTTP
设备在事务处理结束之后将
TCP
连接保持在打开状态,以便为未来的
HTTP
请求重用现存的连接。在事务处理结束之后仍然保持在打开状态的
TCP
连接被称为持久连接。非持久连接会在每个事务结束之后关闭。持久连接会在不同事务之间保持打开状态,直到客户端或服务器决定将其关闭为止。
重用已对目标服务器打开的空闲持久连接,就可以避开缓慢的连接建立阶段。而且,已经打开的连接还可以避免慢启动的拥塞适应阶段,以便更快速地进行数据的传输。
持久连接与并行连接配合使用可能是最高效的方式。现在,很多
Web
应用程序都会打开少量的并行连接,其中的每一个都是持久连接。持久连接有两种类型:比较老
的 HTTP/1.0+“keep-alive”连接,以及现代的
HTTP/1.1“persistent”连接。
1) keep-alive操作
keep-alive
已经不再使用了,而且在当前的
HTTP/1.1
规范中也没有对它的说明了。但浏览器和服务器对
keep-alive
握手的使用仍然相当广泛。
实现
HTTP/1.0 keep-alive
连接的客户端可以通过包含
Connection: Keep-Alive首部请求将一条连接保持在打开状态。
如果服务器愿意为下一条请求将连接保持在打开状态,就在响应中包含相同的首部。如果响应中没有
Connection: Keep-Alive
首部,客户端就认为服务器不支持
keep-alive,会在发回响应报文之后关闭连接。
Keep-Alive
首部必须随所有希望保持持久连接的报文一起发送。如果客户端没有发送
Connection: Keep-Alive
首部,服务器就会在那条请求之后关闭连接。
2)keep-alive选项
注意,keep-Alive
首部只是请求将连接保持在活跃状态。发出
keep-alive
请求之后,客户端和服务器并不一定会同意进行
keep-alive
会话。它们可以在任意时刻关闭空闲的
keep-alive
连接,并可随意限制
keep-alive
连接所处理事务的数量。
可以用 Keep-Alive
通用首部中指定的、由逗号分隔的选项来调节
keep-alive
的行为。
?
参数timeout是在Keep-Alive响应首部发送的。它估计了服务器希望将连接保持在活跃状态的时间。这并不是一个承诺值。
?
参数max是在Keep-Alive响应首部发送的。它估计了服务器还希望为多少个事务保持此连接的活跃状态。这并不是一个承诺值。
?
Keep-Alive
首部还可支持任意未经处理的属性,这些属性主要用于诊断和调试。语法为name
[=value]。
Keep-Alive首部完全是可选的,但只有在提供Connection:
Keep-Alive时才能使用它。这里有个
Keep-Alive
响应首部的例子,这个例子说明服务器最多还会为另外
5
个事务保持连接的打开状态,或者将打开状态保持到连接空闲了
2
分钟之后。
Connection: Keep-AliveKeep-Alive: max=5, timeout=120
3)keep-alive和哑代理
很多老的或简单的代理都是盲中继(blind
relay),它们只是将字节从一个连接转发到另一个连接中去,不对Connection
首部进行特殊的处理。(不理解
Connection
首部,而且不知道在沿着转发链路将其发送出去之前,应该将该首部删除 )
(1)在图
4-15a 中 Web
客户端向代理发送了一条报文,其中包含了
Connection:Keep-Alive
首部,如果可能的话请求建立一条
keep-alive
连接。客户端等待响应,
以确定对方是否认可它对 keep-alive
信道的请求。
(2) 哑代理收到了这条
HTTP
请求,但它并不理解 Connection
首部(只是将其作为一个扩展首部对待)。代理不知道 keep-alive
是什么意思,因此只是沿着转发链
路将报文一字不漏地发送给服务器(图 4-15b)。但
Connection
首部是个逐跳首部,只适用于单条传输链路,不应该沿着传输链路向下传输。接下来,就要发生
一些很糟糕的事情了。
- (3) 在图
4-15b
中,经过中继的 HTTP
请求抵达了 Web
服务器。当 Web
服务器收到经过代理转发的Connection: Keep-Alive首部时,会误以为代理(对服务器来说,这个代理看起来就和所有其他客户端一样)希望进行
keep-alive
对话!对Web
服务器来说这没什么问题——它同意进行
keep-alive
对话,并在图 4-15c
中回送了一个
Connection: Keep-Alive
响应首部。所以,此时
W eb 服务器认为它在与代理进行
keep-alive
对话,会遵循
keep-alive
的规则。但代理却对
keep-alive
一无所知。不妙。 - (4) 在图
4-15d
中,哑代理将 Web
服务器的响应报文回送给客户端,并将来自
Web服务器的Connection:
Keep-Alive首部一起传送过去。客户端看到这个首部,就会认为代理同意进行
keep-alive
对话。所以,此时客户端和服务器都认为它们在进行
keep-alive
对话,但与它们进行对话的代理却对
keep-alive
一无所知。 - (5) 由于代理对
keep-alive
一无所知,所以会将收到的所有数据都回送给客户端,然后等待源端服务器关闭连接。但源端服务器会认为代理已经显式地请求它将连接保持在打开状态了,所以不会去关闭连接。这样,代理就会挂在那里等待连接的关闭。 - (6) 客户端在图
4-15d
中收到了回送的响应报文时,会立即转向下一条请求,在
keep-alive
连接上向代理发送另一条请求(参见图
4-15e)。而代理并不认为同一条连接上会有其他请求到来,请求被忽略,浏览器就在这里转圈,不会有任何进展了。 - (7) 这种错误的通信方式会使浏览器一直处于挂起状态,直到客户端或服务器将连接超时,并将其关闭为止。
为避免此类代理通信问题的发生,现代的代理都绝不能转发
Connection
首部和所有名字出现在
Connection
值中的首部。因此,如果一个代理收到了一个Connection: Keep-Alive
首部,是不应该转发
Connection
首部,或所有名为Keep-Alive
的首部的。
在网景的变通做法是,浏览器会向代理发送非标准的
Proxy-Connection
扩展首部,而不是官方支持的著名的
Connection
首部。如果代理是盲中继,它会将无意义的
Proxy-Connection
首部转发给 W eb
服务器,服务器会忽略此首部,不会带来任何问题。但如果代理是个聪明的代理(能够理解持久连接的握手动作),就用一个
Connection
首部取代无意义的
Proxy-Connection
首部,然后将其发送给服务器,以收到预期的效果。
4)persistent连接
HTTP/1.1 逐渐停止了对
keep-alive
连接的支持,用一种名为持久连接(persistentconnection)的改进型设计取代了它。持久连接的目的与
keep-alive
连接的目的相同,但工作机制更优一些。
与
HTTP/1.0+
的
keep-alive
连接不同,HTTP/1.1
持久连接在默认情况下是激活的。除非特别指明,否则
HTTP/1.1
假定所有连接都是持久的。要在事务处理结束之后将连接关闭,HTTP/1.1
应用程序必须向报文中显式地添加一个
Connection:close
首部。这是与以前的
HTTP
协议版本很重要的区别,在以前的版本中,keep-alive
连接要么是可选的,要么根本就不支持。
HTTP/1.1 客户端假定在收到响应后,除非响应中包含了
Connection: close
首部,不然
HTTP/1.1
连接就仍维持在打开状态。但是,客户端和服务器仍然可以随时关闭空闲的连接。不发送Connection: close并不意味着服务器承诺永远将连接保持在打开状态。
- 只有当连接上所有的报文都有正确的、自定义报文长度时——也就是说,实体主体部分的长度都和相应的
Content-Length
一致,或者是用分块传输编码方式编码的——连接才能持久保持。 (因为是持久连接,无法通过连接的关闭来判断报文发送结束)
3.管道化连接
HTTP/1.1
允许在持久连接上可选地使用请求管道。这是相对于
keep-alive
连接的又一性能优化。在响应到达之前,可以将多条请求放入队列。当第一条请求通过网络流向地球另一端的服务器时,第二条和第三条请求也可以开始发送了。在高时延网络条件下,这样做可以降低网络的环回时间,提高性能。
图
4-18a-c
显示了持久连接是怎样消除
TCP
连接时延,以及管道化请求(参见图4-18c)是如何消除传输时延的。
对管道化连接有几条限制:
- 如果HTTP客户端无法确认连接是持久的,就不应该使用管道。
- 必须按照与请求相同的顺序回送HTTP响应。HTTP报文中没有序列号标签,因
此如果收到的响应失序了,就没办法将其与请求匹配起来了。
- HTTP 客户端必须做好连接会在任意时刻关闭的准备,还要准备好重发所有未完
成的管道化请求。如果客户端打开了一条持久连接,并立即发出了
10 条请求,服务器可能在只处理了,比方说,5
条请求之后关闭连接。剩下的
5 条请求会失败,客户端必须能够应对这些过早关闭连接的情况,重新发出这些请求。 - HTTP 客户端不应该用管道化的方式发送会产生副作用的请求(比如
POST)。总之,出错的时候,管道化方式会阻碍客户端了解服务器执行的是一系列管道化请求中的哪一些。由于无法安全地重试
POST 这样的非幂等请求,所以出错时,就存在某些方法永远不会被执行的风险。