<
  • 主题:
  • + -
  • 清除背景
  • 禁用背景

计算机网络(二)运输层

字数:17343 写于:2020-03-25
最新更新:2020-03-25 阅读本文预计花费您50分钟

运输层

运输层为应用进程之间提供端到端的通信,主要有两个协议:

  • 用户数据报协议UDP(User Datagram Protocol)
  • 传输控制协议TCP(Transmission Control Protocol)

在OSI术语中,它们传输的数据单元称为运输协议数据单元TPDU(Transport Protocol Data Unit),在TCP/IP体系中,分别称为TCP报文段UDP用户数据报

应用层协议/服务 说明 运输层协议
DNS 域名系统 UDP
TFTP 简单文件传送协议 UDP
RIP 路由信息协议 UDP
DHCP 动态主机配置协议 UDP
SNMP 简单网络管理协议 UDP
NFS 网络文件系统 UDP
自拟 IP电话 UDP
自拟 流式多媒体 UDP
IGMP 网际组管理协议 UDP
SMTP 简单邮件传送协议 TCP
TELNET 远程终端协议 TCP
HTTP 超文本传送协议 TCP
FTP 文件传送协议 TCP

端口

端口(又称协议端口号,protocol port number)是运输层用来标识同一主机上不同网络进程的逻辑标识符,使操作系统能够将收到的运输层报文准确地交付给对应的目标应用程序。

TCP/IP协议体系的运输层使用一个16bit的数作为端口号,即允许使用0-65535之间的无符号整型值作为端口号,这些端口号主要分为两大类:

  • 服务端端口号:主要分为以下两种:
    • 熟知端口号:范围为0-1023,它们被保留给一些常见的通用应用层协议,这些值可以在www.iana.ora上查到
    • 登记端口号:范围为1024-49151,通常是一些主流应用程序使用(如MySQL的3306),需要向IANA登记
  • 客户端端口号:范围为49152-65535,它们通常会被动态分配给客户进程,又称为短暂端口号

UDP

UDP的特点

用户数据报协议UDP(User Datagram Protocol)有以下特点:

  • UDP是无连接的,发送数据之前不需要建立连接,之后不需要释放连接
  • UDP只提供尽力而为的交付,不保证可靠交付
  • UDP是面向报文的,在发送方,应用程序交付的报文无论多长,UDP添加部首后就直接立即交付IP层,不拆分不合并。在接收方,对IP层交付的UDP用户数据报,UDP去除首部后直接交付应用层。
  • UDP没有拥塞控制
  • UDP支持一对一、一对多、多对一、多对多的通信
  • UDP的首部开销很小,只有8个字节
  • 当接收方UDP发现报文中的目的端口号不正确,会丢弃报文,并通过ICMP协议发送”端口不可达”差错报文给发送方

UDP的套接字描述

UDP是一种无连接协议,UDP套接字由目的IP地址与目的端口号这一二元组唯一标识,即:

UDP接收套接字 = {(目的IP:目的端口)}

这意味着,只要两个UDP报文段的目的IP与目的端口相同,即便它们的源IP或源端口完全不同,操作系统内核也会将它们定向到同一个UDP套接字的接收缓冲区中。这为一台服务器通过单一端口接收来自任意数量客户端的消息提供了基础支撑;同时,这也要求应用程序必须自行通过报文载荷中的信息(如会话ID)来区分不同的通信对端,因为传输层不再提供基于源信息的自动隔离机制。

UDP用户数据报

UDP用户数据报只有首部字段数据字段两个字段。首部字段有4个字段,每个字段2个字节:

  • 源端口:在需要对方回信时使用,不需要时可用全0
  • 目的端口:用于在终点交付报文
  • 长度:UDP用户数据报的长度(首部+数据),单位为字节,最大值为65535,最小为8(只有首部),但由于IP层限制,实质能携带的数据长度为65535-8(UDP头)-20(IP头)=65507字节
  • 检验和:检测用户数据报是否有错,错误直接丢弃(某些实现会向应用层返回警告),无差错恢复能力
loading
udp

UDP检验和

UDP在计算检验和时,会在UDP用户数据报之前增加12字节伪首部,这些伪首部仅用于计算检验和,不进行传输。伪首部中的信息部分来源于IP层,伪首部包含以下字段:

  • 源IP地址
  • 目的IP地址
  • 1字节的填充0
  • IP首部中的表示协议字段的值,UDP协议的值为17
  • UDP报文长度
loading
udp2

UDP计算检验和会将首部和数据部分都一起进行检验,遵循以下流程:

  • 将伪首部、首部、数据部分割为16位的字串
  • 在最初的检验和字段填充全0
  • 如果UDP报文的数据部分不是偶数字节,则填充一个全0字节(该字节不会被发送,只用于检验和)
  • 对这些16位二进制数求和,如果高位有溢出,需要将结果回卷到低位,如果回卷还有溢出,继续加回到低位(循环该步骤)
  • 对最终结果取反,填入检验和字段
  • 对接收方,将包括检验和在内的所有二进制数相加,结果应该为全1,如果任意比特为0,说明分组出错,应当丢弃该报文
loading
udp3

TCP

TCP的特点

