[+]文章目录

IP,TCP 和 HTTP

当 app 和服务器进行通信的时候,大多数情况下,都是采用 HTTP 协议。HTTP 最初是为 web 浏览器而定制的,如果在浏览器里输入 http://www.objc.io ,浏览器会通过 HTTP 协议和 www.objc.io 所对应的服务器进行通信。

HTTP是运行在应用层上的应用协议,而不同的层级上都有相应的协议在运行。层级的堆栈关系一般可以这么描述:

Application Layer -- e.g. HTTP
----
Transport Layer -- e.g. TCP
----
Internet Layer -- e.g. IP
----
Link Layer -- e.g. IEEE 802.2

所谓的 OSI(Open Systems Interconnection,开放式系统互联)模型定义了七层结构。本文会关注应用层 (application layer)、传输层 (transport layer) 和网络层 (internet layer),它们分别代表了典型的 HTTP 的应用的 HTTP,TCP 以及 IP。在 IP 之下的是数据连接和物理层级,比如像 Ethernet 的实现之类的东西(Ethernet 拥有一个数据连接部分以及一个物理部分)。

如上文所述,我们只关注应用层,传输层和互联网层的部分,更确切的说,着重探讨一种特殊的混合模式:基于 IP 的 TCP,以及基于 TCP 实现的 HTTP。这就是我们每天使用的 app 的基本网络配置。

通过本文,希望大家能够对HTTP工作原理有一个细致的了解,知道一些常见的 HTTP 问题的产生原因,从而能在实践中尽量避免这些问题的发生。

其实在互联网上传递数据的方式并不只 HTTP 一种。HTTP 之所以被广泛使用的原因是其非常稳定、易用,即便是防火墙一般也是允许 HTTP 协议穿透的。

接下来我们从最低的一层谈起,说说 IP 网络协议。

IP网络协议 (IP-Internet Proctocol)

TCP/IP 中的 IP 是网络协议 (Internet Protocol) 的缩写。从字面意思便知,它是互联网众多协议的基础。

IP 实现了分组交换网络。在协议下,机器被叫做 主机 (host),IP 协议明确了 host 之间的资料包(数据包)的传输方式。

所谓数据包是指一段二进制数据,其中包含了发送源主机和目标主机的信息。IP 网络负责源主机与目标主机之间的数据包传输。IP 协议的特点是 best effort(尽力服务,其目标是提供有效服务并尽力传输)。这意味着,在传输过程中,数据包可能会丢失,也有可能被重复传送导致目标主机收到多个同样的数据包。

IP 网络中的主机都配有自己的地址,被称为 IP 地址。每个数据包中都包含了源主机和目标主机的 IP 地址。IP 协议负责路径计算,即 IP 数据包在网络中的传输传输时,数据包所经过的每一个主机节点都会读取数据包中的目标主机地址信息,以便选择朝什么地方传送数据包。

今天,绝大多数的数据包仍旧是 IPv4(Internet Protocol version 4 网际协议版本 4)的,每一个 IPv4 地址是长度为 32 位。常见采用 dotted-decimal(点分十进制)表示法,具体形式如:198.51.100.42。

新的 IPv6 标准也正在逐渐推广中。它有更大的地址空间:长度为 128 位,这使得数据包在网络中传输时的寻址更容易一些。另外,由于有更多的地址可以分配,诸如网络地址转换等问题也迎刃而解。IPv6 的表示形式为:八组十六进制数以冒号分割,比如:2001:0db8:85a3:0042:1000:8a2e:0370:7334。

IP Hearder

一个 IP 数据包通常包含 header (报头信息) 和 payload (有效载荷)。

payload 中的内容即是要传输的真正信息,而 header 承载的是与传输数据有关的元数据 (metadata)。

IPv4 Header

IPv4的 header 信息内容如下:

IPv4 Header Format
Offsets  Octet    0                       1                       2                       3
Octet    Bit      0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31|
 0         0     |Version    |IHL        |DSCP            |ECN  |Total Length                                   |
 4        32     |Identification                                |Flags   |Fragment Offset                       |
 8        64     |Time To Live           |Protocol              |Header Checksum                                |
12        96     |Source IP Address                                                                             |
16       128     |Destination IP Address                                                                        |
20       160     |Options (if IHL > 5)                                                                          |

header 长度为 20 字节(不包含极少用到的可选项信息)。

header 信息中最关键的是源和目标 IP 地址。除此之外,版本信息是 4,代表 IPv4。protocol(协议区)代表 payload 采用的传输协议。TCP 的协议号是 6。Total Length(总长度区)标明了 header 加 payload 整个数据包的大小。

详情参看维基百科中关于 IPv4 的条目,里面有关于 header 各个区域信息的详细介绍。

