RFC 793 是 TCP 正式成为标准时的文档,虽然距今已有 30 多年的历史,并且已经多次被更新,但是要学习 TCP 这份文档仍然值得一读。
文本算是一个读书笔记,把阅读过程中我认为需要注意的部分记录下来,方便自己以后查漏补缺。后续也会有阅读其他相关 RFC 的读书笔记。
RFC 的第一二章节是按照惯例的声明,正式内容从 第三章 FUNCTIONAL SPECIFICATION 开始。
3.1 头部格式
IP 头部带有源地址和目地址等信息,这两者也同样会被 TCP 头部的某些字段使用(比如计算 checksum 的时候)。
TCP 头部格式如下:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
TCP Header Format
Source Port,16位,源端口号
Destination Port,16位,目的端口号
Sequence Number,32位,序列号。
Acknowledgment Number,32位,确认号。
Data Offset,4位,数据偏移量。通过它可以得出一个 TCP 报文的数据的起始位置,它的单位是32位,也就是以4字节为单位,所以能表示最大值为 15*4=60
字节,换句话说 TCP 首部长度最大为60字节。(TCP 首部也是4字节对齐的,不满的部分需要 padding)
Reserved,4位,保留未用。
Control Bits: 6位,从左到右分别为,URG, ACK,PSH,RST,SYN,FIN。
Window,16位,接受窗口大小。指明发送这个 TCP 报文
的发送方
接受窗口的大小
Checksum,16位,校验位。包括了 TCP 的首部和数据都在计算校验值的范围内。计算是以 16bit(2字节)为单位进行的,如果 tcp 报文不能被2字节整除,那么要在最后加0作为 padding。计算这个校验值的时候,IP 层的源地址、目的地址、协议类型、TCP 长度几个字段都在计算的范围内。其中 TCP 长度包括头部和数据长度,这个值没有包含在 IP 或者 TCP 报文的头部,而是在计算 checksum 的时候计算出来的。这4个字段加起来一共 96 bit,12个字节。如下图所示。
+--------+--------+--------+--------+
| Source Address |
+--------+--------+--------+--------+
| Destination Address |
+--------+--------+--------+--------+
| zero | PTCL | TCP Length |
+--------+--------+--------+--------+
熟悉 IP 首部的同学会发现,真正的 IP 首部中这4个字段不是像上面这样排列的,只是在计算 checksum 的时候临时构造了一个“伪首部”。
Urgent Pointer,16位,紧急指针。它的值表示以本报文中数据段为起点的偏移量。
Options,可变长度,选项字段。选项在 TCP 首部的最后,以1字节为单位。所有的选项字段在计算 checksum 的时候都计算在内。选项字段可能包含多个选项,它们没有类似2字节的对齐要求(多个选项可以一个接一个开始)。
选项通常有两种格式:
- 单个字节的选项类型
- 一字节类型,一字节选项长度(长度包含类型和长度,也就是 +2 ),然后紧接选项数据。
TCP 必须实现下面几种选项:
| Kind | Length | Meaning |
| ------ | --------- | ------------ |
| 0 | | End of option list. |
| 1 | | No-Operation. |
| 2 | 4 | Maximum Segment Size. |
第一种,一字节8位全是0,出现在所有选项的最后,代表结束。 正常情况下(没有选项的情况)不出现。当可选项总长度不够32位的倍数,用该可选项来填补。
+--------+
|00000000|
+--------+
第二种,No-Operation,无操作,NOP字段可以作为选项之间不足4倍数字节填充,也可作为选项间分隔。设计该字段主要是用来明确不同可选项之间的分割点,假设有多个可选项的情况下,一般用该可选项来分割下,因此在一个数据包中可以出现多个nop。(似乎第一和第二选项的功能有些重复?)
第三种,Maximum Segment Size (MSS),其作用是发送方告诉接收方能接受的最大报文长度。 MSS 只在发起链接的过程中(例如在 SYN 报文)有用,TCP 正常传输数据的时候,即使有这个选项也会被忽略,MSS 值用2字节表示。
+--------+--------+---------+--------+
|00000010|00000100| max seg size |
+--------+--------+---------+--------+
Kind=2 Length=4
3.2 术语
TCP 的数据结构 TCB(传输控制块)中存储着本地和对方的 socket 标识符(其实就是文件描述符),数据的 buffer,以及一些额外的变量用于记录发送和接受的序列(通信两方都有 发送和接受 这两个概念,因为 TCP 是全双工通信)。
发送序列的变量:
- SND.UNA send unacknowledged
- SND.NXT send next
- SND.WND send window
- SND.UP send urgent pointer
- SND.WL1 segment sequence number used for last window update
- SND.WL2 segment acknowledgment number used for last window update
- ISS initial send sequence number
接收序列的变量:
- RCV.NXT receive next
- RCV.WND receive window
- RCV.UP receive urgent pointer
- IRS initial receive sequence number
发送序列空间:
1 2 3 4
----------|----------|----------|----------
SND.UNA SND.NXT SND.UNA
+SND.WND
- 区间 1 表示已经发送的,并且接收方已经确认的。
- 区间 2 表示已经发送的,但是对方还没有确认的。
- 区间 3 表示将要发送的。
- 区间 4 表示还
不允许
被发送的字节。
发送窗口
指的是 区间 3
接受序列空间:
1 2 3
----------|----------|----------
RCV.NXT RCV.NXT
+RCV.WND
- 区间 1 表示已经被确认的。
- 区间 2 表示等待接受的字节。
- 区间 3 表示目前
不允许
接受的字节。
接受窗口
指的是 区间 2
以下这几个 TCB 成员变量的值是直接从 tcp 报文中获取的;
- SEG.SEQ segment sequence number
- SEG.ACK segment acknowledgment number
- SEG.LEN segment length
- SEG.WND segment window
- SEG.UP segment urgent pointer
- SEG.PRC segment precedence value
TCP 的状态转换图略。
3.3 序列号
TCP 传输的每一个字节都有对应的序列号,同样,每个字节也有对应的确认号。但是并不是每个字节逐个确认,而是采用了累计的方法:一个确认号表示在他之前的都已经收到了。TCP 的序列号的大小是 0 到 2^32-1, 实际中,所有的运算都需要 mod 2^32-1。
通常我们需要对序列号做以下比较:
- 一个 Seq number 空间中哪些已经发送了,但是还未确认。
- 一个报文中的 Seq number 全部被确认了(当需要把这个报文从重传队列中移走的时候需要这些信息)
- 决定一个收到的报文中的 Seq number 是我们想要的(当这个报文与
接收窗口
重叠的时候)
通常对于一个确认号有以下操作:
- SND.UNA = oldest unacknowledged sequence number
- SND.NXT = next sequence number to be sent
- SEG.ACK = acknowledgment from the receiving TCP (next sequence number expected by the receiving TCP)
- SEG.SEQ = first sequence number of a segment
- SEG.LEN = the number of octets occupied by the data in the segment (counting SYN and FIN)
SEG.SEQ+SEG.LEN-1
= last sequence number of a segment
SND.UNA < SEG.ACK =< SND.NXT
在重传队列上的报文,如果它的序列号加长度小于等于当前收到的报文的 ACK 值,那么可以认为这个报文被确认了。
当收到一个新报文时,以下变量要更新:
- RCV.NXT = next sequence number expected on an incoming segments, and is the left or lower edge of the receive window
- RCV.NXT+RCV.WND-1 = last sequence number expected on an incoming segment, and is the right or upper edge of the receive window.
- SEG.SEQ = first sequence number occupied by the incoming segment
- SEG.SEQ+SEG.LEN-1 = last sequence number occupied by the incoming segment
收到一个新报文时,如果它符合下面两个条件,则认为它是在当前的接收空间的,否则这个报文会被丢弃。
RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
第一个检查报文的开始在不在窗口内,第二个检查报文最后一个字节在不在窗口内。而实际情况稍微比这个复杂一点,因为存在 0窗口
和 0 长度的报文。
Segment Receive Test
Length Window
------- ------- -------------------------------------------
0 0 SEG.SEQ = RCV.NXT
0 >0 RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
>0 0 not acceptable
>0 >0 RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
or RCV.NXT =< SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND
注:
就算接收窗口是0,tcp 也要能够接收 ACK 报文,以及带有 RST 和 URG 标志的报文。
初始序列号的选择
随机化初始序列号的理由:如果通信双方快速的建立和断开链接,如果 ISN 都从 0 开始,那么有可能上一个链接的报文会被当成这次链接的报文。(虽然概率非常小,但是还是有可能发生,而且随机 ISN 的另一个理由是防止 SYN flood 攻击)
说了一堆理由总之就是要随机化 ISN,取随机数的简单的方法是用一个clock,类似 CPU 的时钟,取其低32位作为 ISN,这样32位空间的一个轮回大约需要4.55小时,而 MSL(Maximum Segment Lifetime)远远小于这个数字,所以这个方法可行。
知道何时保持安静
旧的 TCP 报文还在网络中传输,此时 TCP 就不能开启一个可能造成序列号冲突的新链接,所以 TCP 要等 MSL 时间。MSL 通常是2 分钟,这是一个经验值。
(我的疑问是现在的 TCP 必须等这么久吗? 况且 Linux 一般还阻止短时间内重复使用端口号,所以这一小节的内容在现实中是怎样的? )
TCP 安静时间的概念
这里讨论的问题主要是主机重启之后,恢复原来的 TCP 链接,可能会造成新链接的报文和老链接遗留的报文产生冲突。解决办法是重启之后 TCP 必须等待 MSL 时间之后才能恢复原来的链接。
(我觉得这一段有点扯蛋,不同 TCP 实现不一定会遵守这个约定,而且黑客也正是可以利用这一点发起攻击)
3.4 建立一个链接
以下罗列了几种建立链接的情况。
最简单的经典三次握手
值得注意的是 A 在收到 B 的 SYN/ACK 之后,它的状态立马就变成了 ESTABLISHED
,也就是图中的第三行。同理,B 在收到 A 对它的 SYN 的 ACK 之后,也立马变成了 ESTABLISHED
。
(SEQ, 序列号,可以把它理解为 对于这个包,我方的数据流从这个数开始
, 4 报文 SEQ = 101,但是没有数据,所以 5 报文 SEQ 仍然为 101)
TCP A TCP B
1. CLOSED LISTEN
2. SYN-SENT --> <SEQ=100><CTL=SYN> --> SYN-RECEIVED
3. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED
4. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK> --> ESTABLISHED
5. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK><DATA> --> ESTABLISHED
Basic 3-Way Handshake for Connection Synchronization
双方同时发起链接
注意,A 和 B 的初始状态都是 CLOSED
,对比上图,B 的状态是 LISTEN。 “…” 表示这个报文还在网络上传输。
TCP A TCP B
1. CLOSED CLOSED
2. SYN-SENT --> <SEQ=100><CTL=SYN> ...
3. SYN-RECEIVED <-- <SEQ=300><CTL=SYN> <-- SYN-SENT
4. ... <SEQ=100><CTL=SYN> --> SYN-RECEIVED
5. SYN-RECEIVED --> <SEQ=100><ACK=301><CTL=SYN,ACK> ...
6. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED
7. ... <SEQ=101><ACK=301><CTL=ACK> --> ESTABLISHED
Simultaneous Connection Synchronization
TCP 建立链接需要三次握手,这样的设计主要是为了防止有重复的报文引起冲突。RST 标志位就是为了处理这样的情况的。
如果接受报文的 TCP 一端是处于刚开始同步状态(例如,SYN-SENT, SYN-RECEIVED),那么当它收到 RST 时,它将返回回 LISTEN 状态。
如果接受报文的 TCP 一端处于已同步状态(例如,ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT),那么当它收到 RST 时,断开链接。
下图是在多个不相关的 SYN 报文中建立正确链接的例子。
TCP A TCP B
1. CLOSED LISTEN
2. SYN-SENT --> <SEQ=100><CTL=SYN> ...
3. (duplicate) ... <SEQ=90><CTL=SYN> --> SYN-RECEIVED
4. SYN-SENT <-- <SEQ=300><ACK=91><CTL=SYN,ACK> <-- SYN-RECEIVED
5. SYN-SENT --> <SEQ=91><CTL=RST> --> LISTEN
6. ... <SEQ=100><CTL=SYN> --> SYN-RECEIVED
7. SYN-SENT <-- <SEQ=400><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED
8. ESTABLISHED --> <SEQ=101><ACK=401><CTL=ACK> --> ESTABLISHED
Recovery from Old Duplicate SYN
半开的链接和其他异常
如果 TCP 链接的一端非正常关闭了,另一端并不知道这一情况,或者,通信两端因为某些情况变的状态不一致。这样的状态被称为 半开链接。
在这种情况下,如果一端尝试发送数据,链接将要被重置。
如果服务器 A 上的 TCP 链接不存在,但是 B 尝试着发送数据给 A,这样导致 B 收到一个 RST 报文,告诉 B 发生了错误,Abort。
假设 A 和 B 建立了一个 TCP 链接,此时 A crash 并且重启了。那么重启后的 A 可能会选择重新开始,或者是接着上次的链接继续。如下图所示,在第3行,A 发送 SYN 希望能建立链接,而此时的 B,它的所处的状态是“已建立链接”,正在等待 A 发送数据,但是很显然,A 的 SYN 并不是它期望的,而且也不在它的接受窗口之内,于是就给 A 回复一个 ACK,重申 “我希望收到的序列号是多少”(这里是不是可以一起黑客攻击?
),此时 A 看到 B 回复的并不是它想要的,“unsynchronized”,可以判定此时双方处于了 半开链接
的状态,于是发送 RST。之后重新建立链接。
TCP A TCP B
1. (CRASH) (send 300,receive 100)
2. CLOSED ESTABLISHED
3. SYN-SENT --> <SEQ=400><CTL=SYN> --> (??)
4. (!!) <-- <SEQ=300><ACK=100><CTL=ACK> <-- ESTABLISHED
5. SYN-SENT --> <SEQ=100><CTL=RST> --> (Abort!!)
6. SYN-SENT CLOSED
7. SYN-SENT --> <SEQ=400><CTL=SYN> -->
Half-Open Connection Discovery
另一个有趣的是,A crash 了,但是 B 不知道,继续发送数据,那么此时的情况就是本小节一开头说的,A 会发送 RST 给 B。
TCP A TCP B
1. (CRASH) (send 300,receive 100)
2. (??) <-- <SEQ=300><ACK=100><DATA=10><CTL=ACK> <-- ESTABLISHED
3. --> <SEQ=100><CTL=RST> --> (ABORT!!)
Active Side Causes Half-Open Connection Discovery
再看一种情况,A B 都在 LISTEN 状态,此时网络上之前滞留的 SYN 报文到达了 B,B 回复ACK,但是 A 收到报文后发现这个 ACK 不能接受,发送 RST 给 B,B 重新回到 LISTEN 状态。
TCP A TCP B
1. LISTEN LISTEN
2. ... <SEQ=Z><CTL=SYN> --> SYN-RECEIVED
3. (??) <-- <SEQ=X><ACK=Z+1><CTL=SYN,ACK> <-- SYN-RECEIVED
4. --> <SEQ=Z+1><CTL=RST> --> (return to LISTEN!)
5. LISTEN LISTEN
Old Duplicate SYN Initiates a Reset on two Passive Sockets
还有很多奇奇怪怪的情况发生,但最终都会导致一方发出 RST,下面就来看看 RST 产生的条件。
发送 RST 报文
通常来说,通信双方 A 和 B,只要有一方收到了明显不应该出现的报文,那么就可以发送 RST。
有三种状态集:
RST 的处理
收到 RST 的时候都要检查它的 seq number 在不在接受窗口内,除了在 SYN-SENT 状态下不用检查,在 SYN-SENT 状态下,只要报文的 ACK 了 ISN,那么 RST 就是有效的。
接收到 RST 的一方先检查正确性,然后改变自己的状态:
- 如果在 LISTEN 状态,则一切照旧,继续
- 在 SYN-RECEIVED 状态,之前是 LISTEN,则返回到 LISTEN 状态。
- 其他情况 Abort,进入 CLOSED
3.5 关闭一个链接
3.6 优先和安全 小节略。
3.7 数据传输
TCP 重传机制确保在网络传输中丢失的包在重传一次,但是这样也可能会导致多个重复的包先后到达对方,在序列号那一小节已经讨论过接收方如何处理这些重复的报文。
发送方时刻关注 SND.NXT 中的 seq number 是多少,同时接收方也关注下一个期望收到的 RCV.NXT 是多少,发送者记录最大的未确认的 seq number 是多少 SND.UNA 。如果所有已发送的字节都被确认的话,这三个变量的值相等。
重传超时
这里只是举了一个例子表示如何决定重传超时的时间。首先,RTT 表示发送一个字节,到收到它的确认,这一个来回的时间为 RTT。然后计算一个所谓的 Smoothed Round Trip Time。
SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)
最后计算重传时间 RTO
RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]]
ALPHA 和 BETA 都是因子,根据经验或者其他计算方法决定它的值,在其他 RFC 中有对它的研究,后续再更新。
Urgent 信息的传输
就是用 TCP 头部的 urgent 字段告诉接受者,这个 TCP 报文数据部分从某个偏移量开始是紧急数据部分。这个字段并不常见,留着以后再研究。
窗口管理
即使当前发送方 TCP 的发送窗口是0,它也要能够发送一个字节的数据。即使窗口为 0,发送方的 TCP 必须能够重传已经发送的报文。
窗口管理的建议:
如果把窗口设置的太小,那么性能就太差了,所以我们也要避免窗口太小。如果接收者到一个报文,窗口值设置的非常小,那么接受者可以延迟更新 TCB 结构中有关窗口的变量,直到对方发送过来一个“合理”的值。
再从发送者的角度来看,发送者也要避免发送非常小的窗口值,所以一个可行的方法值等到窗口扩展到足够大了,再发送报文。
注意
:不能因为要等窗口扩大而延迟 ACK 报文的发送,因为一旦不发送 ACK,对方就会重传报文。 一个解决方法是,当对方有小报文传来(不用更新窗口),发送 ACK 以免引起不必要的重传,之后再等待时机发送新窗口值。
(窗口值为 0 的这一段没有看懂,等看完其他资料再来更新)
3.8 接口
这一章节没有太多新的东西,有 socket 编程经验的人都熟悉,就是一些高级的用户函数,Open(), Close(), Listen(), Connect() 等。
略。
3.9 事件的处理
在实现一个 TCP 栈的时候,可以看做 TCP 是对一系列事件的回应,事件主要分为三大类,用户调用函数、有报文到达、超时。通常还要根据当前 TCP 的状态,加上发生的事件,来决定下一步怎么做。
对于每一个收到的报文,我们可以想象是这样的先后操作:先检查 seq number 在不在接收窗口之内,然后把这个报文以 seq number 大小为序,放在队列里面。如果一个报文的数据和已经收到的报文重叠,那么 TCP 栈要负责识别并且去重复的工作。
OPEN Call
CLOSED STATE ( 例如 TCB 不存在)
创建一个新的 TCB,设置源地址,目的地址,定时器等。在被动打开(passive OPEN,举例 ??)的情况下,一开始并没有目的地址,要等到收到 SYN 报文才有。 如果是主动打开(active open) 发出一个 SYN 报文。
LISTEN STATE
发送一个 SYN,设置 SND.UNA = ISS,SND.NXT = ISS+1。 进入 SYN-SENT 状态。
其他状态下遇到 OPEN Call 没有任何动作。
SEND Call
LISTEN STATE
如果指定了目的地址,那么从被动模式转到主动模式,发送 SYN,设置 SND.UNA = ISS,SND.NXT = ISS+1, 进入 SYN-SENT 状态。SEND 命令所携带的数据可能在这个 SYN 报文发送,也有可能缓存起来等到链接建立好了进入 ESTABLISHED 状态再发送。(听这段话的意思,数据是可以在 SYN 发送的? 现实中好像不可以
)
SYN-SENT STATE, SYN-RECEIVED STATE
缓存用户的这个请求命令,直到链接进入 ESTABLISHED 状态再发送。
ESTABLISHED STATE, CLOSE-WAIT STATE
发送用户数据,并且 ACK 的 value = RCV.NXT
其他状态下遇到 SEND Call 没有任何动作。
RECEIVED Call
CLOSED STATE
返回错误。
LISTEN STATE, SYN-SENT STATE, SYN-RECEIVED STATE
缓存用户的这个请求,直到链接进入 ESTABLISHED 状态再处理。
ESTABLISHED STATE, FIN-WAIT-1 STATE, FIN-WAIT-2 STATE
如果此时接收对方的数据还没接收完,则把用户的请求缓存起来,等数据全部收完了再回复用户的请求。如果已经收完了则立即回复。
CLOSE-WAIT STATE
因为对方已经发送了 FIN(所以这一方才会进入 CLOSE-WAIT 状态),所以剩下的数据在队列中,只是还没有交到用户手中而已,这种情况下直接把数据拷贝过去就行了。
其他状态下遇到 RECEIVED Call 没有任何动作。
CLOSE Call
CLOSED STATE
没啥好说的,直接关闭。
LISTEN STATE
删除 TCB,进入 CLOSED 状态
SYN-SENT STATE
删除 TCB,对所有在队列中的发送或接收请求返回 “error: closeing”
SYN-RECEIVED STATE
如果命令队列中没有发送请求,发送一个 FIN,进入 FIN-WAIT-1 状态。否则缓存这个命令,等到链接进入 ESTABLISED 状态再处理。
ESTABLISHED
把这条命令放在最后,先把队列中该发送的数据发送掉,然后发送 FIN,进入 FIN-WAIT-1 状态。
FIN-WAIT-1 STATE, FIN-WAIT-2 STATE
这种情况下应该是发生错误,直接返回错吧。
CLOSE-WAIT STATE
缓存这个命令知道其他的发送命令全部结束后,发送 FIN,进入 CLOSING 状态。
ABORT Call
STATUS Call
以上这两个状态没有太多要注意的,具体的细节直接看 RFC 原文就行了。
SEGMENT ARRIVES
这是最重要的一个状态转换,当一个报文到达时,每一个状态都要细细讨论。对照文档中的内容,写成伪代码的形式最容易理解,甚至在写真正的代码时可以照搬伪代码。
收到了一个报文,接收方的 TCP 协议栈做如下判断:
if (state == CLOSED) {
丢弃任何收到的报文
return;
}
if (state == LISTEN) {
1. 检查报文如果设置了 RST
return;
2. 检查报文 ACK 标志,在 LISTEN 状态,我们不应该收到 ACK 报文,所以必定有些地方出错了,
发送 RST 报文给对方,其中 SEQ = SEG.ACK, CTL = RST, return;
3. 检查报文SYN。如果 SYN 位设置,则需要检查安全性。
if (SEG.PRC < TCB.PRC) {
RCV.NXT = SEG.SEQ+1;
发送 SYN 报文,<SEQ=ISS><ACK=RCV.NXT><CTL=SYN,ACK>
SND.NXT = ISS + 1, SND.UNA = ISS
链接的状态进入 SYN-RECEIVED,之后收到的报文要在 SYN-RECEIVED 状态处理,
但是这个状态不会再处理 SYN 报文了。
} else {
发送 RST。
}
4. 检查其他控制位。无论其他 flags 是什么,ACK 总是要被置位的,如果这个报文没有 ACK,则 RST
}
if (state == SYN-SENT) {
1. check ACK bit
if (ACK is set) {
if (SEG.ACK <= ISS || SEG.ACK > SND.NXT) {
发送 RST : <SEQ=SEG.ACK><CTL=RST>
return;
}
if (SND.UNA <= SEG.ACK <= SND.NXT) {
这个报文可以接受。
}
}
2. 检查 RST bit,如果设置了,则报错并 close 这个链接。
3. 检查安全和优先级。(略)
4. 检查 SYN bit。只有当 ACK 检查没有问题,或者没有 ACK,但是报文没有设置 RST 的时候才会到这一步。
if (SYN bit is on, 安全性检查没有问题) {
RCV.NXT = SEG.SEQ+1
IRS = SEG.SEQ
SND.UNA + 1 == SEG.ACK,
其他在重传队列的报文,如果因为这个报文而被确认了,则从重传队列中移除。
}
if (SND.UNA > ISS) {
state = ESTABLISHED
发送一个 ACK : <SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>
} else {
state = SYN-RECEIVED
发送一个 SYN,ACK = <SEQ=ISS><ACK=RCV.NXT><CTL=SYN,ACK>
}
5. 如果收到了报文既没有 SYN 也没有 RST,丢弃。
}
当 TCP 栈的状态不在上面列举的三个时,那么它必定在剩下的 8 个中。
此时,收到一个报文:
1. 检查 seq number,先判断这个报文是不是 dup,确认不是 dup 后留下处理
后续的处理是按照 SEG.SEQ 的顺序来,如果这个报文与其他有重叠,则去重后留下新的。
检查的过程有些复杂,还要考虑 接收窗口。
Segment Receive Test
Length Window
------- ------- -------------------------------------------
0 0 SEG.SEQ = RCV.NXT
0 >0 RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
>0 0 not acceptable
>0 >0 RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
or RCV.NXT =< SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND
如果 RCV.WND 是空,不接受新报文。但是允许接收 ACK,URG,RST。
如果这个报文最后判定为不能接收,除了RST以外,需要发送ACK:<SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>
2. 第二步检查 RST
if (state == SYN-RECEIVED) {
if (RST bit is set) {
如果链接是被动打开的(从 LISTEN 状态而来的)
则重新回到 LISTEN,也不需要回复发送者。
如果链接是主动的,则告诉上层用户出错了,进入 CLOSED,删 TCB
}
}
if (state == ESTABLISHED || FIN-WAIT-1 || FIN-WAIT-2 || CLOSE-WAIT) {
告诉上层用户出错了,进入 CLOSED,删 TCB
}
if (state == CLOSING STATE || LAST-ACK STATE || TIME-WAIT) {
进入 CLOSED,删 TCB
}
3. 第三步,检查安全和优先级。
if (state == SYN-RECEIVED) {
如果优先级检查不满足,则发送一个 RST,return
}
if (state == ESTABLISHED STATE) {
告诉上层用户出错了,进入 CLOSED,删 TCB
}
4. 第四步,检查 SYN
if (state == SYN-RECEIVED || ESTABLISHED STATE || FIN-WAIT STATE-1 ||
FIN-WAIT STATE-2 || CLOSE-WAIT STATE || CLOSING STATE ||
LAST-ACK STATE || TIME-WAIT STATE) {
如果这个 SYN 在接收窗口内,则发生了严重的错误,
告诉上层用户出错了,进入 CLOSED,删 TCB
如果 SYN 不在接收窗口内,(这种情况不会遇到,因为在第一步就已经处理了)
}
5. 第五步,检查 ACK
if (ACK 没有设置),直接丢弃这个报文。
if (ACK is set) {
if (state == SYN-RECEIVED) {
if (SND.UNA =< SEG.ACK =< SND.NXT) {
进入 ESTABLISHED 状态继续
if (如果 ACK 不能接收) {
发送一个 RST :<SEQ=SEG.ACK><CTL=RST>
}
}
}
if (state == ESTABLISHED) {
if (SND.UNA < SEG.ACK =< SND.NXT) {
SND.UNA = SEG.ACK
重传队列上被确认的报文都需要移除。
如果 ACK 重复确认了,则忽略。
如果 ACK 确认了不该确认的 (SEG.ACK > SND.NXT)
则发送一个 ACK,丢弃这个报文。
}
if (SND.UNA < SEG.ACK =< SND.NXT) {
需要更新 发送窗口。
if (SND.WL1 < SEG.SEQ or (SND.WL1 = SEG.SEQ and
SND.WL2 =< SEG.ACK)) {
SND.WND = SEG.WND
SND.WL1 = SEG.SEQ
SND.WL2 = SEG.ACK
}
}
需要注意的是,SND.WND 是从 SND.UNA 的偏移量,
SND.WL1 记录了用于更新 SND.WND 的 seq number
SND.WL2 记录了用于更新 SND.WND 的报文的确认号
这样做的目的是阻止使用旧的报文去更新窗口
}
其他的状态略,具体可以查看 RFC
}
6. 第六步,检查 URG, 这一步略。
7. 第七步,处理 segment text。 略
8. 第八步,检查 FIN
if (state == CLOSED, LISTEN or SYN-SENT) {
直接忽略 FIN 报文
}
除此之外,收到 FIN 要通知用户准备关闭连接。
if (state == SYN-RECEIVED || ESTABLISHED) {
进入 CLOSE-WAIT
}
if (state == FIN-WAIT-1) {
if (FIN 被 ACK 了) {
那么进入 TIME-WAIT 状态,
开启 time-wait 计时器
关闭其他计时器
} else {
进入 CLOSING 状态
}
}
if (state == FIN-WAIT-2) {
进入 TIME-WAIT
开启 time-wait 计时器
关闭其他计时器
}
if (state == CLOSE-WAIT || CLOSING || LAST-ACK) {
保持这个状态
}
if (state == TIME-WAIT) {
保持这个状态,重启 2 MSL time-wait 定时器。
}
USER TIMOUT
USER TIMEOUT,定时器超时,任何情况下如果这个定时器超时了,清理队列、TCB,返回给用户一条错误信息,完事。
RETRANSMISSION TIMEOUT,把重传队列头部的报文重新发送一次,并且重置定时器。
TIME-WAIT TIMEOUT,删 TCB,进入 CLOSED 状态。