彻底弄懂 TCP 协议 —— 概述 Domi●Cat

写在最前

大约在两年前,我曾经在网易云课堂上简单复习过一次 TPC/IP 协议簇,当时并没有特别深入,只是大概记录了一些皮毛知识,在后续两年时间至今,虽然也多多少少更深层次去了解过一些 TCP/IP 的相关细节知识,但是还是感觉并没有系统化的深入理解过,遂决定在今年精读《TCP/IP 详解卷1》(第二版)的 TCP 协议部分的章节(12-17章),预估会花费 3 个月左右的时间(因此会持续更新),希望通过这一次精读,能对 TCP 协议有一个更上一层的理解。

TCP协议概述

首先,要对 TCP/IP 簇的各层所传输的数据单元的名词有一个了解,不仅是简单知道中文名称,更要知道其对应的英文名词,我个人感觉这是很重要的,如果概念都做不到清晰,就谈不上深入理解了。

名称
应用层数据或者消息(Data/Message)
传输层UDP: 数据报(Datagram)
TCP: 数据段(Segment)
网络层分组/包(packet)
数据链路层帧(frame)
物理层PDU(bit)

我们知道 TCP 协议(Transmission Control Protocol)是一种面向连接的可靠的基于字节流传输层通信协议,它的三个主要特征是:可靠传输流量控制拥塞控制。本文仅对这三个特性做一个简单阐述,我会在后续的文章对这三个特性分别进行较为全面的剖析。

可靠传输

什么是可靠传输?

举个例子:假如要从主机 A 发生 100byte 的字节流给主机 B,发送方可能把这 100byte 的字节流分割成 10个分组,每个分组 10byte,那么在接收端怎么确保最终收到的字节流是完整的 100byte,且字节流中的字节顺序没有错乱?

TCP 的数据段会被封装成 IP 分组,然后交给网络层,而在 IP 层会遇到几个问题:分组重新排序, 分组复制, 分组丢失,因为 IP 层并不保证可靠性,为了实现可靠传输,就需要在传输层对发送出去的每一个 Segment 做『确认』操作,即 ACK (acknowlegment),若发送端没有收到接收端返回的 ACK 信号,则尝试重新发送,直到收到 ACK;若发送端收到对应的 ACK 后,它再发送下一个分组,这个过程就这样继续。

我们简单梳理一下上面的流程,简单概括一下就是:确认 + 重传,这就是支撑 TCP 可靠传输的基石,其他相关的技术都是围绕这个基石进行加固与优化,比如在上面的流程中,我们思考几个问题:

  1. 发送方应该多久重传一次分组或者说发送方需要等待 ACK 多久?
  2. 发送的分组在网络中丢失或者返回的 ACK 在网络中丢失应该怎么办?
  3. 接收方收到分组后,怎么确保里面的数据没有错误?

第一个问题涉及到了 TCP的超时和重传机制,较为复杂,会在后续文章中详细讲解;第二个问题的解决方式很简单,就是重新发送原分组,TCP 无法区分是原分组丢失还是 ACK 分组,所以可能在接收端会出现收到多个重复的分组,这里又涉及到 TCP 数据段的序列号(sequence number),每个原分组都会生成一个唯一序列号,接收端根据这个序列号来处理收到的重复分组;第三个问题则使用一些编码技术来检查分组中是否有差错,例如校验和或 CRC。

一些简单的校验和算法(例如奇偶校验码)是不具备纠错功能的,它只能检测错误;而CRC算法(Cyclic Redundancy Check,循环冗余校验码)不仅可以检测错误还能纠正错误,但同时也会在原数据上多附带一些额外的冗余数据,用于纠错。

面向连接

我们常常听到别人说 TCP 和 UDP 的区别时都会强调 TCP 是面向连接的,而 UDP 是无连接的。那究竟什么叫面向连接呢?

举一个例子:假如公司打算举行一次团建活动,如果组织者是一个负责的人,就会提前规划好到达团建地点的路线,例如先从公交站 A 乘坐公交 101 路到达公交站 B,然后换乘公交 201 路到达公交站 C,最后沿解放路(好像每个城市都有解放路 - -)步行 300 米到达目的地,且组织者组织大家有序到达;而如果组织者并不是那么负责,仅仅告诉大家团建的目的地,个人需自行前往,此时公司的每个人都会自行选择路线,有人可能自己开车,有人骑自行车…这样每个人的行程路径都可能不一样,到达的时间也可能前后不一。

