TCP
总算是熬过了 rdt, 我写到这里的时候老师已经不知道讲到哪里了.
一些基础的概念
TCP 是一种面向连接的传输协议, 位于传输层. 这里的连接并不是真的连接, 而是一种逻辑连接. 同时, TCP 是一种点对点的通信, 只有两个端系统参与. 连接过程中, 双方都可以同时或者不同时向对方发信息, 是全双工服务.
TCP 数据包
一般来讲操作系统会自己实现 TCP 协议, 并给出一个 socket 供应用层调用. 应用层在将数据交给 socket 后, 数据就由 TCP 控制了. TCP 会把它们放在发送缓存里, 在合适的时候从发送缓存中取出数据打包并发送. 什么时候合适我们不知道, RFC 也没有说.
数据包大小(MSS&MTU)
TCP 取出发送缓存时并不是取出所有缓存, 而是根据 最大报文段长度(MSS) 取出. MSS 通常根据最初确定的由本地发送主机发送的最大链路层帧长度(最大传输单元(MTU))来设置.
要设置这个 MSS 还需要考虑 IP 首部长度和 TCP 首部长度(通常是 40 字节), 以太网和 PPP 链路层协议都具有 1500 字节的 MTU, 因此 MSS 的典型值是 1460 字节.
注意这里的 MSS 指的是报文段中应用层数据的最大长度而不是包括首部的 TCP 报文段的最大长度. TCP 报文段除了应用层数据部分, 还有首部.
数据包结构
常说的 TCP 数据包其实指的是 IP/TCP 数据包, TCP 处理的是 TCP 报文段, IP 层处理后才得到 TCP 数据包.
TCP 数据包包含这些东西:
IP 首部
- 版本
- 首部长度
- 服务类型
- TTL
- 源 ip
- 目标 ip
- 总长度
- 协议
- 首部校验和
- 一些可选项
TCP 首部
- 源端口号(16 位)
- 目标端口号(16 位)
- 序号(seq, 32 位)
- 确认号(ack, 32 位)
- 首部长度(4 位)
- 保留位(6 位)
- 控制位(6 位, URG, ACK, PSH, RST, SYN, FIN)
- 窗口大小(16 位)
- 校验和(16 位)
- 紧急指针(16 位)
- 可选项
数据部分
多少位不必在意, 只是写在这里方便哪天我想看而已.
tcp 的序号和确认号
序号
tcp 的序号并不单单给每一个 TCP 报文段编号, 而是给数据部分的所有字节编了号, 然后写在首部的序号指向数据部分第一个字节. 比如说现在应用层要发 2000 字节的数据, MSS 是 1000 字节, 那 TCP 就会分成两份报文段, 第一个报文段的序号是 0, 第二个报文段的序号是 1000.
注意这里只是一个假设, 实际上序号并不一定从 0 开始, 而是一个随机数. 理由会在后面给出.
确认号
tcp 的确认号是累计确认, 表示到确认号之前的数据都接收到了, 也表示期望的下一个序列号. 假设发送方发送了一个序号为 0, 数据部分长度为 500 的报文段, 那么接收方应该发送一个 ACK500. 这里的 500 表示 0~499 的数据都接收到了, 期望下一个报文段的序列号是 500.
当接收方接收到失序的分组时,比如现在接收到了 0~500, 期望 500, 但是接收到了 1000, RFC 没有给出明确的规则, 一般情况下接收方会保留失序的字节, 跟选择重传一样, 等待缺少的字节.
重传
首先需要注意的是, TCP 跟 rdt3.0 一样无法分辨是一个报文段或者它的 ACK 是损坏了, 丢失了, 还是超时了, 因此接收方在接收到错误的报文段时, 不再发送类似上次确认的 ACK, 而是干脆丢弃掉, 等待超时重传. 如果等不及(快速重传), 那么可以连续发送 3 个 ACK, 发送方在接收到 3 个对某一报文段的冗余 ACK 就知道要重传了.
超时重传
前置
设置超时时间需要考虑往返时间 RTT, 如果超时时间比 RTT 小的话那就很糟糕了, 基本每次都要重传. 这样的话, 就需要估计 RTT 了, 通常根据样本 RTT(SampleRTT, 指某个报文段被交给 IP 层发出到确认的时间量)估计. 大多数 TCP 的实现都仅在某一个时刻做一个 SampleRTT 测量, 不会为每一个报文段都测量一次, 也不会为已经重传过的报文段计算测量(如果是 ACK 延时到达, 那就可能测量量偏小), 只会为一个已经发送但是还没确认的报文段测量.
书上说的感觉有点绕, 感觉就是每隔一定时间就测量挑一个符合条件的报文段测量一次 SampleRTT.
这样会得到一些 SampleRTT, 由于网络复杂可能会有波动, 因此需要及时调整. 书上给了一个公式来计算新的 RTT
这个EstimatedRTT
指的是一个 SampleRTT 的加权平均值, 书上还给了一个 a 的推荐值 0.125(1/8). 这种平均叫做指数加权移动平均.
下面是一个用来估算 RTT 变化程度的式子
如果 SampleRTT 的波动比较小, 那么 DevRTT 的值就比较小; 如果波动比较大, 那么 DevRTT 的值就比较大. b 的推荐值是 0.25
超时时间的计算
推荐的 TimeoutInterval 初始值为 1s. 出现超时后 TimeoutInterval 翻倍. 之后只要收到报文段 ACK 并更新 EstimatedRTT, 就用上面这个公式更新 TimeoutInterval
超时实现
在 SR 中我们为每一个发送但未确认的分组准备了一个定时器, 但是这样开销太大. TCP 只使用了一个定时器, 开销虽然减少了, 但是代价是只为第一个发送但未确认的分组定时.
对于一次上层调用, 假设 TCP 分成了四个报文段(A, B, C, D), 一起发出去. TCP 会为第一个报文段 A 设置定时器. 假设现在收到 ACK B 的, 存入缓存, 不取消定时器; 收到 ACK A 时, 现在还有 C 和 D 没有确认, 那重启定时器, 这次给 C 定时(这里还会移动发送窗口). 现在 C 超时了, 那就会重启定时器, 再次发送 C.
超时时间的翻倍
当超时发生时, TCP 会将超时时间翻倍, 而不是使用之前的公式计算. 当接收到上层调用或者收到 ACK 时, TCP 会根据最近的 EstimatedRTT 和 DevRTT 计算超时时间.
这种超时时间翻倍具有一定的网络拥塞控制.
冗余 ACK 导致的重传
有两种情况会导致冗余 ACK 重传, 一种是该协议实现了快速重传, 允许接收方连续发送 3 个 ACK; 另一种则是接收方已经收到了三个失序且不同的数据包了, 由于 TCP 采用类似累计确认的 ACK, 因此接收方发送的三个 ACK 号都是一样的, 这样发送方会受到 3 个冗余 ACK, 就知道需要重传了.
GBN 和 SR?
TCP 既有 GBN 的累计确认, 也有 SR 的选择重传.
跟 GBN 不同的是, TCP 重传只会重传一个报文段, GBN 会重传这个以及之后的所有数据.
跟 SR 不同的是, TCP 使用累计确认.
流量控制
TCP 双方都会有一个接收缓存, 但是应用不一定立刻会读取, 可能在忙别的事情. 如果发送方发的很快, 接收方处理的很慢, 这样很容易就会导致溢出, 多余的数据会被丢弃, 发送方反复重传.
因此 TCP 提供了流量控制服务, 防止发送方发送太快导致接收方缓存溢出. TCP 让发送方维护一个接收窗口, 这个接收窗口指示发送方接收方还有多少可用的缓存空间.
假如现在 A 要向 B 发送一个大文件, 文件大小是 RevBuffer, B 偶尔会从缓存中读数据出来. 定义这些变量
LastByteRead
: B 上应用程序从缓存读出来的最后一个字节的编号LastByteRcvd
: 从网络中到达的, 并且放在 B 的接收缓存中的最后一个字节的编号
为了保证缓存溢出, 需要让这个不等式成立
用rwnd
来表示接收窗口, 那么应该有这个等式
这个空间是动态的, 因此 B 应该告诉 A 自己接收缓存空间的大小.
在 TCP 报文段中有一个字段就是 rwnd 的值, 告诉发送方接收方的接收缓存剩余空间.
A 需要跟踪两个变量, 一个是上一次发送的最后一个字节编号LastByteSend
, 另一个是最后一个被确认的字节编号LastByteAcked
, 这两个变量之差就是 A 发出去但是还没确认的数据量. 这个数据量应该要小于 rwnd, 也就是
rwnd 为 0 的情况
根据刚刚这个不等式, 可以想到一个极端情况, rwnd 恰好为 0. 在这种情况下发送方看上去不应该继续发任何数据了, 因此进入了阻塞. 如果之后 B 处理好了数据, 但是 B 自己不打算发数据, 那就一直卡在这里了. 为了解决这个问题, TCP 规范要求接收方 rwnd=0 时, A 继续发送报文段, 但是这个报文段只有一个字节数据.
TCP 的连接管理
首先, 发送方会发起连接请求(第一个握手包, 不携带数据), 接收方会响应这个请求, 发送一个响应请求(第二个握手包, 不携带数据), 进入半连接状态, 在得到第二个握手包后, 发送方发送第三个握手包(可以携带数据), 进入连接状态(ESTABLISHED). 接收方接收到第三个握手包, 进入连接状态. 至此, 三次握手完成.
然后就开始正式的数据传输了.
在数据传输结束后, 就是四次挥手了. 主动关闭方会发送一个 FIN 包, 通知对方自己已经没有数据要发了, 然后进入 FIN_WAIT_1 状态. 被动关闭方接收到 FIN 包后, 响应一个 ACK 告知得到了 FIN 包, 主动关闭方进入 FIN_WAIT_2 状态, 在被动关闭方发完数据后, 也会发送一个 FIN 包, 通知对方自己发完了, 主动关闭方收到后响应一个 ACK, 进入 TIME_WAIT 状态. 等待多久与具体实现有关, 等待后主动关闭方所有资源都会被释放.
SYN 洪泛攻击
服务器接收到发送方发送的第一个包时, 需要分配一些资源来保存发送方的序列号和己方的序列号等变量, 以便第三次握手及后续数据传输时使用. 这里就有问题了, 假如有一群 TCP 连接来, 只发了第一个握手包不发第三个, 这样子服务器就会分配大量的资源给这些无用的连接, 之后即使有正常的用户也连接不了.
判断一个握手包是不是攻击是很难的, 只能从分配资源下手. 假如这里不分配资源的话, 那么第三次握手包到来时, 服务端不知道这是哪一个连接, 也不知道 ACK 对不对. 如果能让用户自己发校验信息就好了.
可以简单研究一下第三个握手包会有什么东西: 一个 ACK 号来指示下一个期望序列号, 一个序列号和可能存在的数据. 用户的序列号和数据我们无法决定, 但是 ACK 号是我们可以决定的. ACK 号-1 就是我们发送的初始序列号.
因此只需要将用户的一些信息(源, 目的 IP 和端口号等, 以及服务端的一个 secretKey), 经过一些转换(散列函数)计算出一个初始序列号. 当服务端接收到第三个握手包时, 再计算一次, 然后比对初始序列号, 如果一样说明是合法的连接, 此时服务端在准备资源.
如果用户没有发送第三个握手包也没有所谓, 服务端做的也只是计算一个值而已, 并不用专门准备一块空间来存数据, 这样避免了资源消耗.