IPv6 Header

IPv6 的地址长度为 128 位。IPv6 的 header 信息内容如下:

Offsets  Octet    0                       1                       2                       3
Octet    Bit      0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31|
 0         0     |Version    |Traffic Class         |Flow Label                                                 |
 4        32     |Payload Length                                |Next Header            |Hop Limit              |
 8        64     |Source Address                                                                                |
12        96     |                                                                                              |
16       128     |                                                                                              |
20       160     |                                                                                              |
24       192     |Destination Address                                                                           |
28       224     |                                                                                              |
32       256     |                                                                                              |
36       288     |                                                                                              |

IPv6 header 采用固定长度 40 字节。经过多年来对 IPv4 使用的总结,如今 IPv6 的 header 信息简化了许多。

除了源和目标地址这种必备信息外,IPv6 提供专门的 next header 区域来指明紧接 header 的数据是什么。也就是说,IPv6 允许在数据包中将 header 链接起来。每一个被链接的 IPv6 header 都会有一个 next header 字段,直到到达实际的 payload 数据。比如说,当 next header 的值为 6 (TCP 的协议号) 时,数据包的其他信息就是 TCP 协议要传输的数据。

同样的,更多信息请参考维基百科上关于 IPv6 数据包的条目

Fragmentation (数据分片)

由于底部链路层对所传输的数据帧有最大长度限制(最大传输单元,MTU),所以有时候 IPv4 需要对所传数据包进行分片。具体表现为,如果数据包尺寸超过了所要经过的数据链路的最大传输限制,路由就会对数据包进行分片。当分片数据包到达目标主机后,可以根据分片信息进行数据重组。当然,数据发送源有权决定路由是否启用对传输数据包进行分片,假如所传输的数据超过了输送限制,又禁止了路由分片,发送源会收到 ICMP(Internet Control Message Protocol,Internet报文控制协议) 的数据帧超长报告信息。

在IPv6中,如果数据包超限制,路由会直接丢弃数据包并且向发送源回传 ICMP6数据帧超长报告信息。源和目标两端会基于这个特性来进行路径 MTU 发现,以此寻找两端之间最大传输单元(maximum transfer unit)所在的路由。找到 MTU 路由后,仅当上层数据包的最小 payload 确实超过了 MTU,IPv6 才会进行分片传输。对于 IPv6 下的 TCP 来说,这不会造成什么问题。

TCP - 传输控制协议 (Trasnmission Control Protocol)

TCP 层位于 IP 层之上,是最受欢迎的因特网通讯协议之一,人们通常用 TCP/IP 来泛指整个因特网协议族。

刚刚提到,IP 协议允许两个主机之间传送单一数据包。为了保证对所传送数据包达到尽力服务的目的,最终的传输的结果可能是数据包乱序、重复甚至丢包。

TCP 是基于 IP 层的协议。但是 TCP 是可靠的、有序的、有错误检查机制的基于字节流传输的协议。这样当两个设备上的应用通过 TCP 来传递数据的时候,总能够保证目标接收方收到的数据的顺序和内容与发送方所发出的是一致的。TCP 做的这些事看起来稀松平常,但是比起 IP 层的粗旷处理方式已经是有显著的进步了。

应用程序之间可以通过 TCP 建立链接。TCP 建立的是双向连接,通信双方可以同时进行数据的传输。连接的双方都不需要操心数据是否分块,或者是否采用了尽力服务等。TCP 会确保所传输的数据的正确性,即接受方收到的数据与发出方的数据一致。

HTTP 是典型的 TCP 应用。用户浏览器(应用 1)与 web 服务器(应用 2)建立连接后,浏览器可以通过连接发送服务请求,web 服务器可以通过同样的连接对请求做出响应。

同一个 host 主机上可以有多个应用同时使用 TCP 协议。TCP 用不同的端口来区分应用。作为连接的两端,发送源和接收目标分别拥有自己的 IP 地址和端口号。凭借这样一对 IP 地址和端口号,就可以唯一标识一个连接。

使用 HTTPS 的 web 服务器会监听 443 端口。浏览器作为发送源会启用一个临时端口结合自己的 IP 地址与目标服务器对应的端口和 IP 地址建立 TCP 连接。

TCP 在 IPv4 和 IPv6 上是无差别运行的。所以,如果 IPv4 的 Protocol 或 IPv6 的 Next Hearder的协议号被设置成 6,表示执行 TCP 协议。

TCP Segments (TCP 报文段)

主机之间传输的数据流一般先会被分块,再转化成 TCP 的报文段,最终会生成 IP 数据包中的 payload 载荷数据。

每个 TCP 报文段都有 header 信息和对应的载荷 payload。payload 信息就是待传输的数据块。TCP 报文段的 header 信息中主要包含的是源和目标端口号,至于说源和目标的 IP 地址信息则已经包含在 IP header 信息中了。

