skaiuijing

引言

前面我们查阅了RFC文档,现在让我们重点查看输入代码。

源码采用的是4.4FreeBSD代码,笔者参考的书籍是TCP详解卷二。

书上已经讲得非常详细了,但是笔者为什么还要重复讲一遍呢?

1.因为书上太详细了,甚至包括了很多操作系统的细节,但是对于TCP的原理来说,理解这些细节不是必要的。

2.书上几乎都是一行行代码讲解,但是我们的目标是理解TCP的整体框架

所以,笔者的讲解是在TCP卷一和TCP卷二的源码实现中寻找一个平衡点,既能把理论讲解清楚,又能让读者明白TCP的源码实现框架。

也就是说,笔者专注于的是TCP的整体运行框架。

为了达到这一点,必须要以动态的视角看待TCP源码实现。

我们必须要从数据包的视角看待TCP协议栈的源码处理,所以,我们还是要与三次握手、四次挥手联系起来。

TCP输入与数据包的动态处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
         主动开启者               被动开启者
LISTEN
被动打开
SYN_SENT ------SYN K-----------> SYN_RCVD
/
ESTABLISHED <---SYN L, ACK K + 1--
----ACK K + 1--------> ESTABLISHED
数据传输发生在ESTABLISHED状态
FIN_WAIT_1 -------FIN M + ACK ------> CLOSED_WAIT
/
FIN_WAIT_2 <-----ACK M + 1---------
LAST_ACK 其实这两次操作往往也都会被合并为一次
/
TIME_WAIT <------FIN N---------
| -------ACK N +1 --------> \
|2MSL计时器 CLOSED
CLOSED

TCP结构体字段

首部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct tcphdr {
unsigned short th_sport;
unsigned short th_dport;
unsigned int th_seq;
unsigned int th_ack;
//little ENDIAN!!!
unsigned char th_x2:4;
unsigned char th_off:4;
unsigned char th_flags;
unsigned short th_win;
unsigned short th_sum;
unsigned short th_urp;
}__attribute__((packed));

下面是tcp控制块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

/*
Send Sequence Variables
|---snd_una---|||---snd_nxt---|||---snd_max---|
^snd_wnd

compare: snd_una < snd_nxt <= snd_max

Receive Sequence Variables
|---rcv_nxt---|||---rcv_adv---|
^rcv_wnd

Congestion Control
|---snd_cwnd---|||---snd_ssthresh---|
*
*
*/


struct tcpcb {
struct list_node node;
short t_state; /* state of this connection */
short t_timer[TCPT_NTIMERS]; /* tcp timers */
short t_rxtshift; /* log(2) of rexmt exp. backoff */
short t_rxtcur; /* current retransmit value */
short t_dupacks; /* consecutive dup acks recd */
unsigned short t_maxseg; /* maximum segment size */
char t_force; /* 1 if forcing out a byte */
unsigned short t_flags;

struct tcpiphdr *t_template; /* skeletal packet for transmit */
struct inpcb *t_inpcb; /* back pointer to internet pcb */
/*
* The following fields are used as in the protocol specification.
* See RFC783, Dec. 1981, page 21.
*/
/* send sequence variables */
tcp_seq snd_una; /* send unacknowledged */
tcp_seq snd_nxt; /* send next */
tcp_seq snd_up; /* send urgent pointer */
tcp_seq snd_wl1; /* window update seg seq number */
tcp_seq snd_wl2; /* window update seg ack number */
tcp_seq iss; /* initial send sequence number */
unsigned int snd_wnd; /* send window */
/* receive sequence variables */
unsigned int rcv_wnd; /* receive window */
tcp_seq rcv_nxt; /* receive next */
tcp_seq rcv_up; /* receive urgent pointer */
tcp_seq irs; /* initial receive sequence number */

/* receive variables */
tcp_seq rcv_adv; /* advertised window */
/* retransmit variables */
tcp_seq snd_max; /* highest sequence number sent *
/* congestion control (for slow start, source quench, retransmit after loss) */
unsigned int snd_cwnd; /* congestion-controlled window */
unsigned int snd_ssthresh;
/*
* transmit timing stuff. See below for scale of srtt and rttvar.
* "Variance" is actually smoothed difference.
*/
short t_idle; /* inactivity time */
short t_rtt; /* round trip time */
tcp_seq t_rtseq; /* sequence number being timed */
short t_srtt; /* smoothed round-trip time */
short t_rttvar; /* variance in round-trip time */
unsigned short t_rttmin; /* minimum rtt allowed */
unsigned long max_sndwnd; /* largest window peer has offered */

/* out-of-band data */
char t_oobflags; /* have some */
char t_iobc; /* input character */
short t_softerror; /* possible error not yet reported */

/* RFC 1323 variables */
unsigned char snd_scale; /* window scaling for send window */
unsigned char rcv_scale; /* window scaling for recv window */
unsigned char request_r_scale; /* pending window scaling */
unsigned char requested_s_scale;
unsigned long ts_recent; /* timestamp echo data */
unsigned long ts_recent_age; /* when last updated */
tcp_seq last_ack_sent;

void *t_tuba_pcb; /* next level down pcb for TCP over z */
};

关键字段

  • t_state:TCP 当前状态(CLOSED、SYN_SENT、ESTABLISHED 等),驱动状态机。
  • 发送窗口三元组
    • snd_una:最早未确认的序号(ACK 滑动时前移)。
    • snd_nxt:下一个要发送的序号(发送数据/控制位时推进)。
    • snd_wnd:对端通告的接收窗口大小(流量控制上限)。
  • 接收窗口二元组
    • rcv_nxt:期望接收的下一个序号(ACK 值)。
    • rcv_wnd:本端可用接收窗口大小(通告给对端)。
  • 初始序号
    • iss:本端初始发送序号(握手时生成)。
    • irs:对端初始发送序号(握手时记录)。
  • 拥塞控制
    • snd_cwnd:拥塞窗口,本端自我约束。
    • snd_ssthresh:慢启动阈值,决定指数/线性增长切换点。
  • 定时器/RTT
    • t_srtt:平滑 RTT,用于计算 RTO。
    • t_rttvar:RTT 方差,反映波动。
    • t_rxtcur:当前重传超时时间(RTO)。

这些字段能够方便我们理解后面的TCP输入代码。

序列号、发送窗口与接受窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
Send Sequence Variables
|---snd_una---|||---snd_nxt---|||---snd_max---|
^snd_wnd

compare: snd_una < snd_nxt <= snd_max

Receive Sequence Variables
|---rcv_nxt---|||---rcv_adv---|
^rcv_wnd

Congestion Control
|---snd_cwnd---|||---snd_ssthresh---|
*
*
*/

现在假设有A和B两个主机,A是发送者,发送一个数组中的数据,B是接收者。

序号与确认号

初始化序列号ISN

协议栈中有一个变量ISN,记录的是初始化序列号,参考RFC9293:

3.4.1 初始序列号(ISN)的选择

  • 每个连接的实例(化身)必须有唯一的初始序列号,以避免旧段被误认为新段。
  • ISN 由一个 单调递增的 32 位计数器生成,通常每 4 微秒加 1,约 4.55 小时循环一次,远大于 MSL(最大报文段生存期)。
  • 为防止攻击者预测 ISN,ISN = **计数器值 M + 伪随机函数 F(…)**,其中 F 结合了本地/远程 IP、端口和一个秘密密钥。

所以,在连接建立时,双方会交换序列号。

首部中的seq和ack,在单向传输过程中一个发送,一个确认。

  • **序号 (seq)**:标记报文段中第一个字节在整个字节流中的位置。
  • **确认号 (ack)**:累计确认,表示“到这个序号之前的字节我都收到了”。
    • A 的 snd_una ← B 的 ack
    • B 的 rcv_nxt ← A 的 seq + len

为了详细说明,还是先抓个包吧:

TCP通信示例

用tcp简单抓包:

简约:

1
2
3
4
5
6
7
8
23:36:05.107559 IP skaiuijing-VMware-Virtual-Platform.59746 > DESKTOP-JIO0OM2.9090: Flags [S], seq 2574454702, win 64240, options [mss 1460,sackOK,TS val 2467129094 ecr 0,nop,wscale 7], length 0
23:36:05.108077 IP DESKTOP-JIO0OM2.9090 > skaiuijing-VMware-Virtual-Platform.59746: Flags [S.], seq 2010931041, ack 2574454703, win 64240, options [mss 1460], length 0
23:36:05.108118 IP skaiuijing-VMware-Virtual-Platform.59746 > DESKTOP-JIO0OM2.9090: Flags [.], ack 1, win 64240, length 0
23:36:05.108900 IP DESKTOP-JIO0OM2.9090 > skaiuijing-VMware-Virtual-Platform.59746: Flags [P.], seq 1:1431, ack 1, win 64240, length 1430
23:36:05.108922 IP skaiuijing-VMware-Virtual-Platform.59746 > DESKTOP-JIO0OM2.9090: Flags [.], ack 1431, win 65535, length 0
23:36:05.109153 IP DESKTOP-JIO0OM2.9090 > skaiuijing-VMware-Virtual-Platform.59746: Flags [FP.], seq 1431, ack 1, win 64240, length 0
23:36:05.109245 IP skaiuijing-VMware-Virtual-Platform.59746 > DESKTOP-JIO0OM2.9090: Flags [F.], seq 1, ack 1432, win 65535, length 0
23:36:05.109371 IP DESKTOP-JIO0OM2.9090 > skaiuijing-VMware-Virtual-Platform.59746: Flags [.], ack 2, win 64239, length 0

由于不能很好的看出双方的ACK和SEQ变化,再抓一份详细的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
    192.168.91.129.37790 > 192.168.31.190.9090: Flags [S], cksum 0xfcbe (incorrect -> 0x08d8), seq 3651959169, win 64240, options [mss 1460,sackOK,TS val 2467211849 ecr 0,nop,wscale 7], length 0
