TCP/IP 协议族 - TCP 笔记

TCP 服务

进程到进程的通信

流交付服务

TCP 是一种面向流的协议。TCP 允许发送进程以字节流的形式来传递数据,并且也允许接受进程把数据作为字节流来接收。 TCP 创造了一种环境,让两个进程好像被一个假想的管道连接。

发送缓存和接收缓存

发送方和接收方都有缓存。

报文段

IP 层作为 TCP 的服务提供者,它必须以分组为单位发送数据,而不是按字节流发送。 因此,在运输层,TCP 把若干字节组成一个分组,成为报文段。TCP 给每个报文段添加一个首部(用于控制),然后再把这个报文段交给 IP 层传输。

全双工通信

TCP 提供全双工服务,即数据可在同一时间双向流动。

面向连接的服务

两个进程要使用 TCP 交换数据的话,需要经过三个阶段:

  1. 它们之间建立连接。
  2. 交换数据。
  3. 终止连接。

这是一条虚连接。

可靠的服务

TCP 是一个可靠的运输协议,它使用确认机制来检查数据是否安全完好地到达。

TCP 的特点

编号系统

虽然 TCP 以报文段为单位进行传输,但是报文段首部中并没有存放报文段编号值的字段。 实际上,首部中序号确认号的字段指的是字节的编号而不是报文段编号

字节号

TCP 把一个连接中要发送的所有数据字节都编上号,两个方向的编号是相互独立的。 TCP 随机选择第一个数作为编号。

序号

当所有字节编上号以后,TCP 就给每一个要发送的报文段指派一个序号。每个报文段的序号就是这个报文段中第一个数据字节的序号。

当一个报文不携带数据信息的时候,从逻辑上来讲,它就不定义序号,序号字段总是存在,不过无意义。 不过它还是需要一个序号,便于接收方的确认。这种报文需要消耗一个序号,就像它携带了一个字节的数据,不过实际上是没有数据的。

确认号

确认号的定义是期望收到的下一个序号,也就是说把数据报的最后一个字节编号 + 1。

流量控制

接收方能控制发送方的窗口。

差错控制

伪首部是封装用户数据报的那个 IP 分组的首部一部分。 和 UDP 中的伪首部一样,是为了防止交付错的协议。 协议字段是 6。

拥塞控制

报文段

格式

TCP

首部是 20~60 个字节,紧随其后的是从应用程序传来的数据。 首部在没有选项的时候是 20 字节。

记一下几个自己容易忘的:

TCP 连接

因为 TCP 工作在更高的层次上,所以虽然 IP 是无连接的,IP 并不知情。

连接建立

三次握手

  1. 客户先发送一个报文段,在这个报文中只有 SYN 标识为1,不包含真正的数据。
  2. 服务器发送第二个报文段,即 SYN + ACK 报文段。服务器通过这个报文段同步了自己的 ISN(初始序号),用 ACK 标识来确认已经收到来自客户端的 SYN 报文段,同时给出了期望收到的下一个序号。因为这个报文段包含了确认,所以它还要定义接受窗口大小,也不携带数据。
  3. 客户发送第三个报文段,仅仅确认了服务器的报文段。

同时打开

这时候两个 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 报文段。

状态转换图

state

状态 说明
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 是一个报文段被丢弃之前在因特网中能生存的最大时间。 这是因为:

  1. 如果最后一个 ACK 报文段丢失了,服务器 TCP 以为是它的 FIN 丢失了,因而重传。如果客户已进入 CLOSED 状态,并在 2MSL 计时器超时之前就关闭了这条连接,那么客户就永远也收不到这个重传的 FIN 报文段,服务器也就收不到最后的 ACK,无法关闭这条连接。2MSL 可以使得客户端在丢失一个 ACK 的时候等到下一个 FIN 的到来。如果在 TIME-WAIT 状态中有一个新的 FIN 到来了,客户端就重发一个 ACK,并重新设置计时器。
  2. 某个连接中的重复报文段可能出现在下一个连接中。 假定客户端和服务器都关闭了连接,经过短暂时间又打开了一个新连接,使用相同的源地址和套接字地址,那么前一个连接的报文段可能会到达新连接。为了避免这个问题, 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 中的窗口

发送窗口

类似于选择重传协议的窗口,但是有几点区别:

  1. SR 窗口为分组编号,TCP 中的是字节编号。
  2. 某些 TCP 实现可以保存数据,并稍后发送。
  3. 计时器只有一个。

接收窗口

  1. TCP 允许接收进程按照自己的节奏拉取数据。也就是说,分配给接收方的缓存中,有一部分是已经被确认但是等待进程拉取的。 所以接受窗口大小 = 缓存大小 - 正在等待被拉取的字节数
  2. 确认方式的不同。 TCP 的主流确认机制是累计确认,不过在新版的 TCP 中,既实现了积累确认,也用了选择确认。