传输控制协议TCP(Transmission Control Protocol)有以下特点:

  • TCP是面向连接的运输层协议。传输数据前需先建立连接。传输完毕后须释放连接。
  • 每一条TCP连接只能是一对一的,不允许多播
  • TCP提供可靠交付的服务。TCP连接传送的数据,无差错、不丢失、不重复,并且按序到达。
  • TCP提供全双工通信。允许双方随时发送数据,TCP连接的两端都设有发送和接收缓存,应用将数据写入缓存后即可继续其他操作,TCP会在适当时机发送数据;接收数据时,TCP先将数据存入缓存,供应用进程稍后读取
  • TCP是面向字节流的协议,TCP仅把应用层交付的数据块(大小不等)看成无结构的字节流,然后写入发送缓存。TCP会根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段应包含多少个字节。在发送缓存中,TCP可能会拆分数据块,也可能会积累数据块,接收缓存也可能对收到的数据重组合并。因此,发送的数据块与接收的数据块在大小和分割方式上不具有一一对应关系,但整体字节序列必须完全一致。这意味着TCP不保留报文边界(UDP会将应用层交付的报文整个发送保留边界)

TCP连接的套接字描述

每一条TCP连接所连接的端点是一对套接字(而非主机、进程或端口),套接字由IP地址与端口号拼接而成(RFC793的定义),记作:(IP:Port),如:(192.3.4.1:80),因此,我们可以使用一对套接字构成的四元组来唯一地描述和标识一个TCP连接,即:

TCP连接={(源IP:源端口),(目的IP:目的端口)}

该四元组是操作系统内核和网络设备识别一条TCP连接的唯一凭据,这意味着不同源IP或端口,但有相同目的IP和端口的分组,会被定向到不同套接字。正是由于内核依据四元组而非仅仅是目的端口来区分数据流,一台服务器的单个端口(如443)才能够同时与数万客户端建立独立的连接通道并实现高并发通信。不同客户端的源IP或源端口差异,使得它们在服务器端映射为不同的已连接套接字,从而保证了数据流的严格隔离与正确交付。

可靠传输

由于IP层只提供尽力而为的交付服务,因此TCP需要一些措施来实现可靠传输

停止等待协议

停等协议‌(Stop-and-Wait Protocol):发送方每发送完一个分组就停止发送,等待接收方的确认,在收到确认后再发送下一个分组,它的效率较低,通常用于早期的链路层,不直接用于运输层,但为TCP连接提供了核心思想——丢包重传确认应答

1.无差错与有差错超时重传
  • 发送方A发送分组M1,并保留分组M1的副本(方便重传),然后启动一个超时计时器
  • 接收方B收到分组M1后,如果无差错则发送确认M1,A收到确认M1后取消计时器
  • 如果M1错误,则B丢弃分组后什么都不做,A在超时后仍未收到确认M1,则重新发送分组M1
  • 另一种设计是B收到错误的分组M1后,发送”否认报文”,让发送方A提早重发M1,但该设计会让协议实现复杂化,因此大部分可靠传输协议不使用该设计
loading
tcp-sw
2.确认丢失与确认迟到
  • 如果接收方B发送的确认M1丢失,则A在超时后重发分组M1,B在收到分组M1后,会丢弃分组并向A发送确认
  • 当B发送的确认M1因为延迟,要很久后A才会收到,则A会在超时后重新发送分组M1,B收到后丢弃M1并重传确认分组。A收到确认后开始发送M2,此时再收到迟到的M1确认A会直接丢弃该确认。
loading
tcp-sw
3. 信道利用率

假设发送方A发送分组需要时间Td,该分组传输且B发送的确认传输需要时间RTT,B处理分组需要Ta,则停等协议的信道利用率U为:

U=Td/(Td+RTT+Ta)
连续ARQ协议

停等协议每发送一个分组就要等待确认,效率较低,连续ARQ协议的发送方会维护发送窗口,发送窗口内的5个分组都可以连续发送出去,发送方每收到一个确认,就把发送窗口向前滑动一个分组。以此提升效率。

接收方一般使用累积确认,即接收方不必对每个收到的分组都进行确认,而是对按序到达的最后一个分组进行确认,以表示该分组之前的所有分组已经正确到达。对于错误或丢失的分组,发送方通过回退N来进行重传。如:假设发送方发送了5个分组,第3个分组错误或丢失了,而第1,2,4,5个分组正确到达,则接收方只能确认第2个分组,发送方需要通过回退N的方式来重传后三个分组,第4,5分组即使正确到达,由于它们是乱序到达的(未排在3之后),也会被接收方丢弃。

loading
tcp-arq

TCP的数据传输与缓存

客户进程通过套接字传递数据流后,TCP会将这些数据引导到该连接的发送缓存(send buffer,发送缓存会在三次握手时配置)里,TCP会从发送缓存中取出数据,并为每块数据配上一个TCP首部,从而形成多个TCP报文段(TCP segment)并交付网络层。TCP另一端收到报文段后,会在校验正确后放入接收缓存中,应用程序会从该缓存中读取数据流。

TCP可从缓存中取出并放入报文段中的数据大小受限于最大报文段长度MSS(Maximum Segment Size)。MSS通常根据最初确定、由本地发送主机所支持的最大链路层帧长度(即所谓的最大传输单元MTU(Maximum Transmission Unit))来决定。设置该 MSS 要保证一个TCP报文段(当封装在一个IP数据报中)加上TCP/IP首部长度(通常40字节)将适合单个链路层帧。以太网和PPP链路层协议都具有1500字节的MTU,因此MSS的典型值为1460字节。

