TCP/IP 协议簇本质上是 OSI 三层(网际层)和四层(传输层)协议簇的总结,通常包括TCP、UDP、IP、ICMP和ARP等几种协议。

IP 协议

链路层协议和物理层协议解决了点对点通信的问题。

而在大范围的多个子网通信问题则由 IP 协议解决。

IP地址族的分类

32 位的地址空间可以分为5 类地址(常用的地址空间只用到A/B/C)。

  • A 类地址:0.0.0.0-127.255.255.255
  • B 类地址:128.0.0.0-191.255.255.255
  • C 类地址:192.0.0.0-223.255.255.255

实际上用二进制来看地址开头的话,还有一种巧妙的分法:

  • 如果 32 位的 IP 地址以 0 开头,那么它就是一个 A 类地址。
  • 如果 32 位的 IP 地址以 10 开头,那么它就是一个 B 类地址。
  • 如果 32 位的 IP 地址以 110 开头,那么它就是一个 C 类地址。

这三类地址是用来做unicast(也就是单播)的。我们常见的环回地址127.0.0.0和本机地址0.0.0.0是A类地址。

IP 协议报文格式

IP 的 header 的整体格式还是定长的字段。
值得注意的是TTL字段,这表明这个packet还可以穿过多少个路由,每经过一个路由TTL就会减一。尽管8位的数学空间允许一个 IP packet 最多穿过255个路由,但现实中这个 packet 通常只穿过64或者32个路由。

路由器与路由表

packet 在子网中传输通常大部分情况下只经过路由器,只在到达最后一个目标子网以后由那里等路由器跳到目标机器上。

路由器内部会维持一个类似路由表等东西,用高速等缓存机制来确认某几个特定子网的 packet 应该从去往哪个子网。

可靠性

IP 协议并不是面向连接的协议,也就不存在纠错和可靠传输问题。

ARP 协议与 RARP 协议

ARP 协议大概可以理解为一个子网内的广播协议。就是一个 Host 想知道一个 IP 地址对应的 Mac 地址是多少,向本子网进行一个 broadcasting,寻找答案。配合上面对 IP 协议的解读,基本上就是一个本子网内才会用到的协议。而 RARP 协议则正好反过来。

ICMP(Internet Control Message Protocol) 协议

当传送IP数据包发生错误,比如主机不可达/路由不可达等等,ICMP协议将会把错误信息封包,然后传送回给主机。这导致了我们可以通过一些应用来诊断网络状况。

ping

ping这个单词源自声纳定位,而这个程序的作用也确实如此,它利用ICMP协议包来侦测另一个主机是否可达。原理是用类型码为0的ICMP发请 求,受到请求的主机则用类型码为8的ICMP回应。

ping程序来计算间隔时间,并计算有多少个包被送达。用户就可以判断网络大致的情况。我们可以看到, ping给出来了传送的时间和TTL的数据。

Traceroute

这个工具在不同 OS 上的缩写是不一样的,linux系统中是traceroute,在MS Windows中是tracert。

traceroute 的名称清楚地表明了这是一个追踪路由器工作过程的工具。

它的工作原理是:

traceroute 在收到目标地址以后,向目标地址发送一个 TTL 为1的 UDP packet,当第一个路由器收到这个 packet 以后,自动把 TTL 减为0,而TTL为0的 packet 已然无法向前(所以TTL为1的packet一开始能去哪儿呢?),路由器就扔掉它,并返回ICMP Time Exceeded 报文来报告源主机网络不可达。traceroute在收到 ICMP 报文后,又再发送一条 UDP报文,如此循环往复,直到最后一个 packet 到达目标地址,返回ICMP Echo Reply报文。

根据英文维基百科,实际上任意形式的协议报文都可以拿来做这种探针报文,当然通常我们使用 payload 无意义的 UDP packet。

TCP 协议

报文结构与协议栈

常见的报文格式无外如上。上层报文格式不需要知道下层的报文格式,下层的报文在拿到上层报文后,直接添加一段首部(在以太网层面还需要加入校验和作为尾部),即成为本层的报文。整体上还是一个 header + body(在网络协议里面通常叫做 payload) 的模型。

