TCP/IP 协议族 - TCP 笔记
TCP 服务
进程到进程的通信
流交付服务
TCP 是一种面向流的协议。TCP 允许发送进程以字节流的形式来传递数据,并且也允许接受进程把数据作为字节流来接收。
TCP 创造了一种环境,让两个进程好像被一个假想的管道连接。
发送缓存和接收缓存
发送方和接收方都有缓存。
报文段
IP 层作为 TCP 的服务提供者,它必须以分组为单位发送数据,而不是按字节流发送。
因此,在运输层,TCP 把若干字节组成一个分组,成为报文段。TCP 给每个报文段添加一个首部(用于控制),然后再把这个报文段交给 IP 层传输。
全双工通信
TCP 提供全双工服务,即数据可在同一时间双向流动。
面向连接的服务
两个进程要使用 TCP 交换数据的话,需要经过三个阶段:
- 它们之间建立连接。
- 交换数据。
- 终止连接。
这是一条虚连接。
可靠的服务
TCP 是一个可靠的运输协议,它使用确认机制来检查数据是否安全完好地到达。
TCP 的特点
编号系统
虽然 TCP 以报文段为单位进行传输,但是报文段首部中并没有存放报文段编号值的字段。
实际上,首部中序号和确认号的字段指的是字节的编号而不是报文段编号。
字节号
TCP 把一个连接中要发送的所有数据字节都编上号,两个方向的编号是相互独立的。 TCP 随机选择第一个数作为编号。
序号
当所有字节编上号以后,TCP 就给每一个要发送的报文段指派一个序号。每个报文段的序号就是这个报文段中第一个数据字节的序号。
当一个报文不携带数据信息的时候,从逻辑上来讲,它就不定义序号,序号字段总是存在,不过无意义。 不过它还是需要一个序号,便于接收方的确认。这种报文需要消耗一个序号,就像它携带了一个字节的数据,不过实际上是没有数据的。
确认号
确认号的定义是期望收到的下一个序号,也就是说把数据报的最后一个字节编号 + 1。
流量控制
接收方能控制发送方的窗口。
差错控制
伪首部是封装用户数据报的那个 IP 分组的首部一部分。 和 UDP 中的伪首部一样,是为了防止交付错的协议。 协议字段是 6。
拥塞控制
报文段
格式

