世界上几乎所有的 HTTP 通信都是由 TCP/IP 承载的,客户端可以打开一条TCP/IP连接,连接到任何地方的服务器。一旦连接建立,客户端和服务器之间交换的报文就永远不会丢失、受损或失序
TCP(Transmission Control Protocol)传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。通俗来讲,TCP就是双方通信的一个规范标准,负责对数据的传输进行一定的控制
HTTP 要传送一条报文时,会以流的形式将报文数据的内容通过一条打开的 TCP 连接按序传输。TCP 收到数据流之后,会将数据流分成被称作段的小数据块,并将段封装在 IP 分组中,通过因特网进行传输。所有这些工作都是由 TCP/IP 软件来处理的、HTTP 程序员什么都看不到
每个 TCP 段都是由 IP 分组承载,从一个 IP 地址发送到另一个 IP 地址,每个 IP 分组包括:
IP 首部包含了源和目的 IP 地址、长度和其他一些标记。TCP 段的首部包含了 TCP 端口号、TCP 控制标记,以及用于数据排序和完整性检查的一些数字值
TCP 段首部格式如下:
源端口号就是指本地端口,目的端口号就是远程端口
序号,也称序列号(Sequence Number),用于 TCP 通信过程中某一传输方向上字节流的每个字节的编号,以防止乱序问题。简单来说,就是在传输过程中用序列号来标记自己的位置,保证数据能按序传输
确认序号,也称确认序列号(Acknowledgment Numbe),是接收确认端所期望收到的下一序列号。确认序号应当是上次已成功收到数据字节序号加 1,只有当标志位中的 ACK 标志为 1 时该确认序列号的字段才有效。主要用来解决不丢包的问题
标志位,TCP Flag,TCP 首部中有 6 个标志比特,它们中的多个可同时被设置为 1,主要是用于操控 TCP 的状态机,依次为 URG,ACK,PSH,RST,SYN,FIN
ACK
表示应答域有效,这个标识可以理解为发送端发送数据到接收端,发送的时候 ACK 为 0,标识接收端还未应答,一旦接收端接收数据之后,就将 ACK 置为 1,发送端接收到之后,就知道了接收端已经接收了数据
SYN
表示同步序列号,用来建立 TCP 连接。SYN 标志位和 ACK 标志位搭配使用,当连接请求的时候,SYN = 1,ACK = 0;连接被响应的时候,SYN = 1,ACK = 1;这个标志的数据包经常被用来进行端口扫描。扫描者发送一个只有 SYN 的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口
FIN
表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送 FIN 标志位的 TCP 数据包后,连接将被断开。这个标志的数据包也经常被用于进行端口扫描
窗口大小(Window Size),也称为滑动窗口大小,用来进行流量控制
TCP 三次握手,即建立 TCP 连接,需要客户端和服务端总共发送 3 个包以确认连接的建立。在 Socket 编程中,这一过程由客户端执行 connect 来触发
TCP 握手的目的有三个:
为什么是三次握手,而不是一次、二次呢?因为有可能出现这种情况:客户端发送了一个连接请求,但出现网络延迟,导致客户端没有及时收到服务端的响应,就会认为本次请求失效。而这时原本延迟的请求又来到服务端,服务端确认并保持等待状态,但实际上此时客户端并没有与服务端连接的意思,这就会导致服务器一直处于等待状态,造成资源浪费
TCP 四次挥手,即终止 TCP 连接,需要客户端和服务端总共发送 4 个包以确认连接的断开。在 Socket 编程中,这一过程由客户端或服务端任一方执行 close 来触发
为什么是四次挥手呢?因为关闭连接时,己方收到对方的 FIN 报文,仅仅表示对方不再向自己发送数据,但还能接受数据。己方可能还有数据要发送给对方,所以不能向三次握手一样直接把 ACK 和 SYN 放一起发送,而是先发送 ACK,直到没有数据要发送了,才是 FIN 确认关闭连接
HTTP 位于 TCP 上层,所以 HTTP 事务的性能在很大程度上取决于底层 TCP 通道的性能
为了避免网络延迟导致的数据丢失,TCP 实现了自己的确认机制来确保数据的成功传输
每个 TCP 段都有一个序列号和数据完整性校验和。每个段的接收者收到完好的段时,都会向发送者回送小的确认分组。如果发送者没有在指定的窗口时间内收到确认信息,发送者就认为分组已被破坏或损毁,并重发数据
由于确认报文很小,所以 TCP 允许将返回的确认信息与输出的数据分组结合在一起,更有效地利用网络。为了增加确认报文找到同向传输数据分组的可能性,TCP 实现了一种【延迟确认】算法,延迟确认算法会在一个特定的窗口时间(通常是 100-200 毫秒)内将输出确认存放在缓冲区中,以寻找能够捎带它的输出数据分组。如果在那个时间段内没有输出数据分组,就将确认信息放在单独的分组中传送
TCP 数据传输的性能还取决于 TCP 连接的使用期,TCP 连接会随着时间进行自我调节,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度,这种调节被称为 TCP 慢启动,用于防止因特网的突然过载和拥塞
TCP 慢启动限制了一个 TCP 端点在任意时刻可以传输的分组数。简单来说,每成功接收一个分组,发送端就有了发送另外两个分组的权限。如果某个 HTTP 事务有大量数据要发送,是不能一次将所有分组都发送出去的,必须发送一个分组,等待确认,然后发送两个分组,每个分组都必须被确认,然后发送四个分组,以此类推,这种方式被称为【打开拥塞窗口】
由于存在这种拥塞控制特性,所以新连接的传输速度会比已经交换过一定量数据的连接慢一些
TCP 有一个数据流接口,应用程序可以通过它将任意尺寸的数据放入 TCP 栈中,即使一次只放一个字节。但是,每个 TCP 段都至少装载了40个字节的标记和首部如果 TCP 发送了大量包含少量数据的分组,网络的性能就会严重下降
Nagle 算法试图在发送一个分组之前,将大量 TCP 数据绑定在一起,以提高网络效率。Nagle 算法鼓励发送全尺寸的分组,只有当所有其他分组都被确认之后,Nagle 算法才允许发送非全尺寸的分组。如果其他分组仍然在传输过程中,就将那部分数据缓存起来。只有当挂起分组被确认,或者缓存中积累了足够发送一个全尺寸分组的数据时,才会将缓存的数据发送出去
Nagle 算法会引发几种 HTTP 性能问题,首先,小的 HTTP 报文可能无法填满一个分组,可能会因为等待那些永远不会到来的额外数据而产生时延。其次,Nagle 算法与延迟确认之间的交互存在问题,Nagle 算法会阻止数据的发送,直到有确认分组抵达为止,但确认分组自身会被延迟确认算法延迟 100-200 毫秒
HTTP应用程序常常会禁用 Nagle 算法,如果要使用的话,一定要确保会向TCP写入大块的数据,不会产生一堆小分组
当某个 TCP 端点关闭 TCP 连接时,会在内存中维护一个小的控制块,用来记录最近所关闲连接的 IP 地址和端口号。这类信息只会维持一小段时间,通常是所估计的最大分段使用期的两倍(称为 2MSL,通常为两分钟左右),以确保在这段时间内不会创建具有相同地址和端口号的新连接
2MSL 的连接关闭延迟在某些情况下会出现问题,例如客户端每次连接到服务器时,都会获得一个新的端口,以实现连接的唯一性,但由于可用端口的数量有限(比如 60000 个),而且在 2MSL 秒(比如 120 秒)内连接是无法重用的,连接率就被限制在 60000/120=500 次/秒,如果服务器的连接率高于 500 次/秒,就会遇到端口耗尽问题
如果只对连接进行简单的管理,TCP 的性能时延可能会叠加起来。比如,假设有一个包含了三个嵌入图片的 Web 页面,测览器需要发起四个 HTTP 事务来显示此页面:一个用于顶层的 HTML 页面,三个用于嵌入的图片。如果每个事务都需要建立一条新的连接,那么连接时延和慢启动时证就会叠加起来
除了串行加载引入的实际时延之外,加载一幅图片时,页面上其他地方都没有动静也会让人觉得速度很慢,用户更希望能够同时加载多幅图片。并行加载的另一个缺点是,有些沟览器在对象加载完毕之前无法获知对象的尺寸,而它们可能需要尺寸信息来决定将对象放在屏幕的什么位置,所以在加载了足够多的对象之前,无法在屏靠上显示任何内容
HTTP 允许客户端打开多条连接,并行地执行多个 HTTP 事务,提高加载速度,但并不是绝对的,在带宽较小的情况下,并行执行多个 HTTP 事务带来的性能提升就很小,甚至没什么提升。而且,打开大量连接会消耗大量的资源
Web 客户端经常会打开到同一个站点的连接,比如,一个 Web 页面上的大部分内嵌图片通常都来自同一个 Web 站点。因此,对某服务器 HTTP 请求的应用程序很可能会在不久的将来发起更多的请求。因此,HTTP/1.1 允许 HTTP 设备在事务处理结束之后会将 TCP 连接保持在打开状态,以便在未来重用现存的连接发起 HTTP 请求,称为持久连接,直到客户端或服务器决定将其关闭为止。持久连接可以避免缓慢的连接建立阶段,以及慢启动的拥塞适应阶段,以便更快速地进行数据传输。通常情况下,持久连接和并行连接配合使用
实现 HTTP/1.0 的客户端可以通过包含 Connection:Keep-Alive 首部请求将一条连接保持在打开状态,如果服务器愿意为下一条请求将连接保持在打开状态,就在响应中包含相同的首部。如果响应中没有 Connection:Keep-Alive 首部,客户端就认为服务器不支特 Keep-alive,会在收到响应报文之后关闭连接
可以用 Keep-A1ive 通用首部中指定的,由逗号分隔的选项来调节 keep-alive 的行为
Connection: Keep-Alive
Keep-Alive: max=5, timeout=120
HTTP/1.1 逐渐停止对 keep-alive 连接的支持,用一种名为持久连接(persistentconnection)的改进型设计取代了它。,HTTP/1.1 默认所有连接都是持久的,要在事务处理结束之后关闭连接,必须向报文中显式地添加一个 Connection: close
首部。客户端收到响应后,除非响应中包含 connection: close
首部,否则连接就维持在打开状态。但是,客户端和服务端仍然可以随时关闭空闲的连接,不发送 connection: close
并不意味着服务端承诺永远将连接保持在打开状态
HTTP/1.1 允许在持久连接上可选的使用管道,在响应到达之前,可以将多条请求放入队列,当第一条请求到达服务器,第二条和第三条请求也可以开始发送了。在高时延网络条件下,这样可以降低网络的环回时间,提高性能
对管道化连接有几点注意事项: