HTTP 连接详解

概述

世界上几乎所有的 HTTP 通信都是由 TCP/IP 承载的,客户端可以打开一条TCP/IP连接,连接到任何地方的服务器。一旦连接建立,客户端和服务器之间交换的报文就永远不会丢失、受损或失序

TCP(Transmission Control Protocol)传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。通俗来讲,TCP就是双方通信的一个规范标准,负责对数据的传输进行一定的控制

HTTP 要传送一条报文时,会以流的形式将报文数据的内容通过一条打开的 TCP 连接按序传输。TCP 收到数据流之后,会将数据流分成被称作段的小数据块,并将段封装在 IP 分组中,通过因特网进行传输。所有这些工作都是由 TCP/IP 软件来处理的、HTTP 程序员什么都看不到

每个 TCP 段都是由 IP 分组承载,从一个 IP 地址发送到另一个 IP 地址,每个 IP 分组包括:

  • IP 分组首部(通常为 20 字节)
  • TCP 段首部(通常为 20 字节)
  • TCP 数据块(0 个或多个字节)

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 三次握手,即建立 TCP 连接,需要客户端和服务端总共发送 3 个包以确认连接的建立。在 Socket 编程中,这一过程由客户端执行 connect 来触发

TCP 握手的目的有三个:

  • 确认双方的接收与发送能力是否正常
  • 初始化序列号,为后面的可靠传送做准备
  • 进行数字证书的验证以及加密密钥的生成
  • 第一次握手:客户端发送请求报文将 SYN = 1 同步序列号和初始化序列号 seq = J 发送给服务端,发送完之后客户端处于发送等待状态
  • 第二次握手:服务端受到 SYN 请求报文之后,如果同意连接,会以自己的同步序列号 SYN = 1、初始化序列号 seq = K 和确认序列号(期望下次收到的数据包)ack = J + 1 以及确认号 ACK = 1 报文作为应答,此时服务器为确认接收状态
  • 第三次握手:客户端接收到服务端的 SYN + ACK 之后,知道可以发送下一序列的数据包了,然后发送同步序列号 ack = K + 1 和数据包的序列号 seq = J + 1 以及确认号 ACK = 1 确认包作为应答,客户端转为确认连接状态

为什么是三次握手,而不是一次、二次呢?因为有可能出现这种情况:客户端发送了一个连接请求,但出现网络延迟,导致客户端没有及时收到服务端的响应,就会认为本次请求失效。而这时原本延迟的请求又来到服务端,服务端确认并保持等待状态,但实际上此时客户端并没有与服务端连接的意思,这就会导致服务器一直处于等待状态,造成资源浪费

TCP 四次挥手

TCP 四次挥手,即终止 TCP 连接,需要客户端和服务端总共发送 4 个包以确认连接的断开。在 Socket 编程中,这一过程由客户端或服务端任一方执行 close 来触发

  • 第一次分手:第一次分手无论是客户端还是服务端都可以发起,因为 TCP 是全双工的。假如客户端发送的数据已经发送完毕,发送 FIN = 1 告诉服务端,客户端所有数据已经全发完了,服务端可以关闭接收了,但如果服务端还有数据要发给客户端,客户端照样可以接收的。此时客户端处于 FIN_WAIT_1 等待关闭状态
  • 第二次分手:服务端接收到客户端的释放请求连接之后,知道客户端没有数据要发给自己了,然后服务端发送 ACK = 1 告诉客户端已经收到发给自己的信息,此时服务端处于 CLOSE_WAIT 等待关闭状态
  • 第三次分手:此时服务端已经没有数据向客户端发送了,然后发送一个 FIN = 1,用于告诉客户端,服务端的所有数据发送完毕,客户端可以关闭接收数据连接了。此时服务端状态处于 LAST_ACK 确认关闭状态
  • 第四次分手:此时如果客户端收到了服务端发送完的信息之后,就发送 ACK = 1,告诉服务端,客户端已经收到了服务端的信息,服务端处于 CLOSED 状态,四次挥手全部完成

为什么是四次挥手呢?因为关闭连接时,己方收到对方的 FIN 报文,仅仅表示对方不再向自己发送数据,但还能接受数据。己方可能还有数据要发送给对方,所以不能向三次握手一样直接把 ACK 和 SYN 放一起发送,而是先发送 ACK,直到没有数据要发送了,才是 FIN 确认关闭连接

TCP 性能优化

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

1. 延迟确认

为了避免网络延迟导致的数据丢失,TCP 实现了自己的确认机制来确保数据的成功传输

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

由于确认报文很小,所以 TCP 允许将返回的确认信息与输出的数据分组结合在一起,更有效地利用网络。为了增加确认报文找到同向传输数据分组的可能性,TCP 实现了一种【延迟确认】算法,延迟确认算法会在一个特定的窗口时间(通常是 100-200 毫秒)内将输出确认存放在缓冲区中,以寻找能够捎带它的输出数据分组。如果在那个时间段内没有输出数据分组,就将确认信息放在单独的分组中传送

