IP, TCP 和 HTTP(一)

原文链接:objc.io by Daniel Eggert

App 跟服务器交互时,多半是通过 HTTP。HTTP 的开发被用于浏览器:当你在浏览器中输入 http://www.objc.io 时,浏览器会与名为 www.objc.io 的服务器通过 HTTP 交流。

HTTP 是一个运行在应用层 ( applicaiton layer ) 的协议( protocol )。在各个层级之间有很多协议。协议栈通常被描绘成这样:

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

这种被称为 OSI(Open System Interconnection) 的模型定义了七层,我们将介绍应用层,传输层,以及网络层对于典型的协议的利用:HTTP,TCP 以及 IP。网络层下面是数据链路层以及物理层,这些是实现了以太网( Ethernet )(以太网有数据链路部分以及物理链接部分)。

我们将只介绍应用层,传输层以及网络层,实际上只关注一种特定的结合:HTTP 运行于 TCP 之上,同样运行在 IP 之上。这是我们 app 日常使用的网络最典型的配置。

我们希望这篇文章能够帮助你理解 HTTP 底层是如何工作的,以及遇到常见的问题你该如何去避免他们。

除了 HTTP 以外还有很多在 Internet 上传输数据的方法。HTTP 之所以会如此流行的原因是它几乎总是工作的,即使网络设备位于防火墙之下。

IP —— Internet Protocol

TCP/IP 中的 IP 是 Internet Protocol 的简称。正如名称所说,是 Internet 基础协议之一。

IP 实现了分组交换网络( packet-switched networking )。它有一种 主机( hosts) 的概念,也就是网络设备。IP 协议规定了 报文/数据包( datagrams/packets ) 如何在不同主机之间转发。

数据包是具有源主机和目标主机的二进制数据块。IP 网络简单的将数据包由源主机发送到目标主机。IP 网络会尽最大努力传输数据包,这个过程数据包可能会丢失永远抵达不了目标主机,或者它将被复制并抵达目标主机。

IP 网络中的每个主机都有一个地址——也被称为 IP 地址。每个数据包包含源主机和目标主机的地址。IP 的职责是 路由报文( routing datagrams )—— IP 数据包穿过网络,每个数据包穿过的节点(主机)查看它的目标节点的地址并规划出数据包应该走的路径。

今天,大部分的数据包都是基于 IPv4( Internet Protocol version 4 ),每个 IPv4 地址的长度是 32 位。它们常被写为 点分十进制(Dotted-decimal),如:192.168.100.1

最新的 IPv6 标准实施进展缓慢。它拥有更大的 128 位的地址长度,这对于数据包的路由将更加便捷。因为有更多可以利用的 IP 地址,一些技巧如 Network address translation 将不再是必须的。IPv6 的地址以8组16进制数字表示,如:2001:0db8:85a3:0042:1000:8a2e:0370:7334.

IP Header

一个 IP 数据包由 包头( header )有效载荷( payload ) 组成。载荷包含实际发送的数据,包头是元数据( metadata )。

IPv4 Header

一个 IPv4的包头通常像这样:

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)                                                                          |

包头通常有 20 字节的长度(没有例外,通常很少被完全利用)。

包头最有趣的部分是源主机( Source IP Address )和目标主机( Destination IP Address )的地址。除此之外:

  • version 域将被设为4 —— 因为是 IPv4。
  • protocol 域定义了载荷( payload )使用的 protocol。
  • TCP 协议的数字是6。
  • total length 域定义了整个数据包的长度 —— 头部和载荷之和。

IPv4 header 的详细介绍。

IPv6 Header

IPv6 使用的地址有128位。一个 IPv6 的包头通常是这样:

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 的包头有固定的 40 字节长度,相较 IPv4 利用率更高。

源主机和目标主机的地址同样是最有趣的部分,除此之外,next header 域定义了紧随 header 的数据。IPv6 允许数据包中存在 header 链(chaining of headers)。每个子 IPv6 header 同样包含一个 next header 域直到到达实际的 payload. 当 next header 值域的数值为 6( TCP 协议数值) 时,余下的数据包都将是 TCP 数据。

IPv6 packet 的详细介绍。

Fragmentation

