TCP 是因特网协议栈中运输层(传输层)常用的协议,该协议的全称为 Transmission Control Protocol(传输控制协议),它提供了一种可靠的数据传输,而因特网协议栈中运输层的另一种常用的协议则不提供可靠的数据传输,它在网络层提供的服务基础上不提供不必要的服务。本篇我们将从 TCP 的可靠数据传输、流量控制和拥塞控制几个方面简单介绍作为运输层的 TCP 协议提供的服务,顺便简单介绍一下 TCP 的连接管理。
0. TCP 报文段(Segment)结构
源端口:发送方应用程序所在的端口;
目的端口:接收方应用程序所在的端口(可能不存在);
序号:报文段中的数据部分首个字节的编号;
确认号:标识接收方期望接收的下一个数组分组的最小编号;
检验和:用于差错检验;
接收窗口:用于流量控制,接收方允许发送方发送的数据的最大长度。
1. 可靠数据传输
在了解 TCP 的可靠数据传输的具体实现之前,我们先了解一下什么样的数据传输可以称为是可靠数据传输。简单来说就是接收端收到的数据要和发送端发送的原始数据是完全相同的,这就需要保证接收端收到的数据没有差错,没有丢包,没有冗余同时还是有序的。因为 TCP 协议作为运输层的协议,它所提供的数据服务是建立在尽力而为的网络层协议——IP 协议之上的,因此可靠数据传输的保证需要在 TCP 协议中去实现。
TCP 使用这些主要技术来实现可靠数据传输:滑动窗口,超时重传,快速重传,累积确认,超时间隔加倍。
发送端主要通过维护这几个变量来实现滑动窗口协议,表示最早的未被确认分组序号 send_base,下一个要发送的数据分组的序号 nextseqnum(TCP 会隐式的为应用层到达的数据的每一个字节进行编号,每个传输层报文段中数据字段的首个字节的序号被用来作为该报文段的序号,假如一个分组中数据字段的首个字节的序号为 100,即该分组被发送前的 nextseqnum 值为 100,假设该分组中数据字段的长度为 500 字节,则该数据被发送之后,nextseqnum 就是 600),以及表示窗口长度的变量 N,N 的大小由流量控制中的接收窗口以及拥塞控制中的拥塞窗口决定。
初始状态时,发送端可以一次连续发送 N 个数据分组而不必等待接收端的确认。当发送端收到确认报文段,且报文段中的 ACK 的值 y,则表示接收端已经收到了序号 y 之前的所有报文段,这种确认方式就叫做累积确认。假如 y > send_base,发送端会将 send_base 的值置为 y(这里注意,就算序号小于 y 的多个报文段都在等待确认,也会认为这些都被正确接收,这时基于累积确认得到的结果),可以发送的数据分组的序号必须落在 [send_base, send_base + N] 之间,因此可发送报文段序号区间可以被看作长度为 N,且随着确认的接收而不断向前滑动的窗口。当新的数据分组从应用层到达运输层时,发送端会判断数据分组的序号是否在发送窗口中,在的话会将分组发送,同时更新 nextseqnum 的值。
当某一批数据分组被发送时,该批分组中最早的分组被发送后会启动一个定时器(超时时间通过公式计算得到),加入在定时器超时(可能由于分组或者 ACK 丢包导致)之前未收到该分组的确认,发送端会重传该分组(超时重传)并重启定时器,并将超时时间设置为之前的两倍(这种方式被称为超时时间加倍,),当再次收到正常的确认报文段后,会将超时时间恢复为通过公式计算的值,但是实际中有可能在超时之前发送端会收到来自接收端发送的冗余 ACK 来进行快速重传(当接收端收到来自发送端的失序的分组时,会发送 3 个冗余的 ACK 来通知发送端进行快速重传),假设接收端在等待序号为 x 的报文段的到达,但是却收到了序号小于 x 的报文段失序报文段,这时接收端会假定 x 序号的报文段丢失(实际可能由于在数据链路中的排队导致了乱序,或者真的发生了丢包),并发送三个冗余的 ACK 报文段,报文段中确认号为 x,以此来通知发送端重发,快速重传机制可以有效的降低超时重传带来的长时间等待,但同时也可能引起不必要的重传(针对不必要的重传,接收端会忽略冗余的报文段)。只有当乱序报文段中的间隙被填充时,接收端才会将多个连续的报文段交给上层去处理,这样保证了报文段的有序性。
上面描述的这些机制,保证了接收端收到的数据没有丢包、没有冗余并且有序,那么没有差错是如何保证的呢?TCP 协议主要使用报文段首部字段中的检验和字段来进行差错检验,具体检验过程与 UDP 协议的基本一样,将报文段中所有 16 比特字进行求和,求和过程中遇到溢出就会回卷,最后将得到的和进行反码运算得到的就是检验和的值,接收端收到报文段之后会将报文段中所有 16 比特字进行求和,求和过程中遇到溢出就会回卷,将得到的和同检验和字段相加,得到的结果必须为 1111 1111 1111 1111,否则说明报文端出现差错,需要发送端重传。
2. 流量控制
接收方接收到的数据会先放在接收缓存中,接收缓存是在连接建立期间创建的,接收窗口的最大值不能超过缓存的大小,RcvBuffer 表示接收缓存的大小,rwnd 表示接收窗口的大小,rwnd = RcvBuffer - 缓存中未处理的数据大小。缓存中未处理的数据大小 = 接收的总字节数 - 已处理的总字节数。由此可知窗口的大小是在不断变化的,当接收方读取数据的速度快时,窗口的尺寸就大,发送方可以连续发送的数据包的数量就相对较多,否则就少。在发送端要保证已发送的数据大小 - 已确认的数据大小 <= rwnd,以此来实现流量的控制,使得发送方的数据发送速率和接收方的处理速度相匹配。
3. 拥塞控制
流量控制主要是为了达到发送方的数据发送速率和接收方的处理速度相匹配。而发送方也可能因为网络拥塞而被遏制,发送方对这种情况的处理称为拥塞控制。
在了解拥塞控制之前,我们先了解一下发送放是如何推断网络拥塞控制的,对于 TCP 协议而言,主要是通过两种方式,一种是客户端通过超时判断,另一种是根据来自接收端的冗余 ACK 来判断。接收端会在每批发送的数据发送时启动一个定时器,在定时器超时之前如果没有收到应答,则预测链路发生了拥塞(可能是数据分组或应答发生丢包或长时间排队)。接收端每次收到一个来自发送端的分组时,也会启动一个定时器,在定时器超时之前没有收到预期的数据分组,会发送 3 个冗余的 ACK 提示客户端出现网络拥塞。
TCP 的拥塞控制算法主要包括三部分:慢启动,避免拥塞和快速恢复。
TCP 的发送方除了维护接收窗口外,还会维护一个拥塞窗口 cwnd,在任意时刻发送方向接收方连续发送的数据不能超过 min(rwnd, cwnd),在拥塞控制中还有一个重要变量叫慢启动阈值(ssthresh,初始大小为 64kB),当 cwnd 到达这个阈值时,发送发就由慢启动进入避免拥塞状态。
慢启动:当一条 TCP 连接开始时,cwnd 被设置成 1 个 MSS 的大小(Maximum Segment Size,最大报文段尺寸),即一个 RTT(Round-Trip Time,往返时间,从发送端发送报文段到收到接收端的确认之间经历的时间)中只发送一个报文段,每当接收到一个应答,就将 cwnd 增加一个 MSS,因此慢启动阶段发送速率是指数增长的。
避免拥塞:当 cwnd 的值增加到慢启动阈值后,就进入避免拥塞状态,该状态下,每经过一个 RTT,cwnd 的大小只会增加一个 MSS,因此这个阶段 cwnd 是线性增长的。
快速恢复:慢启动或者是避免拥塞阶段如果根据冗余 ACK 推断出拥塞,会进入快速恢复阶段,快速恢复阶段会等待新的 ACK 的到达,如果在超时之前收到新的 ACK,则切换到避免拥塞阶段,否则切换到慢启动阶段,切换到慢启动状态时会将慢启动阈值降低至拥塞窗口的一半。
TCP 的拥塞控制 FSM 描述如下图:
TCP 的这种拥塞控制算法被称为加性增乘性减的拥塞控制方式,这种方式在实现了拥塞控制的同时,也给网络带宽中的 TCP 连接提供了相对的公平性,同时也提升了在空闲带宽的利用率,当大家发现网络拥塞时,会试图降低其发送速率来降低拥塞,发现网络畅通时就适当增加发送速率。
通过对拥塞控制的了解,我们不难发现,当发送端需要向一个距离较远的主机发送一个数据量较大的应用报文时,可能需要等待若干个 RTT 的时间在能收到响应。TCP 分岔可以很好的解决这个问题。简单来说,TCP 分岔就是服务提供方将请求通过 CDN 定向到距离用户比较近的一台前端服务器,这台服务器与远程的数据中心保持有一条带宽比较大的连接,客户与该服务器建立 TCP 连接,并发送数据到该服务器。前端服务器将请求转发给数据中心处理(只需要一个 RTT 的时间),然后再将响应转发给用户。我们用如下参数表示请求过程中的时间:
R
T
T
C
S
−
表
示
客
户
端
到
数
据
中
心
的
数
据
往
返
时
间
;
R
T
T
C
F
−
表
示
客
户
端
到
前
端
服
务
器
的
数
据
往
返
时
间
;
R
T
T
F
S
−
表
示
前
端
服
务
器
到
数
据
中
心
的
数
据
往
返
时
间
;
当
客
户
端
发
送
一
个
请
求
到
远
程
的
数
据
中
心
时
,
慢
启
动
阶
段
通
常
要
经
历
4
个
R
T
T
C
S
的
时
延
(
不
考
虑
远
程
服
务
器
处
理
时
间
)
才
能
得
到
请
求
的
结
果
(
一
个
用
于
T
C
P
连
接
的
建
立
,
三
个
用
于
应
用
层
数
据
传
输
)
,
如
果
通
过
前
端
服
务
器
进
行
转
发
,
只
需
要
4
∗
R
T
T
C
F
+
R
T
T
F
S
,
而
R
T
T
C
F
相
比
R
T
T
F
S
及
R
T
T
C
S
很
小
,
因
此
可
以
忽
略
,
进
而
得
到
4
∗
R
T
T
C
F
+
R
T
T
F
S
≈
R
T
T
C
S
。
因
此
,
使
用
T
C
P
分
岔
将
用
户
感
受
到
的
时
延
由
4
∗
R
T
T
C
S
降
到
了
R
T
T
C
S
。
RTT_{CS}-表示客户端到数据中心的数据往返时间;\\ RTT_{CF}-表示客户端到前端服务器的数据往返时间;\\ RTT_{FS}-表示前端服务器到数据中心的数据往返时间;\\ 当客户端发送一个请求到远程的数据中心时,慢启动阶段\\ 通常要经历 4 个 RTT_{CS} 的时延(不考虑远程服务器处理时间)\\ 才能得到请求的结果(一个用于 TCP 连接的建立,三个用于应用\\ 层数据传输),如果通过前端服务器进行转发,\\ 只需要 4 * RTT_{CF} + RTT_{FS},而 RTT_{CF} 相比 RTT_{FS} \\ 及 RTT_{CS} 很小,因此可以忽略,进而得到 \\ 4 * RTT_{CF} + RTT_{FS} \approx RTT_{CS}。因此,使用 TCP 分岔将\\ 用户感受到的时延由 4*RTT_{CS} 降到了 RTT_{CS}。
RTTCS−表示客户端到数据中心的数据往返时间;RTTCF−表示客户端到前端服务器的数据往返时间;RTTFS−表示前端服务器到数据中心的数据往返时间;当客户端发送一个请求到远程的数据中心时,慢启动阶段通常要经历4个RTTCS的时延(不考虑远程服务器处理时间)才能得到请求的结果(一个用于TCP连接的建立,三个用于应用层数据传输),如果通过前端服务器进行转发,只需要4∗RTTCF+RTTFS,而RTTCF相比RTTFS及RTTCS很小,因此可以忽略,进而得到4∗RTTCF+RTTFS≈RTTCS。因此,使用TCP分岔将用户感受到的时延由4∗RTTCS降到了RTTCS。
4. 连接管理
4.1 连接创建
连接建立之前需要服务端在某个端口运行一个进程来监听来自客户的请求。
第一步,客户端的 TCP 首先向服务端的 TCP 发送一个特殊的报文段,报文段中不包含应用层数据。报文段的首部中 SYN 被置为 1,并且会选择一个随机的序号作为初始序号,记为 seq = client_isn。
第二步,当服务端收到来自客户端的报文段后,提取出 TCP SYN 标识,为该连接分配变量和缓存后,向客户端返回允许连接的报文段,报文段中也不包含应用层数据,报文段中的 SYN 也被置为 1,确认号 ack = client_isn + 1,同时服务端也会生成一个自己的初始序号,记为 seq = server_isn。
第三步,客户端收到来自服务端的确认后,也要为服务端分配变量和缓存,并向服务方发送应答报文段,这个报文段中可以捎带应用层数据,这时 SYN 被置为 0,因为是对服务端报文段的确认,因此确认号 ack = server_isn + 1,报文段的序号为 seq = client_isn + 1。
4.2 连接关闭
连接的关闭可以由客户端发起,也可以由服务端发起,我们假设是由客户端发起的关闭连接。
第一步,客户端发送关闭连接的特殊报文段,报文段中的首部 FIN 位被置为 1。
第二步,服务端收到报文段后,会返回一个确认报文段,服务端准备好之后才会进入下一步。
第三步,服务端发送关闭连接的特殊报文段,报文段中的首部 FIN 位被置为 1,服务端收到该报文段的确认之后会关闭连接。
第四步,客户端收到报文段后,会返回一个确认报文,发送确认之后会等待 2MSL(最大段生命周期)的时间来等待由于丢包或差错导致的服务端的重发,之后客户端关闭连接。
4.3 连接的生命周期
4.4 SYN 洪范攻击
前面在介绍 TCP 连接的建立时,当服务端收到客户端的 SYN 报文段后,会为连接分配变量和接收缓存,处于此种状态的连接我们称为半连接,如果某台恶意的机器虚拟大量的 IP 地址作为源地址,向某台服务器发送 SYN 报文,这时就会导致服务端由于资源被大量半连接占用二导致正常的请求无法建立,这种 DoS 攻击被称为 SYN 洪范攻击。
针对这种类型的攻击,SYN cookie 是一种很好的防御措施。
SYN cookie 的工作过程如下:
当服务端收到来自客户端的 SYN 报文段后,并不建立半开的连接(不分配变量和缓存)。同时,在确认报文段中使用的初始序号经特殊的算法生成,该算法使用源地址和端口号以及目的地址和端口号作为入参(得到的值被称为 cookie),而且服务端并不保存该值得状态。当服务端收到客户端 ACK 后,会根据源地址和端口号以及目的地址和端口号,生成序列号,将该序列号加一得到的结果与 ACK 中的确认号比较,如果相等再为连接分配变量和缓存。
参考资料:《计算机网络——自顶向下方法(原书第7版)》