原文: http://www.objc.io/issue-10/ip-tcp-http.html
应用与服务器之间的沟通往往是建立在 HTTP 之上的。HTTP被构建在浏览器中:当你在浏览器中输入 www.objc.io 时,浏览器是使用 HTTP 在跟名为 www.objc.io 的服务器沟通。
HTTP 是一种对于应用的协议,它在应用层。有好几种协议构建在另一种之上。这些层的栈通常描述为这样:
| 应用层 , 例如 HTTP
| ----
| 传输层 , 例如 TCP
| ----
| 网络层 , 例如 IP
| ----
| 链路层 , 例如 IEEE 802.2
被称为 OSI(Open Systems Interconnection) 的模型定义了七个层次。 让我们来看一下应用层,传输层和网络层,对应到 HTTP 来说就是最典型的用法:HTTP,TCP 和 IP。在 IP 层以下就是数据链路层和物理层。层次就是这么划分的。例如以太网的实现。
我们只需要关注应用层,传输层,和网络层,实际上只需要关注一个特别的组合:HTTP 跑在 TCP 上,TCP 跑在 IP 上。 在我们日复一日使用的众多应用中, 这是一组典型的组合。
我们希望这篇文章能让你了解到 HTTP 工作的一些细节,在使用中会存在的一些问题,以及如何去避免它们。
除了 HTTP 之外,还有很多种方式可以在 Internet 上发送数据。HTTP能如此流行的原因之一就是它几乎在任何场景下都能正常工作,哪怕你的机器在防火墙后面。
让我们从最底层开始,了解一下 IP, 因特网协议(Internet Protocol)
IP - Internet Protocol
TCP/IP 协议栈中的 IP 协议是 Internet Protocol 的缩写。如它的名称所示,它是互联网的基础协议之一。
IP 实现了一种 分组交换网络。 它有主机的概念,对应了物理机器。 IP协议规定了数据报(分组)如何在主机之间发送。
一个分组是一段包含了源主机和目标主机信息的二进制数据块。IP网络可以容易得把一个分组从源主机发送到目的主机。对于 IP 有一点很重要那就是 分组是 尽力交付(best effort) 的。一个分组可能在路上丢失而永远到不了目标主机。或者它会被复制多份,多次到达目的主机。
IP 网络中的每个主机都有一个地址 - IP地址。每个分组包含了源主机和目的主机的地址信息。IP协议负责路由数据报:在IP分组包传输的过程中,它经过的每个节点(主机) 查看分组包中的目的地址并且指出分组包应该往什么方向继续发送。
现在,大部分的包依然是 IPv4的(Internet Procotol version 4),每个IPv4的地址是32比特长。它们通常以点分形式来记录,比如: 198.51.100.42
更新的 IPv6 标准正在逐渐推广。它的地址是 128比特长,这样使得分组包在网络中传输时可以更容易得路由。因为它有更充足得地址空间,所以类似于 网络地址转换 的一些技巧就不再必要了。IPv6地址用十六进制数表示 并且以冒号分割为八个组,例如
2001:0db8:85a3:0042:1000:8a2e:0370:7334
IP 协议头
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比特 (不算 options,options通常用不到)
头协议中最有趣的部分是源主机和目标主机的IP地址。除了这个,版本字段需要设置为4,表示是 IPv4,协议字段需要说明数据包的内容使用的协议。TCP协议的号码是6,总长度字段设置了整个分组数据包的长度,即头部加上内容部分的总长度。
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的协议头简单得多,因为从IPv4上学到了不少教训。
源主机和目的主机的地址依然是最有趣的字段。在IPv6的 后续头(next header) 字段指定了在这个头信息之后是什么数据。IPv6允许在分组包中的 头信息链,每段随后的IPv6头信息都包含 后续头 字段直到头信息结束,数据实体到达。如果 后续头字段是6(TCP的号码),表示剩下的分组数据包是TCP数据。
分段
在IPv4中,分组信息(数据报)可以被分段,相关的数据传输层会有一个支持数据分组的最大长度上限值。在IPv4中,一个路由器可能会将分组信息分段,如果在它路由到的数据链路上认为分组信息过大的话。这些分组信息会在到达目的主机后进行重组。
在IPv6中,路由器会丢弃这个过大的分组信息,并且返回一个 分组信息过大 的消息给发送者。终端通过这个去指定 最大传输单元 (MTU) 。只有在最小的分组信息实体的大小对于MTU来说都太大时IPv6才使用分段。
链接管理是TCP的核心组件。协议需要使用很多技巧去对外隐藏复杂且不可靠的IP层。我们来快速得浏览一下链接建立,数据流转,连接终止。
一个链接过程中经历的状态事务变化会非常复杂,不过在大多数场景下,这些事情也比较简单。
链接建立
在 TCP 中,链接总是建立在一个主机与另一个主机之间,在链接建立的过程中,他俩担任两个完全不同的角色:一端(例如web服务器)监听链接,另一端(例如app)连接到监听的程序(例如web服务器)。服务器端的行为是被动打开,它总是从监听开始。客户端的行为是主动对服务器打开连接。
链接是通过 三次握手 来建立的。它是这样进行的:
- 客户端发送一个 SYN 给服务器端,并且携带一个随机序列号 A
- 服务器端回复一个 SYN-ACK 并且携带一个值为 A+1 的确认号码 和 另一个随机序列号 B
- 客户端发送一个 ACK 给服务器端 并且携带一个值为 B+1 的确认号码 和 一个 值为 A+1 的序列号
SYN 是 synchronize sequence numbers 的简称。一旦数据在两端之间流动,所有的TCP分段数据都有一个序列号。TCP就是通过它来确认数据的所有部分都已经到达了另一端,并且把它们按正确的顺序放到一起。在通讯开始之前,两端都需要同步一下第一个数据段的序列号。
ACK 是 acknowledgment 的简称。当一个数据分段到达一端时,那端需要通过回复一个数据分段的确认号的方式来确认已经收到了这个数据分段。
如果我们执行
curl -4 http://www.apple.com/contact
这个命令会指示 curl 去建立一个到 www.apple.com 80端口 的TCP链接
www.apple.com / 23.63.125.15 这台服务器监听着80端口。在输出内容中我们可以看到我们的地址是 10.0.1.6 ,我们的 ephemeral 端口是 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分组头信息中的标识: S 是 SYN, . 是 ACK , p 是 PUSH, F 是 FIN, 还有一些是我们在这里看不到的。注意这三行是怎么进行 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)的。TS val部分是发送者的时间戳,ecr是响应回复时候的时间戳,通常它是发送者至今为止收到的最后一个时间戳。TCP通过往返时间来执行拥塞控制的算法。
两端都返回了 sackOK,它启用了 选择性确认机制。 它让序列号和确认号使用字节范围(byte range)而不是TCP分组号。并且它允许两端通过 字节范围 来确认收到数据。这些被写在了 RFC2018 的 第三节。
mss项指定了 最大分段尺寸(Maximum Segment Size),即是这端愿意接受的单个数据分段的最大字节长度。 wscale 是 窗口尺寸因子,我们会稍后讨论。
数据流转
当连接建立起来后,两端都可以发送数据给对方。每个发送出去的数据分段的序列号都比之前的分段大,接受方需要确认数据分组已经收到,通过回发携带正确ACK的分段信息。
理论上,数据流是这样的:
host A sends segment with seq 0
host A sends segment with seq 1
host A sends segment with seq 2 host B sends segment with ack 0
host A sends segment with seq 3 host B sends segment with ack 1
host B sends segment with ack 2
host B sends segment with ack 3
这个机制在任何方向都有效。主机A持续发送数据分组,当它们到达主机B时,主机B必须发送对于这些数据分组得确认信息给主机A。但是主机A会继续发送数据分组而不等待主机B的确认回复。
TCP包括流量控制和许多复杂的机制去做拥塞控制。这些都是为了发现 1. 是否分段丢失了,需要重发 。 2. 已经发出去的某段分组数据需要矫正。
流量控制的含义是确保发送端不会发送数据过快,超过接收端的处理能力。接收端发送一个叫 接收窗口 的东西,告诉发送端它还可以缓冲多少数据。中间一些微妙的细节我们可以跳过去,但是注意 tcpdump 的输出中,我们看到 win 65535 and a wscale 4 ,开头是窗口大小,然后是缩放因子。因此它表示 10.0.1.6 主机的接收窗口是 4*64KB = 256KB,另外主机 23.63.125.15 表示 win 14480 and wscale 1,即大约 14KB。无论哪一方收到数据,都需要更新一下接收窗口给对方。
拥塞控制是有够复杂的。这些机制都是为了确定哪部分数据可以通过网络。它真的是非常脆弱的平衡。一方面非常希望数据能尽快发送出去,另一方面,当发送太多数据时,性能会显著得降低。这称为 拥塞崩溃,是 分组交换网络得显著特点之一。当太多分组数据发送出去,分组数据之间会相互碰撞,分组数据丢失率会显著提高。
拥塞控制机制同样需要保证它在其他的数据流动中同样有效。目前TCP的拥塞控制机制的细节在 RFC 5681 中花了大约6000个单词描述。它的基本思路就是发送方根据拿到的确认信息来。这是非常需要技巧的业务,其中要做非常多的选择。想想IP数据分组可能乱序到达,可能不到,也可以到两次。发送方需要估计往返时间是多少,并且根据它去确定是不是应该已经收到某个确认信息的回复了。重发数据分组的代价显然是很昂贵的,但是没有重发会引起连接的驻留,并且网络上的负载是动态变化的。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
主机 10.0.1.6 上的客户端发送了第一段数据,长度为85(HTTP request请求,85个字节)。ACK数字 是一样的,因为还没有收到新的确认数据。
23.63.125.15上的服务器端接着发送了收到数据的确认信息(同时没有发出数据),长度为0。因为连接使用了 选择性确认,因此序列号和确认号是 字节范围: 1721092980 到 1721093065 是 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
连接结束
最后,连接会终结。每端都发送 FIN 标识给另一端表示自己已经完成了发送。FIN 标识会得到确认。当每端都发送了FIN并且都得到了回应连接就完全结束了:
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,同时确认了另一端的FIN通过ACK,在同一个数据段中完成这些。
HTTP - Hypertext Transfer Protocol
世界范围内的内链接超文本和通过浏览器去浏览这个网络的概念事从 1989年的CERN 开始的。用于数据交换的协议是 Hypertext Transfer Protocol,或者就叫做 HTTP 。 当前的版本是 HTTP/1.1 ,定义在 RFC 2616 中 。
请求和响应
HTTP 使用简单的 请求 和 响应 机制。当我们在 Safari 中输入 http://www.apple.com 后,它发送一个 HTTP 请求 到 位于 www.apple.com 的服务器。服务器反馈一个响应,包含请求的文档。
总是一个请求对应着一个响应。所有的请求和响应都有相同的格式。 第一行是 请求行(request line,在请求中) 或者 状态行(status line,在响应中)。这行后面是头信息,头信息以一个空行结束,之后是可选的消息体。
一个简单的请求。
当 Safari 加载 http://www.objc.io/about.html 的HTML页面,它给 www.objc.io 发了一个内容如下的请求:
GET /about.html HTTP/1.1
Host: www.objc.io
Accept-Encoding: gzip, deflate
Connection: keep-alive
If-None-Match: "a54907f38b306fe3ae4f32c003ddd507"
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
If-Modified-Since: Mon, 10 Feb 2014 18:08:48 GMT
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.74.9 (KHTML, like Gecko) Version/7.0.2 Safari/537.74.9
Referer: http://www.objc.io/
DNT: 1
Accept-Language: en-us
第一行是请求行,它指定了想要进行的动作: GET。 这称作 HTTP 方法。接下来是这次动作的资源路径 /about 。最后是 HTTP的版本,在这个场景下,我们是想 拿到 这个文档。
接着,我们有10行也是10个头信息,最后接一个空行。这个请求是没有消息体的。
每个头信息都有不同的含义。它们传达了一些额外的信息给web服务器。维基百科上有一个很好的列表 常见的HTTP头字段 。第一个 Host 字段,www.objc.io 信息告诉服务器哪个名字的服务器是这个请求想要的。这样强制性的请求头可以允许同一台物理机为多个域名服务 。
我们来看看几个比较通用的:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us
这段告诉了服务器 Safari 想接收的是哪种媒介类型,服务器可能发送多种格式的响应。 text/html 是 因特网数据类型,也被称为 MIME 或者 content-types. q=0.9允许safari在关联的指定媒体类型下传达质量因子。Accept-Language 告诉服务器哪种语言是 Safari更愿意接收的。它让服务器如果可以的话,就挑选匹配的语言。
Accept-Encoding: gzip, deflate
通过这个头信息,Safari告诉服务器响应体可以被压缩。如果这个头没有设置,服务器必须发送没被压缩的数据。特别是对于纯文本(例如HTML),压缩可以显著得减少发送得数据量。
If-Modified-Since: Mon, 10 Feb 2014 18:08:48 GMT
If-None-Match: "a54907f38b306fe3ae4f32c003ddd507"
这两行是因为事实上Safari已经从缓存中拿到了结果文档。Safari告诉服务器只有在2月10号之后文档有改动才发送过来。或者 ETag 跟 a54907f38b306fe3ae4f32c003ddd507 不一致。
User-Agent 头信息告诉服务器客户端得型号。
一个简单的响应
对于上面请求的响应,是这样的:
HTTP/1.1 304 Not Modified
Connection: keep-alive
Date: Mon, 03 Mar 2014 21:09:45 GMT
Cache-Control: max-age=3600
ETag: "a54907f38b306fe3ae4f32c003ddd507"
Last-Modified: Mon, 10 Feb 2014 18:08:48 GMT
Age: 6
X-Cache: Hit from cloudfront
Via: 1.1 eb67cb25620df959ba21a943fbc49ef6.cloudfront.net (CloudFront)
X-Amz-Cf-Id: dDSBgR86EKBemW6el-pBI9kAnuYJEaPQYEqGmBnilD12CbixCuZYVQ==
第一行就是状态行,它包括了HTTP版本,之后是 状态码(304)和状态信息。
HTTP定义了一系列的状态码以及它们的含义。这里我们收到了 304,意味着请求的资源没有被修改。
这个响应没有包含消息体,只是告诉接收者:你那里的版本已经是最新的了。
//DONE