TCP报文段

TCP报文段分为首部数据两部分,首部的前20字节是固定的,后面的4n字节选项根据需要添加,因此首部最小长度是20字节

loading
tcp-header

首部的各字段作用:

  • 源端口与目的端口:各占2字节,分别写入源端口号与目标端口号
  • 序号seq(Sequence Number):又称报文段序号,占4字节,序号范围是0-4294967295(0至232-1)。TCP是面向字节流的,所传输的每一个字节都按顺序编号。起始序号在TCP连接时确定,首部中的序号用来指定本报文段数据部分第一个字节的序号。如:一报文段起始序号为301,一共携带100字节数据,那么本报文段的数据段中的第一个字节序号为301,最后一个字节序号为400,下一个报文段序号序号应该为401
  • 确认号ack(Acknowledgment Number):占4字节,用于指定接收方期望收到的下一个报文段第一个数据字节的序号,同时也隐含表示该序号之前的所有数据都已正确、有序地收到,因此值为已接收数据的最后一个字节序号+1。如:发送方A发送的报文段序号为501,数据长度200字节,接收方B成功接收,那么B发送给A的确认报文段中确认号应该为701。
  • 数据偏移:占4位,指出TCP报文段的首部长度(以32位字为单位),同时也是TCP报文段数据部分距离TCP报文段起始处的偏移量。TCP报文段首部以32位(4字节)为字长(注意:仅首部有对齐要求,数据部分没有32位字对齐的要求),当该字段值为n时,代表TCP报文段首部长度为4n字节,报文段数据部分从4n+1字节开始。由于首部包含长度可变的选项,因此需要数据偏移字段显式指定TCP报文段数据部分起始位置。4位二进制数能表示的最大值是15,意味着TCP报文段首部最大长度为15x4=60字节。
  • 保留:占6位,保留为今后使用,目前需置为0
  • 6个控制位:用来说明本报文段的性质:
    • 紧急URG(Urgent):当URG置为1时,表示该报文段中包含紧急数据,需要优先传送,发送方TCP会将紧急数据插入到本报文段数据的最前面,该字段需要与紧急指针字段配合使用
    • 确认ACK(Acknowledgment):仅当ACK=1时确认号字段才有效。当ACK=0确认号无效。TCP规定,在连接建立后所有传送的报文段都必须把ACK置1。
    • 推送PSH(Push)当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP就可以使用push操作。发送方TCP把PSH置1,并立即创建一个报文段发送出去,而不再等待缓存填满。接收方TCP收到PSH=1的报文段,也不再等待接收缓存填满,而是尽快将数据交付给接收应用进程,该操作较少使用。
    • 复位RST(Reset):当RST=1时,表明TCP连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。RST 置1还用来拒绝一个非法的报文段或拒绝打开一个连接。RST也可称为重建位或重置位。
    • 同步SYN(Synchronization)在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1和ACK=1。因此,SYN置为1就表示这是一个连接请求或连接接受报文
    • 终止FIN(Finish):用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已发送完毕,并要求释放运输连接。
  • 窗口:占2字节,用于TCP的流量控制,窗口值是0-65535之间的整数。该字段的值是接收方告诉发送方,从本报文段确认号所指明的字节开始,自己当前允许对方发送的字节数量,即接收窗口的大小。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。由于最大值是65535字节,因此接收方窗口最大为63.999KiB。TCP选项字段中还可以包含窗口扩大因子选项,用于将窗口值左移相应的位数,从而实现更大的接收窗口。
  • 检验和:占2字节。检验和字段检验的范围包括首部和数据这两部分。和UDP用户数据报一样,在计算检验和时,要在TCP报文段的前面加上12字节的伪首部。伪首部的格式与UDP用户数据报的伪首部一样。但应把伪首部第4个字段中的17改为6(TCP 的协议号是6),把第5字段中的UDP 长度改为TCP 长度。接收方收到此报文段后,仍要加上这个伪首部来计算检验和。若使用IPv6,则相应的伪首部也要改变。
  • 紧急指针:占2字节。紧急指针仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据)。因此,紧急指针指出了紧急数据的末尾在报文段中的位置。当所有紧急数据都处理完时,TCP就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为零时也可发送紧急数据。
  • 选项:长度可变,最长可达40字节,主要用于提供扩展功能,如:提升传输效率、增强安全性、优化性能等。

选项

  • 最大报文长度MSS(Maximum Segment Size):它定义了单个TCP报文段能携带的最大有效数据量(不包含TCP头部),即数据字段的长度。其核心作用是避免IP分片(TCP交付的报文段过长,从而导致在IP层被拆分为多个短数据报片),提高性能。MSS在TCP三次握手时协商,双方在SYN/SYN-ACK包中通告各自支持的最大值,最终取较小值作为连接使用的MSS,MSS的默认值是536字节,以太网环境下经典值为1460字节。
  • 窗口扩大:默认的TCP窗口字段只有16位,最大窗口大小只有约64KiB,为了满足现代通讯需求,需要更大的窗口大小。窗口扩大选项占3字节,1字节表示移位值S,新窗口位数为16+S,S允许的最大值为14,因此窗口最大值为230-1字节,约1GiB。
  • 时间戳:占用10字节,包括发送方添加的当前时间戳值(TSval)(4字节),以及接收方回显该时间戳(TSecr)(4字节),主要有以下功能:
  • 发送方在发送报文中写入当前时间戳,接收方在确认报文中复制该时间戳回显,方便发送方计算往返时间RTT,且能避免重传二义性问题。
  • 保护回绕序列号PAWS(Protect Against Wrapped Sequence numbers):TCP报文段序号只有32位,这些序号会被重复使用,在高速网络中,如10Gbps链路上,这些序列号约1.7秒就会用完,为了防止新报文和旧报文使用的序列号冲突,接收方将报文时间戳与最近接收时间戳比较,丢弃过期的旧报文,避免序列号回绕导致的数据混乱