流量控制

糊涂窗口综合征

如果每次报文段数据只有 1 个字节,那么效率就很低。 解决这个问题的算法是 Nagle 算法

  1. 发送 TCP 把收到的第一块数据发送出去,哪怕只有 1 个字节。
  2. 之后累积等待,直到收到确认或者积累了足够的数据。
  3. 不断重复 2。

接收方产生的症状

每次消耗 1 字节,这时候就会宣布有 1 字节大小的窗口。然后发送方就会发送 1 字节大小的报文段,又回到了上一种情况。

Clark 解决方法

只要有数据到就发送确认,但在缓存中有足够大的重建放入最长报文短之前,或者至少有一般的缓存空间为空之前,一直宣布窗口大小为 0。

推迟确认

直到缓存有一定空间的时候才确认。目前定义最迟的推迟确认不超过 500ms。

确认的一些规则

  1. 向另一端发送数据时,必须捎带一个确认。
  2. 当接收方没有数据要发送,但是收到了按序到达的报文段,同时前一个报文段也确认过了,那么接收方将推迟发送确认报文短,直到另一个报文段到达。也就是说,如果仅仅有一个按序到达的报文段还没有被确认,接收方需要推迟发送确认报文段。
  3. 任何时候,不能有两个以上的按序报文段未被确认。
  4. 当序号比期望序号还大的失序报文段到达时,接收方立即发送确认报文段,这将导致对丢失报文段的快重传。
  5. 当一个丢失报文段到达时,接收方要发送确认报文段,并宣布下一个期望的序号。
  6. 如果到达一个重复的报文段,丢弃该报文段,但是要发送一个确认,指出下一个希望收到的报文段。

重传

RTO 之后的重传

TCP 的每一条连接都有一个重传超时计时器,当计时器时间到,发送队列中序号最小的报文段会被重传,并重启计时器。

三个重复的 ACK 报文段之后的重传

如果收到了三个重复的 ACK 报文段,这时候就进入快重传,立即重传丢失的报文。

几种情况

因确认丢失而产生死锁

接收方发送确认,并且设置 rwnd 为 0。过了一段时间,接收方打算取消这个限制,但是这个确认丢失了。那么发送方一直在等待非零的 rwnd,而接收方认为对方是没数据发送了。

拥塞控制

拥塞策略

慢开始

从 cwnd = 1 开始,每收到一个 ACK,cwnd 增大一倍,一直到门限值。

拥塞避免

每个 RTT 增长 1 的 cwnd。

拥塞检测

  1. RTO 计时器超时,出现拥塞的可能性很大。所以:
    • 门限值设置为当前窗口的一半。
    • cwnd 置 1.
    • 再次从慢开始开始。
  2. 收到三个重复的 ACk:
    • 门限值设成当前窗口的一半。
    • cwnd 置为门限值(有些是+3)。
    • 启动拥塞避免。

计时器

持续计时器

为了避免死锁,每个连接有一个持续计时器。到时间的时候,发送 1 个字节的新数据,它有序号,但是永远不会被确认。 作用只是让另一端重传一个确认。 持续计时器的长度被设置为重传时间的值。如果没有收到回应,上限×2,发送另一个探测报文段,计时器复位。重复过程,直到到达一个门限。然后一直以这个门限为时间,直到窗口重新打开。

保活计时器

为了防止两个 TCP 之间长时间空闲。

TIME-WAIT 计时器

在四次挥手时候用的。防止自己确认丢失了,保证能在再次收到对方的重传。

选项

选项结束

用来在选项区结尾进行填充。

无操作

用于选项之间的填充,通常在另一个选项之前,可多次使用。

最大报文段长度

定义了能被重点接收的 TCP 报文段的最大数据长度,是在连接建立的时候确定的,连接期间保持不变。

窗口扩大因子

虽然首部的窗口大小有了,但是最大只能表示 65535 字节。有了扩大因子,就能表示 $x*2^y$ 大小的窗口。 虽然是 1 字节,但是 TCP 允许的最大只能到 $2^14$。 窗口大小不能超过序号最大值。 窗口扩大因子只能在连接建立的时候确认,连接期间不能改变。

时间戳

用来测量 RTT 和防止序号绕回,10 字节。

##允许 SACK 和 SACK 选项

如果建立的时候,一方打开这个选项,那么双方在数据传输的阶段就能使用 SACK(选择确认)。

TCP 软件包

太多了。。写出来就相当于理解了 TCP 三次握手和四次挥手的自动机了。以后再写。。

Powered by Jekyll and Theme by solid