引言
在阅读 tornado.iostream 模块时涉及到一些网络异常的处理,在深入了解异常的定义和处理后,觉得有必要对 TCP/IP 协议做一个简单的复习整理,尤其针对直接涉及 socket 编程的 TCP 协议部分。
以下总结的内容依次从 TCP/IP 的分层结构、数据封装与分用、以太网帧结构、IP 数据包结构、TCP 数据段(或报文段)结构来进行,并专门对 TCP 的三次握手和四次挥手做一个较为详细的介绍。
TCP/IP 协议
TCP/IP 的分层结构
OSI (Open System Interconnect, 开放系统互连参考模型)为开放式互连信息系统提供了一种理论上的网络模型,而 TCP/IP 则是实际实现运行的网络模型。TCP/IP 采用四层结构,它与 OSI 七层结构的对应关系如下图所示:
注:上述协议分层不是完美的,ICMP/IGMP 作为 IP 的附属协议,其数据是封装在 IP 数据包中的,所以在逻辑上它们是在 IP 的上层,这个在后面数据分用的示意图中会体现;ARP/RARP 也遇到同样的问题,它们与 IP 数据报一样都有各自的以太网帧结构,但又是为 IP 地址转换服务的,所以在逻辑上它们是在 IP 的下层。在《TCP-IP详解》中就将 ARP/RARP 划分到网络接口层。在这个图中,将这几个协议都放在与 IP 同一层只是为了选择一种分类方式,不是绝对的。
如上图所示,TCP/IP 各层的功能和协议情况可以简述为:
网络接口层(Host-to-Net Layer),对应 OSI 七层模型的下两层,负责实际数据的传输。主要协议包括:Ethernet、FDDI、PPP、SLIP和其他能传输 IP 数据包的任何协议。
网际层(Inter-network Layer),对应 OSI 七层模型的第三层,负责网络间的寻址和数据传输。主要协议:IP、ARP、RARP、ICMP、IGMP。
传输层(Transport Layer),对应 OSI 七层模型的第四层,负责提供可靠的传输服务。主要协议包括:TCP、UDP。
应用层(Application Layer),对应 OSI 七层模型的上面三层,负责实现一切与应用程序相关的功能。主要协议包括:FTP、HTTP、DNS、SMTP、NFS等。
数据封装与分用
对于我们介绍的 TCP/IP 协议,可以通过《TCP-IP详解》 的另一种协议分层方式来一览几个重要协议之间的关系。如下图所示:
对于数据的下行,我们简单称为 “封装”,反之称为 “分用”。
注:据说在早期 DEC-10 系统上字节并不是 8bit 的,所以有些早期文献中用 octet 这个术语标识字节。
封装
应用程序使用 TCP/IP 协议传输应用数据时候,数据要被送入协议栈经过逐层封装,最终作为比特流在媒体上传送出去。其过程示意图如下所示:
注:从上图可以看到以太网帧的数据长度是有大小限制的,这个最大值称为 MTU,所以当 IP 数据包长度大于 MTU 时会被拆成多个帧传输,称为 “IP分片”。
协议栈中的每一层都需要向传图该层的数据添加相应的协议头。 UDP 与 TCP 数据结构基本一致,唯一的不同是 UDP 传给 IP 的信息单元称作 UDP 数据报(UDP datagram),而且 UDP 的首部长为 8 字节。许多应用程序都会使用 TCP/UDP 协议,所以需要在报文的首部区分应用程序,这个区分标识就是 16bit 的“端口”。
ICMP、IGMP、TCP、UDP 都要使用 IP,所以 IP 数据包首部有一个 8bit 的标识字段专门用于区分,1表示为 ICMP, 2表示为 IGMP, 6表示为 TCP , 17表示为 UDP。
IP、ARP、RARP 都要使用以太网帧,所以帧结构中也有一个标识字段用于标识上层协议。
TCP、IP、以太网帧的数据结构将在后面介绍。
分用
当数据被媒体送达网络接口层时,会执行与 “封装” 相反的拆包过程,每层协议都要去检查报文首部中的协议标识,已确定接受数据的上层协议,这个过程就是 “分用”。其示意图如下所示:
以太网帧结构
实际上还有另外一种帧格式:IEEE 802.2/802.3,是在 RFC 1042 中定义的。与在 RFC 894 定义的以太网帧格式稍微有些不同,但是在 TCP/IP 的世界里后者更常见。
简单提一句,ARP/RARP 是用于在 32bit 的 IP 地址和 48bit 的 MAC 地址之间进行映射。具体协议内容请查阅相关资料。
IP 数据包结构
注意一下,上面描述的首部,不包括选项字段的 IP 头部长度为 20 字节长度,最高位在左边,记为 0bit。最低位在右边,记为 31bit。采用 “大端” 字节序进行传输,也就是对于 4 字节的 32bit 数据,从高位字节(0bit)开始传输 0~7,8~15,15~23,24~31bit。各字段的含义如下:
- 4 位版本号,指协议版本号,值为4代表 IPv4。
- 4 位首部长度,每一个计量单位是 32bit(4 byte),指的是包括选项字段在内的 IP 首部长度,由于是 4bit,所以 IP 首部最长只能是 60 字节(15 * 4)。
- 8 位服务类型(TOS),包括 3bit 优先权字段(现在已经不用了),4bit TOS字段, 1bit 备用位。4bit TOS位分别代表:最小时延、最大吞吐量、最高可靠性和最小费用,只能设置其中 1bit,如果所有 4bit 均为0,那么就表示是一般服务。
- 16 位 IP 数据包长度,计量单位 byte,包括首部和数据部分。能表示的最大长度为 65535,且这个字段是必须的,当 IP 数据包小于 46 字节时在以太网帧中数据将会被填充到 46 字节,这时候如果没有这个字段我们接收到帧后便不能得到正确的 IP 数据包。
- 16 位标识字段,是数据包的唯一标识,通常主机每发送一个数据包就会 +1 ,在分片时会被复制到每一个分片中。
- 3 位标志字段和 13 位(片)偏移字段,用于数据包分片和重组。3 位标志字段,0bit 保留;1bit 为 DF :0表示可以分片,1表示不能分片;2bit 为 MF:0表示最后一个分片,1表示还有分片。13 位(片)偏移字段,指示了这个分片在所属数据包中的位置,分片偏移以 8byte 做为计量单位,第一个分片偏移为 0。
- 8 位生存时间(TTL),设置了数据包可以经过的最多路由器数量。
- 8 位协议字段,前面已经提过了,标识上层协议的字段。
- 16 位首部校验和,根据 IP 首部计算的检验和码,它不对首部后面的数据进行计算。采用的是 16bit 二进制反码求和,具体算法请查阅资料。
- 32 位源 IP 地址和32位目标 IP 地址。
- 选项字段,可变长的数据信息,具体选项定义请查询相关文档。尤其注意的是,选项必须以 32bit 作为计量单位,不满 32bit 需要填充 0。
TCP 数据段结构
不计算选项字段,TCP 首部的长度为 20byte。
- 16 位源端口与目标端口号,用于标识发送端应用程序和接收端应用程序。
- 32 位序号,无符号数,用来标识从 TCP 发送端向 TCP 接收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节,简单的可理解为对发送的数据(这个数据不一定是指数据字段的数据,比如建立连接时 SYN 字段设置为 1,也会消耗一个计数)按 byte 进行循环计数。
- 32 位确认序号,无符号数,用于表示期望收到的下一个序号,ACK=1 时有效。
- 4 位置首部长度,计量单位为 32bit,同 IP 首部长度字段。
- URG,紧急指针有效。
- ACK,确认序号有效。
- PSH,接收方应该尽快将这个报文段交给应用层。
- RST,重建连接。
- SYN,同步序号用来发起一个连接。
- FIN,发送端完成发送,用来结束一个连接。
- 16 位窗口字段,这个与 TCP 的滑动窗口流量控制有关。
- 16 位校验和,覆盖了整个的 TCP 报文段,包括首部和数据。与 UDP 数据报一样,TCP 数据报段在计算校验和时也包括一个 12 字节长的伪首部。
- 16 位紧急指针,这是一个正向偏移值,和序号字段中的值相加表示紧急数据最后一个字节的序号。
- 选项字段,最常见的可选字段是最长报文大小,又称为 MSS (Maximum Segment Size)。每个连接方通常都在通信的第一个报文段(为建立连接而设置 SYN 标志的那个段)中指明这个选项。它指明本端所能接收的最大长度的报文段。
12 字节长的 TCP/UDP 伪首部
注:TCP 数据报段伪首部起到双重校验的作用:1、通过伪首部的 IP 地址检验,TCP 可以确认 IP 没有接受不是发给本机的数据报;2、通过伪首部的协议字段检验,TCP 可以确认 IP 没有把应该传给其他高层协议(比如UDP、ICMP或者IGMP)的数据报传给 TCP 。
TCP 三次握手
客户端与服务端建立一个 TCP 连接共计需要发送 3 个包才能完成,这个过程称为三次握手(Three-way Handshake)。如上面所述,数据段的序号、确认序号以及滑动窗口大小都在这个过程中完成。socket 编程中,客户端执行 connect() 时,将触发三次握手。
注:
- 1415531521:1415531521(0) 表示分组的序号是 1415531521,而报文的数据字节数为 0。
- WIN:4096 表示发送端通告的窗口大小为 4096,上图中由于没有交换任何数据所以窗口维持在 4096。
- < MSS 1024> 表示由发端指明的最大报文段长度选项为 1024。
- ACK 1415531521 表示确认序号,只在首部 ACK 标志位设置为 1 时才有效。
如上图所示完成 TCP 连接的建立,客户端与服务端共计发送了 3 个报文段:
- 报文段1:客户端发送一个 SYN 报文段指明客户打算连接的服务器的端口,以及初始序号(Initial Sequence Number,这个例子中 ISN=1415531521)。ISN 的实现目前会随着时间的变化而变化,所以每次建立连接时的 ISN 都不同;
- 报文段2:服务器发回包含服务器的初始序号的 SYN 报文段(这个例子中 ISN=1823083521)作为应答。同时,将确认序号设置为客户的 ISN+1 以报文段1进行确认。一个 SYN 将占用一个序号;
- 报文段3:客户必须将确认序号设置为服务器的 ISN+1(1823083522) 以对服务器的 SYN 报文段进行确认;
- 连接建立,开始进行数据通信。
上面所示的是一方主动连接另外一方的情况,实际上 TCP 也允许双方同时主动连接,这种情况下就要求连接双方提前知道对方的端口。实际中很少出现这种需求。这种情况下,要发送 4 个报文段才能建立起连接。
附:SYN 洪水攻击
由三次握手可以看出,当服务器收到 SYN 数据报文段后将为连接分配资源,如果服务器没有收到 ACK 报文段就会造成半开连接,浪费服务器资源。SYN 洪水攻击就是利用 TCP 的这个缺陷,通过向服务器发送海量的 SYN 报文段而耗尽服务器资源。一般有两种方式:1、客户端恶意不发送 ACK;2、在发送给服务器的 SYN 报文段中提供虚假的 IP 地址,造成服务器永远收不到 ACK。
这种攻击手段对于现代网络效果不大,但并不能完全防范。一般较新的 TCP/IP 协议栈实现提都供了防范手段,主要手段包括 SYN cookie 、 SynAttackProtect 保护机制、增加最大半连接、缩短超时时间和限定某一段时间内来自同一来源请求新连接的数量等。
Linux 下可以通过命令: # netstat -na TCP | grep SYN_RECV | more,来查看 SYN 攻击的情况。
这里有一片不错的文章介绍如何防御 SYN 洪水攻击:TCP洪水攻击(SYN Flood)的诊断和处理
TCP 四次挥手
建立一个连接需要 3 次握手,而终止一个连接要经过 4 次握手。这由 TCP 的半关闭(half-close,连接的一端在结束它的发送后还能接收来自另一端数据的能力。具体的请查阅 TCP 半关闭的相关资料)造成的。 TCP 连接是全双工,因此每个方向必须单独地进行关闭。也就是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向连接,当一端收到一个 FIN,它必须通知应用层另一端已经终止了那个方向的数据传送。发送 FIN 通常是应用层进行关闭的结果。
连接双方都可发起这个操作,socket 编程中,任何一方执行 close() 触发挥手操作。
上图的 4 次挥手示意图是接着上面 3 次握手进行的,假设没有应用数据传输,所以报文段4的序号紧接着报文段1的序号(ACK 的发送是没有任何代价的,不会消耗序号)。图中所示的是一方主动关闭(首先发送 FIN 数据报),另一方被动关闭,实际上 TCP 也允许双方同时主动关闭。
结束语
上面对 TCP/IP 协议的知识点进行了简单整理,内容基本来自教科书或者网络资料。文中的示意图为了理解方便进行了重新绘制和加入了一些说明(TCP 部分),主要是为了清晰地观察连接的状态变化。