IPv4 中,数据包可以被分割 get fragmented。传输层的底层拥有它支持的数据包大小的上限。IPv4 中,路由器可以将一个路由到数据链路层的数据包分割,以防它过大。被分割后的数据包将在目标主机被重组( get reassembled )。发送者可以决定是否允许路由器分割数据包,同样路由器可以返回 Packet Too Big ICMP 警告给发送者。

In IPv6, a router will always drop the packet and send back a Packet Too Big ICMP6 message to the sender. The end points use this to do a path MTU discovery to figure out what the optimal so-called maximum transfer unit (MTU) along the path between the two hosts is. Only when the upper layer has a minimum payload size that is too big for this MTU will IPv6 use fragmentation. With TCP over IPv6, this is not the case.

IPv6 中,路由器常丢弃数据包并返回 Packet Too Big ICMP6 给发送端。终端利用这种机制去做path MTU discovery从而得出两台主机间最佳的 最大传输单元 maximum transfer unit ( MTU )。 只有当上层的最小有效载荷大小对于该MTU来说太大时,IPv6才会使用 fragmentation。TCP 位于 IPv6 上层,情况就并非如此。

TCP —— Transmission Control Protocol

目前位于 IP 之上最常见的协议是 TCP,我们常统称为 TCP/IP。

IP 协议允许在两个主机之间发送独立的数据包。数据包将尽最大努力被传输:抵达目标主机的顺序可能跟源主机发送数据包的顺序不同,达到目标主机多次,或者一次都到不了。

TCP 构建于 IP 之上。TCP(传输层控制协议)可以提供在不同程序之间传递可靠的,有序的,查错的数据流。通过 TCP ,一台设备上运行的应用可以按照预先设想的方式向运行在另一台设备上的应用传输数据。这也许看起来微不足道,但是与 IP 协议的工作方式却形成鲜明的对比。

通过 TCP ,应用可以相互建立连接,一个 TCP 连接是双工的,支持双向发送数据。位于 TCP 连接两端的应用无须担心数据被分割成更小的数据包,或者增加了内容的数据包。TCP 保证数据到达另一端保持原本的状态。

一个典型的 TCP 用例便是 HTTP. 我们浏览器( Client ) 连接到 Web 服务器( Server )。一旦连接建立,浏览器可以通过连接发送请求,Web 服务器同样可以通过连接发送响应。

同一台主机上的不用程序可以同时的使用 TCP. 为了唯一的标识每个应用,TCP 有一种 端口( ports ) 的概念。两个应用之间的 TCP 连接拥有一个以源端口结尾的源 IP 地址和目标端口结尾的目标 IP 地址。这种配对的地址和端口号,唯一的标识了连接。

一个 Web 服务器使用 HTTPS 将监听 443 号端口。浏览器将使用被称为 短暂端口( ephemeral port ) 作为源端口然后使用 TCP 去建立配对的地址和端口连接。

TCP 未经修改地运行在 IPv4 和 IPv6 之上。需要将 IPv4 的 protocol 值域或 IPv6 的 Next Header 值域设置为6 —— 即 TCP 的协议号

TCP Segments

在主机之间流动的数据将被切割成小块,就变成了 TCP 报文段。TCP 报文段接着成为 IP 数据包的载荷( payload )。
任意 TCP 报文段有报头( header )和载荷( payload )。载荷表示实际需要发送的数据块。TCP 报头包含源端口和目标端口 —— 源地址和目标地址已经包含在 IP 头部了。

报文段头部同样包含 序列号( sequence numer) 以及 确认码( acknowledgement number) ,和一些其他值域被 TCP 用于管理连接。

我们仔细看下序列号的更多细节:它是一种给任意报文段一串独立的数字序列的机制。第一个报文段有一串随机数字,如 :1721092979,下一个报文段的序列号按次序增长,如:1721092980,1721092981...同样,确认码帮助终端通过收到的报文段找到发送端。因为 TCP 是双工的,这种场景发生在收发的两端。

TCP Connections

连接管理是 TCP 的核心组件,协议需要使用很多 tricks 去隐藏不可靠的 IP 层的复杂性。我们将快速浏览连接的建立,实际的数据流,以及连接的终结。连接状态的过渡非常的复杂( 参见 TCP state diagram )。但大多数情况下,事情还是相对简单的。

TCP 连接的建立

