Lever's Castle

Let's code a TCP/IP stack, 3 - TCP Basics & Handshake

January 16, 2019

阅读原文:http://www.saminiir.com/lets-code-tcp-ip-stack-3-tcp-handshake/

TCP 运行在 OSI 模型的第 4 层 —— 传输层上,TCP 的职责是修复数据包传输过程中的错误连接和故障。

可靠性机制

需要解决的主要是以下几个问题:

  • 发送方等待接收方的确认需要多久?
  • 如果接收方没办法快速响应该怎么办?
  • 如果中间的网络设备(比如:路由器)没办法快速处理,该怎么办?

同样的,来自接收方的确认信息也面临着上述问题 —— 确认信息在传输过程中丢失或者损坏,让发送方很尴尬。

我们可以采取一些机制来解决这些问题。其中最常见的策略是滑动窗口,窗口数据可以理解成一个有序的数组,当双方确认数据时,窗口向前滑动。

滑动窗口还可以用来做流量控制。当接收方没办法快速响应时,流控就很有必要了,在遇到这种情况时,发送发会把窗口调小,以限制发送速度。

拥塞控制也可以提高网络通信的可靠性,一旦网络变的拥挤,发送方就要考虑减慢发送速度。一般有两种方式做拥塞控制,一种是在协议中明确定义一个字段用于告知发送发网络的拥塞状态,另一种是靠发送方根据自己记录的信息猜测网络的拥塞状况。

TCP 核心

TCP 会比 UDP、IP 等协议更加复杂。TCP 是一种面向连接的协议,也就是说,他要做的第一件事情就是在通信双方建立好沟通的渠道。这需要两边的积极配合——握手通信,告知彼此的状态。

TCP 是一种面向字节流的协议。与 UDP 不同(UDP 是面向报文的),TCP 不会保证应用在发送和接受数据时能够保持稳定大小的数据块。相反,TCP 会把数据缓冲到 buffer 中,当数据包丢失、顺序错乱或者损坏时,都要重新组织 buffer 中的数据,只有当数据被确认完整时,TCP 才会把数据传输到应用程序的 socket 接口。

TCP 处理的流,要处理成正常的可被 IP 协议携带的数据包才行。TCP 头中包含当前流的索引序号,这是用来做分组处理的。依赖这个属性,字节流可以被分割成多个大小不同的段,并且 TCP 协议还能知道如何重新组织他们。

类似于 IP 协议,TCP 协议会检查信息的完整性。校验完整性的方法与 IP 协议相同 —— 校验和算法,但在 TCP 协议中增加了更多的细节。协议头和协议体都会计算在校验和中,从 IP 报头构建的伪报头也会计算进来。

TCP 收到损坏的分段时,会直接丢弃掉,也不会通知给发送方。发送方会有个定时器来自己发现问题 —— 发送方在指定时间内没收到接收方的 ACK,就要再次重试发送该分段。

TCP 是全双工的,也就是说数据是可以同时双向流动的。这就意味着通信方必须在内存中维持两个方向的数据。

TCP Header 格式

接下来,我们看下 TCP Header 中会包含哪些信息。TCP Header 看上去很简单,但是包含了很多关于通信状态的信息。

图片

从图中可以看到,TCP 表头中包含来源端口信息和目的端口信息,通过端口,网络堆栈知道将流量定向到何处。字段占 16 bit,因此端口值的范围是 0 ~ 65535。

序列号码表示 TCP 段的窗口索引,TCP 流中的每个字节都被编号串联起来。在进行握手的阶段,会先生成一个初始序列号码,之后在此基础上递增。

确认号码是发送方期望接收的下一个字节的窗口索引,握手完成后,必须始终填充 ACK 字段。

第四行中的几个缩写对应的含义如下:

  1. CWR —— Congestion Window Reduced 用于通知发送方,降低发送速率
  2. ECE —— ECN ECHO 通知发送方收到拥塞控制
  3. URG —— 为 1 表示高优先级数据包
  4. ACK —— 为 1 表示确认号码字段有效
  5. PSH —— 为 1 是带有 “PUSH” 标志的数据,指示接收方应当尽快将这个报文段交给应用层,而不用等待缓冲区装满
  6. RST —— 为 1 表示出现严重差错,需要重置 TCP 连接
  7. SYN —— 为 1 表示这是连接请求或者连接接受请求,用于创建连接和使序列号同步
  8. FIN —— 为 1 表示发送方没有数据要传输了,要求释放连接