首部是 20~60 个字节,紧随其后的是从应用程序传来的数据。 首部在没有选项的时候是 20 字节。
记一下几个自己容易忘的:
- 首部长度 这个 4 位字段指出 TCP 首部一共有多少个 4 字节字。首部长度可以在 20~60 之间,所以这个字段的值可以在 5(5*4=20) 到 15(15*4=60) 之间。
- 窗口大小 这个字段定义的是 TCP 的发送窗口大小,以字节为单位。这个字段为 16 位,也就是说最大能发送 65536 字节的数据,通常被成为接收窗口。
- 检验和 UDP 是否使用检验和是可选的,而 TCP 是强制的。
TCP 连接
因为 TCP 工作在更高的层次上,所以虽然 IP 是无连接的,IP 并不知情。
连接建立
三次握手
- 客户先发送一个报文段,在这个报文中只有 SYN 标识为1,不包含真正的数据。
- 服务器发送第二个报文段,即 SYN + ACK 报文段。服务器通过这个报文段同步了自己的 ISN(初始序号),用 ACK 标识来确认已经收到来自客户端的 SYN 报文段,同时给出了期望收到的下一个序号。因为这个报文段包含了确认,所以它还要定义接受窗口大小,也不携带数据。
- 客户发送第三个报文段,仅仅确认了服务器的报文段。
同时打开
这时候两个 TCP 都向对方发出 SYN + ACK 报文段。
SYN 泛洪攻击
恶意向服务器发送大量 SYN 报文段,让服务器分配资源,最后服务器资源不足无法分配给正常的请求。
数据传送
推送数据
发送 TCP 利用缓存来存储应用程序传来的数据流。 但是有些场合,我们需要立刻交付报文段,这时候发送方的应用程序可以请求推送操作。也就是说,发送方不必等待窗口被填满,必须马上创建一个报文段并发送,置 PSH 为1. 不过目前大多数 TCP 的实现都忽略了此类请求。
紧急数据
如果应用程序需要发送紧急字节,也就是有些字节要被另一端特殊地对待,就要置 URG 为1。 这里要注意,此处的紧急并不是有更高的优先权,而是只是标记了某一段字节流。
连接终止
三次挥手
客户发送一个 FIN 位置为 1 的报文,消耗一个序号。 服务器 TCP 收到这个报文,发送 FIN + ACK 报文段,消耗一个序号。 客户发送 ACK 报文,证实收到了服务器发的,不消耗序号
半关闭
连接的一方可以先行停止发送数据,但仍旧可以接收数据。 四次挥手就是这种情况。
连接复位
通过 RST 标识来完成。
拒绝连接请求
假定某一端的 TCP 向一个不存在的端口请求连接,另一端发送 RST 为 1 的报文段来拒绝这个请求。
异常中止连接
由于出现了异常情况,某一端的 TCP 可能放弃一条在用的连接,发送一个 RST 报文段。
终止空闲的连接
某一端 TCP 发现另一端的 TCP 已经空闲了很长的时间,发送一个 RST 报文段。
状态转换图
| 状态 | 说明 | 
| CLOSED | 没有连接 | 
| LISTEN | 收到了被动打开:等待 SYN | 
| SYN-SENT | 已发送 SYN,等待 ACK | 
| SYN-RCVD | 已发送 SYN + ACK,等待 ACK | 
| ESTABLISHED | 连接建立 | 
| FIN-WAIT-1 | 第一个 FIN 已发送,等待 ACK | 
| FIN-WAIT-2 | 对第一个 FIN 的 ACK 已收到,等待第二个 FIN | 
| CLOSE-WAIT | 收到第一个 FIN,已发送 ACK,等待应用程序关闭 | 
| TIME-WAIT | 收到第二个 FIN,已发送 ACK,等待 2MSL 超时 | 
| LAST-ACK | 已发送第二个 FIN,等待 ACK | 
| CLOSING | 双方都已决定同时关闭 | 
几种情况
客户状态
客户发送一个 SYN 报文段,进入到 SYN-SENT 状态。在收到服务器的 SYN + ACK 报文段之后,发送 ACK 报文段,并进入 ESTABLISHED 状态,此后数据开始传送。
关闭连接时,客户端发送 FIN 报文段,进入 FIN-WAIT-1 状态,当他收到对刚才发送的 FIN 报文段的 ACK 之后,进入 FIN-WAIT-2 状态,并继续停留在这个状态,直到收到一个服务器的 FIN 报文段为止。 在收到这个 FIN 报文段后,客户就发送 ACK 报文段,并进入 TIME-WAIT 状态,同时设置一个计数器,超时时间是 2MSL。 MSL 是一个报文段被丢弃之前在因特网中能生存的最大时间。 这是因为:
- 如果最后一个 ACK 报文段丢失了,服务器 TCP 以为是它的 FIN 丢失了,因而重传。如果客户已进入 CLOSED 状态,并在 2MSL 计时器超时之前就关闭了这条连接,那么客户就永远也收不到这个重传的 FIN 报文段,服务器也就收不到最后的 ACK,无法关闭这条连接。2MSL 可以使得客户端在丢失一个 ACK 的时候等到下一个 FIN 的到来。如果在 TIME-WAIT 状态中有一个新的 FIN 到来了,客户端就重发一个 ACK,并重新设置计时器。
- 某个连接中的重复报文段可能出现在下一个连接中。 假定客户端和服务器都关闭了连接,经过短暂时间又打开了一个新连接,使用相同的源地址和套接字地址,那么前一个连接的报文段可能会到达新连接。为了避免这个问题, TCP 规定这种化身必须经过 2MSL 才能出现。如果这个化身的初始序号大于前一个连接使用的最后一个序号,可以忽略这个规则。
服务器状态
服务器先处在 LISTEN 状态,然后收到一个 SYN,就发送 SYN + ACK 报文段,进入 SYN-RCVD 状态。在收到 ACK 之后,进入 ESTABLISHED 状态。 关闭的时候,收到客户的 FIN 报文段,发送一个 ACK,进入 CLOSE-WAIT 状态。如果服务器也结束数据传输了,就发送 FIN 报文段,进入 LAST-ACK 状态,直到收到客户端的 ACK,进入 CLOSED 状态。
三次挥手情况
简单来说就是客户端在进入到 FIN-WAIT-1 状态的时候,收到了服务器端的 FIN + ACK,那么便跳过 FIN-WAIT-2 状态直接进入到了 TIME-WAIT 状态。
同时打开
这时候自己充当了客户端和服务器端,进入 SYN-SENT -> SYN-RCVD 然后建立连接。
同时关闭
双方都发送了 FIN 报文段,进入 FIN-WAIT-1 状态。收到之后,每一端都进入 CLOSING 状态,并发送 ACK 报文段。在收到 ACK 之后,双方进入 TIME-WAIT 状态。
拒绝连接
服务器拒绝了连接,收到 SYN 之后发送了 RST + ACK 报文段,这时候客户端收到后进入 CLOSED 状态。 对 RST 报文段不用响应 ACK。
异常中止连接
通过 RST 报文段双方都进入 CLOSED 状态。
TCP 中的窗口
发送窗口
类似于选择重传协议的窗口,但是有几点区别:
- SR 窗口为分组编号,TCP 中的是字节编号。
- 某些 TCP 实现可以保存数据,并稍后发送。
- 计时器只有一个。
接收窗口
- TCP 允许接收进程按照自己的节奏拉取数据。也就是说,分配给接收方的缓存中,有一部分是已经被确认但是等待进程拉取的。 所以接受窗口大小 = 缓存大小 - 正在等待被拉取的字节数
- 确认方式的不同。 TCP 的主流确认机制是累计确认,不过在新版的 TCP 中,既实现了积累确认,也用了选择确认。
流量控制
糊涂窗口综合征
如果每次报文段数据只有 1 个字节,那么效率就很低。 解决这个问题的算法是 Nagle 算法。
- 发送 TCP 把收到的第一块数据发送出去,哪怕只有 1 个字节。
- 之后累积等待,直到收到确认或者积累了足够的数据。
- 不断重复 2。
接收方产生的症状
每次消耗 1 字节,这时候就会宣布有 1 字节大小的窗口。然后发送方就会发送 1 字节大小的报文段,又回到了上一种情况。
Clark 解决方法
只要有数据到就发送确认,但在缓存中有足够大的重建放入最长报文短之前,或者至少有一般的缓存空间为空之前,一直宣布窗口大小为 0。
推迟确认
直到缓存有一定空间的时候才确认。目前定义最迟的推迟确认不超过 500ms。
确认的一些规则
- 向另一端发送数据时,必须捎带一个确认。
- 当接收方没有数据要发送,但是收到了按序到达的报文段,同时前一个报文段也确认过了,那么接收方将推迟发送确认报文短,直到另一个报文段到达。也就是说,如果仅仅有一个按序到达的报文段还没有被确认,接收方需要推迟发送确认报文段。
- 任何时候,不能有两个以上的按序报文段未被确认。
- 当序号比期望序号还大的失序报文段到达时,接收方立即发送确认报文段,这将导致对丢失报文段的快重传。
- 当一个丢失报文段到达时,接收方要发送确认报文段,并宣布下一个期望的序号。
- 如果到达一个重复的报文段,丢弃该报文段,但是要发送一个确认,指出下一个希望收到的报文段。
重传
RTO 之后的重传
TCP 的每一条连接都有一个重传超时计时器,当计时器时间到,发送队列中序号最小的报文段会被重传,并重启计时器。
三个重复的 ACK 报文段之后的重传
如果收到了三个重复的 ACK 报文段,这时候就进入快重传,立即重传丢失的报文。
几种情况
因确认丢失而产生死锁
接收方发送确认,并且设置 rwnd 为 0。过了一段时间,接收方打算取消这个限制,但是这个确认丢失了。那么发送方一直在等待非零的 rwnd,而接收方认为对方是没数据发送了。
拥塞控制
拥塞策略
慢开始
从 cwnd = 1 开始,每收到一个 ACK,cwnd 增大一倍,一直到门限值。
拥塞避免
每个 RTT 增长 1 的 cwnd。
拥塞检测
- RTO 计时器超时,出现拥塞的可能性很大。所以:
    - 门限值设置为当前窗口的一半。