0x0000: 0050 56f1 7990 000c 29a8 3f54 0800 4500 .PV.y...).?T..E.
0x0010: 003c 7e6e 4000 4006 bfbd c0a8 5b81 c0a8 .<~n@.@.....[...
0x0020: 1fbe 939e 2382 d9ac 7981 0000 0000 a002 ....#...y.......
0x0030: faf0 fcbe 0000 0204 05b4 0402 080a 930e ................
0x0040: aa49 0000 0000 0103 0307 .I........
23:37:27.863131 IP (tos 0x0, ttl 128, id 46358, offset 0, flags [none], proto TCP (6), length 44)
192.168.31.190.9090 > 192.168.91.129.37790: Flags [S.], cksum 0xe08e (correct), seq 1067742738, ack 3651959170, win 64240, options [mss 1460], length 0
0x0000: 000c 29a8 3f54 0050 56f1 7990 0800 4500 ..).?T.PV.y...E.
0x0010: 002c b516 0000 8006 8925 c0a8 1fbe c0a8 .,.......%......
0x0020: 5b81 2382 939e 3fa4 7612 d9ac 7982 6012 [.#...?.v...y.`.
0x0030: faf0 e08e 0000 0204 05b4 0000 ............
23:37:27.863197 IP (tos 0x0, ttl 64, id 32367, offset 0, flags [DF], proto TCP (6), length 40)
192.168.91.129.37790 > 192.168.31.190.9090: Flags [.], cksum 0xfcaa (incorrect -> 0xf84b), seq 1, ack 1, win 64240, length 0
0x0000: 0050 56f1 7990 000c 29a8 3f54 0800 4500 .PV.y...).?T..E.
0x0010: 0028 7e6f 4000 4006 bfd0 c0a8 5b81 c0a8 .(~o@.@.....[...
0x0020: 1fbe 939e 2382 d9ac 7982 3fa4 7613 5010 ....#...y.?.v.P.
0x0030: faf0 fcaa 0000 ......
23:37:27.864539 IP (tos 0x0, ttl 128, id 46359, offset 0, flags [none], proto TCP (6), length 826)
192.168.31.190.9090 > 192.168.91.129.37790: Flags [P.], cksum 0x20ae (correct), seq 1:787, ack 1, win 64240, length 786
0x0000: 000c 29a8 3f54 0050 56f1 7990 0800 4500 ..).?T.PV.y...E.
0x0010: 033a b517 0000 8006 8616 c0a8 1fbe c0a8 .:..............
0x0020: 5b81 2382 939e 3fa4 7613 d9ac 7982 5018 [.#...?.v...y.P.
0x0030: faf0 20ae 0000 4b4f 4d53 4d59 474b 4d53 ......KOMSMYGKMS
0x0040: 5949 434c 4154 4348 4447 434a 4e49 485a YICLATCHDGCJNIHZ
0x0050: 4754 5251 5542 444b 5048 5843 4a49 4a4b GTRQUBDKPHXCJIJK
0x0060: 4858 4a52 4e42 414e 444f 4152 5456 4b42 HXJRNBANDOARTVKB
0x0070: 454a 4457 424c 424e 4555 485a 4e42 554c EJDWBLBNEUHZNBUL
0x0080: 4b52 4a41 4f41 4a47 4c46 5948 4359 4b48 KRJAOAJGLFYHCYKH
0x0090: 4a53 4257 4844 524c 5447 5345 4d5a 5647 JSBWHDRLTGSEMZVG
0x00a0: 5548 4358 5153 4450 4c4d 4941 5657 4548 UHCXQSDPLMIAVWEH
\\省略一堆数据
23:37:27.864559 IP (tos 0x0, ttl 64, id 32368, offset 0, flags [DF], proto TCP (6), length 40)
192.168.91.129.37790 > 192.168.31.190.9090: Flags [.], cksum 0xfcaa (incorrect -> 0xf84b), seq 1, ack 787, win 63454, length 0
0x0000: 0050 56f1 7990 000c 29a8 3f54 0800 4500 .PV.y...).?T..E.
0x0010: 0028 7e70 4000 4006 bfcf c0a8 5b81 c0a8 .(~p@.@.....[...
0x0020: 1fbe 939e 2382 d9ac 7982 3fa4 7925 5010 ....#...y.?.y%P.
0x0030: f7de fcaa 0000 ......
23:37:27.864694 IP (tos 0x0, ttl 128, id 46360, offset 0, flags [none], proto TCP (6), length 40)
192.168.31.190.9090 > 192.168.91.129.37790: Flags [FP.], cksum 0xf530 (correct), seq 787, ack 1, win 64240, length 0
0x0000: 000c 29a8 3f54 0050 56f1 7990 0800 4500 ..).?T.PV.y...E.
0x0010: 0028 b518 0000 8006 8927 c0a8 1fbe c0a8 .(.......'......
0x0020: 5b81 2382 939e 3fa4 7925 d9ac 7982 5019 [.#...?.y%..y.P.
0x0030: faf0 f530 0000 0000 0000 0000 ...0........
23:37:27.864796 IP (tos 0x0, ttl 64, id 32369, offset 0, flags [DF], proto TCP (6), length 40)
192.168.91.129.37790 > 192.168.31.190.9090: Flags [F.], cksum 0xfcaa (incorrect -> 0xf84a), seq 1, ack 788, win 63453, length 0
0x0000: 0050 56f1 7990 000c 29a8 3f54 0800 4500 .PV.y...).?T..E.
0x0010: 0028 7e71 4000 4006 bfce c0a8 5b81 c0a8 .(~q@.@.....[...
0x0020: 1fbe 939e 2382 d9ac 7982 3fa4 7926 5011 ....#...y.?.y&P.
0x0030: f7dd fcaa 0000 ......
23:37:27.864958 IP (tos 0x0, ttl 128, id 46361, offset 0, flags [none], proto TCP (6), length 40)
192.168.31.190.9090 > 192.168.91.129.37790: Flags [.], cksum 0xf538 (correct), seq 788, ack 2, win 64239, length 0
0x0000: 000c 29a8 3f54 0050 56f1 7990 0800 4500 ..).?T.PV.y...E.
0x0010: 0028 b519 0000 8006 8926 c0a8 1fbe c0a8 .(.......&......
0x0020: 5b81 2382 939e 3fa4 7926 d9ac 7983 5010 [.#...?.y&..y.P.
0x0030: faef f538 0000 0000 0000 0000 ...8........
^C

抓包内容表

阶段 报文方向 Flags Seq / Ack (tcpdump显示) 长度 状态机含义
1 192.168.91.129:37790 → 192.168.31.190:9090 [S] seq=3651959169, ack=0 0 A 发 SYN,请求建立连接,ISN_A=3651959169
2 192.168.31.190:9090 → 192.168.91.129:37790 [S.] seq=1067742738, ack=3651959170 0 B 回 SYN+ACK,ISN_B=1067742738,确认 A 的 SYN
3 192.168.91.129:37790 → 192.168.31.190:9090 [.] seq=1, ack=1 0 A 回 ACK,确认 B 的 SYN,三次握手完成,进入 ESTABLISHED
4 192.168.31.190:9090 → 192.168.91.129:37790 [P.] seq=1:787, ack=1 786 B 发送数据 786 字节,PSH 表示尽快交付应用
5 192.168.91.129:37790 → 192.168.31.190:9090 [.] seq=1, ack=787 0 A 确认收到 B 的 786 字节,累计确认 ack=787
6 192.168.31.190:9090 → 192.168.91.129:37790 [FP.] seq=787, ack=1 0 B 发送 FIN+PSH,表示数据发送完毕并请求关闭
7 192.168.91.129:37790 → 192.168.31.190:9090 [F.] seq=1, ack=788 0 A 回 ACK 并发送 FIN,表示自己也准备关闭
8 192.168.31.190:9090 → 192.168.91.129:37790 [.] seq=788, ack=2 0 B 确认 A 的 FIN,连接完全关闭

发送窗口(Send Window)

  • snd_una:最早未确认的字节序号。
  • snd_nxt:下一个要发送的字节序号。
  • snd_max:历史上发出的最大序号。
  • snd_wnd:对端通告的接收窗口大小。

可发送量 = min(snd_wnd, snd_cwnd) - (snd_nxt - snd_una)

抓包对应:

  • SYN 阶段:
    • A 发 SYN(seq=3651959169),此时 snd_una=3651959169snd_nxt=3651959170
  • 数据传输:
    • B 发数据段(seq=1:787,len=786),A 收到后回 ACK=787。
    • 对 A 来说:snd_una 随着 B 的 ACK 推进;对 B 来说:snd_una 从 1 → 787,snd_nxt 从 1 → 787+len。

接收窗口(Receive Window)

  • rcv_nxt:期望的下一个字节序号。
  • rcv_adv:应用层读取数据后,窗口右边界前移。
  • rcv_wnd = rcv_adv - rcv_nxt:当前可用接收窗口大小。

作用:流量控制,防止发送方淹没接收方。

抓包对应:

  • B 发数据:
    • 报文 seq=1:787,A 收到后更新 rcv_nxt=787
    • A 回 ACK=787,表示“我期望的下一个字节是 787”。
  • A 的 ACK 报文:
    • ack=787 就是 A 的 rcv_nxt
  • 窗口通告:
    • 报文里 win 64240,这是接收方通告的 rcv_wnd
    • 当应用层读取数据后,rcv_adv 前移,窗口会回升。

拥塞控制(Congestion Control)

  • cwnd:拥塞窗口,限制发送方在网络中未确认的数据量。
  • ssthresh:慢启动阈值,决定 cwnd 增长模式。
  • 实际可发送量 = min(rwnd, cwnd)

抓包对应:

  • B 初始发送:
    • cwnd 初始为 1 MSS,B 先发一个数据段(786 字节)。
  • A 回 ACK=787:
    • B 的 cwnd 增长,可以继续发送更多数据。
  • 若发生丢包:
    • B 会下调 ssthresh,并收缩 cwnd。
  • 在这次抓包中没有丢包,cwnd 会逐步放大。

小结

  • 发送窗口:在报文中体现为 seq 的推进和对端 ack 的确认。
  • 接收窗口:在报文中体现为 ack 的数值和 win 通告的大小。
  • 拥塞控制:在报文中看不到 cwnd 的具体值,但能通过发送行为(一次发多少数据、是否停顿)推测。

示例

  1. 三次握手
    • A 发 SYN (ISN=3651959169),B 回 SYN+ACK (ISN=1067742738, ack=3651959170),A 回 ACK (ack=1067742739)。
    • 双方进入 ESTABLISHED。
  2. 数据传输
    • B 发了 786 字节数据(seq=1:787),A 回 ACK=787。
    • 注意这里的 seq=1 是因为 tcpdump 默认把初始序号归一化显示为 0/1,方便阅读。
  3. 四次挥手
    • B 先发 FIN(seq=787, ack=1),进入 FIN-WAIT-1。
    • A 回 ACK=788,并同时发 FIN(seq=1, ack=788)。
    • B 回 ACK=2,确认 A 的 FIN,连接关闭。

A.th_ack 是 A 对 B 的数据流的累计确认。单向场景中,B 不发数据,因此 A.th_ack 固定不变。

B.th_seq同样保持,不随时间变化,除非 B 开始发送数据(此时才会随 B 的发送推进)。

例如,我们可以随便抓一个包:

整理

时间戳 源 → 目的 Flags Seq 区间 Ack Win Len
07:44:29.219344 api.snapcraft.io.https → ubuntu.47266 [P.] 28193:41153 8363 64240 12960
07:44:29.219376 ubuntu.47266 → api.snapcraft.io.https [.] 41153 55480 0
07:44:29.410105 api.snapcraft.io.https → ubuntu.47266 [P.] 41153:45473 8363 64240 4320
07:44:29.410169 ubuntu.47266 → api.snapcraft.io.https [.] 45473 61320 0
07:44:29.412027 api.snapcraft.io.https → ubuntu.47266 [P.] 45473:59873 8363 64240 14400
07:44:29.412047 ubuntu.47266 → api.snapcraft.io.https [.] 59873 55480 0
07:44:29.467687 api.snapcraft.io.https → ubuntu.47266 [P.] 59873:67073 8363 64240 7200
07:44:29.467729 ubuntu.47266 → api.snapcraft.io.https [.] 67073 59860 0

可以看出,其实发送方的ACK没什么变化,接收方的seq也同理,当然,因为不重要,所以抓包直接不显示接收方的seq。

伪代码参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
void tcp_input()
{
检查校验和
寻找对应的PCB控制块
重置idle time为0,设置保活keepalive时间为2小时,这是协议规范,可以参考上面的文档
这里可以使用首部预测算法简化TCP处理流程,由于是FreeBSD操作系统实现的机制,在后面的源码讲解环节再详细展开
switch(tp->state) {
case TCPS_LISTEN:
如果SYN没有设置,接收新的连接请求
可以跳到后面的更新窗口信息的代码处了
case TCPS_SYN_SENT:
如果对方确认了我们发出的 SYN,那么连接就建立完成了
可以跳到后面的更新窗口信息的代码处了
}
遵循RFC 1323的处理(后面再补充)
如果数据部分超出了接收端当前通告的接收窗口(RCV.NXT 到 RCV.NXT + RCV.WND - 1 之间的范围),那么接收端必须丢弃(裁剪掉, trim)那些不在窗口范围内的数据字节,只保留窗口范围内的数据进行处理
if (RST flag set) {
处理依赖于状态
丢弃该包
}
if (ACK flag set) {
if (是SYN_RCVD状态)
双方几乎同时主动发起连接请求(SYN)并且完成握手
if (重复 ACK) {
启用快速恢复算法
我们知道如果是三次重复ACK会触发重传,但是在FreeBSD遵守的RFC 793中并没有这一条,这是在后面的RFC文档才规定的
(但是,最新进展(RACK, RFC 8985,RACK(Recent ACKnowledgment)算法基于时间和确认信息来判断丢包,也就是说, 三次重复ACK会触发重传这一条已经不适用了)
}
如果某个报文段(segment)是被用来测量往返时间 (RTT) 的,就用它的 ACK 来更新 RTT 估计器
根据收到的 ACK 更新拥塞窗口 (cwnd),具体增长方式取决于算法:慢启动:cwnd指数增长,拥塞避免:cwnd线性增长
把已经被确认的数据从发送缓冲区中移除
如果当前连接状态是 FIN-WAIT-1、CLOSING 或 LAST-ACK,就根据 ACK 的含义改变状态
}
更新窗口信息
处理URG标志。但是,现代应用几乎不用URG,因为它的语义模糊、实现差异大,很多防火墙甚至把带URG的段当作可疑流量
处理段中的数据,当一个 TCP 段到达时,如果它携带了应用层数据(payload),接收端需要检查它的序列号范围是否在接收窗口内。如果合 法,就把这部分数据交给 TCP 的接收逻辑,但不能马上交付给应用层,因为 TCP 必须保证 按序交付。
if (FIN flag is set) {
process depending on state
}
/*如果套接字开启了 SO_DEBUG 选项,就调用 tcp_trace() 记录调试信息*/
if (SO_DEBUG socket option) {
tcp_trace(TA_INPUT)
}
/*如果需要立即发送输出(例如确认 ACK、窗口更新、重传等),就调用 tcp_output() 发送*/
if (need output || ACK now) {
tcp_output()
}
return;
dropafterack:
先发一个 ACK(通常是对非法段的回应),然后调用 tcp_output() 生成一个 RST,再返回。
tcp_output() to generate RST
return;

dropwithreset:
直接调用 tcp_respond() 生成一个 RST 段作为回应,然后返回
tcp_respond() to generate RST
return;

drop:
if (SO_DEBUG socket option)
tcp_trace(TA_DROP);
return;
}

定时器

这里简单介绍一下TCP定时器的原理,方便后面理解RTT、保活机制相关的代码。

详细的定时器类型、设置、触发等等,笔者后面会单独写几篇文章。

  1. TCP 定时器数组

    • 在 BSD 风格的 TCP 实现中,每个连接的 struct tcpcb里有一个定时器数组:

      1
      tp->t_timer[TCPT_NTIMERS];
  2. 定时器调度机制

    • 内核有一个周期性运行的 **tcp_slowtimo()**(通常每 500ms 调用一次)。
    • 它会遍历所有 TCP 控制块,把每个非零定时器减一。
    • tp->t_timer[TCPT_NTIMERS] 的值递减到 0 时,就调用对应的函数来处理事件。

所以TCP协议栈中,往往会有设置对应的定时器的值的代码,通过这种方式,我们可以设置对应的函数处理时间。

定时器类型

定时器又有周期性的和一次性触发的。

在 TCP 协议栈中,周期性触发的函数主要是全局的定时器调度器(如 tcp_slowtimo()、tcp_fasttimo()),它们每隔固定时间运行一次,递减各连接的定时器。一次性触发的函数则是具体的定时器处理函数(如重传、延迟 ACK、保活等),它们只在对应的定时器归零时被调用。

TCP 每个连接有 4 个核心定时器,存在 tp->t_timer[] 里,对应的宏是:

  • 重传定时器TCPT_REXMT —— 数据超时重传。
  • 坚持定时器TCPT_PERSIST —— 对端窗口为 0 时,周期性探测。
  • 保活定时器TCPT_KEEP —— 长时间空闲时探测对端是否存活。
  • 2MSL 定时器TCPT_2MSL —— TIME_WAIT 状态保持两倍 MSL。

此外还有 延迟 ACKTF_DELACK 标志配合快速定时器处理,不在 t_timer[] 数组里。

RFC文档指导

3.10.7. 段到达(SEGMENT ARRIVES)

3.10.7.1. CLOSED 状态

如果状态为 CLOSED(即 TCB 不存在),则:

  • 丢弃传入段中的所有数据。

  • 如果传入段包含 RST,则丢弃该段。

  • 如果传入段 不包含 RST,则发送一个 RST 作为响应。

    • 确认号和序列号字段的值应选择为,使得该复位序列对发送该错误段的 TCP 端点是可接受的。
  • 如果 ACK 位关闭,则使用序列号零:

    1
    <SEQ=0><ACK=SEG.SEQ+SEG.LEN><CTL=RST,ACK>
  • 如果 ACK 位开启:

    1
    <SEQ=SEG.ACK><CTL=RST>

然后返回。

3.10.7.2. LISTEN 状态

如果状态为 LISTEN,则:

第一步,检查 RST:

  • 传入的 RST 段不可能有效,因为它不可能是对该连接实例所发送内容的响应。
  • 因此,传入的 RST 应被忽略。返回。

第二步,检查 ACK:

  • 在 LISTEN 状态下,任何到达的确认都是错误的。

  • 对于任何带有 ACK 的传入段,应形成一个复位段作为响应,其格式为:

    1
    <SEQ=SEG.ACK><CTL=RST>

然后返回。

第三步,检查 SYN:

  • 如果 SYN 位被设置,则检查安全属性。

  • 如果传入段中的 security/compartment 与 TCB 中的不完全匹配,则发送复位并返回:

    1
    <SEQ=0><ACK=SEG.SEQ+SEG.LEN><CTL=RST,ACK>
  • 否则:

    • 设置 RCV.NXT = SEG.SEQ + 1IRS = SEG.SEQ

    • 其他控制或数据应排队,稍后处理。

    • 选择一个 ISS,并发送如下格式的 SYN 段:

      1
      <SEQ=ISS><ACK=RCV.NXT><CTL=SYN,ACK>
    • 设置 SND.NXT = ISS + 1SND.UNA = ISS

    • 将连接状态改为 SYN-RECEIVED

    • 注意:任何与 SYN 一同到达的控制或数据将在 SYN-RECEIVED 状态下处理,但 SYN 和 ACK 的处理不应重复。

    • 如果 LISTEN 未完全指定(即远程套接字未完全指定),则现在应填写未指定的字段。

第四步,其他数据或控制:

  • 不应到达此处。丢弃该段并返回。
  • 任何其他不包含 SYN 的控制或数据段必须带有 ACK,因此会在第二步的 ACK 检查中被丢弃,除非它已在第一步的 RST 检查中被丢弃。

3.10.7.3. SYN-SENT 状态

如果当前状态为 SYN-SENT,则:

第一步,检查 ACK 位:

  • 如果 ACK 位被设置:

    • 如果 SEG.ACK <= ISSSEG.ACK > SND.NXT,则发送一个复位(除非 RST 位已设置,如果是,则丢弃该段并返回):

      1
      <SEQ=SEG.ACK><CTL=RST>

      然后丢弃该段并返回。

    • 如果 SND.UNA < SEG.ACK <= SND.NXT,则该 ACK 是可接受的。

      注意:一些已部署的 TCP 代码使用了 SEG.ACK == SND.NXT(使用“==”而不是“<=”)的检查,但当协议栈支持在 SYN 上携带数据时,这种做法是不合适的,因为对端可能不会接受并确认 SYN 上的所有数据。

第二步,检查 RST 位:

  • 如果 RST 位被设置:
    • RFC 5961 [9] 描述了一种潜在的 盲复位攻击。该文档中提出的缓解措施有特定适用性,但不能替代加密保护(例如 IPsec 或 TCP-AO)。
    • 支持 RFC 5961 缓解措施的 TCP 实现 应当 在执行下一步之前,首先检查序列号是否与 RCV.NXT 完全匹配。
    • 如果 ACK 是可接受的,则向用户发出 “错误:连接重置” 的信号,丢弃该段,进入 CLOSED 状态,删除 TCB,然后返回。
    • 否则(没有 ACK),丢弃该段并返回。

第三步,检查安全性:

  • 如果段中的 security/compartment 与 TCB 中的不完全匹配,则发送复位:

    • 如果有 ACK:

      1
      <SEQ=SEG.ACK><CTL=RST>
    • 否则:

      1
      <SEQ=0><ACK=SEG.SEQ+SEG.LEN><CTL=RST,ACK>
  • 如果发送了复位,则丢弃该段并返回。

第四步,检查 SYN 位:

  • 仅当 ACK 合法,或没有 ACK,且该段不包含 RST 时,才会执行此步骤。

  • 如果 SYN 位被设置,且安全/隔间检查通过:

    • 设置 RCV.NXT = SEG.SEQ + 1IRS = SEG.SEQ
    • 如果有 ACK,则将 SND.UNA 推进到等于 SEG.ACK,并删除重传队列中已被确认的段。
  • 如果 SND.UNA > ISS(即我们的 SYN 已被确认):

    • 将连接状态改为 ESTABLISHED,形成一个 ACK 段:

      1
      <SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>
    • 并发送它。已排队的数据或控制信息 可以 一并发送。

    • 一些 TCP 实现会在接收段已包含数据时抑制发送该 ACK,因为后续处理步骤无论如何都会生成 ACK,从而避免额外的 SYN 确认。

    • 如果该段中还有其他控制或数据,则继续执行 3.10.7.4 第六步(检查 URG 位);否则返回。

  • 否则,进入 SYN-RECEIVED 状态,形成一个 SYN+ACK 段:

    1
    <SEQ=ISS><ACK=RCV.NXT><CTL=SYN,ACK>

    并发送它。设置以下变量:

    1
    2
    3
    SND.WND <- SEG.WND
    SND.WL1 <- SEG.SEQ
    SND.WL2 <- SEG.ACK

    如果该段中还有其他控制或数据,则将其排队,待进入 ESTABLISHED 状态后再处理,然后返回。

注意:在 SYN 段上发送和接收应用数据是合法的(即上文提到的“段中的文本”)。历史上对此存在大量误解和错误信息。一些防火墙和安全设备会将其视为可疑。然而,该能力曾用于 **T/TCP [21]**,并被 TCP Fast Open (TFO) [48] 使用,因此实现和网络设备必须允许。

第五步,如果 SYN 和 RST 位均未设置,则丢弃该段并返回。

3.10.7.4. 其他状态

否则,

第一步,检查序列号:

适用状态:

  • SYN-RECEIVED
  • ESTABLISHED
  • FIN-WAIT-1
  • FIN-WAIT-2
  • CLOSE-WAIT
  • CLOSING
  • LAST-ACK
  • TIME-WAIT

段按序处理。到达时的初始检查用于丢弃旧的重复段,但进一步处理是按照 SEG.SEQ 顺序进行的。
如果一个段的内容跨越了“旧数据”和“新数据”的边界,则只处理其中的新部分。

通常,接收段的处理 必须 尽可能聚合 ACK 段(MUST-58)。例如,当 TCP 端点正在处理一系列排队的段时,它 必须 在发送任何 ACK 段之前先处理完所有这些段(MUST-59)。

段可接受性测试有四种情况:

表 6:段可接受性测试

段长度 接收窗口 测试条件
0 0 SEG.SEQ = RCV.NXT
0 >0 RCV.NXT <= SEG.SEQ < RCV.NXT+RCV.WND
>0 0 不可接受
>0 >0 RCV.NXT <= SEG.SEQ < RCV.NXT+RCV.WND RCV.NXT <= SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND

实现序列号验证时,请注意附录 A.2。

如果 RCV.WND=0,则没有段是可接受的,但应作特殊处理以接受合法的 ACK、URG 和 RST。

如果传入段不可接受,应发送一个确认作为回复(除非该段带有 RST 位,此时应丢弃并返回):

1
<SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>

发送确认后,丢弃该不可接受段并返回。

注意:对于 TIME-WAIT 状态,有一种改进算法(见 [40]),使用时间戳来处理传入的 SYN 段,而不是依赖这里描述的序列号检查。如果实现了该改进算法,则上述逻辑不适用于在 TIME-WAIT 状态下接收到带时间戳选项的 SYN 段。

在以下描述中,假设段是理想化的,即它从 RCV.NXT 开始且不超过窗口。实际段可以通过裁剪窗口外的部分(包括 SYN 和 FIN)来满足这一假设,并且只有当段从 RCV.NXT 开始时才继续处理。序列号更高的段 应当 保留以便稍后处理 (SHLD-31)。

第二步,检查 RST 位:

RFC 5961 第 3 节描述了一种潜在的盲重置攻击及可选的缓解方法。这不是加密保护(如 IPsec 或 TCP-AO),但在 RFC 5961 描述的场景中可适用。对于实现了 RFC 5961 所述保护的协议栈,以下三条检查适用;否则,处理逻辑见后续描述。

  1. 如果 RST 位被设置,且序列号在当前接收窗口之外 → 静默丢弃该段。

  2. 如果 RST 位被设置,且序列号 正好等于 下一个期望的序列号 (RCV.NXT) → TCP 端点 必须 按照下述方式根据连接状态复位连接。

  3. 如果 RST 位被设置,且序列号在当前接收窗口内,但 不等于 RCV.NXT → TCP 端点 必须 发送一个确认(质询 ACK):

    1
    <SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>

    发送质询 ACK 后,TCP 端点 必须 丢弃该不可接受段,并停止进一步处理该报文。注意:RFC 5961 和勘误 ID 4772 [99] 对 ACK 限速有额外考虑。

各状态下的 RST 处理

SYN-RECEIVED 状态

  • 如果 RST 位被设置:
    • 如果连接是由 被动打开(来自 LISTEN 状态)发起的 → 返回 LISTEN 状态并返回,用户无需通知。
    • 如果连接是由 主动打开(来自 SYN-SENT 状态)发起的 → 连接被拒绝;通知用户“连接被拒绝”。
    • 在两种情况下,都应清空重传队列。
    • 在主动打开的情况下,还应进入 CLOSED 状态并删除 TCB,然后返回。

ESTABLISHED、FIN-WAIT-1、FIN-WAIT-2、CLOSE-WAIT 状态

  • 如果 RST 位被设置:
    • 所有未完成的接收和发送操作应收到“复位”响应。
    • 所有段队列应清空。
    • 用户应收到一个非请求的通用“连接复位”信号。
    • 进入 CLOSED 状态,删除 TCB,并返回。

CLOSING、LAST-ACK、TIME-WAIT 状态

  • 如果 RST 位被设置:
    • 进入 CLOSED 状态,删除 TCB,并返回。

第三步,检查安全性:

  • ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2 / CLOSE-WAIT / CLOSING / LAST-ACK / TIME-WAIT 状态

    如果段中的

    security/compartment

    与 TCB 中的不完全匹配,则发送复位;所有未完成的 RECEIVE 和 SEND 应收到“reset”响应。所有段队列应被清空。用户还应收到一个未经请求的一般性“connection reset”信号。进入

    CLOSED

    状态,删除 TCB,然后返回。

    注意:此检查放在序列号检查之后,以防止来自旧连接(相同端口号但不同安全属性)的段导致当前连接被错误中止。

第四步,检查 SYN 位:

  • SYN-RECEIVED 状态
    如果连接是通过被动 OPEN 建立的,则将该连接返回到 LISTEN 状态并返回。否则,按下文同步状态的规则处理。

  • ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2 / CLOSE-WAIT / CLOSING / LAST-ACK / TIME-WAIT 状态
    如果在这些同步状态下 SYN 位被设置,它可能表示:

    • 一个合法的新连接尝试(例如 TIME-WAIT 状态下),

    • 一个应当复位的错误,

    • 或一次攻击尝试(RFC 5961 [9] 所述)。

    • 对于 TIME-WAIT 状态,如果使用了时间戳选项并符合预期(见 [40]),则可以接受新连接。

    • 对于其他情况,RFC 5961 建议:在这些同步状态下,如果 SYN 位被设置,不论序列号如何,TCP 端点 必须 向对端发送一个“质询 ACK”:

      1
      <SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>

      发送确认后,TCP 实现必须丢弃该段并停止进一步处理。

      注意:RFC 5961 和勘误 ID 4772 [99] 对 ACK 限速有额外说明。

    • 如果未实现 RFC 5961,则遵循 RFC 793 的原始行为:

      • 如果 SYN 在窗口内,这是错误:发送复位,所有未完成的 RECEIVE 和 SEND 返回“reset”,清空队列,用户收到“connection reset”,进入 CLOSED,删除 TCB,返回。
      • 如果 SYN 不在窗口内,则不会到达此步骤,而会在第一步(序列号检查)中发送 ACK。

第五步,检查 ACK 字段:

  • 如果 ACK 位关闭 → 丢弃该段并返回。

  • 如果 ACK 位开启:

    • RFC 5961 [9] 第 5 节描述了潜在的 盲数据注入攻击,并给出可选缓解措施(MAY-12)。

    • 实现 RFC 5961 的 TCP 栈必须检查 ACK 值是否在以下范围内:

      1
      (SND.UNA - MAX.SND.WND) <= SEG.ACK <= SND.NXT

      不满足条件的段必须被丢弃,并发送一个 ACK。

      • MAX.SND.WND 定义为本端曾从对端接收的最大窗口(考虑窗口缩放),或可硬编码为允许的最大窗口值。
    • 当 ACK 值可接受时,按状态处理:

      • SYN-RECEIVED

        • 如果

          1
          SND.UNA < SEG.ACK <= SND.NXT

          则进入ESTABLISHED,并设置:

          1
          2
          3
          SND.WND <- SEG.WND
          SND.WL1 <- SEG.SEQ
          SND.WL2 <- SEG.ACK
        • 如果 ACK 不可接受 → 发送复位:

          1
          <SEQ=SEG.ACK><CTL=RST>
      • ESTABLISHED

        • 如果 SND.UNA < SEG.ACK <= SND.NXT,则更新 SND.UNA <- SEG.ACK,并删除重传队列中已确认的段。用户应收到对已发送并完全确认缓冲区的“ok”响应。

        • 如果 ACK 是重复的(SEG.ACK <= SND.UNA),忽略。

        • 如果 ACK 确认了尚未发送的数据(SEG.ACK > SND.NXT),则发送 ACK,丢弃该段并返回。

        • 如果

          1
          SND.UNA <= SEG.ACK <= SND.NXT

          则更新发送窗口:

          • 1
            (SND.WL1 < SEG.SEQ)

            1
            (SND.WL1 = SEG.SEQ 且 SND.WL2 <= SEG.ACK)

            则:

            1
            2
            3
            SND.WND <- SEG.WND
            SND.WL1 <- SEG.SEQ
            SND.WL2 <- SEG.ACK
          • 注意:SND.WND 是相对于 SND.UNA 的偏移量;SND.WL1 记录上次更新窗口的段序列号;SND.WL2 记录上次更新窗口的确认号。此检查防止旧段更新窗口。

      • FIN-WAIT-1

        • 除了 ESTABLISHED 的处理外,如果 FIN 已被确认 → 进入 FIN-WAIT-2
      • FIN-WAIT-2

        • 除了 ESTABLISHED 的处理外,如果重传队列为空 → 用户的 CLOSE 可被确认“ok”,但不删除 TCB。
      • CLOSE-WAIT

        • 与 ESTABLISHED 相同。
      • CLOSING

        • 除了 ESTABLISHED 的处理外,如果 ACK 确认了我们的 FIN → 进入 TIME-WAIT;否则忽略该段。
      • LAST-ACK

        • 唯一可能到达的是对我们 FIN 的确认。如果 FIN 被确认 → 删除 TCB,进入 CLOSED。
      • TIME-WAIT

        • 唯一可能到达的是远端 FIN 的重传。确认它,并重启 2MSL 定时器。

第六步,检查 URG 位:

  • ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2
    • 如果 URG 位被设置:
      • RCV.UP <- max(RCV.UP, SEG.UP)
      • 如果紧急指针在已消费数据之前,则通知用户远端有紧急数据。
      • 如果用户已被通知(或仍处于“紧急模式”),则不再重复通知。
  • CLOSE-WAIT / CLOSING / LAST-ACK / TIME-WAIT
    • 不应出现(因为远端已发送 FIN),忽略 URG。

第七步,处理段数据:

  • ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2

    • 可以将段数据交付到用户接收缓冲区,直到缓冲区满或段耗尽。

    • 如果段耗尽且带有 PUSH 标志 → 当缓冲区返回时通知用户“PUSH 已收到”。

    • 一旦 TCP 端点负责交付数据 → 必须确认接收。

    • RCV.NXT 前进,RCV.WND 根据缓冲区可用性调整。RCV.NXT + RCV.WND 总和不得减少。

    • TCP 实现 可以 在段位于窗口内但不在左边界时发送 ACK(MAY-13)。

    • 确认格式:

      1
      <SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>

      应尽可能搭载在传输段上,避免额外延迟。

  • CLOSE-WAIT / CLOSING / LAST-ACK / TIME-WAIT

    • 不应出现(远端已发送 FIN),忽略数据。

第八步,检查 FIN 位:

  • SYN-RECEIVED / ESTABLISHED
    • 如果收到 FIN,则进入 CLOSE-WAIT 状态。
  • FIN-WAIT-1
    • 如果我们的 FIN 已被确认(可能在此段中),则进入 TIME-WAIT 状态,启动 time-wait 定时器,并关闭其他定时器;
    • 否则,进入 CLOSING 状态。
  • FIN-WAIT-2
    • 进入 TIME-WAIT 状态,启动 time-wait 定时器,并关闭其他定时器。
  • CLOSE-WAIT
    • 保持在 CLOSE-WAIT 状态。
  • CLOSING
    • 保持在 CLOSING 状态。
  • LAST-ACK
    • 保持在 LAST-ACK 状态。
  • TIME-WAIT
    • 保持在 TIME-WAIT 状态,并重新启动 2MSL 的 time-wait 超时。

然后返回。

3.10.8 超时

  • 用户超时
    • 刷新队列,通知“错误:用户超时中止连接”。
    • 删除 TCB,进入 CLOSED。
  • 重传超时
    • 重传队列中的段,重置定时器。
  • TIME-WAIT 超时
    • 删除 TCB,进入 CLOSED。

代码

下面是函数开头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

/*
* TCP input routine, follows pages 65-76 of the
* protocol specification dated September, 1981 very closely.
*/
void
tcp_input(m, iphlen)
register struct mbuf *m;
int iphlen;
{
register struct tcpiphdr *ti;
register struct inpcb *inp;
caddr_t optp = NULL;
int optlen;
int len, tlen, off;
register struct tcpcb *tp = 0;
register int tiflags;
struct socket *so;
int todrop, acked, ourfinisacked, needoutput = 0;
short ostate;
struct in_addr laddr;
int dropsocket = 0;
int iss = 0;
u_long tiwin, ts_val, ts_ecr;
int ts_present = 0;

tcpstat.tcps_rcvtotal++;
/*
* Get IP and TCP header together in first mbuf.
* Note: IP leaves IP header in first mbuf.
*/

预处理并检查校验和

这部分没什么好讲的,主要是验证数据,然后计算得到纯TCP数据包的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

/*
* Get IP and TCP header together in first mbuf.
* Note: IP leaves IP header in first mbuf.
*/
ti = mtod(m, struct tcpiphdr *);
if (iphlen > sizeof (struct ip))
ip_stripoptions(m, (struct mbuf *)0);
if (m->m_len < sizeof (struct tcpiphdr)) {
if ((m = m_pullup(m, sizeof (struct tcpiphdr))) == 0) {
tcpstat.tcps_rcvshort++;
return;
}
ti = mtod(m, struct tcpiphdr *);
}

/*
* Checksum extended TCP header and data.
*/
tlen = ((struct ip *)ti)->ip_len;
len = sizeof (struct ip) + tlen;
ti->ti_next = ti->ti_prev = 0;
ti->ti_x1 = 0;
ti->ti_len = (u_short)tlen;
HTONS(ti->ti_len);
if (ti->ti_sum = in_cksum(m, len)) {
tcpstat.tcps_rcvbadsum++;
goto drop;
}
#endif /* TUBA_INCLUDE */

处理特定TCP选项

如果首部长度大于20,那么说明存在TCP选项,这里主要是调整数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

/*
* Check that TCP offset makes sense,
* pull out TCP options and adjust length. XXX
*/
off = ti->ti_off << 2;
if (off < sizeof (struct tcphdr) || off > tlen) {
tcpstat.tcps_rcvbadoff++;
goto drop;
}
tlen -= off;
ti->ti_len = tlen;
if (off > sizeof (struct tcphdr)) {
if (m->m_len < sizeof(struct ip) + off) {
if ((m = m_pullup(m, sizeof (struct ip) + off)) == 0) {
tcpstat.tcps_rcvshort++;
return;
}
ti = mtod(m, struct tcpiphdr *);
}

处理时间戳

TCP 的 Timestamp 选项最早在 RFC 1323 中引入,用来解决两大问题:
1.精确测量往返时延(RTT),即发送方在发送段时记录一条时间戳(TSval),接收方在应答段中将该时间戳回显(TSecr),发送方据此计算 RTT,即使发生重传也能获得准确结果。
2.防止序号回绕,当序号空间很大且链路带宽×时延乘积很大时,通过时间戳可以鉴别过时或重复到达的报文段,这在RFC文档中提到过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
	optlen = off - sizeof (struct tcphdr);
optp = mtod(m, caddr_t) + sizeof (struct tcpiphdr);
/*
* Do quick retrieval of timestamp options ("options
* prediction?"). If timestamp is the only option and it's
* formatted as recommended in RFC 1323 appendix A, we
* quickly get the values now and not bother calling
* tcp_dooptions(), etc.
*/
if ((optlen == TCPOLEN_TSTAMP_APPA ||
(optlen > TCPOLEN_TSTAMP_APPA &&
optp[TCPOLEN_TSTAMP_APPA] == TCPOPT_EOL)) &&
*(u_long *)optp == htonl(TCPOPT_TSTAMP_HDR) &&
(ti->ti_flags & TH_SYN) == 0) {
ts_present = 1;
ts_val = ntohl(*(u_long *)(optp + 4));
ts_ecr = ntohl(*(u_long *)(optp + 8));
optp = NULL; /* we've parsed the options */
}
}

1.先计算 optlen 并定位 optp,检查这段区间是否恰好只包含一个格式符合 RFC 1323 附录 A 推荐的 Timestamp 选项。

2.如果满足条件且本段不是 SYN 握手包(ti->ti_flags & TH_SYN == 0),就直接从 optp 中读取 TSval 和 TSecr,并将 optp 置空,跳过后续通用选项解析。

3.设置 ts_present = 1、ts_val、ts_ecr,后面 tcp 栈的代码会看到 ts_present,把这两个值交给 RTT 估计模块(如 tcp_xmit_timer() 或 tcp_rtt())进行更新,同时在后续发送的 ACK 或数据包中,将本次接收到的 TSval 填到 TSecr 字段,以完成回显。

字段及函数解释

  • optlen:TCP 头部中 Options 字段的长度(字节数)。

  • optp:指向 Options 区域的指针,用来逐个解析选项。

  • TSval (Timestamp Value):发送方在报文中写入的当前时间戳值。

    • 用于 RTT 测量:接收方在 ACK 中回显它,发送方就能计算往返时间。
    • 用于 PAWS(Protect Against Wrapped Sequence numbers):防止序列号回绕导致的旧包被误接收。
  • TSecr (Timestamp Echo Reply):回显字段。

    • 当一端收到对方的 TSval 时,会在自己发出的报文里把该值填入 TSecr。
    • 这样对方就能知道“我发出的 TSval 在什么时候被对端收到了”。
  • ts_present:一个布尔标志,表示当前报文是否携带了合法的时间戳选项。如果为 1,说明 ts_valts_ecr 有效,后续 TCP 栈(RTT 模块、ACK 生成逻辑)会使用它们。

  • ts_val:从报文中解析出来的 TSval 值。

  • ts_ecr:从报文中解析出来的 TSecr 值。

  • 它们会被保存到 TCP 控制块中,供 RTT 估计和 ACK 回显使用。

tcp_xmit_timer() / tcp_rtt()

  • 这些是 TCP 内部的 RTT 估计函数。
  • ts_present=1 时,它们会用 ts_val/ts_ecr 来更新 RTT 平滑估计值,从而调整重传超时 (RTO)。

寻找InPCB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

/*
* Locate pcb for segment.
*/
findpcb:
inp = tcp_last_inpcb;
if (inp->inp_lport != ti->ti_dport ||
inp->inp_fport != ti->ti_sport ||
inp->inp_faddr.s_addr != ti->ti_src.s_addr ||
inp->inp_laddr.s_addr != ti->ti_dst.s_addr) {
inp = in_pcblookup(&tcb, ti->ti_src, ti->ti_sport,
ti->ti_dst, ti->ti_dport, INPLOOKUP_WILDCARD);
if (inp)
tcp_last_inpcb = inp;
++tcpstat.tcps_pcbcachemiss;
}
}

通告窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
so = inp->inp_socket;
if (so->so_options & (SO_DEBUG|SO_ACCEPTCONN)) {
if (so->so_options & SO_DEBUG) {
ostate = tp->t_state;
tcp_saveti = *ti;
}
if (so->so_options & SO_ACCEPTCONN) {
so = sonewconn(so, 0);
if (so == 0)
goto drop;
/*
* This is ugly, but ....
*
* Mark socket as temporary until we're
* committed to keeping it. The code at
* ``drop'' and ``dropwithreset'' check the
* flag dropsocket to see if the temporary
* socket created here should be discarded.
* We mark the socket as discardable until
* we're committed to it below in TCPS_LISTEN.
*/
dropsocket++;
inp = (struct inpcb *)so->so_pcb;
inp->inp_laddr = ti->ti_dst;
inp->inp_lport = ti->ti_dport;
#if BSD>=43
inp->inp_options = ip_srcroute();
#endif
tp = intotcpcb(inp);
tp->t_state = TCPS_LISTEN;

/* Compute proper scaling value from buffer space
*/
while (tp->request_r_scale < TCP_MAX_WINSHIFT &&
TCP_MAXWIN << tp->request_r_scale < so->so_rcv.sb_hiwat)
tp->request_r_scale++;
}
}
/*
* Segment received on connection.
* Reset idle time and keep-alive timer.
*/
tp->t_idle = 0;
tp->t_timer[TCPT_KEEP] = tcp_keepidle;

/*
* Process options if not in LISTEN state,
* else do it below (after getting remote address).
*/
if (optp && tp->t_state != TCPS_LISTEN)
tcp_dooptions(tp, optp, optlen, ti,
&ts_present, &ts_val, &ts_ecr);

1.tp->request_r_scale 就是本端在握手时要通告给对端的窗口缩放因子.

2.RFC 1323 引入了 窗口缩放选项 (Window Scale Option):在握手时双方协商一个 scale 值,表示报文头里的窗口值要左移 scale 位,实际窗口大小 = 报文头窗口值 << scale。
3.窗口缩放因子是在 三次握手的 SYN 报文中作为 TCP 选项传输的。主动方在 SYN 中携带 Window Scale 选项,被动方如果支持,会在 SYN+ACK 中回显自己的缩放值。最终,每个方向的缩放因子独立确定,并且只在握手时协商,一旦连接建立就固定下来。

处理TCP选项的函数tcp_dooptions我们后面再讲,它会处理五个选项:EOL, NOP, MSS, 窗口大小和时间戳。

首部预测

首部预测用于处理两种常见现象:

1.如果TCP发送数据,连接上等待接收的下一个报文段是对已经发送数据的ACK

2.如果TCP接收数据,连接上等待接收的下一个报文段是顺序到达的数据报文段

这种做法比通用处理快得多。

当然,由于我们的目的是TCP协议栈的整体实现,因此需要尽力摆脱操作系统的优化细节,所以简单介绍一下,我们的重点是通用处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

/*
* Header prediction: check for the two common cases
* of a uni-directional data xfer. If the packet has
* no control flags, is in-sequence, the window didn't
* change and we're not retransmitting, it's a
* candidate. If the length is zero and the ack moved
* forward, we're the sender side of the xfer. Just
* free the data acked & wake any higher level process
* that was blocked waiting for space. If the length
* is non-zero and the ack didn't move, we're the
* receiver side. If we're getting packets in-order
* (the reassembly queue is empty), add the data to
* the socket buffer and note that we need a delayed ack.
*/
if (tp->t_state == TCPS_ESTABLISHED &&
(tiflags & (TH_SYN|TH_FIN|TH_RST|TH_URG|TH_ACK)) == TH_ACK &&
(!ts_present || TSTMP_GEQ(ts_val, tp->ts_recent)) &&
ti->ti_seq == tp->rcv_nxt &&
tiwin && tiwin == tp->snd_wnd &&
tp->snd_nxt == tp->snd_max) {

/*
* If last ACK falls within this segment's sequence numbers,
* record the timestamp.
*/
if (ts_present && SEQ_LEQ(ti->ti_seq, tp->last_ack_sent) &&
SEQ_LT(tp->last_ack_sent, ti->ti_seq + ti->ti_len)) {
tp->ts_recent_age = tcp_now;
tp->ts_recent = ts_val;
}

if (ti->ti_len == 0) {
if (SEQ_GT(ti->ti_ack, tp->snd_una) &&
SEQ_LEQ(ti->ti_ack, tp->snd_max) &&
tp->snd_cwnd >= tp->snd_wnd) {
/*
* this is a pure ack for outstanding data.
*/
++tcpstat.tcps_predack;
if (ts_present)
tcp_xmit_timer(tp, tcp_now-ts_ecr+1);
else if (tp->t_rtt &&
SEQ_GT(ti->ti_ack, tp->t_rtseq))
tcp_xmit_timer(tp, tp->t_rtt);
acked = ti->ti_ack - tp->snd_una;
tcpstat.tcps_rcvackpack++;
tcpstat.tcps_rcvackbyte += acked;
sbdrop(&so->so_snd, acked);
tp->snd_una = ti->ti_ack;
m_freem(m);

/*
* If all outstanding data are acked, stop
* retransmit timer, otherwise restart timer
* using current (possibly backed-off) value.
* If process is waiting for space,
* wakeup/selwakeup/signal. If data
* are ready to send, let tcp_output
* decide between more output or persist.
*/
if (tp->snd_una == tp->snd_max)
tp->t_timer[TCPT_REXMT] = 0;
else if (tp->t_timer[TCPT_PERSIST] == 0)
tp->t_timer[TCPT_REXMT] = tp->t_rxtcur;

if (so->so_snd.sb_flags & SB_NOTIFY)
sowwakeup(so);
if (so->so_snd.sb_cc)
(void) tcp_output(tp);
return;
}
} else if (ti->ti_ack == tp->snd_una &&
tp->seg_next == (struct tcpiphdr *)tp &&
ti->ti_len <= sbspace(&so->so_rcv)) {
/*
* this is a pure, in-sequence data packet
* with nothing on the reassembly queue and
* we have enough buffer space to take it.
*/
++tcpstat.tcps_preddat;
tp->rcv_nxt += ti->ti_len;
tcpstat.tcps_rcvpack++;
tcpstat.tcps_rcvbyte += ti->ti_len;
/*
* Drop TCP, IP headers and TCP options then add data
* to socket buffer.
*/
m->m_data += sizeof(struct tcpiphdr)+off-sizeof(struct tcphdr);
m->m_len -= sizeof(struct tcpiphdr)+off-sizeof(struct tcphdr);
sbappend(&so->so_rcv, m);
sorwakeup(so);
tp->t_flags |= TF_DELACK;
return;
}
}

1.对于上面的两种情况,发送时,其实重点在于SEG.SEQ == RCV.NXT,也就是发送的下一个序列号等于接收的下一个序列号,那么其实我们大概率可以判断这个数据报就是接收方期待的数据报。当然,内核为了保证可靠,添加了多个判断。

2.如果报文不包含数据,那么就是ACK了,我们需要完成以下工作:

 - 更新RTT值
 - 从发送缓冲中删除被确认的字节
 - 终止重传定时器
 - 唤醒等待进程
 - 滑动窗口向右移动,继续输出

3.如果数据大于0,且字段这些正确,那么就是主机作为接收方期待的数据了,我们需要更新序列号、移动指针、唤醒并把数据递交给应用层。这里调用的函数就是sbappend() ,它是 BSD 套接字缓冲区(socket buffer)子系统中的一个内部函数,用来把新到的数据 mbuf 链接到 socket 的接收缓冲区。

注意,最后的代码启用了延迟ACK标志,这个我们后面再讲,并顺便讲讲TCP关于小包优化的算法,这些算是理论篇的东西。TCP太庞大了,东讲一点西讲一点,越讲越乱,这里我们只关注其框架。

通用处理这里我们一行行代码分析,讲详细一点:

通用处理

先进行一些准备工作,调整一下字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* Drop TCP, IP headers and TCP options.
*/
m->m_data += sizeof(struct tcpiphdr)+off-sizeof(struct tcphdr);
m->m_len -= sizeof(struct tcpiphdr)+off-sizeof(struct tcphdr);

/*
* Calculate amount of space in receive window,
* and then do TCP input processing.
* Receive window is amount of space in rcv queue,
* but not less than advertised window.
*/
{ int win;

win = sbspace(&so->so_rcv);
if (win < 0)
win = 0;
tp->rcv_wnd = max(win, (int)(tp->rcv_adv - tp->rcv_nxt));
}

1.调整一下指针及大小,我们直接跳过TCP首部,来到数据部分

2.调用sbspace,计算接收窗口,介绍一下sbspace

3.sbspace可能出现问题,所以当结果是负数的时候,需要修正为0

4.tcp协议是不允许缩小窗口的,这样会导致数据混乱,所以只接受一种情况,那就是win大于通告窗口时,接受窗口更新。

sbspace

1
2
3
4
5
6
7
8
9
/*
* How much space is there in a socket buffer (so->so_snd or so->so_rcv)?
* This is problematical if the fields are unsigned, as the space might
* still be negative (cc > hiwat or mbcnt > mbmax). Should detect
* overflow and return 0. Should use "lmin" but it doesn't exist now.
*/
#define sbspace(sb) \
((long) imin((int)((sb)->sb_hiwat - (sb)->sb_cc), \
(int)((sb)->sb_mbmax - (sb)->sb_mbcnt)))

该函数被用来计算当前 socket 缓冲区里还能放多少数据。它通常用于计算 so->so_snd(发送缓冲)或 so->so_rcv(接收缓冲)。

问题

我们必须要考虑一个问题,那就是内存限制,不仅仅是应用层的缓冲区可能内存空间不够,mbuf在高并发情况下,需要向内核申请内存,这种情况下内存可能也不够。

所以让我们看看字段:

字段

  • sb_hiwat:缓冲区的高水位(high-water mark),即逻辑上的最大字节数限制。
  • sb_cc:当前缓冲区里已经占用的字节数(character count)。
  • sb_mbmax:缓冲区允许占用的最大 mbuf 内存总量。
  • sb_mbcnt:当前缓冲区实际占用的 mbuf 内存总量。

代码

  1. 计算 字节数限制剩余空间sb_hiwat - sb_cc
    • 表示逻辑上还能再放多少字节。
  2. 计算 mbuf 内存限制剩余空间sb_mbmax - sb_mbcnt
    • 表示内存分配层面还能再放多少。
  3. 取两者的 最小值 (imin)。
    • 因为既要满足字节数限制,也要满足内存限制。
    • 任何一个限制先到达,就不能再放数据。
  4. 强制转换为 long,避免溢出。

注释

  • 如果 sb_cc > sb_hiwatsb_mbcnt > sb_mbmax,差值可能为负数。
  • 由于字段类型可能是无符号数,负数会变成大正数,导致错误。
  • 所以这里强制转成 int 再比较,确保结果正确
  • 注释里说“应该用 lmin,但现在没有”,意思是如果有 long 版本的 min 宏会更安全,但是还没有用

用途

  • 在 TCP 发送路径中,sbspace(so->so_snd) 用来判断还能写多少数据到发送缓冲。
  • 在接收路径中,sbspace(so->so_rcv) 用来判断还能收多少数据
  • 如果返回 0,说明缓冲区满了,TCP 就会阻塞应用写入或丢弃接收数据
  • 这也是 TCP 流量控制的底层实现之一:应用层缓冲区大小直接影响 TCP 通告窗口

状态处理

现在终于来到了状态处理,不知道大家还记不记得RFC文档和之前的伪代码了。

listen状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

switch (tp->t_state) {

/*
* 如果当前状态是 LISTEN,那么:
* - 如果报文段包含 RST,则忽略该段。
* - 如果报文段包含 ACK,则这是非法的,需要发送一个 RST。
* - 如果报文段不包含 SYN,则没有意义;直接丢弃。
* - 如果目标地址是广播地址,则无需回应。
*
* 否则(即收到一个合法的 SYN):
* - 初始化 tp->rcv_nxt 和 tp->irs,
* - 选择一个初始的 tp->iss,
* - 并发送一个报文段:
* <SEQ=ISS><ACK=RCV_NXT><CTL=SYN,ACK>
*
* 同时初始化:
* - tp->snd_nxt = tp->iss + 1
* - tp->snd_una = tp->iss
*
* 如果远端对等方地址字段尚未指定,则填写它们。
*
* 将状态切换到 SYN_RECEIVED,
* 并在该状态下处理此报文段的其他字段。
*/

case TCPS_LISTEN: {
struct mbuf *am;
register struct sockaddr_in *sin;

if (tiflags & TH_RST)
goto drop;
if (tiflags & TH_ACK)
goto dropwithreset;
if ((tiflags & TH_SYN) == 0)
goto drop;
/*
* RFC1122 4.2.3.10, p. 104: discard bcast/mcast SYN
* in_broadcast() should never return true on a received
* packet with M_BCAST not set.
*/
if (m->m_flags & (M_BCAST|M_MCAST) ||
IN_MULTICAST(ti->ti_dst.s_addr))
goto drop;
am = m_get(M_DONTWAIT, MT_SONAME); /* XXX */
if (am == NULL)
goto drop;

1.通过前面的RFC文档我们可知,RST、ACK、非SYN都是非法的,都应该丢弃

2.只有UDP才支持多播广播这些,TCP只支持单播,所以广播多播这些也丢弃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
am->m_len = sizeof (struct sockaddr_in);
sin = mtod(am, struct sockaddr_in *);
sin->sin_family = AF_INET;
sin->sin_len = sizeof(*sin);
sin->sin_addr = ti->ti_src;
sin->sin_port = ti->ti_sport;
bzero((caddr_t)sin->sin_zero, sizeof(sin->sin_zero));
laddr = inp->inp_laddr;
if (inp->inp_laddr.s_addr == INADDR_ANY)
inp->inp_laddr = ti->ti_dst;
if (in_pcbconnect(inp, am)) {
inp->inp_laddr = laddr;
(void) m_free(am);
goto drop;
}
(void) m_free(am);
tp->t_template = tcp_template(tp);
if (tp->t_template == 0) {
tp = tcp_drop(tp, ENOBUFS);
dropsocket = 0; /* socket is already gone */
goto drop;
}
if (optp)
tcp_dooptions(tp, optp, optlen, ti,
&ts_present, &ts_val, &ts_ecr);
if (iss)
tp->iss = iss;
else
tp->iss = tcp_iss;
tcp_iss += TCP_ISSINCR/2;
tp->irs = ti->ti_seq;
tcp_sendseqinit(tp);
tcp_rcvseqinit(tp);
tp->t_flags |= TF_ACKNOW;
tp->t_state = TCPS_SYN_RECEIVED;
tp->t_timer[TCPT_KEEP] = TCPTV_KEEP_INIT;
dropsocket = 0; /* committed to socket */
tcpstat.tcps_accepts++;
goto trimthenstep6;
}

1.还记得开头的三次握手和四次挥手图吗?当处于监听状态的TCP服务器接收到了一个合法的数据报,那么就意味着要开始建立联系了

2.in_pcbconnect负责把 socket 和目标四元组(源 IP/端口 + 目的 IP/端口)绑定起来,以后解析出数据报的网络四元组,就知道对应哪个socket,也就是哪个进程了。

3.处理TCP选项,然后初始化序列变量,也就是RFC中的设置 SND.NXT = ISS + 1SND.UNA = ISS

4.这里有意思的是TCP_ACKNOW,在tcp_output输出中,检查到标志后,会立刻构造数据报

5.更新状态为TCPS_SYN_RECEIVED并设置好保活定时器。

现在该进入下一个状态了,也就是主动打开状态。

SYN_SENT状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
* 如果当前状态是 SYN_SENT:
* 如果报文段包含 ACK,但并不是对我们 SYN 的确认,则丢弃该输入。
* 如果报文段包含 RST,则丢弃该连接。
* 如果报文段不包含 SYN,则丢弃它。
* 否则,这就是一个可接受的 SYN 报文段:
* 初始化 tp->rcv_nxt 和 tp->irs
* 如果报文段包含 ACK,则推进 tp->snd_una
* 如果我们的 SYN 已经被确认,则状态改为 ESTABLISHED,否则改为 SYN_RCVD
* 安排对该报文段进行确认(稍后发送 ACK)
* 继续处理剩余的数据/控制信息,从 URG 开始
*/

case TCPS_SYN_SENT:
if ((tiflags & TH_ACK) &&
(SEQ_LEQ(ti->ti_ack, tp->iss) ||
SEQ_GT(ti->ti_ack, tp->snd_max)))
goto dropwithreset;
if (tiflags & TH_RST) {
if (tiflags & TH_ACK)
tp = tcp_drop(tp, ECONNREFUSED);
goto drop;
}
if ((tiflags & TH_SYN) == 0)
goto drop;

处理ACK

我们可以将最早未确认的序号更新到ti->ti_ack了,ti是接收数据报的头部,所以其中的ti_ack就是期望的下一个序列。

1
2
3
4
5
6
if (tiflags & TH_ACK) {
tp->snd_una = ti->ti_ack;
if (SEQ_LT(tp->snd_nxt, tp->snd_una))
tp->snd_nxt = tp->snd_una;
}

关闭连接建立定时器:

其实这里的代码是不严谨的,因为有可能会收到不带ACK的SYN报文:

1
tp->t_timer[TCPT_REXMT] = 0;		

初始化接收序号,现在主动打开已经处理完毕,可以进入ESTABLISHED状态了:

snd_scale这些是RFC 1323 引入的规范,也就是查看窗口大小选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
tp->irs = ti->ti_seq;
tcp_rcvseqinit(tp);
tp->t_flags |= TF_ACKNOW;
if (tiflags & TH_ACK && SEQ_GT(tp->snd_una, tp->iss)) {
tcpstat.tcps_connects++;
soisconnected(so);
tp->t_state = TCPS_ESTABLISHED;
/* Do window scaling on this connection? */
if ((tp->t_flags & (TF_RCVD_SCALE|TF_REQ_SCALE)) ==
(TF_RCVD_SCALE|TF_REQ_SCALE)) {
tp->snd_scale = tp->requested_s_scale;
tp->rcv_scale = tp->request_r_scale;
}
//允许 SYN 携带数据:BSD 栈会先缓存,等状态变成 ESTABLISHED 后再通过 tcp_reass() 处理。
//其实这样效率有点低,Linux内核使用fast open技术,可以直接握手并传输数据
(void) tcp_reass(tp, (struct tcpiphdr *)0,
(struct mbuf *)0);
/*
* if we didn't have to retransmit the SYN,
* use its rtt as our initial srtt & rtt var.
*/
if (tp->t_rtt)
//更新RTT估计值
tcp_xmit_timer(tp, tp->t_rtt);
} else //同时打开
tp->t_state = TCPS_SYN_RECEIVED;

同时打开

现在还有一种极端情况需要处理,也就是通信双方同时打开,也就是TCP在SYN_SENT状态收到不带ACK的SYN,此时需要将状态转移到SYN_RCVD状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

trimthenstep6:
/*
* Advance ti->ti_seq to correspond to first data byte.
* If data, trim to stay within window,
* dropping FIN if necessary.
*/
ti->ti_seq++;
if (ti->ti_len > tp->rcv_wnd) {
todrop = ti->ti_len - tp->rcv_wnd;
m_adj(m, -todrop);
ti->ti_len = tp->rcv_wnd;
tiflags &= ~TH_FIN;
tcpstat.tcps_rcvpackafterwin++;
tcpstat.tcps_rcvbyteafterwin += todrop;
}
tp->snd_wl1 = ti->ti_seq - 1;
tp->rcv_up = ti->ti_seq;
goto step6;
}

其他状态的处理

处理序号绕回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

/*
* 除了 LISTEN 或 SYN_SENT 状态之外的其他状态。
* 首先检查时间戳(如果存在)。
* 然后检查报文段是否至少有部分字节落在接收窗口内。
* 如果报文段起始序号早于 rcv_nxt,
* 则丢弃前导数据(以及 SYN);如果什么都不剩,就仅发送 ACK。
*
* RFC 1323 PAWS:如果该报文段带有时间戳回显,
* 且其值小于 ts_recent,则丢弃该报文段。
*/

if (ts_present && (tiflags & TH_RST) == 0 && tp->ts_recent &&
TSTMP_LT(ts_val, tp->ts_recent)) {

/* Check to see if ts_recent is over 24 days old. */
if ((int)(tcp_now - tp->ts_recent_age) > TCP_PAWS_IDLE) {
/*
* Invalidate ts_recent. If this segment updates
* ts_recent, the age will be reset later and ts_recent
* will get a valid value. If it does not, setting
* ts_recent to zero will at least satisfy the
* requirement that zero be placed in the timestamp
* echo reply when ts_recent isn't valid. The
* age isn't reset until we get a valid ts_recent
* because we don't want out-of-order segments to be
* dropped when ts_recent is old.
*/
tp->ts_recent = 0;
} else {
tcpstat.tcps_rcvduppack++;
tcpstat.tcps_rcvdupbyte += ti->ti_len;
tcpstat.tcps_pawsdrop++;
goto dropafterack;
}
}

1.在千兆网络中,序列号可能十几秒就溢出一次,所以我们需要别的手段进行检查,也就是时间戳

2..假设1ms计数一次,那么int计数到24天时才会溢出,也就是说,时间戳往往是准确可靠的

3.尽管数据报几乎不可能隔了24小时再发包,但是也需要检查。因为TCP如果不启动保活机制这些,即使经过了几十天,不管两台主机之间的路由器重启了多少次,这两台主机都默认是连接状态。

接下来再介绍一些细节处理。

数据报可能

查看是否存在重复数据

1
2
3
4
5
6
7
8
9
10
11
if (todrop > 0) {
if (tiflags & TH_SYN) {
tiflags &= ~TH_SYN; // 丢弃 SYN 标志(因为 SYN 也占一个序号)
ti->ti_seq++; // 把序号前移一位
if (ti->ti_urp > 1)
ti->ti_urp--; // 如果有紧急指针,跟着调整
else
tiflags &= ~TH_URG; // 否则去掉 URG 标志
todrop--; // 已经丢弃了一个序号(SYN),差值减一
}
}

丢弃重复SYN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

if (todrop >= ti->ti_len) {
tcpstat.tcps_rcvduppack++;
tcpstat.tcps_rcvdupbyte += ti->ti_len;
/*
* If segment is just one to the left of the window,
* check two special cases:
* 1. Don't toss RST in response to 4.2-style keepalive.
* 2. If the only thing to drop is a FIN, we can drop
* it, but check the ACK or we will get into FIN
* wars if our FINs crossed (both CLOSING).
* In either case, send ACK to resynchronize,
* but keep on processing for RST or ACK.
*/
if ((tiflags & TH_FIN && todrop == ti->ti_len + 1)
#ifdef TCP_COMPAT_42
|| (tiflags & TH_RST && ti->ti_seq == tp->rcv_nxt - 1)
#endif
) {
todrop = ti->ti_len;
tiflags &= ~TH_FIN;
tp->t_flags |= TF_ACKNOW;
} else {
/*
* Handle the case when a bound socket connects
* to itself. Allow packets with a SYN and
* an ACK to continue with the processing.
*/
if (todrop != 0 || (tiflags & TH_ACK) == 0)
goto dropafterack;
}
} else {
tcpstat.tcps_rcvpartduppack++;
tcpstat.tcps_rcvpartdupbyte += todrop;
}
m_adj(m, todrop);
ti->ti_seq += todrop;
ti->ti_len -= todrop;
if (ti->ti_urp > todrop)
ti->ti_urp -= todrop;
else {
tiflags &= ~TH_URG;
ti->ti_urp = 0;
}
}

/*
* If new data are received on a connection after the
* user processes are gone, then RST the other end.
*/
if ((so->so_state & SS_NOFDREF) &&
tp->t_state > TCPS_CLOSE_WAIT && ti->ti_len) {
tp = tcp_close(tp);
tcpstat.tcps_rcvafterclose++;
goto dropwithreset;
}

删除落在窗口右侧的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
* If segment ends after window, drop trailing data
* (and PUSH and FIN); if nothing left, just ACK.
*/
todrop = (ti->ti_seq+ti->ti_len) - (tp->rcv_nxt+tp->rcv_wnd);
if (todrop > 0) {
tcpstat.tcps_rcvpackafterwin++;
if (todrop >= ti->ti_len) {
tcpstat.tcps_rcvbyteafterwin += ti->ti_len;
/*
* If a new connection request is received
* while in TIME_WAIT, drop the old connection
* and start over if the sequence numbers
* are above the previous ones.
*/
if (tiflags & TH_SYN &&
tp->t_state == TCPS_TIME_WAIT &&
SEQ_GT(ti->ti_seq, tp->rcv_nxt)) {
iss = tp->rcv_nxt + TCP_ISSINCR;
tp = tcp_close(tp);
goto findpcb;
}
/*
* If window is closed can only take segments at
* window edge, and have to drop data and PUSH from
* incoming segments. Continue processing, but
* remember to ack. Otherwise, drop segment
* and ack.
*/
if (tp->rcv_wnd == 0 && ti->ti_seq == tp->rcv_nxt) {
tp->t_flags |= TF_ACKNOW;
tcpstat.tcps_rcvwinprobe++;
} else
goto dropafterack;
} else
tcpstat.tcps_rcvbyteafterwin += todrop;
m_adj(m, -todrop);
ti->ti_len -= todrop;
tiflags &= ~(TH_PUSH|TH_FIN);
}

记录时间戳

1
2
3
4
5
6
7
8
9
10
/*
* If last ACK falls within this segment's sequence numbers,
* record its timestamp.
*/
if (ts_present && SEQ_LEQ(ti->ti_seq, tp->last_ack_sent) &&
SEQ_LT(tp->last_ack_sent, ti->ti_seq + ti->ti_len +
((tiflags & (TH_SYN|TH_FIN)) != 0))) {
tp->ts_recent_age = tcp_now;
tp->ts_recent = ts_val;
}

标志处理

SYN-RECEIVED

  • 收到 RST:

    • 被动打开 → 回到 LISTEN,不通知用户,清空重传队列。
    • SYN_SENT主动打开 → 通知“连接被拒绝”,清空队列,进入 CLOSED,删除 TCB。

    关于主动打开为什么会进入SYN_SENT状态,这里解释一下,只有在同时打开时才会发生。

ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2 / CLOSE-WAIT

  • 收到 RST:
    • 所有收发操作返回“reset”,清空队列,通知用户“连接复位”,进入 CLOSED,删除 TCB。

CLOSING / LAST-ACK / TIME-WAIT

  • 收到 RST → 直接进入 CLOSED,删除 TCB。

安全检查

  • 在同步状态(ESTABLISHED 等)下,如果段的安全属性与 TCB 不符 → 发送 RST,清空队列,通知用户“reset”,进入 CLOSED。

SYN 位检查

  • SYN-RECEIVED:被动打开 → 回到 LISTEN;否则按同步状态规则。
  • 其他同步状态:
    • TIME-WAIT:若时间戳匹配,可接受新连接。
    • 否则按 RFC 5961 → 发送“质询 ACK”,丢弃段。
    • 若未实现 RFC 5961 → 按 RFC 793:SYN 在窗口内 → 复位并关闭;不在窗口内 → 已在序号检查阶段被 ACK。

ACK 位检查

  • ACK=0 → 丢弃。
  • ACK=1:
    • 检查 ACK 是否在合法范围 (SND.UNA - MAX.SND.WND) <= SEG.ACK <= SND.NXT,否则丢弃并回 ACK。
    • 合法时按状态更新:
      • SYN-RECEIVED:ACK 合法 → 进入 ESTABLISHED,更新窗口;否则发 RST。
      • ESTABLISHED:更新 SND.UNA、删除已确认段;重复 ACK 忽略;超前 ACK → 发 ACK 丢弃。
      • FIN-WAIT-1:若 FIN 被确认 → 进入 FIN-WAIT-2。
      • FIN-WAIT-2:若队列空 → CLOSE 可确认。
      • CLOSE-WAIT:同 ESTABLISHED。
      • CLOSING:若 FIN 被确认 → 进入 TIME-WAIT。
      • LAST-ACK:若 FIN 被确认 → CLOSED。
      • TIME-WAIT:确认对端 FIN 重传,重启 2MSL。

URG 位检查

  • ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2:更新紧急指针,必要时通知用户。
  • 其他状态:忽略。

数据处理

  • ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2:交付数据,必要时通知 PUSH,更新 RCV.NXT/RCV.WND,发送 ACK。
  • 其他状态:忽略数据。

FIN 位检查

  • SYN-RECEIVED / ESTABLISHED:收到 FIN → 进入 CLOSE-WAIT。
  • FIN-WAIT-1:若 FIN 已确认 → TIME-WAIT,否则 CLOSING。
  • FIN-WAIT-2:→ TIME-WAIT。
  • CLOSE-WAIT / CLOSING / LAST-ACK / TIME-WAIT:保持原状态(TIME-WAIT 重启定时器)。

RST处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
* If the RST bit is set examine the state:
* SYN_RECEIVED STATE:
* If passive open, return to LISTEN state.
* If active open, inform user that connection was refused.
* ESTABLISHED, FIN_WAIT_1, FIN_WAIT2, CLOSE_WAIT STATES:
* Inform user that connection was reset, and close tcb.
* CLOSING, LAST_ACK, TIME_WAIT STATES
* Close the tcb.
*/
if (tiflags&TH_RST) switch (tp->t_state) {

case TCPS_SYN_RECEIVED:
so->so_error = ECONNREFUSED;
goto close;

case TCPS_ESTABLISHED:
case TCPS_FIN_WAIT_1:
case TCPS_FIN_WAIT_2:
case TCPS_CLOSE_WAIT:
so->so_error = ECONNRESET;
close:
tp->t_state = TCPS_CLOSED;
tcpstat.tcps_drops++;
tp = tcp_close(tp);
goto drop;

case TCPS_CLOSING:
case TCPS_LAST_ACK:
case TCPS_TIME_WAIT:
tp = tcp_close(tp);
goto drop;
}

其他标志处理

1.如果是连接阶段,由于前面经过了控制流判断,所以现在一定是syn_recived状态,带有ack

2.在 TCP 连接一旦建立(ESTABLISHED)之后,所有报文段都必须带 ACK 标志。

所以带SYN和不带ack的,都应该丢弃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* If a SYN is in the window, then this is an
* error and we send an RST and drop the connection.
*/
if (tiflags & TH_SYN) {
tp = tcp_drop(tp, ECONNRESET);
goto dropwithreset;
}

/*
* If the ACK bit is off we drop the segment and return.
*/
if ((tiflags & TH_ACK) == 0)
goto drop;

TCP的输入续

ACK处理

视角

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
	/*
* Ack processing.
*/
switch (tp->t_state) {

/*
* In SYN_RECEIVED state if the ack ACKs our SYN then enter
* ESTABLISHED state and continue processing, otherwise
* send an RST.
*/
case TCPS_SYN_RECEIVED:
if (SEQ_GT(tp->snd_una, ti->ti_ack) ||
SEQ_GT(ti->ti_ack, tp->snd_max))
goto dropwithreset;
tcpstat.tcps_connects++;
soisconnected(so);
tp->t_state = TCPS_ESTABLISHED;
/* Do window scaling? */
if ((tp->t_flags & (TF_RCVD_SCALE|TF_REQ_SCALE)) ==
(TF_RCVD_SCALE|TF_REQ_SCALE)) {
tp->snd_scale = tp->requested_s_scale;
tp->rcv_scale = tp->request_r_scale;
}
(void) tcp_reass(tp, (struct tcpiphdr *)0, (struct mbuf *)0);
tp->snd_wl1 = ti->ti_seq - 1;
/* fall into ... */

/*
* In ESTABLISHED state: drop duplicate ACKs; ACK out of range
* ACKs. If the ack is in the range
* tp->snd_una < ti->ti_ack <= tp->snd_max
* then advance tp->snd_una to ti->ti_ack and drop
* data from the retransmission queue. If this ACK reflects
* more up to date window information we update our window information.
*/
case TCPS_ESTABLISHED:
case TCPS_FIN_WAIT_1:
case TCPS_FIN_WAIT_2:
case TCPS_CLOSE_WAIT:
case TCPS_CLOSING:
case TCPS_LAST_ACK:
case TCPS_TIME_WAIT:

if (SEQ_LEQ(ti->ti_ack, tp->snd_una)) {
if (ti->ti_len == 0 && tiwin == tp->snd_wnd) {
tcpstat.tcps_rcvdupack++;
/*
* If we have outstanding data (other than
* a window probe), this is a completely
* duplicate ack (ie, window info didn't
* change), the ack is the biggest we've
* seen and we've seen exactly our rexmt
* threshhold of them, assume a packet
* has been dropped and retransmit it.
* Kludge snd_nxt & the congestion
* window so we send only this one
* packet.
*
* We know we're losing at the current
* window size so do congestion avoidance
* (set ssthresh to half the current window
* and pull our congestion window back to
* the new ssthresh).
*
* Dup acks mean that packets have left the
* network (they're now cached at the receiver)
* so bump cwnd by the amount in the receiver
* to keep a constant cwnd packets in the
* network.
*/
if (tp->t_timer[TCPT_REXMT] == 0 ||
ti->ti_ack != tp->snd_una)
tp->t_dupacks = 0;
else if (++tp->t_dupacks == tcprexmtthresh) {
tcp_seq onxt = tp->snd_nxt;
u_int win =
min(tp->snd_wnd, tp->snd_cwnd) / 2 /
tp->t_maxseg;

if (win < 2)
win = 2;
tp->snd_ssthresh = win * tp->t_maxseg;
tp->t_timer[TCPT_REXMT] = 0;
tp->t_rtt = 0;
tp->snd_nxt = ti->ti_ack;
tp->snd_cwnd = tp->t_maxseg;
(void) tcp_output(tp);
tp->snd_cwnd = tp->snd_ssthresh +
tp->t_maxseg * tp->t_dupacks;
if (SEQ_GT(onxt, tp->snd_nxt))
tp->snd_nxt = onxt;
goto drop;
} else if (tp->t_dupacks > tcprexmtthresh) {
tp->snd_cwnd += tp->t_maxseg;
(void) tcp_output(tp);
goto drop;
}
} else
tp->t_dupacks = 0;
break;
}
/*
* If the congestion window was inflated to account
* for the other side's cached packets, retract it.
*/
if (tp->t_dupacks > tcprexmtthresh &&
tp->snd_cwnd > tp->snd_ssthresh)
tp->snd_cwnd = tp->snd_ssthresh;
tp->t_dupacks = 0;
if (SEQ_GT(ti->ti_ack, tp->snd_max)) {
tcpstat.tcps_rcvacktoomuch++;
goto dropafterack;
}
acked = ti->ti_ack - tp->snd_una;
tcpstat.tcps_rcvackpack++;
tcpstat.tcps_rcvackbyte += acked;

/*
* If we have a timestamp reply, update smoothed
* round trip time. If no timestamp is present but
* transmit timer is running and timed sequence
* number was acked, update smoothed round trip time.
* Since we now have an rtt measurement, cancel the
* timer backoff (cf., Phil Karn's retransmit alg.).
* Recompute the initial retransmit timer.
*/
if (ts_present)
tcp_xmit_timer(tp, tcp_now-ts_ecr+1);
else if (tp->t_rtt && SEQ_GT(ti->ti_ack, tp->t_rtseq))
tcp_xmit_timer(tp,tp->t_rtt);

/*
* If all outstanding data is acked, stop retransmit
* timer and remember to restart (more output or persist).
* If there is more data to be acked, restart retransmit
* timer, using current (possibly backed-off) value.
*/
if (ti->ti_ack == tp->snd_max) {
tp->t_timer[TCPT_REXMT] = 0;
needoutput = 1;
} else if (tp->t_timer[TCPT_PERSIST] == 0)
tp->t_timer[TCPT_REXMT] = tp->t_rxtcur;
/*
* When new data is acked, open the congestion window.
* If the window gives us less than ssthresh packets
* in flight, open exponentially (maxseg per packet).
* Otherwise open linearly: maxseg per window
* (maxseg^2 / cwnd per packet), plus a constant
* fraction of a packet (maxseg/8) to help larger windows
* open quickly enough.
*/
{
register u_int cw = tp->snd_cwnd;
register u_int incr = tp->t_maxseg;

if (cw > tp->snd_ssthresh)
incr = incr * incr / cw + incr / 8;
tp->snd_cwnd = min(cw + incr, TCP_MAXWIN<<tp->snd_scale);
}
if (acked > so->so_snd.sb_cc) {
tp->snd_wnd -= so->so_snd.sb_cc;
sbdrop(&so->so_snd, (int)so->so_snd.sb_cc);
ourfinisacked = 1;
} else {
sbdrop(&so->so_snd, acked);
tp->snd_wnd -= acked;
ourfinisacked = 0;
}
if (so->so_snd.sb_flags & SB_NOTIFY)
sowwakeup(so);
tp->snd_una = ti->ti_ack;
if (SEQ_LT(tp->snd_nxt, tp->snd_una))
tp->snd_nxt = tp->snd_una;

switch (tp->t_state) {

/*
* In FIN_WAIT_1 STATE in addition to the processing
* for the ESTABLISHED state if our FIN is now acknowledged
* then enter FIN_WAIT_2.
*/
case TCPS_FIN_WAIT_1:
if (ourfinisacked) {
/*
* If we can't receive any more
* data, then closing user can proceed.
* Starting the timer is contrary to the
* specification, but if we don't get a FIN
* we'll hang forever.
*/
if (so->so_state & SS_CANTRCVMORE) {
soisdisconnected(so);
tp->t_timer[TCPT_2MSL] = tcp_maxidle;
}
tp->t_state = TCPS_FIN_WAIT_2;
}
break;

/*
* In CLOSING STATE in addition to the processing for
* the ESTABLISHED state if the ACK acknowledges our FIN
* then enter the TIME-WAIT state, otherwise ignore
* the segment.
*/
case TCPS_CLOSING:
if (ourfinisacked) {
tp->t_state = TCPS_TIME_WAIT;
tcp_canceltimers(tp);
tp->t_timer[TCPT_2MSL] = 2 * TCPTV_MSL;
soisdisconnected(so);
}
break;

/*
* In LAST_ACK, we may still be waiting for data to drain
* and/or to be acked, as well as for the ack of our FIN.
* If our FIN is now acknowledged, delete the TCB,
* enter the closed state and return.
*/
case TCPS_LAST_ACK:
if (ourfinisacked) {
tp = tcp_close(tp);
goto drop;
}
break;

/*
* In TIME_WAIT state the only thing that should arrive
* is a retransmission of the remote FIN. Acknowledge
* it and restart the finack timer.
*/
case TCPS_TIME_WAIT:
tp->t_timer[TCPT_2MSL] = 2 * TCPTV_MSL;
goto dropafterack;
}
}

step6:
/*
* Update window information.
* Don't look at window if no ACK: TAC's send garbage on first SYN.
*/
if ((tiflags & TH_ACK) &&
(SEQ_LT(tp->snd_wl1, ti->ti_seq) || tp->snd_wl1 == ti->ti_seq &&
(SEQ_LT(tp->snd_wl2, ti->ti_ack) ||
tp->snd_wl2 == ti->ti_ack && tiwin > tp->snd_wnd))) {
/* keep track of pure window updates */
if (ti->ti_len == 0 &&
tp->snd_wl2 == ti->ti_ack && tiwin > tp->snd_wnd)
tcpstat.tcps_rcvwinupd++;
tp->snd_wnd = tiwin;
tp->snd_wl1 = ti->ti_seq;
tp->snd_wl2 = ti->ti_ack;
if (tp->snd_wnd > tp->max_sndwnd)
tp->max_sndwnd = tp->snd_wnd;
needoutput = 1;
}

/*
* Process segments with URG.
*/
if ((tiflags & TH_URG) && ti->ti_urp &&
TCPS_HAVERCVDFIN(tp->t_state) == 0) {
/*
* This is a kludge, but if we receive and accept
* random urgent pointers, we'll crash in
* soreceive. It's hard to imagine someone
* actually wanting to send this much urgent data.
*/
if (ti->ti_urp + so->so_rcv.sb_cc > sb_max) {
ti->ti_urp = 0; /* XXX */
tiflags &= ~TH_URG; /* XXX */
goto dodata; /* XXX */
}
/*
* If this segment advances the known urgent pointer,
* then mark the data stream. This should not happen
* in CLOSE_WAIT, CLOSING, LAST_ACK or TIME_WAIT STATES since
* a FIN has been received from the remote side.
* In these states we ignore the URG.
*
* According to RFC961 (Assigned Protocols),
* the urgent pointer points to the last octet
* of urgent data. We continue, however,
* to consider it to indicate the first octet
* of data past the urgent section as the original
* spec states (in one of two places).
*/
if (SEQ_GT(ti->ti_seq+ti->ti_urp, tp->rcv_up)) {
tp->rcv_up = ti->ti_seq + ti->ti_urp;
so->so_oobmark = so->so_rcv.sb_cc +
(tp->rcv_up - tp->rcv_nxt) - 1;
if (so->so_oobmark == 0)
so->so_state |= SS_RCVATMARK;
sohasoutofband(so);
tp->t_oobflags &= ~(TCPOOB_HAVEDATA | TCPOOB_HADDATA);
}
/*
* Remove out of band data so doesn't get presented to user.
* This can happen independent of advancing the URG pointer,
* but if two URG's are pending at once, some out-of-band
* data may creep in... ick.
*/
if (ti->ti_urp <= ti->ti_len
#ifdef SO_OOBINLINE
&& (so->so_options & SO_OOBINLINE) == 0
#endif
)
tcp_pulloutofband(so, ti, m);
} else
/*
* If no out of band data is expected,
* pull receive urgent pointer along
* with the receive window.
*/
if (SEQ_GT(tp->rcv_nxt, tp->rcv_up))
tp->rcv_up = tp->rcv_nxt;
dodata: /* XXX */

/*
* Process the segment text, merging it into the TCP sequencing queue,
* and arranging for acknowledgment of receipt if necessary.
* This process logically involves adjusting tp->rcv_wnd as data
* is presented to the user (this happens in tcp_usrreq.c,
* case PRU_RCVD). If a FIN has already been received on this
* connection then we just ignore the text.
*/
if ((ti->ti_len || (tiflags&TH_FIN)) &&
TCPS_HAVERCVDFIN(tp->t_state) == 0) {
TCP_REASS(tp, ti, m, so, tiflags);
/*
* Note the amount of data that peer has sent into
* our window, in order to estimate the sender's
* buffer size.
*/
len = so->so_rcv.sb_hiwat - (tp->rcv_adv - tp->rcv_nxt);
} else {
m_freem(m);
tiflags &= ~TH_FIN;
}

/*
* If FIN is received ACK the FIN and let the user know
* that the connection is closing.
*/
if (tiflags & TH_FIN) {
if (TCPS_HAVERCVDFIN(tp->t_state) == 0) {
socantrcvmore(so);
tp->t_flags |= TF_ACKNOW;
tp->rcv_nxt++;
}
switch (tp->t_state) {

/*
* In SYN_RECEIVED and ESTABLISHED STATES
* enter the CLOSE_WAIT state.
*/
case TCPS_SYN_RECEIVED:
case TCPS_ESTABLISHED:
tp->t_state = TCPS_CLOSE_WAIT;
break;

/*
* If still in FIN_WAIT_1 STATE FIN has not been acked so
* enter the CLOSING state.
*/
case TCPS_FIN_WAIT_1:
tp->t_state = TCPS_CLOSING;
break;

/*
* In FIN_WAIT_2 state enter the TIME_WAIT state,
* starting the time-wait timer, turning off the other
* standard timers.
*/
case TCPS_FIN_WAIT_2:
tp->t_state = TCPS_TIME_WAIT;
tcp_canceltimers(tp);
tp->t_timer[TCPT_2MSL] = 2 * TCPTV_MSL;
soisdisconnected(so);
break;

/*
* In TIME_WAIT state restart the 2 MSL time_wait timer.
*/
case TCPS_TIME_WAIT:
tp->t_timer[TCPT_2MSL] = 2 * TCPTV_MSL;
break;
}
}
if (so->so_options & SO_DEBUG)
tcp_trace(TA_INPUT, ostate, tp, &tcp_saveti, 0);

/*
* Return any desired output.
*/
if (needoutput || (tp->t_flags & TF_ACKNOW))
(void) tcp_output(tp);
return;

dropafterack:
/*
* Generate an ACK dropping incoming segment if it occupies
* sequence space, where the ACK reflects our state.
*/
if (tiflags & TH_RST)
goto drop;
m_freem(m);
tp->t_flags |= TF_ACKNOW;
(void) tcp_output(tp);
return;

dropwithreset:
/*
* Generate a RST, dropping incoming segment.
* Make ACK acceptable to originator of segment.
* Don't bother to respond if destination was broadcast/multicast.
*/
if ((tiflags & TH_RST) || m->m_flags & (M_BCAST|M_MCAST) ||
IN_MULTICAST(ti->ti_dst.s_addr))
goto drop;
if (tiflags & TH_ACK)
tcp_respond(tp, ti, m, (tcp_seq)0, ti->ti_ack, TH_RST);
else {
if (tiflags & TH_SYN)
ti->ti_len++;
tcp_respond(tp, ti, m, ti->ti_seq+ti->ti_len, (tcp_seq)0,
TH_RST|TH_ACK);
}
/* destroy temporarily created socket */
if (dropsocket)
(void) soabort(so);
return;

drop:
/*
* Drop space held by incoming segment and return.
*/
if (tp && (tp->t_inpcb->inp_socket->so_options & SO_DEBUG))
tcp_trace(TA_DROP, ostate, tp, &tcp_saveti, 0);
m_freem(m);
/* destroy temporarily created socket */
if (dropsocket)
(void) soabort(so);
return;
#ifndef TUBA_INCLUDE
}