哈工大计算机网络传输层协议详解之:TCP协议
文章目录
TCP概述
- 点对点通信
- 一个发送方、一个接收方
- 可靠的、按序的字节流
- 流水线机制
- TCP拥塞控制和流量控制机制
- 设置窗口尺寸(基于拥塞控制、流量控制动态的调整窗口尺寸)
- 介于GBN和SR之间的机制
- 发送方/接收方缓存
- 全双工(full-duplex)
- 同一连接中能够传输双向数据流
- 面向连接
- 通信双发在发送数据之前必须建立连接。
- 连接状态只在连接的两端中维护,在沿途节点中并不维护状态。
- TCP连接包括:两台主机上的缓存、连接状态变量、socket等。
- 拥塞控制、流量控制机制
TCP段结构
在传输层处理的是一个个的TCP段Segment。
TCP段里的序列号和ACK号不是段的编号,而是利用数据的字节数来计数得到的。
- U: urgent data,表示紧急数据
- A: 表示一个标志位,来指示ACK number的字段是否为一个有效的ACK。
- P: Push data now,标识把数据立即向上层传递
- RST、SYN、FIN: 连接的建立、销毁等等状态的标志。
- Receive window: 接收方窗口的大小,标识还能接收的字节的数目,可以用来做流量控制。
- checksum: 校验和
序列号和ACK
序列号:
- 序列号指的是段segment中第一个字节的编号,而不是segment的编号
- 比如,有一个1KB的数据拆成两段,第二个段的序列号不是1(假设序列号从0开始),而是500(从字节0开始编号,500是第二个段的字节起始位)。也就是说,这个序列号是每个段第一个起始字节位的编号,而不是个数的编号。
- 为什么要这么选?
- 建立TCP连接时,双方随即选择开始的序列号,并在连接过程中,相互交彼此的信息。
ACKs
- 表示希望接收到的下一个字节的序列号
- 累计确认:该序列号之前的所有字节均已被正确接收到
Q:接收方如何处理乱序到达的Segment?
- TCP规范中没有规定,由TCP的实现者做出决策。
- 比如在GBN协议中,对于乱序到达的Segment会丢弃,而在SR协议中,乱序到达的Segment会被缓存起来。
这里给出一个telnet
远程登陆的应用例子:
上图中,在发送数据前,主机A和B已经建立连接并交换过信息,因此,这里初始的Seq=42和Ack=79都是之前建立连接时随机选取的初始序号。
- 在主机A向主机B发送数据C时,该Segment的初始序列号为42,期望收到的下个ACK的序号为79。
- 主机B接收到数据C后,会返回ACK确认,此时的Segment段的序列号为79,对应于上一个Segment的期望ACK。而段的ACK值为43,是因为本次发送的数据C是一个字符,只有一个字节,因此接收方期望的下一个段的开始字节应该是43。
- 主机A收到ACK后,还要再发一个ACK确认,此时不携带任何数据。序列号是43,对应于上一个ACK=43。ACK=80,对应于上一个Seq=79,表示发送方期望的下一个序列号是80开始。
TCP可靠数据传输
- TCP在IP层提供的不可靠服务基础上实现可靠数据传输服务
- 流水线机制,以便提高性能
- 累计确认
- TCP使用单一重传定时器
- 触发重传的事件包括
- 超时
- 收到重复ACK
RTT和超时
问题:如何设置定时器的超时时间?
- 大于RTT:但是RTT并不是一个常量,RTT是变化的,根据网络传输情况而变化,同时RTT本身的测量也是个问题。
- 过短:导致不必要的重传
- 多长:对段丢失反应慢
问题:如何估计RTT的时间
- SampleRTT:测量从段发出去到收到ACK的时间(需要忽略重传)
- SampleRTT变化:测量多个SampleRTT,求平均值,形成RTT的估计值EstimatedRTT。
采用指数加权移动平均的方法,使用新的SampleRTT来更新当前的估计值,即考虑了历史结果,也考虑了最新的测量数值。
确认了RTT的估计值后,需要依据RTT来设置对应的定时器
定时器超时时间的设置:
- EstimatedRTT+“安全边界“。
- EstimatedRTT变化大—>较大的边界。
因为实际需要设置的超时时间是要比EsitimatedRTT大,因此还需要加上一个边界,这个边界值的设置需要与网络当前的状况相关联。如果网络传输状态较好,则可以设置一个较小的边界,否则需要设置一个较大的边界。
测量RTT的变化值:SampleRTT与EstimatedRTT的差值
类似于做方差,得到的差值DevRTT一定程度上表示网络的拥塞状况。
因此,有了上述值后,定时器超时时间的设置就可以如下所示:
思考:为什么是4倍的DevRTT?这样的设置是否合理?
TCP发送方事件
从应用层接收数据
- 创建Segment
- 序列号是Segment第一个字节的编号
- 开启计时器
- 设置超时时间TimeOutInterval
超时
- 重传引起超时的Segment(只会重传引起超时的那一个Segment)
- 重启定时器
收到ACK
- 如果确认是此前未确认的Segment
- 更新SendBase
- 如果窗口中还有未被确认的分组,重新启动定时器
TCP发送端程序伪代码
TCP发送端程序伪代码如下所示:
首先初始化一个SendBase和NextSeqNum变量,用于标识发送端滑动窗口的相关参数。接下来是一个无限循环的过程,用switch方法区分不同的event事件。
- 如果event事件是接收从上层应用层传递的数据,则创建该数据对应的TCP Segment段,其中Seq序列号即为最初初始化的NextSeqNum。如果不存在定时器,则启动定时器。将段传递到IP层。下一个NextSeqNum = NextSeqNum+该段传输数据的字节大小。
- 如果发生的是timeout事件:重传具有最小序列号的,并且还没有被确认过的的段Segment。并重启启动定时器
- 如果发生的是ACK确认:如果ACK确认的序列号是大于SendBase的,意味着有新的数据被确认了,由于TCP采用的是累计确认的机制,接收到ACK序列号y表示序列号y之前的Segment都被确认了。因此,更新SendBase = y。如果还有未被确认的Segment,则重启启动定时器。
TCP重传示例
左图表示了一个ACK丢失的场景。主机A向主机B发送一个8字节大小的数据,序列号为92。主机B向主机A返回一个ACK=100,因为上一个请求的序列号为92,而发送数据大小为为8字节,因此接收方期望的发送方下一个Segment的Seq应该为100。
但是这个返回的ACK丢失了,主机A发送timeout时间,因此又重传了刚刚的8字节数据。
第二次发送方收到了这个ACK=100,因此将SendBase更新为100。下一次再发送Segment则使用100这个序号。‘
右图表示了timeout时间设置过短的场景。主机A基于流水线机制,连续发送了两个Segment,一个为8字节,一个为20字节。主机B两个Segment都收到了,分别返回ACK=100,ACK=120。但是发送端的超时时间设置多短,导致还未收到ACK=100时,直接重传了第一个Seq=92的数据,然后才陆续收到这两个返回的ACK,并根据ACK序号更新SendBase。而之后,接收方又收到了重传的Seq=92的Segment,由于TCP采用的是累计确认机制,因此即使接收方收到Seq=92,仍然返回的ACK=120。因此在累计确认机制下,每个ACK的的确认序号都表示当前序号前的Segment已经被确认了。
再来看下另一个例子
这次主机A在发送Seq=92和Seq=100的两个序号后,第一个ACK=100的确认丢失了,但是第二个ACK=120的确认正确到达了发送方。还是由于采用的是累计确认机制,发送端在ACK=120后,有理由相信序号120前的Segment都已经被接收方正确接收了,因此直接更新SendBase = 120,而不关心ACK=100丢失的问题。
TCP接收方 ACK生成
TCP接收方,重点是ACK的生成原理和对于场景
左边表示接收方的事件,右边表示该事件触发的接收方操作。
- 在接收方到了一个按需到达的Segment,序列号为seq的事件。此时接收方会稍微延迟发送ACK,因为是累计确认机制,会延迟等待(最多500ms)看是否还有下一个Segment到达,如果没有,则发送这个Segment的ACK。
- 如果有按序到达的Segment,并且前面还有一个Segment在等待发送ACK确认(也就是上一个场景下的等待500ms时有另一个段到达)。此时,接收方触发的操作是,立即发送这两个段的ACK确认。
- 如果有一个乱序到的Segment,此时接收方触发的操作是,会立即发送一个重复的ACK,表示期望的下一字节的seq。
TCP快速重传机制
TCP实现中,如果发生超时,超时时间间隔将重新设置,即将超时时间间隔加倍,导致其很大。
- 重发丢失的分组之前要等待很长时间
因此,需要设置额外的重传机制来应对这种情况
通过重复ACK检测分组丢失
- Sender会背靠背地发送多个分组
- 如果某个分组丢失,可能会引发多个重发地ACK
因此TCP采用的是流水线和累计确认机制,通过上面的分析我们已经直到,如果接收方收到了乱序的Segment,发送的ACK仍然是那个期望序列号的ACK,也就是期望按序到达的Segment的序号。因此,在流水线机制下,如果有一个Segment丢失了,那么其他的Segment到达后,接收方发送的ACK序号都是那个丢失Segment的序号。因此如果发送方检测到多个某个序号的ACK,就可能说明这个Segment丢失了。
如果Sender收到对同一数据的3个ACK,则假定该数据的段已经丢失
- 快速重传:在定时器超时之前即进行重传。
快速重传算法伪代码:
思考:为什么是3次ACK进行快速重传?
TCP流量控制
接收方为TCP连接分配buffer
左边是有数据从IP层发送过来,然后将数据放到RcvBuffer缓存里,最后向上交付到应用层。蓝色部分表示目前接收端可以用来接收buffer部分。
此时,如果上层应用处理buffer中数据的速度较慢,而发送方发送的数据速率过快,就会导致接收方没有那么多的缓存大小来接收数据,导致buffer缓存溢出。
因此,流量控制的目的就是让发送方不会传输的太多、太快以至于淹没接收方(buffer溢出)。
本质上,流量控制是一场速度匹配机制。
由上图可知,Buffer中的可用空间(spare room)
= RcvWindow
= RcvBuffer - [LastByteRcvd - LastByteRead]
Receiver通过在Segment的头部字段(TCP段的Receive Window字段)将RcvWindow告诉Sender,Sender限制自己已经发送的但还未收到ACK的数据不超过接收方的空闲RcvWindow尺寸。
假设接收方Buffer已经满了,Receiver告知Sender RcvWindow=0,会出现什么情况?
当RcvWindow=0时,即接收方缓存满了,要求发送方不能发送数据了。但是这种情况下,发送方和接收方就无法交互了,也就是说,当后续接收方即使有了空余的buffer,也无法再发送给接收端告知它继续发送数据了。
因此,对于这种情况,需要一些额外的处理。即使RcvWindow=0,在TCP中,发送端仍然可以发送一个很小的一个段Segment,从而带回来接收端的最新buffer大小,以此来解决上述问题。
TCP连接管理
TCP是一个面向连接的协议,在传输实际数据前,需要首先建立连接。除了建立连接外,TCP还需要初始化TCP变量,比如SeqNumber、Buffer和流量控制信息等。数据传输完后,还需要处理连接的拆除。
TCP在建立连接的过程中,采用的是三次握手的机制。
- 客户端向服务端发送一个TCP段Segment,该Segment的标志位为SYN,SYN=1,表示该段为SYN报文段,是不携带任何数据的。同时还需要生成一个初始的序列号(一般随机生成,或者使用定义的机制)。
- 服务器收到了SYN报文段后,会返回SYNACK报文段。此时,服务器会为这个连接建议缓存和分配对应的资源,比如Socket。也会初始化服务端的序列号告知客户端。
- 客户端收到服务端的SYNACK报文段后,会再答复一个ACK报文段,此时段中的SYN标志位就不再置1了。在这个报文段中,是可以包含数据的。
思考,为什么建立连接是3次握手?2次行不行?经典的TCP建立连接问题
TCP连接建立示意图如下所示:
TCP连接管理:关闭
TCP连接的关闭,从服务端或客户端发起都可以,一般主要从客户端发起。
- 客户端向服务端发送TCP FIN控制报文段segment(也是利用了TCP报文段中的标志位FIN)。
- 服务器收到FIN报文段后,回复ACK,关闭连接,发送FIN。
- 客户端收到FIN后,回复ACK。进入“等待”状态,确保服务器端能够正确的关闭并且释放资源。在等待的过程中,如果重复收到FIN报文段,会重新发送ACK。
- 当服务端收到ACK,连接关闭。
TCP客户端生命周期
TCP服务端生命周期