TCP 的报文段 header 信息中还有报文序列号、确认号等其他一些用于管理连接的信息。

所谓序列号信息,其实就是为每个报文段分配的唯一编号。第一个报文段的序列号是随机的,比如:1721092979,其后的每一个报文段的序列号都以此号为基础依次加 1,1721092980,1721092981 等等。至于确认号,是目标端反馈给源的确认信息,通知源目前已经接到哪些报文段了。由于 TCP 是双向的,所以数据和确认信息发送也都是双向的。

TCP 连接

连接管理是 TCP 的核心功能之一,而且协议需要解决由于IP层采用不可靠传输引发的一系列复杂问题。下面会分别介绍TCP的连接建立、数据传输以及连接终止的详细过程。

TCP 连接全过程的状态变化是很复杂的(参考 TCP 状态图)。但是大多数情况下还是比较简单的。

连接建立

TCP 连接都是建立在两个主机之间的。所以,每个连接建立过程中都存在两个角色:一端(例如 web 服务器)监听连接,另一端(例如应用)主动连接正在监听的一端(web 服务器)。服务器端的这种监听行为被称为 passive open(被动打开)。客户端主动连接服务器的行为被称为 active open(主动打开)。

TCP 会通过三次握手来完成连接建立,具体过程是这样的:

  1. 客户端首先向服务端发送一个 SYN 包和一个随机序列号 A
  2. 服务端收到后会回复客户端一个 SYN-ACK 包以及一个确认号(用于确认收到 SYN)A+1,同时再发送一个随机序列号 B
  3. 客户端收到后会发送一个 ACK 包以及确认号(用于确认收到 SYN-ACK)B+1 和序列号 A+1 给服务端

SYNsynchronize sequence numbers (同步序列号) 的缩写。两端在传递数据时,所传递的每个 TCP 报文段都有一个序列号。就是利用这种机制,TCP 可以确保分块传输的数据包最终都以正确的个数和顺序抵达目标端。在正式传输开始之前,源和目标端需要同步确认第一个报文的序列号。

ACKacknowledgment (确认)的缩写。当某一端接到了报文包后,通过回传已报文序列号来确认接收到报文这件事。

运行如下语句:

curl -4 http://www.apple.com/contact/

这是通过 curl 命令与 www.apple.com 的 80 端口创建一个 TCP 连接。

www.apple.com 所在服务器 23.63.125.15(注意,整个 IP 不是固定的)会监听 80 端口。我们自己的 IP 地址是 10.0.1.6,启用的临时端口 52181(这个端口是从可用端口中随机选择的)。利用 tcpdump(1) 输出的三次握手过程是这样的:

% sudo tcpdump -c 3 -i en3 -nS host 23.63.125.15
18:31:29.140787 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [S], seq 1721092979, win 65535, options [mss 1460,nop,wscale 4,nop,nop,TS val 743929763 ecr 0,sackOK,eol], length 0
18:31:29.150866 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [S.], seq 673593777, ack 1721092980, win 14480, options [mss 1460,sackOK,TS val 1433256622 ecr 743929763,nop,wscale 1], length 0
18:31:29.150908 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673593778, win 8235, options [nop,nop,TS val 743929773 ecr 1433256622], length 0

这里信息量很大。下面要逐个分析一下。

最左边是系统时间。当时执行命令的时间是晚上18:31。后面的 IP 代表的是这些都是 IP 协议数据包。

接下来看这段 10.0.1.6.52181 > 23.63.125.15.80,这一对是源和目标端的 IP 地址+端口。第一行和第三行是客户端发向服务端的信息,第二行是服务端发向客户端的。tcpdump 会自动把端口号加到 IP 地址后头,比如 10.0.1.6.52181 表示 IP 地址为 10.0.1.6,端口号为 52181。

Flags 表示 TCP 报文段 header 信息中的一些缩写标识:S 代表 SYN,. 代表ACK,P 代表PUSH,F 是 FIN。还有一些其他的标识,这边就不罗列了。注意上面三行 Flags 中先是携带 SYN ,接着是 SYN-ACK,最后是 ACK,这就是三次握手确认的全过程。

另外,第一行中客户端发送了一个随机序列号 1721092979 (就是上文所说的A)给服务器。第二行展示的是服务器回传给客户端的确认号 1721092980 (A+1) 和一个随机序列号 673593777 (B)。 最后在第三行,客户端将自己的确认号 673593778 (B+1) 发还给服务端。

其他选项

当然,在连接建立过程中还会配置一些其他的信息。比如第一行中客户端发送的内容:

[mss 1460,nop,wscale 4,nop,nop,TS val 743929763 ecr 0,sackOK,eol]

