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