TCP滑动窗口

TCP滑动窗口是以字节为单位的,在下述示例中,假定A发送数据,B接收数据,A收到来自B的确认报文,确认号是31,窗口字段值为20。A的发送窗口大小小于等于B接收端口的大小,受到拥塞窗口影响,这里不考虑拥塞,因此A的发送窗口为20字节。

loading
tcp_window

A的发送窗口表示:在没有收到B的确认的情况下,A可以连续把窗口内的数据都发送出去,已经发送的数据,需要保留一份副本,用于超时重传。发送窗口的后沿只能前移,前沿通常也只能前移。特殊情况下,当接收方接收窗口缩小,前沿也可以向后收缩,但TCP标准强烈不建议这么做,因为发送方在收到接收窗口缩小的通知前,有可能已经发送了很多数据,可能会造成数据错误。

数据发送与接收
loading
tcp_window

假定A发送了序号31-41的数据,由于未收到确认,窗口不移动,此时,P2-P3为允许发送但尚未发送的字节数,称为可用窗口有效窗口。A可以继续发送数据,直到指针P2与P3重合,此时如果依旧没有收到确认,可用窗口已经为零,则A必须停止发送。

对于B,接收窗口为20字节,26-30为已经交付应用层的数据,接收窗口不再保留。假设B收到了32和33的数据,但31丢失了,由于32和33未按序到达,因此确认号依旧是31。

当B收到了31数据,并将31-33交付应用层,则B可以删除这些数据,将接收窗口前移3个序号,同时给A发送确认,确认号为34。A收到确认后,就可以把发送窗口向前滑动3个序号,可用窗口也会随着增大。B的确认也可能会丢失,导致A无法收到确认,A会在超时计时器触发时重传数据,并重置计时器重新开始等待,直到收到B的确认。

发送缓存与发送窗口
loading
tcp_window

发送方的发送窗口通常只是发送缓存的一部分,发送缓存包含部分应用层已经写入但尚未进入发送窗口的数据,已被确认的数据会从发送缓存中删除,因此二者的后沿是重合的。接收方的接收窗口和接收缓存的关系类似,需要注意的是,虽然发送方A的发送窗口是根据接收方B的接收窗口设置的,但在同一时刻,A的发送窗口并不总是和 B 的接收窗口一样大,这是因为通过网络传送窗口值需要经历一定的时间滞后,且发送窗口还受网络拥塞情况影响。

选择确认SACK

对于无差错但未按序到达的报文,如果一味丢弃,不仅会造成带宽的浪费,还会增加发送方的重传开销。为此,TCP引入了选择确认SACK(Selective Acknowledgment)机制,允许接收方在确认报文中,显式告知发送方哪些非连续的数据块已经成功接收。SACK并不是强制使用的,需要在 TCP 连接建立时的三次握手期间通过SYN包中的SACK-Permitted选项进行协商,只有双方都支持时才启用。

loading
tcp_sack

对于不连续的字节块,都有左右两个边界,如上图中的L1=1501,R1=3001,接收方返回的ACK包中,标记的是已接收的不连续数据范围,边界以左闭右开区间形式表示,即左边界已经收到,右边界为期待收到的序号。SACK块会添加到TCP头部的选项字段,该块包含以下字段:

  • 选项类型Kind:占用1字节,固定值为5,标识这是一个SACK数据块
  • 选项长度Length:占用1字节,指名这个SACK块占用多少字节
  • 左边界Left Edge:占用4字节(因为序号有32位),数据块起始序号(接收方已拿到该序号的数据块)
  • 右边界Right Edge:占用4字节,数据块结束序号(接收方尚未拿到该序号的数据块)
SACK块格式: | Kind=5 | Length=18 | Left Edge 1 | Right Edge 1 | Left Edge 2 | Right Edge 2 |

由于左右边界各占4字节,而TCP首部选项最大为40字节,因此SACK最多能指明4个字节块的边界信息(4x8+2=34<40)

超时重传时间的选择