还有第二行服务端发送的:

[mss 1460,sackOK,TS val 1433256622 ecr 743929763,nop,wscale 1]

其中 TS val / ecr 是 TCP 用来创建 RTT 往返时间 (round-trip time) 的。TS val 是发送方的 时间戳 (time stamp),ecr 是相应应答 (echo reply) 时间戳,通常情况下就是发送方收到的最后时间戳。TCP 以 RTT 作为其拥塞控制算法 (congestion-control algorithms) 的依据。

连接的两端都发送 sackOK。这样会启用选择性确认 (Selective Acknowledgement) 机制,使连接双方能够确认收到的字节范围。一般情况下,确认机制只是确认接受方已收到的数据的字节总数。RFC 2018 第 3 部分有对 SACK 的详细阐述。

mss 选项声明了最大报文长度 (Maximum Segment Size),表示接收端希望接收的单个报文的最大长度(以字节为单位)。wscale 是 窗口放大因子 (window scale factor),稍后会详细说明。

数据传输

一旦建立了连接,双方就可以互发数据了。发送端所发出的每个报文段都有一个序列号,这个序列号与当下已传送的字节总数有关。接收端会针对已接收的数据包向源端发送确认报文,确认信息同样是由报文 header 所携带的 ACK

假设现在传送的信息是除最后一个报文 5 字节外,其他都是 10 字节。具体是这样的:

host A sends segment with seq 10
host A sends segment with seq 20
host A sends segment with seq 30    host B sends segment with ack 10
host A sends segment with seq 35    host B sends segment with ack 20
                                    host B sends segment with ack 30
                                    host B sends segment with ack 35

整个机制是双向运转的。A 主机会持续的发送数据包。B 收到数据包后会向 A 发送确认信息。A 发送数据包的过程不需要等待 B 的确认。

TCP 将流量控制和其他一系列复杂机制结合起来进行拥塞控制。需要处理以下问题:针对丢失的报文采用重发机制,同时还需要动态的调整发送报文的频率。

流量控制的原则是发送方发送数据的速度不能比接收方处理数据的速度快。接收方,也就是所谓的 接收窗口 (receive window) 会告知发送方自身接收窗口数据缓冲区的大小。从上面 tcpdump 的输出来看,窗口大小是 win 65535,wscale(窗口放大因子)是 4。这些数字的意思是说,10.0.1.6 主机的接收窗口大小是 4*64 kB = 256 kB,23.63.125.15 主机的 win 是 14480,wscale 是 1,接收窗口约为 14KB。总之,不管哪一方作为数据接收方,都会向对方通报自己的接收窗口大小。

拥塞控制要更复杂一些。所有拥塞控制的目标都是要计算出当前网络中数据传输的最佳速率。所谓最佳速率就是要达到一种微妙的平衡。一方面,是希望速度越快越好,另一方面,速度快意味着数据传输多,这样处理性能会大打折扣甚至导致崩溃。而这种超负荷崩溃是分组交换网络的固有特点。当负载过大,数据包之间会产生拥塞,直接导致丢包率急速上升。

拥塞控制还需要充分考虑对流量的影响。RFC 5681 中对 TCP 拥塞控制有 6,000 字左右的阐述。发送方要时刻关注来自接收方的确认信息。要做到这点并不简单,有的时候还需要一定的妥协。要知道底部 IP 协议数据包是无序传输的,数据包会丢失也会重复。发送方需要评估 RTT 往返时间,然后基于 RTT 去确定是否收到了接收方的确认信息。重发数据包也有很大代价,除了连接延迟问题,网络的负载也会发生明显的波动。导致 TCP 需要不停的去适应当前网络情况。

更重要的是,TCP 连接本身是易变的。除了数据传输,连接的两端还会不时的发送一些提醒和确认信息以便可以适当的调整状态来维持连接。

基于这种一直在相互协调中的连接关系,TCP 连接往往会是短暂而低效的。在建立连接的初期,TCP 协议算法还不能完全了解当前网络状况。而在连接将要结束的时候,反馈给发送方的信息又可能不充分,这样就很难对连接状况做出实时的合理的评估。

之前展示了客户端和服务端之间交换的三段报文。再看看关于连接的其他信息:

18:31:29.150955 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [P.], seq 1721092980:1721093065, ack 673593778, win 8235, options [nop,nop,TS val 743929773 ecr 1433256622], length 85
18:31:29.161213 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], ack 1721093065, win 7240, options [nop,nop,TS val 1433256633 ecr 743929773], length 0

客户端 10.0.1.6 发送的第一段报文长度是 85 bytes (HTTP 请求)。由于在上一个报文发送后没有收到来自服务端的信息,所以 ACK 确认号的值不变。

服务端