第3章 TCP/IP 3. 1 TCP TCP 是传输层最重要的协议,提供了可靠、有序的数据传输,是多个广泛使用的表示层 协议的运行基础,理解了TCP 的基本原理,有助于更好地掌握仓颉语言网络编程。 3.1 TCP 报文格式 1. 1981 年IETF 的RFC793 定义了TCP,该协议的报文格式如图3-1所示。 图3-1 RFC793TCP 报文格式 2022 年8月IETF 公布了RFC9293,该协议替代了RFC793,两者的差异很小,在 RFC9293 中,保留位变成了4位,也就是最后两个保留位被使用了,作为控制位的前两位, 协议报文格式如图3-2所示。 TCP 报文包括首部和数据两部分,其中首部包括20 字节的固定部分和变长的选项,数 据部分也是变长的。TCP 的功能体现在首部各字段上,下面分别介绍这些字段的作用。 1. 源端口和目的端口 每个主机上都运行着多个应用,当发送端主机发送的报文经过网络到达目的主机时,目 的主机需要知道这个报文应该传递给哪一个具体的应用,这个区分是通过端口号实现的。 IP 地址标识了一台主机,端口号标识了一个具体的应用; IP 地址就相 借用寄送快递来类比, 当于一个具体的物理地址,如山东省青岛市崂山区海尔路1号××× 大厦,但是,这个×× 36 图3-2 RFC9293TCP 报文格式 ×大厦里面有几百人,光凭这个地址还不能顺利送达,还需要具体的收件人姓名,例如张磊, 这个姓名就相当于端口号,在这个地址里是唯一的。 源端口是发送端主机的端口号,目的端口是接收端主机的端口号,每个端口号用16 位 表示,占用2字节,由此可以推算出,每个主机最多有65536 个端口(在实际应用中端口号0 不直接使用,一般用来表示所有端口或者动态端口)。 2. 序号和确认号 序号和确认号都分别占用32 位,也就是4字节,它们是TCP 实现可靠传输的关键。假 如有10000 字节需要通过TCP 进行传输,每次传输的数据量是1000 字节,那么,可以对这 10000 字节进行编号,从第1字节的0到最后一个的9999,如图3-3所示。 图3-3 报文序号 当第1次传输数据时,序号为第1个报文段的开始序号,也就是0(这个0是逻辑上的, 实际上可能是0~232 的一个数字), 确认号表示已经收到了报文,期望发送方继续发送下一 个报文段的第1字节数据的编号,这里因为是第1次发送报文,所以确认号是0。当接收方 接收到发送方的报文时,就回复一个确认报文,这时确认报文的序号还是0,因为这个序号 表示接收方第1个报文段的开始序号,但是,确认号变成了1000,表示接收方已经收到了序 号1000 以前的所有报文,希望发送方开始发送从序号1000 开始的下一个报文段(这里假设 接收方只确认数据包,不发送其他数据,也就是确认报文的数据部分的长度为0)。发送方 收到该确认报文后,继续发送第2个报文段,此时序号变成了1000,确认号变成了1,这样一 直下去,直到发送完所有数据,假如每次都可以顺利发送和接收,前4次收发报文的序号和 确认号如表3-1所示。 表3- 1 前4次收发报文的序号和确认号 次序方向序号确认号 1 发送方到接收方0 0 2 接收方到发送方0 1000 37 续表 次序方向序号确认号 3 发送方到接收方1000 1 4 接收方到发送方1 2000 3. 首部长度 表示首部占用多少个32 位,也就是多少个4字节,换句话说,首部的字节数量必须是4 的整数倍数。因为4位最多表示15,所以首部最多60 字节,去掉首部固定部分的20 字节, 首部选项部分最多占用40 字节。 4. 保留位 在RFC793 中占用6位,在RFC9293 中占用4位,保留以后使用,目前都置为0。 5. 控制位 在RFC793 中占用6位,在RFC9293 中占用8位,功能如下所示。 1)CWR CWR(CongestionWindowReduced)标志与后面的ECE(ECN-Echo)标志都用于IP 首 部的ECN 字段,如果ECE 标志为1,则通知对方已将拥塞窗口缩小。 2)ECE 若其值为1,则会通知对方,从对方到本地的网络有阻塞。在收到数据包的IP 首部中 当ECN 为1时将TCP 首部中的ECE 设为1。 3)URG 将URG(UrgentPointerFieldisSignificant)设为1,表示包中有需要紧急处理的数据, 对于需要紧急处理的数据,与后面的紧急指针有关。 4)ACK 将ACK(AcknowledgementFieldisSignificant)设为1,确认应答的字段有效,TCP 规 定除了最初建立连接时的SYN 包之外该位必须设为1。 5)PSH 将PSH(PushFunction)设为1,表示需要将收到的数据立刻传给上层应用协议,若设 为0,则先对数据进行缓存。 6)RST 将RST(ResettheConnection)设为1,表示TCP 连接出现异常必须强制断开连接。 7)SYN(SynchronizeSequenceNumbers) 用于建立连接,将该位设为1,表示这是一个连接请求或连接接受报文。 8)FIN 将FIN(NomoreDatafromSender)设为1,表明此报文段的发送端的数据已发送完 毕,并要求释放连接。 38 6. 窗口大小 窗口大小字段用来控制对方发送的数据量,单位为字节。TCP 连接的一端根据设置的 缓存空间大小确定自己的接收窗口大小,然后通知对方以确定对方的发送窗口的上限。窗 口大小是一个16 位的字段,所以窗口最大为65535 。 7. 校验和 长度为16 位,由发送端填充,接收端对TCP 报文段执行CRC 校验以验证TCP 报文段 在传输过程中是否损坏(校验过程中包括一个伪首部,伪首部的数据是从IP 数据报头获取 的,其目的是检测TCP 数据段是否已经正确到达), 如果校验失败,则TCP 直接丢弃这个报 文段,这是TCP 可靠传输的一个重要保障。 8. 紧急指针 长度为16 位,它只在URG 标志被设置为1时生效。 3.2 三次握手 1. TCP 传输的可靠性是建立在TCP 连接的基础上的,TCP 的传输是双工的,也就是说报 文可以同时在两个方向上传输,为了建立稳定的连接,传输的双方需要分别确认自己和对方 都具备数据发送和接收能力,为了验证这种能力,就需要双方进行三次报文发送和接收,简 称三次握手。下面通过一个实际的示例进行演示,客户端IP 地址为10.42.端口为 191.206, 19516, 15. 服务器端IP 地址为1.31,端口为6379,为了直观地展示握手过程,使用 152. Wireshark捕获了双方通信的数据包,如图3-4所示。 图3-4 三次握手 1. 第1次握手 客户端首先向服务器端发起连接请求,重点关注请求报文中的两个字段:一个是SYN 标志,要置为1;另一个是序号,会随机生成一个序号作为起始序号,第1次握手的报文如 图3-5所示,图中封包字节区中的高亮部分即为TCP 协议的首部。 根据TCP 的格式,标识出序号、确认号和控制位等关键字段所占用的字节,如图3-6 所示。 这里随机生成的初始序号为11011101001101011101111001101100,即十进制的 3711295084;因为是发起连接请求,所以确认号是00000000000000000000000000000000,也 就是十进制的0;12 位控制位为000000000010,只有SYN 控制位被设置为1,其余均为0。 2. 第2次握手 服务器端收到客户端的连接请求后,如果同意连接就向客户端发送应答包,服务器端将 第1次握手收到的客户端序号加1作为确认号,同时将ACK 控制位设置为1,然后生成服 务器端的随机序号,把SYN 控制位也设置为1,第2次握手的报文如图3-7所示。 39 图3-5 第1次握手 图3-6 第1次握手的关键字段 根据TCP 的格式,标识出序号、确认号和控制位等关键字段所占用的字节,如图3-8 所示。 这里随机生成的服务器端初始序号为11101001010111010011111100111111,即十进 制的3915202367;确认号是11011101001101011101111001101101,也就是十进制的 3711295085,正好比客户端发送的序号多1;12 位控制位为000000010010,其中ACK 和 SYN 控制位都被设置为1,其余均为0。 3. 第3次握手 客户端收到了服务器端发送的应答包,并且发现ACK 和确认号是正确的,表示从客户 端到服务器端的通信链路是正常的,并且服务器端同意连接请求,这时客户端给服务器端回 复一个应答包,在应答包里把收到的服务器端序号加1作为确认号,同时将ACK 控制位设 40 图3-7 第2次握手 图3-8 第2次握手的关键字段 置为1,服务器端收到应答包后发现ACK 和确认号也是正确的,就表明客户端收到了服务 器端的报文,这样客户端和服务器端就建立起了TCP 连接。第3次握手的报文如图3-9 所示。 根据TCP 的格式,标识出序号、确认号和控制位等关键字段所占用的字节,如图3-10 所示。 这里序号为11011101001101011101111001101101,即十进制的3711295085,虽然 SYN 报文不包含数据,但是,TCP 协议规定了SYN 报文也占用1个序号,所以客户端第2 次向服务器端发送报文时,序号就比初始序号多1;确认号是111010010101110100111111 01000000,也就是十进制的3915202368,比服务器端发送的序号多1;12 位控制位为0000 00010000,其中ACK 控制位被设置为1,其余均为0。 41 图3-9 第3次握手 图3-10 第3次握手的关键字段 3.3 四次挥手 1. 在TCP 连接建立以后可以进行数据的传输,当数据传输完毕后,任何一方都可以提出 断开连接的请求,这一点和建立连接时不同,建立连接时只能客户端发起连接。TCP 断开 连接的过程需要发送4次数据包,被称为四次挥手。 下面通过一个实际的示例进行演示,客户端IP 地址为10.42.端口为21826,服 191.206, 15.31, ehr 务器端IP 地址为1.152.端口为6379,为了直观地展示挥手过程,使用Wirsak捕 获了双方通信的数据包,本次断开连接是由服务器端发起的,如图3-11 所示。 图3-11 四次挥手 42 1. 第1次挥手 服务器端发起断开连接请求,将控制位FIN 设置为1,如图3-12 所示。 图3-12 第1次挥手 根据TCP 的格式,标识出序号、确认号和控制位等关键字段所占用的字节,如图3-13 所示。 图3-13 第1次挥手的关键字段 可以看到,序号为11101010010110111001100001110111,十进制形式为3931871351;控 制位为000000010001,其中ACK 和FIN 控制位都是1,其余均为0。因为断开连接前双方 已经发送了多次数据包,所以本次ACK 为1是正常的,这和发起连接时不同,那时为第1 次发送数据包,ACK 为0;此时服务器端进入FIN_WAIT_1状态。 2. 第2次挥手 客户端收到断开连接的请求后,发送应答包,表示收到了该请求,如图3-14 所示。 根据TCP 的格式,标识出序号、确认号和控制位等关键字段所占用的字节,如图3-15 43 图3-14 第2次挥手 图3-15 第2次挥手的关键字段 所示。 可以看到,序号为01100110111011101111010100010101,十进制形式为1726936341; 确认号为11101010010110111001100001111000,十进制形式为3931871352;控制位为 000000010000,ACK 控制位为1,其余均为0;此时客户端进入CLOSE_WAIT 状态。 3. 第3次挥手 第2次挥手后,如果客户端还有需要发送给服务器端的数据,就继续发送,否则就将 FIN 数据包发送给服务器端,表示客户端也做好了断开连接的准备,报文如图3-16 所示。 根据TCP 的格式,标识出序号、确认号和控制位等关键字段所占用的字节,如图3-17 所示。 可以看到,序号为01100110111011101111010100010101,十进制形式为1726936341, 和第2次挥手时的序号一样,这是因为四次挥手的报文都不包含数据,序号不改变,只有发 送FIN 数据包时序号才会加1,第2次挥手发送的是确认包,不改变发送方的序号。确认号 44 图3-16 第3次挥手 图3-17 第3次挥手的关键字段 为11101010010110111001100001111000,十进制形式为3931871352,因为没有收到新 的服务器端数据包,所以也和第2次挥手时一样;控制位为000000010001,ACK 和FIN 控 制位都为1,其余均为0;此时客户端进入LAST_ACK 状态。 4. 第4次挥手 服务器端收到客户端发送的FIN 数据包后,进入TIME_WAIT 状态,然后将一个应答 包回复给客户端,客户端收到该应答包后就进入CLOSED 状态;服务器端在TIME_WAIT 状态等待2×MSL(MaximumSegmentLifetime,报文最大生存时间)时间后也进入 CLOSED 状态;这样双方就断开了TCP 连接。第4次挥手的报文如图3-18 所示。 根据TCP 的格式,标识出序号、确认号和控制位等关键字段所占用的字节,如图3-19 所示。可 以看到,序号为11101010010110111001100001111000,十进制形式为3931871352; 确认号为01100110111011101111010100010110,十进制形式为1726936342;控制位为 45 图3-18 第4次挥手 图3-19 第4次挥手的关键字段 000000010000,ACK 控制位为1,其余均为0。 3.4 滑动窗口机制 1. 在不可靠链路上实现可靠传输的基本原理是发送确认和超时重传,也就是说,当发送端 发送完一个分组后,就处于等待状态,接收端将确认应答发送给发送端,发送端收到确认应 答后开始下一次分组传输;如果发送端在规定的超时时间内没有收到确认应答,就重新传输 该分组。这种确保分组可靠传输的机制又称为停等协议(stop-and-wait), 是一种最简单、最 基础的数据传输协议,但是,这种协议有一个非常致命的缺点,就是信道利用率非常低,每发 送一个分组都需要等待确认,大部分时间被用于等待应答包。 解决停等协议低效问题的方法是发送端可以连续发送多个分组,不必每个分组都等待 确认,接收端告诉发送端它的接收能力,只要在这个接收能力之内,发送端可以一直发送。 46 接收端回复确认应答时,可以累计确认,例如,接收端收到了3个连续的分组,只需向发送端 发送最后一个分组的确认信息,表示最后一个及前面的分组全部收到了;这种机制被TCP 采纳,称为滑动窗口机制。 TCP 是双工协议,会话的双方都各自维护一个发送端窗口和接收端窗口,接收端窗口 的大小是由自己的系统、硬件及处理能力等因素决定的,发送端窗口的大小取决于对方的通 知,在TCP 的报文格式中,有一个窗口大小字段,是接收端对自己接收能力的声明。对于发 送端的数据,结合滑动窗口的数据确认状态,可以分为4种数据类型,如图3-20 所示。 图3-20 滑动窗口的4种数据类型 1. 已发送已确认 表示已经发送给接收端,并且接收端确认收到了数据,该数据不属于发送端窗口的范 围,在图3-20 中编号104 及以前的字节都处于已发送已确认状态。 2. 已发送未确认 表示已经从发送端发送出去了,但是还没有收到接收端确认报文的数据,该数据属于发 送端窗口的范围,如果被接收端确认,则确认的数据将变为已发送已确认状态,同时窗口位 置会右移。在图3-20 中编号105~109 的字节都处于已发送未确认状态。 3. 未发送(接收端允许) 表示还没有从发送端发送出去,但接收端同意接收数据,该数据属于发送端窗口范围, 已经被加载到缓存中,需要尽快发送出去。在图3-20 中编号110~114 的字节都处于未发 送(接收端允许)状态。 4. 未发送(接收端未允许) 表示还没有从发送端发送出去,而且接收端也没有同意接收数据,这部分数据超出了当 前接收端所能接收的能力。在图3-20 中编号115 及之后的字节都处于未发送(接收端未允 许)状态。 假如上述发送端数据有了如下变化:一是发送端收到了接收端的确认报文,确认编号 105 和106 的数据已接收;二是原先未发送的数据110 已经发送了出去,那么此时滑动窗口 的状态如图3-21 所示。 图3-21 窗口滑动 可以看到,窗口整体向右滑动了两字节,并且窗口范围内已发送未确认和未发送(接收 端允许)的数据范围也发生了相应的改变。 47 3. 2 UDP UDP 也是传输层协议的一种,相对TCP 来讲,它不需要建立连接,是不可靠、无序的, 但是报文更简单,在特定场景下有更高的数据传输效率,在现代的网络通信中同样有广泛的 应用。 UDP 报文格式如图3-22 所示,包括固定8字节的首部和可变的数据部分。 图3-22 UDP 报文格式 UDP 报文首部各个字段的说明如下。 1. 源端口 该字段占用16 位,表示应用程序使用哪个端口发送数据,如果不需要对方回复,则可以 使用0。 2. 目的端口 该字段占用16 位,表示接收端的端口。 3. 长度 该字段占用16 位,表示包括首部字段在内的UDP 数据包的长度。 4. 校验和 该字段占用16 位,用来对数据包进行校验,检查在传输过程中是否存在差错,如果出错 就丢弃。 一个典型的UDP 报文如图3-23 所示,该报文使用Wireshark捕获,图中封包字节区中 图3-23 UDP 报文 48 的高亮部分即为UDP 的首部。 3.P 3I 目前IP 的两个版本(vv都处于正常使用状态, IP6会逐 IP4和IP6) 随着时间的推移,v 渐取代IPv4的统治地位,但这是一个漫长的过程,对于网络编程开发者来讲,两个协议都需 要了解,特别是两个协议的报文格式有较大的区别,下面分别进行说明。 3.1 IP4 3.v IPv4的报文格式如图3-24 所示,如果不包含选项和数据,则单纯的报文首部占用固定 的20 字节,如果包含选项,则选项的字节数必须为4的倍数,不足部分使用0填充。 图3-24IPv4报文格式 IPv4报文首部各个字段的说明如下。 1. 版本 该字段占用4位,对于IP 来讲,可能是0100 或者0110,分别代表IPv4和IPv6,该字段 处于报文头位置,非常重要,决定了后续数据按照哪一个IP 版本进行解析。 2. 首部长度 表示首部占用多少个32 位,也就是多少个4字节。因为4位二进制数最多表示15,所 以首部最多60 字节,去掉首部固定部分的20 字节,首部选项部分最多占用40 字节。 3. 服务类型 占用8位,指定特殊的报文处理方式,用于为不同的IP 数据包定义不同的服务质量。 4. 总长度 占用16 位,表示IP 报文的总大小,包括报文首部和携带的数据,按照字节计数,所以 IPv4报文的最大长度为65535 。 5. 标识 用来实现IP 分片的重组,标识分片属于哪个进程,不同进程通过不同ID 区分,与标志 位和分片偏移字段一起使用。 49 6. 标志位 占用3位,用来确认是否还有IP 分片或是否能执行分片操作。 7. 分片偏移 占用13 位,用于标识IP 分片的位置,实现IP 分片的重组。 8. 存活时间 IPv4报文可以经过最多三层设备(如路由器)数,存活时间(TimeToLive,TTL)的初 始值由源主机设置,该值在经过一个处理它的三层设备时减1,当TTL 值减为0时被丢弃, 此时会发送ICMP 报文通知源主机,其所发送的报文并未到达目标地址。该字段主要是为 了防止路由出现环路问题而导致IP 报文在网络中不停地被转发。 9. 协议 标识上层所使用的协议,常用协议号如下。 .ICM:1 .IGMP:2 .TCP:6 .EGP:8 .UDP:17 10. 首部校验和 用于校验IP 数据包是否完整或被修改,不包含数据部分,若校验失败,则丢弃数据包。 11. 源IP地址和目的IP地址 标识发送端和接收端的IP 地址,各占用32 位。 一个典型的IPv4报文如图3-25 所示,该报文使用Wireshark捕获,图中封包字节区中 的高亮部分即为IPv4协议的首部。 3.2 IP6 3.v IPv6的报文格式如图3-26 所示,首部中去除了IPv4中选项的概念,使用扩展首部代替 首部中的选项功能;6首部固定占用40 字节,这样可以大幅地提高路由器的处理效率, IPv 从而有助于提高网络的整体吞吐量。 IPv6报文首部各个字段的说明如下。 1. 版本 该字段占用4位,和IPv4的作用一样,在IPv6中固定为0110 。 2. 流量区分 类似于IPv4中的服务类型,为报文赋予不同的类型,占用8位。 3. 流标识 用来标识对传输有特殊要求的流,占用20 位。 4. 有效载荷长度 表示除固定首部以外的报文字节数,占用16 位。 50 图3-25IPv4报文 图3-26IPv6报文格式 5. 下一个首部 相当于IPv4的协议字段,占用8位。 6. 跳数限制 相当于IPv4中的存活时间,占用8位。 7. 源IP地址和目的IP地址 标识发送端和接收端的IP 地址,各占用128 位。 一个典型的IPv6报文如图3-27 所示,该报文使用Wireshark捕获,图中封包字节区中 的高亮部分即为IPv6的首部。 51 图3-27IPv6报文 3.4 TCP/IP高级选项 在TCP的发送端和接收端,对于报文的发送和确认可以配置不同的策略,这就是TCP 套接字的NODELAY与QUICKACK;对于连接存活的检测,可以通过KEEPALIVE配 置,这些都是TCPSocket通信的常用高级选项,下面分别进行介绍。 1.NODELAY 以太网的最大传输单元(MaximumTransmisionUnit,MTU)为1500字节,TCP在发 送报文时,为了避免被发送方分片会主动把数据分割成小段后再交给网络层,这个最大的分 段大小称为MSS(MaxSegmentSize),MSS的计算公式如下: MSS=MTU-IP首部大小-TCP首部大小 因为IP报文首部(占用20字节,TCP报文首部也占用20字节,所以MSS的大小 IPv4) 为1460字节,如果一个报文的有效数据小于MSS,则该报文可以被称为小包。 在一些特定情况下,如Telnet会有一字节一字节进行报文发送的情景,每次发送一字 节的有效数据,就要额外发送40字节的首部信息,这在广域网上容易导致拥塞。为了解决 类似的小包问题,TCP引入了Nagle算法,该算法的规则如下: (1)如果包长度达到MSS,则允许发送。 (2)如果该包包含FIN,则允许发送。 (3)如果所有发出去的数据包均被确认,则允许发送。 (4)如果发生了超时,则立即发送。 如果不满足上述条件,则Nagle算法会把数据累积起来不发送,使用TCP的 NODELAY选项可以禁用Nagle算法,在默认情况下Nagle算法是启用的。 52 2.QUICKACK 接收端在收到数据后,并不是马上回复确认ACK,而是延迟一段时间再确认,主要基于 以下考虑: (1)为了合并ACK,如果连续收到两个TCP报文,并不一定需要ACK两次,只要回复 最终的ACK就可以确认,从而降低网络流量。 (2)如果接收端恰好有数据要发送,就可以在发送数据的TCP报文里带上ACK信息。 这样避免了大量的ACK以一个单独的TCP报文发送,也可以减少网络流量。 在默认情况下TCP套接字启用了延迟确认,但是,如果发送端启用了Nagle算法,而且 接收端启用了延迟确认会出现什么情况呢? 发送端尽可能延迟发送,接收端尽可能延迟确 认,这会使网络延迟问题变得严重,所以尽可能不要同时启用,要么关闭Nagle算法,要么关 闭延迟确认;通过设置TCP套接字的QUICKACK即可关闭延迟确认。 3.KEEPALIVE 在TCP连接建立后,如果网络一切正常,并且双方都没有主动发起关闭连接的请求,则 此TCP连接理论上可以永久保持,但是,网络情况比较复杂,在双方长时间未通信时,如何 得知对方还活着? 如何得知当前连接是否仍然具备通信能力? 而且,TCP建立连接需要3 次握手,需要双方分配资源,这说明TCP连接的建立是昂贵的,如果有必要,则可以持续保 持连接,如果确认对方不再存活了,就可以关闭连接,从而释放资源。 TCP的连接保活机制正是用来解决这个问题的,被称为KEEPALIVE,当连接的一方 等待超过一定时间后自动给对方发送一个空的报文,如果对方回复确认了,则证明连接还存 活着,如果对方没有确认且进行多次尝试都是一样的结果,就认为连接已经丢失,没必要继 续保持。该机制默认为关闭的,连接的任何一方都可开启此功能,通过3个主要参数来配置 该功能。 1)tcp_kepalive_time 空闲时长,即每次正常发送心跳的周期,默认值为7200s(2h )。 2)tcp_kepalive_intvl 探测报文的发送间隔,默认值为75s。 3)tcp_kepalive_probes 计数器,如果没有接收到对方的确认报文,则继续发送保活探测报文次数,默认值为9。