TCP 中,连接总是由一台主机到另一台主机建立。因此,连接的建立有两种不同的角色出现:一台终端(如 Web 服务器)监听连接,另一边的终端(如 App )连接到监听服务器( Web 服务器)。服务器表现出所谓的 被动打开( passive open ) 的方式 —— 它由监听开始。客户端表现出所谓的 主动打开( active open ) 的方式。

连接通过三次握手的方式建立,它是像这样工作的:

  • 客户端向服务端发送 SYN 随机的序列号A
  • 服务端返回给客户端 SYN-ACK 确认码A+1和随机序列号 B
  • 客户端向服务端发送 ACK确认码 B+1

SYN同步序列号( synchronize sequence numbers ) 的简写。一旦数据流抵达两端,任意 TCP 数据报都将拥有序列号。这是 TCP 确保所有的数据分批抵达另一端,以及将它们按照次序整理排列的原理。在传输开始之前,收发端都需要通过第一个报文段同步序列号。

ACK确认( acknowledgment ) 的简写。当一个报文段抵达另一端的时候,接收端将根据接收的报文段的序列号返回确认码。

例如在 Terminal 中输入:
curl -4 http://www.apple.com/contact/

这将通过 curl 创建一个 TCP 连接到 www.apple.com 的 80 端口。

www.apple.com 的服务器监听 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 表示配对的源和目标地址及端口。第一行和第三行是从客户端到服务端的访问,第二行是从服务端到客户端。tcpdump 简单的将端口号拼接到 IP 地址后面 —— 10.0.1.6.52181 意味着 IP 地址是 10.0.1.6 端口号是 52181

Flags 标识符指 TCP 报文段头部的 flag:

  • SSYN
  • .ACK
  • PPUSH
  • FFIN

还有更多 flag 这里没有出现。注意这三行的顺序: SYN -> SYN-ACK -> ACK ,表示三次握手的次序。

  • 第一行表示客户端发送了随机的序列号 1721092979(A) 到服务端
  • 第二行服务端返回客户端确认码1721092980(A+1)以及随机序列号673593777(B)
  • 第三行客户端向服务端发送确认码673593778(B+1)

Options

在连接设置期间发生的另一件事是两端交换 options. 第一行客户端发送:

[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/erc 被用于评估 TCP 往返时间( round-trip time,RTT)TS val是发送端的时间戳,ercecho reply 时间戳 —— 通常是指发送端收到的最新的时间戳。TCP 利用 RTT 去使用 拥塞控制( congestion-control ) 算法。

两端都发送了sackOK,即 选择性确认( Selective Acknowledgement )。这将允许两端确认收到字节的范围。通常确认机制仅允许接收端确认收到的总字节数。SACK 的概述在 section 3 of RFC 2018.

mss 选项定义了 最大报文段大小( Maximum Segment Size ),表示接收端期望接收的单个报文段最大的字节数。wscale 表示屏幕伸缩条件,后面将会详细描述。

连接数据流 Connection Data Flow

TCP 连接建立后,连接的两端都可以相互发送数据。任意报文段都有与目前发送字节数相对应的序列号。接收端将根据接收到的报文段头部的确认码( ACK )识别相应的数据包。

假设我们每次发送 10 字节的报文段,最后一次发送 5 字节,就像这样:

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

这种机制发生在 TCP 连接的两边。主机 A 保持发送数据包。当这些数据包抵达主机 B 时,B 将发送返回确认码到 A,此时 A 仍在持续发送数据包而不必等待 B 返回确认码。

TCP 的协同流控制( incorporate flow control )以及很多复杂的拥塞控制( congestion control )机制,这些机制都将用于:

  • 辨别哪些报文段发生丢失,需要重发
  • 分析哪些报文段的发送速率需要作出调整

流控制( flow control )是为了确保发生端的发送速率不至于高于接收端的处理速率。接收端发送所谓的 receive window,告知发送端目前接收端可以接收缓冲的数据大小。其中的一些微妙细节我们将跳过,不过根据上面 tcpdump 的输出,我们可以看到 win 65535wscale 4。第一个表示 window 的大小,第二个表示 scale 因子,结果就是 IP 地址为 10.0.1.6 的主机表示它的 receive window4 * 64 kB = 256 kB。IP 为 23.63.125.15 通告的 win 14480wscale 1, 大致为 14 kB。当任意一方接收数据,它将反馈 receive window 到另一方。

拥塞控制( congestion control )机制略显复杂一些。这里的各种机制都是控制可以通过网络发送数据的比率,这是种精巧的平衡。一方面,收发双方都希望尽可能快的发送数据,另一方面,发送太多数据会导致性能显著的下降。这种被称为 拥塞性崩溃( congestive collapse) 这是数据包交换网络( packet-switched networks )的固有属性。当有太多的数据包被发送时,数据包之间将会发生碰撞,丢包率会显著上升。

拥塞控制机制同样需要与其他控制机制保持良好的合作。今天的拥塞控制机制在 TCP 的概述( RFC 5681 )中有 6000 多字。TCP 基本的理论是发送端需要检验其收到的确认码,这是一件非常棘手的事,需要做非常多的权衡。注意底层的 IP 数据包会无序的到达接收端。发送端需要评估往返时间( round-trip time )然后判定其是否已经收到确认码。重发数据包的代价显然是昂贵的,但是不发的话将会引发连接停滞,网络上的下载多半是非常动态的。目前的 TCP 算法需要不断地适应当前的场景。

需要指出的是 TCP 连接是非常生动和灵活的。除去实际的数据流,收发两端都会不断地发送提示( hints )和来来回回一直微调连接。

正因为有这种调整,短暂的 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

IP 为 10.0.1.6 的客户端发送第一个长度为85 length 85 的报文段。确认码( ACK )被以相同的数字返回 (1721093065),因为是最后一条报文段已经没有新的数据发送过来了。

IP 为 23.63.125.15 服务端确认收到数据(实际上未发送任何数据):length 0。因为连接使用 选择性确认( Selective acknowledgments ),序列号和确认码为字节范围:17210929801721093065 即 85 字节。服务端返回确认码ack 1721093065,这意味着:我所拥有的字节数是 1721093065。这个数字如此之大的原因是序列号是以一串随机数开始,字节数只与初始数字相关。

这种模式将持续到所以数据被发送:

18:31:29.189335 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673593778:673595226, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190280 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673595226:673596674, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190350 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673596674, win 8101, options [nop,nop,TS val 743929811 ecr 1433256660], length 0
18:31:29.190597 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673596674:673598122, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190601 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673598122:673599570, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190614 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673599570:673601018, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190616 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673601018:673602466, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190617 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673602466:673603914, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190619 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673603914:673605362, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190621 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673605362:673606810, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.190679 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673599570, win 8011, options [nop,nop,TS val 743929812 ecr 1433256660], length 0
18:31:29.190683 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673602466, win 7830, options [nop,nop,TS val 743929812 ecr 1433256660], length 0
18:31:29.190688 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673605362, win 7830, options [nop,nop,TS val 743929812 ecr 1433256660], length 0
18:31:29.190703 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673605362, win 8192, options [nop,nop,TS val 743929812 ecr 1433256660], length 0
18:31:29.190743 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673606810, win 8192, options [nop,nop,TS val 743929812 ecr 1433256660], length 0
18:31:29.190870 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [.], seq 673606810:673608258, ack 1721093065, win 7240, options [nop,nop,TS val 1433256660 ecr 743929773], length 1448
18:31:29.198582 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [P.], seq 673608258:673608401, ack 1721093065, win 7240, options [nop,nop,TS val 1433256670 ecr 743929811], length 143
18:31:29.198672 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673608401, win 8183, options [nop,nop,TS val 743929819 ecr 1433256660], length 0

连接终止 Connection Termination

最终连接被终止。两端都相互发送 FIN 标示告诉双方连接已终止。这个 FIN 随后被确认。当双方都发送了 FIN 标示以及确认结束后,TCP 连接被完全终止:

18:31:29.199029 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [F.], seq 1721093065, ack 673608401, win 8192, options [nop,nop,TS val 743929819 ecr 1433256660], length 0
18:31:29.208416 IP 23.63.125.15.80 > 10.0.1.6.52181: Flags [F.], seq 673608401, ack 1721093066, win 7240, options [nop,nop,TS val 1433256680 ecr 743929819], length 0
18:31:29.208493 IP 10.0.1.6.52181 > 23.63.125.15.80: Flags [.], ack 673608402, win 8192, options [nop,nop,TS val 743929828 ecr 1433256680], length 0

需要注意的是第二行,23.63.125.15主机发送它的 FIN 标示,与此同时确认使用 ACK 确认对方的 FIN 标示。所有的内容在一个报文段内。

推荐阅读更多精彩内容