窗口大小是指该窗口的大小,即接收方将要接受的字节数,该字段占 16 bit,因此窗口大小最大为 65535 bytes。

校验和字段用于验证 TCP 段的完整性,其算法本质上与 IP 中的校验和算法相同。

紧急指针,是在 URG 设置为 1 时生效,这个指针表示紧急数据在整个流中的位置。

TCP 握手机制

一个 TCP 连接通常会经历以下 3 个阶段:

  1. 建立连接(握手)
  2. 数据传输
  3. 关闭连接

下面的示意图是用来表示握手过程的:

          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
  1. 主机 A 的端口处于关闭状态,不接受任何连接,同时,主机 B 的端口处于开启监听状态,正在等待新的连接
  2. 主机 A 希望与主机 B 通信,于是主动发起了建立连接的请求。A 创建了一个携带 SYN 标志的 TCP 段,并把 SEQ 值设置为 100 发送给了 B
  3. 主机 B 响应了 A 的请求,在响应信息的 TCP 段中携带了 SYN 和 ACK 标志,并且通过对 A 传来的 SEQ 序列号 + 1 来告知 A —— B 承认了你的请求,同样的 B 也会设置一个自己的 SEQ(300)
  4. 主机 A 把 B 传来的 SEQ 填到 ACK 中,并将自己的 SEQ 序号一同返回给 B,完成 3 次握手
  5. 数据开始流动,主要是因为双方已经确认了对方的 TCP 段号

这是建立一个 TCP 连接的标准剧情。然后思考以下几个问题:

  1. 初始的 SEQ 值是如何确定的?
  2. 如果双发同时向对方发起了建立连接的请求怎么办?
  3. 如果 TCP 段被延迟了一段时间送达或者直接丢失了怎么办?

初始 SEQ 值(ISN)是在连接开始被建立时,通信双方各自独立确认的。由于 SEQ 是识别连接的关键字段,因此必须要确保这个值唯一而且不容易被猜到。TCP 序列号攻击就是通过猜测 SEQ 值,复制了 TCP 连接然后冒充授信主机与目标主机通信。

在最原始的规范中,ISN 是由计数器产生的,这个数字每隔 4 微秒递增一次。但这种方式太容易被攻击者猜到 SEQ 值了,所以在 TCP 实现中使用了更复杂的方式生成。

TCP 通信双方同时请求对方,A 发送给 B SYN ,同时 B 也发送给 A SYN,这是 A、B 都没有收到期望的 ACK,于是 A 响应 B 的 SYN,并将其发来的 SEQ + 1 返回给 B,同样的,B 也确认了 A 发来的 SYN,A、B 各自完成了三次握手,建立起了连接。也就是说,双方都需要进行 SYN - ACK。(可以参考这里的讨论: https://www.zhihu.com/question/47795741

最后,TCP 必须实现一个定时器,用于在连接超时时放弃连接。除此之外,还会支持连接重试,通常会使用指数退避的策略,但即便如此,也要设置一个最大的重试次数或者时间阈值,防止无限重试的问题,一旦触发阈值,就放弃连接。

TCP 可选项

TCP 头中的最后一个字段是一些附加的可选项,原始规范中提供了 3 个选项,但后来的规范中,不断增加了新的选项,接下来我们看看都有哪些常用的选项:

  • MSS(Maximum Segment Size): 该 TCP 协议实现的可以接收的最大 TCP 段的大小,比较典型的例子是 IPv4 中 TCP 的最大段为 1460 bytes
  • SACK(Selective Acknowledgment): 这个选项优化了数据包大量丢失并且接受者的数据窗口存在漏洞的情况。主要是因为 TCP 接收到的分组必须要能够根据其顺序组成完整的信息,丢掉其中任何一个都需要整个重传。而 SACK 就是允许 TCP 协议接收不连续的块,最后只需要重传丢失的块就可以了。
  • Window Scale: 窗口缩放选项,用于把窗口的大小从 65535 bytes 扩大到 1 G。具有更大的数据窗口,有利于批量的数据传输
  • Timestamps: TCP 时间戳,可以用这个时间戳计算每个 ACK 的 RTT,可以用来计算 TCP 重传超时

Lever

痕迹
没有过去,就没法认定现在的自己