January 16, 2019
阅读原文:http://www.saminiir.com/lets-code-tcp-ip-stack-3-tcp-handshake/
TCP 运行在 OSI 模型的第 4 层 —— 传输层上,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 表头中包含来源端口信息和目的端口信息,通过端口,网络堆栈知道将流量定向到何处。字段占 16 bit,因此端口值的范围是 0 ~ 65535。
序列号码表示 TCP 段的窗口索引,TCP 流中的每个字节都被编号串联起来。在进行握手的阶段,会先生成一个初始序列号码,之后在此基础上递增。
确认号码是发送方期望接收的下一个字节的窗口索引,握手完成后,必须始终填充 ACK 字段。
第四行中的几个缩写对应的含义如下:
窗口大小是指该窗口的大小,即接收方将要接受的字节数,该字段占 16 bit,因此窗口大小最大为 65535 bytes。
校验和字段用于验证 TCP 段的完整性,其算法本质上与 IP 中的校验和算法相同。
紧急指针,是在 URG 设置为 1 时生效,这个指针表示紧急数据在整个流中的位置。
一个 TCP 连接通常会经历以下 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
这是建立一个 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 头中的最后一个字段是一些附加的可选项,原始规范中提供了 3 个选项,但后来的规范中,不断增加了新的选项,接下来我们看看都有哪些常用的选项:
痕迹
没有过去,就没法认定现在的自己