- cwnd 置 1.
- 再次从慢开始开始。
 
- 收到三个重复的 ACk:
    - 门限值设成当前窗口的一半。
- cwnd 置为门限值(有些是+3)。
- 启动拥塞避免。
 
计时器
持续计时器
为了避免死锁,每个连接有一个持续计时器。到时间的时候,发送 1 个字节的新数据,它有序号,但是永远不会被确认。 作用只是让另一端重传一个确认。 持续计时器的长度被设置为重传时间的值。如果没有收到回应,上限×2,发送另一个探测报文段,计时器复位。重复过程,直到到达一个门限。然后一直以这个门限为时间,直到窗口重新打开。
保活计时器
为了防止两个 TCP 之间长时间空闲。
TIME-WAIT 计时器
在四次挥手时候用的。防止自己确认丢失了,保证能在再次收到对方的重传。
选项
选项结束
用来在选项区结尾进行填充。
无操作
用于选项之间的填充,通常在另一个选项之前,可多次使用。
最大报文段长度
定义了能被重点接收的 TCP 报文段的最大数据长度,是在连接建立的时候确定的,连接期间保持不变。
窗口扩大因子
虽然首部的窗口大小有了,但是最大只能表示 65535 字节。有了扩大因子,就能表示 $x*2^y$ 大小的窗口。 虽然是 1 字节,但是 TCP 允许的最大只能到 $2^14$。 窗口大小不能超过序号最大值。 窗口扩大因子只能在连接建立的时候确认,连接期间不能改变。
时间戳
用来测量 RTT 和防止序号绕回,10 字节。
##允许 SACK 和 SACK 选项
如果建立的时候,一方打开这个选项,那么双方在数据传输的阶段就能使用 SACK(选择确认)。
TCP 软件包
太多了。。写出来就相当于理解了 TCP 三次握手和四次挥手的自动机了。以后再写。。
- 上一篇 TCP/IP 协议族 - UDP 笔记
- 下一篇 Redis 源码阅读 - Dict