TCP采用了一种自适应算法来动态决定超时重传时间,选择过程主要基于以下机制:

  1. 测量往返时间RTT
    TCP会记录报文段发出的时间,以及收到确认的时间,由此计算出报文段的往返时间RTT。
  2. 计算平滑的往返时间
    通过加权移动平均算法,将多次 RTT 样本平滑化,以此计算出加权平均往返时间RTTs(又称为平滑的往返时间SRTT,S表示Smoothed)。每当第一次测量到RTT样本时,RTTs值就取为所测量到的RTT值。但以后每测量到一个新的RTT样本,就按下式重新计算一次RTTs:
    新的RTTs=(1−α)×旧的RTTs+α×新的RTT样本
    α通常取0.125
  3. 计算偏差
    网络抖动(即RTT的波动)对超时时间影响很大。现代TCP引入了 RTT偏差来捕捉这种变化,并计算其加权平均值RTTD
    新RTTD=(1−β)x旧的RTTD+β×|RTTs-新的RTT样本|
    β通常取 0.25
  4. 确定超时重传时间RTO
    基于RTTs和RTTD计算超时重传时间:
    RTO=RTTs+4×RTTD
  5. 处理重传二义性(Karn算法)
    当发送方发出一个报文段后很长一段时间没有收到确认报文,超时计时器会重传该报文段。一段时候后,当接收方收到确认报文时,无法判断这是对第一次发送报文段的确认还是对重传报文段的确认,此即重传二义性。如果此时直接测量 RTT 会严重失真,为此,Karn 算法规定:
    • 重传后不更新 RTT/RTO:只要报文段被重传,本次传输的 RTT 样本就不参与计算
    • 指数退避:每当重传超时发生时,RTO 值翻倍(直到达到上限),避免在拥塞时持续发送大量数据加剧问题。当后续收到新数据的ACK后,才恢复使用上述公式计算RTO

TCP流量控制

TCP的流量控制是一种端到端的机制,目的是防止发送方发送数据过快,导致接收方来不及处理。TCP使用滑动窗口进行流量控制,接收方在TCP报头中通过window字段告诉发送方当前还有多少接收缓存空间(教材中常用rwnd(receiver window,接收窗口)表示接收缓存大小,该值实际上通过window字段传输)。

持续计时器:当接收方通告接收窗口为0(rwnd=0)时,发送方需要停止发送,若之后接收缓存空出,接收方发出了非零窗口通知,但该通知丢失,则可能导致双方相互等待的死锁局面。为了解决该问题,TCP设计了持续计时器,只要TCP连接的一方收到零窗口通知,就启动计时器,并在计时器到期时发送零窗口探测报文段(仅携带1字节数据),对方需要在确认该报文段时给出当前的窗口值,如果仍然为零,则重新计时执行,如果不为零,则可以打破死锁局面。

糊涂窗口综合征(Silly Window Syndrome, SWS):TCP每个报文段都有40字节的首部(20字节TCP + 20字节IP)。如果应用程序频繁发送少量数据(如1字节),就会出现大量首部开销大、有效载荷小的报文段,浪费网络带宽,这种现象称为糊涂窗口综合征。常见原因:

  • 发送方应用层每次只写入很少数据,接收方确认。
  • 接收方缓存已满,应用层每次只取走少量字节数据,接收方每次释放的缓冲区都很小。由于接收方发送的确认中都只通告了一个很小的窗口,发送方也只能发送少量的数据

通常使用以下解决策略:

  • Nagle算法:当应用层逐个字节写入TCP的发送缓存时,发送方将第一个数据字节先发送出去,把后面到达的数据字节缓存起来,等收到第一个数据字节的确认后,再把缓存的数据组装成一个报文段发送出去。确保一条TCP连接上最多只能有一个未被确认的小数据包。
  • Clark算法:接收方不通告小窗口,而是等待接收缓存有足够空间容纳一个最长报文段(空闲空间>MSS),或者接收缓存已有一半空闲空间。

拥塞控制

拥塞(congestion)指在一段时间内,网络中对某一资源(带宽、路由器缓存等)的需求超过了该资源所能提供的可用部分,导致网络性能急剧下降。TCP进行拥塞控制的算法有四种:慢开始(slow-start)拥塞避免(congestionavoidance)快重传(fast retransmit)快恢复(fast recovery)

TCP使用基于窗口的拥塞控制。发送方会维护一个称为拥塞窗口(cwnd)的状态变量,它的大小会根据网络的拥塞程度动态变化,并让发送方的发送窗口等于该拥塞窗口(这里暂不考虑接收方窗口带来的影响)。

当网络发生拥塞时,路由器会丢弃分组,导致发送方无法按时收到预期的确认报文,从而触发超时,因此发送方通过超时来判断网络是否发生了拥塞,控制拥塞窗口的核心原则也很简单:当网络没有出现拥塞时,发送方会适当增大拥塞窗口,以发送更多分组。一旦检测到网络出现拥塞,就减小拥塞窗口,减少注入网络的分组数量,从而缓解拥塞

慢开始

慢开始算法的思路是:主机刚开始发送数据时,不清楚网络负载情况,为避免一开始就引发拥塞,采用由小到大逐渐增大拥塞窗口的探测式策略。

慢开始的初始拥塞窗口值cwnd通常与发送方最大文段SMSS(Sender Maximum Segment Size)有关,刚开始发送报文段时,通常把初始拥塞窗口值cwnd设置为以下值:

  • 若 SMSS > 2190字节:初始拥塞窗口cwnd = 2 × SMSS字节,且不超过2个报文段
  • 若 1095 < SMSS ≤ 2190字节:初始拥塞窗口cwnd = 3 × SMSS字节,且不超过3个报文段
  • 若 SMSS ≤ 1095字节:初始拥塞窗口cwnd = 4 × SMSS字节,且不超过4个报文段