报文在发送方的一端,看起来像是一个不断入栈的过程,而走发送方而言,就像是一个不断出栈的过程。虽然因为物理因素,发送方和接收方实际上是在操纵两个栈,但这两个栈理论上应该是对等的,发送方的报文穿过多层栈,接收方也会重建多层栈。

三次握手

首先再来详细看看 TCP 当报文格式:

序号就是我们经常说当seq,占32位,因为 TCP 是面向字节流的(byte stream oriented),所以要给流里中的一个 packet 编号。

确认号就是 Acknowledgment number (if ACK set),它只有在 ack 标志位被置位以后才生效。它等于 seq + 1。

而标志位的含义是:

(A)URG:紧急指针(urgent pointer)有效。
(B)ACK:确认序号有效。
(C)PSH:接收方应该尽快将这个报文交给应用层。
(D)RST:重置连接。
(E)SYN:发起一个新连接。
(F)FIN:释放一个连接。

三次握手的过程大致上如下:

第一次握手,client 把 SYN 标志位设置为1,然后生成一个随机的 seq 作为通信的起点,发送以后本机进入 SYN_SENT 状态。

第二次握手,server 在收到 SYN 报文以后,知道 client 要建立连接,于是生成一个 SYN 和 ACK 标志位都为1的报文,其中ack号为收到的 seq 值加一(可以看出ack不仅表明了收到的报文的序号,也表明了期待接下来收到的seq的序号),然后随机产生一个值seq=K(client 和 server 使用两个随机数来通信),并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

第三次握手,Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态(由此可见,只有收到ACK为1的报文,并且ack为自己发送的值加一的报文的时候,一端才算进入Client和Server进入ESTABLISHED状态),完成三次握手,随后Client与Server之间可以开始传输数据了。

四次挥手

由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

第一次挥手,Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
第二次挥手,Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
第三次挥手,Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
第四次挥手,Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

由上述过程可以看出,握手和挥手都会用到 ACK 标志位和ack 序号。它们 ack 到包到标志位是 SYN 还是 FIN 才是挥手和握手的主要区别。

为什么握手是三次而挥手是四次?

双工通信要求了我们建立连接和取消连接,其实是建立和取消两条管道(其实是两个 seq 序列)。

建立连接到时候必然要做成双工的,所以两端的握手必须紧密安排在一起,而第一个 ACK 和第二个 SYN 是可以为了简便而合二为一的。

挥手的时候,可以完全关闭,也可以双工变单工,特别是想要关闭连接的一方没有数据要发送了,不代表另一方的数据已经发送完了,因此不是那么适合把第一次关闭的 ACK 和第二次关闭的 FIN 合二为一。

有些时候,我们不喜欢TIME_WAIT 状态(如当MSL数值设置过大导致服务器端有太多TIME_WAIT状态的TCP连接,减少这些条目数可以更快地关闭连接,为新连接释放更多资源),这时我们可以通过设置SOCKET变量的SO_LINGER标志来避免SOCKET在close()之后进入TIME_WAIT状态,这时将通过发送RST强制终止TCP连接(取代正常的TCP四次握手的终止方式)。

用 tcpdump 来查看tcp协议通信传输的过程

首先用ping 查看tianya的地址:

ping tianya.com
PING tianya.com (120.24.90.198): 56 data bytes
64 bytes from 120.24.90.198: icmp_seq=0 ttl=49 time=40.042 ms
64 bytes from 120.24.90.198: icmp_seq=1 ttl=49 time=37.354 ms
64 bytes from 120.24.90.198: icmp_seq=2 ttl=49 time=37.271 ms
64 bytes from 120.24.90.198: icmp_seq=3 ttl=49 time=39.367 ms
64 bytes from 120.24.90.198: icmp_seq=4 ttl=49 time=37.655 ms
64 bytes from 120.24.90.198: icmp_seq=5 ttl=49 time=37.335 ms

然后用以下命令开始监控:

1
2
tcpdump host 120.24.90.198
curl 120.24.90.198

得到的全部输出如下:

17:57:13.415883 IP bogon.65290 > 120.24.90.198.http: Flags [S], seq 477782453, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 213769344 ecr 0,sackOK,eol], length 0
17:57:13.452814 IP 120.24.90.198.http > bogon.65290: Flags [S.], seq 2061846025, ack 477782454, win 14600, options [mss 1386,nop,nop,sackOK,nop,wscale 7], length 0
17:57:13.452857 IP bogon.65290 > 120.24.90.198.http: Flags [.], ack 1, win 8192, length 0
17:57:13.452949 IP bogon.65290 > 120.24.90.198.http: Flags [P.], seq 1:78, ack 1, win 8192, length 77: HTTP: GET / HTTP/1.1
17:57:13.490112 IP 120.24.90.198.http > bogon.65290: Flags [.], ack 78, win 115, length 0
17:57:13.516730 IP 120.24.90.198.http > bogon.65290: Flags [.], seq 1:1387, ack 78, win 115, length 1386: HTTP: HTTP/1.1 200 OK
17:57:13.516736 IP 120.24.90.198.http > bogon.65290: Flags [.], seq 1387:2773, ack 78, win 115, length 1386: HTTP
17:57:13.516771 IP bogon.65290 > 120.24.90.198.http: Flags [.], ack 2773, win 8105, length 0
17:57:13.516826 IP bogon.65290 > 120.24.90.198.http: Flags [.], ack 2773, win 8192, length 0
17:57:13.517050 IP 120.24.90.198.http > bogon.65290: Flags [.], seq 2773:4159, ack 78, win 115, length 1386: HTTP
17:57:13.517053 IP 120.24.90.198.http > bogon.65290: Flags [.], seq 4159:5545, ack 78, win 115, length 1386: HTTP
17:57:13.517055 IP 120.24.90.198.http > bogon.65290: Flags [.], seq 5545:6931, ack 78, win 115, length 1386: HTTP
17:57:13.517056 IP 120.24.90.198.http > bogon.65290: Flags [.], seq 6931:8317, ack 78, win 115, length 1386: HTTP
17:57:13.517058 IP 120.24.90.198.http > bogon.65290: Flags [P.], seq 8317:9001, ack 78, win 115, length 684: HTTP
17:57:13.517079 IP bogon.65290 > 120.24.90.198.http: Flags [.], ack 5545, win 8105, length 0
17:57:13.517089 IP bogon.65290 > 120.24.90.198.http: Flags [.], ack 8317, win 8018, length 0
17:57:13.517095 IP bogon.65290 > 120.24.90.198.http: Flags [.], ack 9001, win 7997, length 0
17:57:13.517504 IP 120.24.90.198.http > bogon.65290: Flags [.], seq 9001:10387, ack 78, win 115, length 1386: HTTP
17:57:13.517507 IP 120.24.90.198.http > bogon.65290: Flags [P.], seq 10387:11135, ack 78, win 115, length 748: HTTP
17:57:13.517523 IP bogon.65290 > 120.24.90.198.http: Flags [.], ack 11135, win 7930, length 0
17:57:13.519626 IP bogon.65290 > 120.24.90.198.http: Flags [.], ack 11135, win 8192, length 0
17:57:13.526151 IP bogon.65290 > 120.24.90.198.http: Flags [F.], seq 78, ack 11135, win 8192, length 0
17:57:13.563681 IP 120.24.90.198.http > bogon.65290: Flags [F.], seq 11135, ack 79, win 115, length 0
17:57:13.563735 IP bogon.65290 > 120.24.90.198.http: Flags [.], ack 11136, win 8192, length 0

要注意:

  • 初始的seq数字往往都很大。
  • ack的结果是下一个希望收到的字节流的第一个字节的序号。比如这一次收到的字节是以10000个字节结尾的,ack显示的结果就需要下一段字节流以10001号字节开头。因为并不是每一个包都每次只发送一个字节,所以ack并不一定等于收到的包的seq加一,而是等于seq加上实际的length加一。
  • tcpdump 显示的seq号,只有一开始显示syn的时候的seq号是绝对seq号,接下来都是相对seq号。
  • 这个.是ACK的意思。原因见。缩写的含义是S (SYN), F (FIN), P (PUSH), R (RST), U(URG), W (ECN CWR), E (ECN-Echo) or '.' (ACK)