在上面的例子中,负责任的组织者使用的策略就是 TCP,而后者不负责的组织者使用的策略就是 UDP。可以看到一个明显的区别,TCP 在正式通信前,会先寻找并确认一条链路,这条链路中由多个路由器组成,且链路中的每个路由器都会维护着上游和下游的路由器的位置,这条预先建立好的链路称为“虚电路”

序列号

TCP 是一个流式协议,即它传输的是字节流,而字节流的特点就是无明确的消息边界,例如:发送端第 1 次写入 20 byte,第 2 次写入 30 byte,第 3 次写入 50 byte,而在接收端是不知道对方写入了多少次以及每次写入了多少字节。

就上面的例子来看,发送端前三次总共写入的 100 byte 可能被分割成两个数据段(segment),然后由 IP 层封装并发送这两个分组,当接收端收到这两个分组后,需要提取出分组中的数据段重新组包(repacketization),在这个重组过程中就需要使用到序列号,如果没有序列号,接收端是没有办法确保收到的分组数据的次序,毕竟网络世界是极其复杂,可能最后发送的分组却被最先接收到。

当然,序列号并不是简单的分组序号(比如1、2、3…这样连续的编号),它实际代表了每个分组数据的第一个字节在整个数据流中的字节偏移(即字节号),假如上面的两个分组数据分别为 30 byte 和 70 byte,那么数据段 1 的序列号为 1,数据段 2 的序列号为 31。不过在 TCP 具体的实现中,第一个序列号会进行随机处理,称为初始序列号(Initial Sequence Number, ISN),增加传输数据被他人识别的难度,假如第一个字节号随机为 100,此时数据段 1 的序列号为 101,数据段 2 的序列号为 131。

确认号

确认号(acknowledgment)也称 ACK 号,它表示接收方期待接收到发送方的下一个序列号,其值为最后被成功接收的数据字节的序列号加 1,也指明了在接收方已经顺序收到的最大字节( 加1 ) 。

上面的表述可能会让人懵逼,还是通过一个例子来理解:

假设发送端分三个数据段发送 100 byte的字节流数据,这三个数据段的数据大小分别为20 byte、30 byte、50 byte,为了方便理解,设初始序列号为 1,那么这三个数据段的序列号分别为 1、21、51。当接收端成功接收了第一个数据段后,返回给发送端的 ACK 号为 21,因为第 1 个数据段的数据大小为 20byte,也就是说对于接收端来说,这 100 byte 的数据已经成功接收了前 20 个 byte,它期待接收的下一个字节序号就是 21,也就是第 1 个数据段的确认号。后面的数据段确认号依次为 51、101(若后续还有数据被发送)。

这里思考一个问题,若接收端收到并确认了数据段 1 后,第二次收到的是数据段 3,那么 TCP 该如何确认呢?

流量控制

吞吐性能

在上面我们已经了解了 TCP 协议发送分组的一个大致流程:发送方发送并确认一个分组,然后继续发送、确认下一个分组,以此循环。虽然流程非常,但是效率也是很低,或者说这个协议的吞吐性能低。通常衡量吞吐性能有两个指标:

  • 吞吐量
  • 吞吐质

吞吐量是指单位时间内在网络中发送的数据量,例如:M = 分组大小,R = 往返时间(Round-Trip Time, RTT),则吞吐量 = M/R;而吞吐质是指单位时间内在网络中发送的有效数据量。前者注重量,后者注重质。

分组窗口

为了提升 TCP 协议的吞吐性能,需要把之前的单线流程改为多线流程,让网络更加的“繁忙”起来,由一次发送一个分组变成一次向网络中发送多个分组。我们把已被发送方注入但还被确认的分组集合称为分组窗口(window),或简称窗口,把在这个集合内的分组数据大小称为窗口大小(window size),即窗口的字节容量(在 TCP 包头中用 16bit 来表示,因此窗口大小范围在0~65535)。

因为分组窗口的引入,可以把分组队列分为三个部分(窗日结构在发送方和接收方都存在),对于发送方分为:已发送并确认等待ACK(发送窗口)未被发送三个部分;而对于接收方则分为:已被接收和确认期望接收(接收窗口)未接收并未准备接收

对于发送窗口来说,处于窗口内的分组又可以分为两类:已发送等待对端确认、在窗口内但还未发送。 发送窗口

发送窗口

在上图中,第 2、3 部分合起来为发送窗口,其中第 2 部分的数据表示已经发送但是还未被对端确认,第 3 部分表示还没有发送到网络中。

滑动窗口

拥塞控制

参考

  1. 面向连接和无连接的套接字到底有什么区别
  2. TCP滑动窗口(发送窗口和接受窗口)