拥有了初始拥塞窗口后,每收到一个对新报文段的确认,拥塞窗口的增加量会根据该确认报文实际新确认的字节数 N 来确定,但一次最多增加一个发送方最大报文段大小(SMSS),即:

cwnd每次增加量=min(N,SMSS)

这里的N指新确认的字节数,通常而言,由于TCP使用累积确认,可能积累了多个报文段携带的字节数据没有确认,此时N的值会很大,如果直接扩大N字节拥塞窗口,有可能会导致窗口增长过快,因此需要限制最大增长为SMSS允许的字节;当确认的字节数N较小时(N<SMSS),直接增大SMSS可能又会导致接收方窗口紧张,因此慢启动时,拥塞窗口的增大总是取N和SMSS的较小值

慢开始的增长速率
loading
tcp-cwnd

通过图示介绍慢开始的增长速率,上述示例中

  • 发送方一开始的cwnd=1,然后发送第一个报文段M1,接收方收到后确认M1。
  • 发送方收到对M1的确认后,把cwnd增大到2,于是发送方接着发送M2和M3两个报文段。接收方收到后发回对M2和M3的确认。
  • 发送方在收到两个确认后,cwnd就从2增大到4,并可发送M4~M7共4个报文段
  • 上述过程,发送方只要收到对一个新报文段的确认,cwnd就会立即增加1,并立即发送新的报文段,而不需要该轮次中所有确认都收到后再发送新报文段

因此使用慢开始算法,每经过一个传输轮次(transmission round),拥塞窗口cwnd就加倍,因此慢开始只是开始慢,增长速率是很快的。

慢开始门限ssthresh

为了防止拥塞窗口cwnd 增长过大引起网络拥塞,还需要设置一个慢开始门限ssthresh状态变量。慢开始门限 ssthresh的用法如下:

  • 当cwnd<ssthresh时,使用上述的慢开始算法。
  • 当cwnd<ssthresh时,停止使用慢开始算法而改用拥塞避免算法。
  • 当cwnd=ssthresh时,既可使用慢开始算法,也可使用拥塞避免算法。
拥塞避免

拥塞避免算法的思路是让拥塞窗口 cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1

loading
tcp-cwnd2

将慢开始、拥塞避免搭配使用得到上述示例:

  • 初始cwnd设为1,初始慢开始门限ssthresh=16
  • 刚开始执行慢开始算法,拥塞窗口cwnd随着传输轮次指数增长;达到慢开始门限16后,改为执行拥塞避免算法,拥塞窗口随着轮次线性增长
  • 当拥塞窗口cwnd=24 时,网络出现了超时(图中的点2),发送方判断为网络拥塞。于是调整门限值ssthresh=cwnd/2=12,同时设置拥塞窗口cwnd=1,重新进入慢开始阶段,并重复上述流程
  • 在点4,发送方收到3个重复确认,开始执行快重传和快恢复,快速重传接收方缺少的字节数据后,根据快恢复的策略调整ssthresh=cwnd/2=8,设置cwnd=ssthresh=8,然后开始执行拥塞避免(而不是慢开始)
快重传

上述图示中的点4(cwnd=16),出现了一个新的情况,发送方一连收到3个对同一个数据的重复确认,这是因为有时个别报文段会在网络中丢失,而后续报文段正确到达,由于字节数据乱序到达,接收方发出对正确到达字节数据的重复确认。该情况下,实际上网络并未发生拥塞,只是某报文段及其携带的字节数据意外丢失了。如果不进行干涉,发送方会由于迟迟收不到该字节数据的确认,产生超时,并误认为网络发生了拥塞。导致发送方错误地进入慢开始,把拥塞窗口cwnd又设置为1,从而降低传输效率。

TCP使用快重传算法来避免在上述情况发生时进入慢开始,即当发送方收到3个重复确认(实际上是1个原始ACK+3个重复冗余ACK,这里的3个重复确认指不把原始ACK视为重复ACK),就知道了接收方只是没有收到某个序号的数据,而不是拥塞了,具体场景如下:

loading
tcp-cwnd3

如上图所示:接收方收到了M1和M2后都分别及时发出了确认。现假定接收方没有收到M3但却收到了M4。本来接收方可以什么都不做。但按照快重传算法,接收方必须立即发送对M2的重复确认,以便让发送方及早知道接收方没有收到报文段M3。发送方接着发送M5和M6。接收方收到后也仍要再次分别发出对M2的重复确认。这样,发送方共收到了接收方的4个对M2的确认,其中后3个都是重复确认

快重传的执行策略

  • 快重传算法首先要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认,即使收到了失序的报文段也要立即发出对已收到的报文段的重复确认,以便让发送方尽早知道某个中间报文段丢失
  • 发送方需要立即重传该字节数据(如上图中的M3数据)。在具体实现中,发送缓存中会始终维护着一份“已发送但未确认的数据副本(按报文段分割),因此发送方能根据接收方的ack快速确认需要发送哪些序号的字节数据并重传
快速恢复

快速恢复(Fast Recovery)是和快速重传配套的一段拥塞控制逻辑,即在触发了快速重传后,进入恢复状态来尽量保持数据流动,而不是慢启动。具体执行策略为:

  • 调整门限值ssthresh = cwnd / 2
  • 同时设置cwnd = ssthresh + 3 * MSS,加3是因为既然发送方收到3个重复确认,就表明已经有3个分组离开了网络,因此可用适当扩大拥塞窗口
  • 然后开始执行拥塞避免算法