2. TCP 慢启动

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

TCP 慢启动限制了一个 TCP 端点在任意时刻可以传输的分组数。简单来说,每成功接收一个分组,发送端就有了发送另外两个分组的权限。如果某个 HTTP 事务有大量数据要发送,是不能一次将所有分组都发送出去的,必须发送一个分组,等待确认,然后发送两个分组,每个分组都必须被确认,然后发送四个分组,以此类推,这种方式被称为【打开拥塞窗口】

由于存在这种拥塞控制特性,所以新连接的传输速度会比已经交换过一定量数据的连接慢一些

3. Nagle 算法与 TCP_NODELAY

TCP 有一个数据流接口,应用程序可以通过它将任意尺寸的数据放入 TCP 栈中,即使一次只放一个字节。但是,每个 TCP 段都至少装载了40个字节的标记和首部如果 TCP 发送了大量包含少量数据的分组,网络的性能就会严重下降

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

Nagle 算法会引发几种 HTTP 性能问题,首先,小的 HTTP 报文可能无法填满一个分组,可能会因为等待那些永远不会到来的额外数据而产生时延。其次,Nagle 算法与延迟确认之间的交互存在问题,Nagle 算法会阻止数据的发送,直到有确认分组抵达为止,但确认分组自身会被延迟确认算法延迟 100-200 毫秒

HTTP应用程序常常会禁用 Nagle 算法,如果要使用的话,一定要确保会向TCP写入大块的数据,不会产生一堆小分组

4. TIME_WAIT 累积与端口耗尽

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

2MSL 的连接关闭延迟在某些情况下会出现问题,例如客户端每次连接到服务器时,都会获得一个新的端口,以实现连接的唯一性,但由于可用端口的数量有限(比如 60000 个),而且在 2MSL 秒(比如 120 秒)内连接是无法重用的,连接率就被限制在 60000/120=500 次/秒,如果服务器的连接率高于 500 次/秒,就会遇到端口耗尽问题

HTTP 连接处理

1. 串行事务处理延时

如果只对连接进行简单的管理,TCP 的性能时延可能会叠加起来。比如,假设有一个包含了三个嵌入图片的 Web 页面,测览器需要发起四个 HTTP 事务来显示此页面:一个用于顶层的 HTML 页面,三个用于嵌入的图片。如果每个事务都需要建立一条新的连接,那么连接时延和慢启动时证就会叠加起来

除了串行加载引入的实际时延之外,加载一幅图片时,页面上其他地方都没有动静也会让人觉得速度很慢,用户更希望能够同时加载多幅图片。并行加载的另一个缺点是,有些沟览器在对象加载完毕之前无法获知对象的尺寸,而它们可能需要尺寸信息来决定将对象放在屏幕的什么位置,所以在加载了足够多的对象之前,无法在屏靠上显示任何内容

2. 并行连接

HTTP 允许客户端打开多条连接,并行地执行多个 HTTP 事务,提高加载速度,但并不是绝对的,在带宽较小的情况下,并行执行多个 HTTP 事务带来的性能提升就很小,甚至没什么提升。而且,打开大量连接会消耗大量的资源

3. 持久连接

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
  • timeout:估计服务器希望将连接保持在活跃状态的时间,这并不是一个承诺值
  • max:估计服务器还希望为多少个事务保持此连接的活跃状态,这并不是一个承诺值

HTTP/1.1 逐渐停止对 keep-alive 连接的支持,用一种名为持久连接(persistentconnection)的改进型设计取代了它。,HTTP/1.1 默认所有连接都是持久的,要在事务处理结束之后关闭连接,必须向报文中显式地添加一个 Connection: close 首部。客户端收到响应后,除非响应中包含 connection: close 首部,否则连接就维持在打开状态。但是,客户端和服务端仍然可以随时关闭空闲的连接,不发送 connection: close 并不意味着服务端承诺永远将连接保持在打开状态

4. 管道化连接

HTTP/1.1 允许在持久连接上可选的使用管道,在响应到达之前,可以将多条请求放入队列,当第一条请求到达服务器,第二条和第三条请求也可以开始发送了。在高时延网络条件下,这样可以降低网络的环回时间,提高性能

对管道化连接有几点注意事项:

  • 如果 HTTP 客户端无法确认连接是持久的,就不应该使用管道
  • 必须按照与请求相同的顺序回送 HTTP 响应,HTTP 报文中没有序列号标签,如果收到的响应失序了,就没办法将其与请求匹配起来了
  • HTTP 客户端必须做好连接会在任意时刻关闭的准备,还要准备好重发所有未完成的管道化请求。如果客户端打开了一条持久连接,并立即发出了十条请求,服务器可能只处理了五条请求之后关闭连接,客户端必须能够应对过早关闭连接的情况,重新发送请求
  • HTTP 客户端不应该用管道化的方式发送会产生副作用的请求(比如 POST),如果出错,管道化方式会阻碍客户端了解服务器执行的是一系列管道化请求中的哪一个