拥塞避免阶段拥塞窗口按线性规律增大,称为加法增大AI(Additive Increase)。而一旦出现拥塞或3个重复确认,需要把门限值设置为当前拥塞窗口值的一半,并大大减小拥塞窗口,称为乘法减小MD(Multiplicative Decrease),二者合称为AIMD算法

流量控制与拥塞控制

流量控制与拥塞控制共同影响着TCP的发送速率,它们所针对的问题不一样

对比项 流量控制 拥塞控制
目的 防止发送方压垮接收方 防止过多数据注入网络,造成网络拥堵
作用范围 端到端(发送方与接收方之间) 全局(涉及网络中的路由器、链路)
依据 接收方通告的接收窗口 网络拥塞程度(通过丢包、RTT变化等判断)
窗口变量 接收窗口 拥塞窗口

发送方的发送窗口不能超过接收方接收窗口值rwnd,也不能大于拥塞窗口cwnd,因此发送窗口最终取两者的较小值:

发送窗口上限值=min(rwnd,cwnd)

网络层主动队列管理AQM

TCP拥塞控制还深受网络层策略的影响,通常情况下,路由器的队列都是按照”先进先出”FIFO(First In First Out)规则进行处理,当队列己满时,后续到达的所有分组都将被丢弃,称为尾部丢弃策略(tail-droppolicy)。

路由器的尾部丢弃会导致一连串分组丢失,触发发送方超时重传,使TCP连接进入慢启动状态,发送速率骤降。更为严重的是,网络中通常存在大量TCP连接,一旦发生尾部丢弃,往往会同时影响多条连接,使它们在同一时间同步进入慢启动状态,这种现象称为全局同步。其结果表现为:全网通信量先急剧下降,待网络恢复后又骤然激增,造成流量剧烈震荡,严重降低链路利用率和传输效率。

主动队列管理AQM(Activeueue Management)是为了避免全局同步现象而提出的一种拥塞控制机制,其核心思想是”主动”预防,即不要等到路由器的队列长度已经达到最大值时才丢弃后面到达的分组,而是应当在队列长度达到某个值得警惕的数值时,就主动丢弃到达的分组,以提醒发送方放慢发送的速率。AQM有多种不同实现方法:

RED随机早期检测

随机早期检测 RED(Random Early Detection)是一种经典的AQM算法,但目前不再使用。实现RED时需要使路由器维持两个参数:队列长度最小门限和最大门限。当每一个分组到达时,RED就按照规定的算法先计算当前的平均队列长度。

  • 若平均队列长度小于最小门限,则把新到达的分组放入队列进行排队
  • 若平均队列长度超过最大门限,则把新到达的分组丢弃
  • 若平均队列长度在最小门限和最大门限之间,则按照某一丢弃概率p把新到达的分且丢弃(随机丢弃)
CoDel控制延迟

CoDel(Controlled Delay)的核心思想是控制排队延迟而非队列长度。它通过测量数据包在队列中的停留时间(sojourn time),一旦持续超过设定阈值(如 5ms),就开始有节奏地丢包,并动态调整丢包频率,从而将延迟压制在目标范围内

FQ-CoDel公平队列受控延迟

FQ-CoDel(Flow Queueing + CoDel)公平队列受控延迟将流级公平调度(Flow Queueing)与 CoDel 的延迟控制结合:为每个 flow 分配独立子队列并轮询发送,同时对每个子队列应用 CoDel 控制延迟。这种设计同时解决了公平性问题(大流不压制小流)和延迟问题。由于其无需调参、效果稳定、实现简单,目前已成为最主流的 AQM 方案之一,在 Linux(默认队列管理)、WiFi 驱动、家用路由器/OpenWrt 中广泛部署

TCP三次握手建立连接

loading
tcp-handshake

  • 最初两端的TCP进程都处于CLOSED(关闭)状态,客户A主动打开连接,服务器B被动打开连接
  • B的TCP服务器进程创建传输控制块TCB(存储有指向发送缓存、接收缓存的指针,当前发送序号和接收序号等信息),等待客户的连接请求,服务器进程处于LISTEN(监听)状态
  • A的TCP客户进程创建传输控制块TCB,并向B发出连接请求报文段,这时首部中的同步位SYN=1,同时选择一个初始序号seq=x。TCP规定,SYN报文段(即SYN=1的报文段)不能携带数据,但要消耗掉一个序号。此时,TCP客户进程进入SYN-SENT(同步已发送)状态。
  • B 收到连接请求报文段后,如同意建立连接,则向A发送确认。在确认报文段中应把SYN位和ACK位都置1,确认号是ack=x+1,同时也为自己选择一个初始序号seq=y。请注意,这个报文段也不能携带数据,但同样要消耗掉一个序号。这时,TCP服务器进程进入SYN-RCVD(同步收到)状态。
  • TCP客户进程收到B的确认后,还要向B给出确认。确认报文段的ACK置1,确认号ack=y+1,而自己的序号seq=x+1。TCP的标准规定,ACK报文段可以携带数据。但如果不携带数据则不消耗序号,在这种情况下,下一个数据报文段的序号仍是seq=x+1。这时,TCP连接已经建立,A进入ESTABLISHED(已建立连接)状态。当B收到A的确认后,也进入ESTABLISHED状态。

为什么需要第三次握手?

  1. 为了确认双方收发能力都正常,并同步双方的初始序列号(SEQ)
  • 第一次握手,客户端发送SYN,表明自己具备发送能力。

  • 第二次握手,服务端回复SYN+ACK,客户端知道了服务端能收到自己的报文段,自己也能收到来自服务端的报文段。但服务端只知道自己能收到客户端的报文段,不清楚客户端是否能收到自己的报文段

  • 第三次握手,客户端回复ACK,服务端才确认:客户端能收到自己的报文段。客户端需要明确让服务端确信彼此都能发、都能收,从而进入可靠的数据传输阶段

  1. 为了防止已失效的连接请求突然又传送到了B,因而产生错误。假设A发出连接请求,该请求在某些网络结点长时间滞留了,由于未收到确认,于是A重传了一次连接请求,收到了确认,并与B建立了连接,数据传输完毕后,就释放了连接。随后第一个连接请求在某个时间到达了B,本来这是一个早已失效的报文段。但B收到此失效的连接请求报文段后,就误认为是A又发出一次新的连接请求。于是就向A发出确认报文段,假定不采用第三次报文握手,那么只要B发出确认,新的连接就建立了。但由于A并没有发出建立连接的请求,因此不会理睬B的确认,也不会向B发送数据。但B却以为新的运输连接已经建立了,并一直等待A发来数据,B的资源会因此白白浪费。

TCP四次挥手释放连接

loading
tcp-handshake
  • 双方刚开始处于ESTABLISHED状态
  • A主动关闭TCP连接时,需要把连接释放报文段首部的终止控制位FIN置1,其序号seq=u,它等于前面已传送过的数据的最后一个字节的序号加1。这时A进入FIN-WAIT-1(终止等待1)状态。TCP规定,FIN报文段即使不携带数据,它也消耗掉一个序号
  • B收到连接释放报文段后发出确认,确认号是ack=u+1,而这个报文段自己的序号是v,等于B前面已传送过的数据的最后一个字节的序号加1。然后B就进入CLOSE-WAIT(关闭等待)状态
  • 此时A到B这个方向的连接就释放了,这时的TCP连接处于半关闭(half-close)状态,即A已经没有数据要发送了,但B若发送数据,A仍要接收。
  • A收到来自B的确认后,就进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文段
  • 若B也没有要向A发送的数据了,需要关闭连接时,则B发出的连接释放报文段也必须使FIN=1,B的序号为w(在半关闭状态B可能又发送了一些数据,因此与v不构成+1关系),B还必须重复上次已发送过的确认号ack=u+1,这时B就进入LAST-ACK(最后确认)状态,等待A的确认
  • A在收到B的连接释放报文段后,在确认报文段中把ACK置1,确认号ack=w+1,而自己的序号是seq=u+1(根据TCP标准,前面发送过的FIN报文段要消耗一个序号)。然后进入到TIME-WAIT(时间等待)状态
  • 注意,现在TCP连接依旧还没有释放掉。必须经过时间等待计时器(TIME-WAITtimer)设置的时间2MSL后,A才进入到CLOSED状态。时间MSL叫做最长文段寿命(Maximum Segment Lifetime),RFC793建议设为2分钟,但TCP允许不同的实现可根据具体情况使用更小的MSL值。因此,从A进入到TIME-WAIT状态后,要经过4分钟才能进入到CLOSED状态,才能开始建立下一个新的连接。当A撤销相应的传输控制块TCB后,就结束了这次的TCP连接。

为什么需要等待2MSL?

第一,为了保证A发送的最后一个ACK报文段能够到达B。这个ACK报文段有可能丢失,因而使处在LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认。B会超时重传这个FIN+ACK报文段,而A就能在2MSL时间内收到这个重传的FIN+ACK报文段。接着A重传一次确认,重新启动2MSL 计时器。最后,A和B 都正常进入到CLOSED状态。如果A在TIME-WAIT状态不等待一段时间,而是在发送完ACK 报文段后立即释放连接,那么就无法收到B重传的FIN+ACK报文段,因而也不会再发送一次确认报文段。这样,B就无法按照正常步骤进入CLOSED状态。

第二,防止之前提到的“已失效的连接请求报文段”出现在本连接中。A在发送完最后一个ACK报文段后,再经过时间2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。B只要收到了A发出的确认,就进入CLOSED状态。同样,B在撤销相应的传输控制块TCB后,就结束了这次的TCP连接(B结束TCP连接的时间要比A早一些)。

保活计时器

除时间等待计时器外,TCP 还设有一个保活计时器(keepalivetimer)。以应对下述情况:客户已经与服务器建立了TCP连接,但后来客户端突然出现故障并异常关闭,服务器以后就不能再收到客户发来的数据。因此,应当有措施使服务器不要再白白等待下去。这就是使用保活计时器。服务器每收到一次客户的数据,就重新设置保活计时器,时间的设置通常是两小时。若两小时没有收到客户的数据,服务器就发送一个探测报文段,以后则每隔75秒钟发送一次。若一连发送10个探测报文段后仍无客户的响应,服务器就认为客户端出了故障,接着就关闭这个连接。

上一篇:计算机网络(三)网络层
下一篇:计算机网络(一)计算机网络概述与应用层
z z z z z