第五章 传输层
5.1 运输层的功能
运输层功能:
-
运输层为应用进程之间提供 端到端的逻辑通信(但网络层是为主机之间提供逻辑通信)。
-
运输层还要对收到的报文进行差错检测。
-
运输层提供面向连接和无连接的服务
从通信和信息处理的角度看,运输层向它上面的应用层提供通信服务,它属于面向通信部分的最高层,同时也是用户功能中的最低层。
5.2 运输层协议 UDP 和 TCP
区别
运输层主要有两个协议:
- 用户数据报协议 UDP(User Datagram Protocol);
- 传输控制协议 TCP(Transmission Control Protocol)。
两个协议传输的数据单元:
两个对等运输实体在通信是传送的数据单位叫作运输协议数据单元 TPDU(Transport Protocol Data Unit)。
-
TCP 传送的协议数据单元是 TCP 报文段(segment);
-
UDP 传送的协议数据单元是 UDP 报文或用户数据报。
是否面向连接:
- UDP 在数据传输之前不需要建立连接。对方的运输层在收到 UDP 报文后,不需要给出确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 是一种最有效的工作方式;
- TCP 则提供面向连接的服务。TCP 不提供广播或多播服务。由于 TCP 要提供可靠的、面向连接的服务。因此不了避免地增加了许多的开销。这不仅是协议数据单元的首部增大很多,还要占用许多的处理机资源。
TCP端口
含义与作用
端口用一个 16 位端口号进行标志。
端口只是具有本地意义,即端口号只是为了标志本计算机应用层中的各进程。在因特网中不同计算机的相同端口号是没有联系的。
三类端口
-
熟知端口,数值一般为0 - 1023;
协议 FTP 21 TELNET 23 SMTP 25 DNS 53 HTTP 80 https 443 RDP 3389 … … -
登记端口号,为没有熟知端口号的应用程序使用的。使用这个范围的端口号必须在 IANA 登记,以防重复。数值为:1024 - 49151;
-
客户端口号,或短暂端口号,留给客户进程选择暂时使用。当服务器进程收到客户进程的报文时,就知道了客户端所使用的动态端口号。通信结束后,这个端口号可供其他客户进程以后使用。数值为:49152 - 65535。
UDP 用户数据报
面向报文的 UDP 对应用程序交下来的报文,在添加 UDP 首部后就向下交付到 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。
UDP 的首部格式
- 在计算校验和时,临时把“伪首部”和 UDP 用户数据报放在一起。伪首部仅仅是为了计算校验和;
- 首部中的长度为首部的长度 8 字节加上数据部分的长度;
- 伪首部中的 UDP 长度等于首部中的长度。
UDP 检验和举例
UDP 的主要特点
- UDP 是无连接的,即发送数据之前不需要建立连接;
- UDP 使用尽最大努力交付,即不保证可靠交付,同时也不使用拥塞控制;
- UDP 是面向报文的。UDP 没有拥塞控制,和合适多媒体通信的要求;
- UDP 支持一对一 、一对多、多对一的交互通信;
- UDP 的首部开销小,只有 8 个字节。
TCP 传输控制协议
概述
-
TCP 协议是面向连接的传输层协议;
-
每一条 TCP 连接只能有两个端点(endpoint),每一条 TCP 连接只能是点对点的(一对一);
-
TCP 提供可靠交付的服务;
-
TCP 提供全双工通信;
-
TCP 面向字节流。
向流的概念
- TCP 连接是一条虚连接而不是一条真正的物理连接;
- TCP 对应用进程一次把多长的报文发送到 TCP 的缓存中是不关心的;
- TCP 根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段包含多少个字节;
- TCP 连接的端点不是主机,不是主机的 IP 地址,不是应用进程,也不是传输层协议端口。TCP 连接的端点叫做套接字(socket),端口号拼接到 IP 地址即构成了套接字,套接字 = (IP : 端口号);
- 每一条 TCP 连接有两个端点,端点指的是就是套接字;
TCP 报文段的首部格式
源端口(source port):源的端口号;
目的端口(destination port):目的端口号;
序号(sequence number):发送的分组首字节在整个数据中是第几个字节;
确认号(Acknowledgement number):接收方在发送确认时,通过确认号告知发送者下一次发送的数据是从第几个字节开始;发送方在接收到确认时也会将之前的数据从发送缓存中清除;
数据偏移:用来表示从多少个字节开始是数据部分,偏移值=数据偏移 * 4;
URG:用于标记该数据包比较紧急,优先传输,会将 URG=1 的数据包放到发送缓存的前面优先发送;
PSH:接收端接收到的数据包中 PSH=1 时会将数据放到接收缓存的前面,优先提交给应用程序;
ACK:用于标记确认号是否有效,只有在请求建立连接时ACK=0,之后就一直为1;
SYN:为1时表示这是一个发起建立连接请求的数据包,对方同意建立连接时的数据报中SYN也会置1;
RST:用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求。如果接收到RST位时候,通常发生了某些错误;比如浏览器正打开网页,此时和服务器建立了连接,这时浏览器被强制关闭,那么此时客户端会自动的发送一个 RST=1 的数据包给服务器,用于复位该条链接;
Linger 就是通过发送 RST 来复位连接,从而跳过了TIME_WAIT
FIN:用来释放TCP连接,表明发送方已经没有数据发送了。
注:MSS(Maximum Segment Size)表示最大数据包大小为1460字节,在抓包时能看到MSS
窗口:最早TCP协议涉及用来大范围网络传输时候,其实是没有超过56Kb/s的连接速度的。因此,TCP包头中只保留了16bit用来标识窗口大小,允许的最大缓存大小不超过64KB。为了打破这一限制,RFC1323规定了TCP窗口尺寸选择,是在TCP连接开始的时候三步握手的时候协商的(SYN, SYN-ACK,ACK),会协商一个 Window size scaling factor(在前两次握手的Option中协商),之后交互数据中的是Window size value,所以最终的窗口大小是二者的乘积.
Window size value: 64 or 0000 0000 0100 0000 (16 bits)
Window size scaling factor: 256 or 2 ^ 8 (as advertised by the 1st packet)
The actual window size is 16,384 (64 * 256)
这里的窗口大小就意味着,直到发送16384个字节,才会停止等待对方的ACK.随着双方回话继续,窗口的大小可以修改window size value 参数完成变窄或变宽,但是注意:Window size scaling factor乘积因子必须保持不变。在RFC1323中规定的偏移(shift count)是14,也就是说最大的窗口可以达到Gbit,很大。
校验和:计算方法和 UDP 校验和计算是一样的,只需要把 UDP 协议号 17 换成TCP协议号 6 即可;
紧急指针:只有当 URG = 1时,紧急指针才会有,比如紧急指针为 50,表示从数据包的开头(包含首部在内)往后 50 字节为紧急数据,之后的数据为非紧急数据;
选项:
- 最大数据报长度 MSS;
- 是否支持选择性确认 SACK;
- Window scale
- …
填充:如果选项不够 4 字节的正数被则填充。
TCP 建立连接过程
TCP 协议如何实现可靠传输
停止等待协议
使用确认和重传机制,我们就可以在不可靠的传输网络上实现可靠的通信。这种可靠的传输协议常称为自动重传请求ARQ(Automatic Repeat reQuest)。ARQ 表明重传的请求是自动进行的。接收方不需要请求发送方重传某个出错的部分。
- 超时时间一般为 RTT(一个来回的时间)时间稍大点;
- 发送端在超时时间内还没收到确认就会重传。
确认丢失和确认迟到
- 确认消息丢失后,发送端会重新发送,此时接收端收到重传的数据后会丢弃重复的M1,然后发送重传确认;
- 如果确认消息迟到,发送端会重传,接受端收到重复的M1会丢弃,发送端在接收到迟到的确认消息后什么也不做,不做任何处理。
信道利用率
停止等待协议的优点是简单,但缺点是信道利用率太低。
在每次发送一个分组后都要等待 T D T_D TD+RTT+ T A T_A TA 时间才能才能发送下一个分组,这样信道的利用率很低;
信道利用率 U = T D T D + R T T + T A U = \frac{T_D}{T_D+RTT+T_A} U=TD+RTT+TATD,当 T D T_D TD 越大时,信道利用率 U 就越大;
流水线传输
发送方可连续发送多个分组,不必每发完一个分组就停下来等待对方的确认。由于信道上一直有数据不间断地传送,这种传输方式可获得很高的信道利用率。
连续 ARQ 协议
连续 ARQ 协议规定,发送方每收到一个确认,就把发送窗口向前滑动一个分组的位置。上图(b)表示发送方收到了对第 1 个分组的确认,于是把发送窗口向前移动一个分组的位置。如果原来已经发送了前 5 个分组,那么现在就可以发送窗口内的第 6 个分组了。
接收方一般都是采用累积确认的方式。这就是说,接收方不必对收到的分组逐个发送确认,而是在收到几个分组后, 对按序到达的最后一个分组发送确认,这就表示:到这个分组为止的所有分组都已正确收到了。
累积确认优点和缺点:
- 优点:容易实现,即使确认丢失也不必重传;
- 缺点:不能向发送方反映出接收方已经正确收到的所有分组的信息。
例如,如果发送方发送了前 5 个分组,而中间的第 3 个分组丢失了。这时接收方只能对前两个分组发出确认。发送方无法知道后面三个分组的下落,而只好把后面的三个分组都再重传一次。这就叫做 Go-back-N(回退 N),表示需要再退回来重传已发送过的 N 个分组。可见当通信线路质量不好时,连续 ARQ 协议会带来负面的影响。
重传超时等待时间
TCP 每发完一个报文段,就对这个报文段设置一次计时器。只要计时器设置的重传时间到但没有就收到确认,就要重传这一报文段。
新的 R T T s = ( 1 − α ) ∗ ( 旧的 R T T s ) + α ∗ ( 新的 R T T s 样本 ) 新的RTT_s = (1-α) * (旧的RTT_s) + α * (新的RTT_s样本) 新的RTTs=(1−α)∗(旧的RTTs)+α∗(新的RTTs样本)
超时重传时间应略大于上面得出的加权平均往返时间 R T T s RTT_s RTTs
RFC2988推荐的 α 值为 1 / 8 1/8 1/8
TCP 协议如何实现流量控制
滑动窗口技术
为什么要使用滑动窗口技术
一个包在网络中发送出去,其实就像古时候你给别人用信鸽寄了一封信一样,在外界环境非常复杂的情况下,你完全没有把握这封信能不能真的送达收信人那边,除非有一天你收到了收信人的回信。
类似的,在复杂网络环境下,TCP 为了能保证每个包真的送达了,并且接收端收到包的顺序和发送端是一致的,我们每发出一个包,自然也需要一个类似回信的机制。
这个回信也就是 ACK 包,每个包发送的时候会有一个序列号,接收端回 ACK 包的时候会把序列号 +1 发送回来,发送端如果没有收到某个包的 ACK 包,会在一段时间之后尝试重新发送,直到收到 ACK 为止。这其实也是在网络和各种分布式系统中能确保消息可达的唯一方式。
那问题来了,我们为了确保消息保序可达,难道每次发送一个新的包,都等待上一个包的 ACK 回来之后才能发送吗?这样一来一回的效率显然是很低的,也就是每经过一个 RTT 的时间,我们只能发送一个包,假设一个 RTT 是 100ms,那在一秒中我们甚至只能发送 10 个包,这完全是不可接受的。
其实我们在等待 ACK 的时候没有必要停止后续包的发送,因为网络传输虽然不稳定,但大部分包往往还是可达的,这样我们就可以获得数倍的传输效率提升。如果真的不幸遇到了丢包,接收端 ACK 姗姗来迟的时候,也就告诉了我们某个序列号之前的所有包全部收到,我们再根据一定的策略,尝试重新发送对应丢失的包就可以了。
所以自然而然的,发送方需要缓存已发出但尚未收到 ACK 的包,接收方收到包但没有被用户进程消费之前也得把收到的包留着。
但是,缓存是有大小限制的,程序消费数据和链路传输数据的能力也是有限的,发送端和接受端都需要一种机制来限制可发送或者可接收数据的最大范围。
于是,滑动窗口和拥塞窗口应运而生。
这两个算法核心都是为了防止像网络中发送的包太多。不同的是两者的目的,滑动窗口机制,可以用来控制流量,防止接收方处理不过来消息;同样基于窗口机制的拥塞控制算法,则用来处理网络上数据包太多的情况,以避免网络中出现拥塞。
滑动窗口技术实现流量控制
我们先来看看如何用滑动窗口控制流量。
这里说流量控制,主要就是为了防止接收方处理数据的速度跟不上发送方,避免随着时间推移,数据自然溢出接收方的缓冲区。
虽然协议可以保证发送方没有收到 ACK,最终会重试重新发送,但如果需要大量反复发送冗余的数据,所占用的网络资源就被白白浪费了,在网络资源很紧缺的时候,这也会造成网络环境的恶化。
TCP 控制流量的方式也很简单,就是滑动窗口机制。
接收端会建立一个滑动窗口,由接收方向发送方通告,TCP 首部里的 window 字段就是用来表示窗口大小的,窗口表示的就是接收方目前能接收的缓冲区的剩余大小。
但是发送方也会根据这个通告窗口的大小建立自己的滑动窗口。为了兼顾效率和可靠性,在发送方,所有未收到 ACK 的消息虽然可以发送,但是在收到 ACK 之前是一定要在缓冲区中保存的。
发送窗口
发送窗口根据三个标准来划分:是否发送、是否收到 ACK、是否在接收方通告处理范围内,分成了四个部分。
-
第一部分就是已经发送且收到 ACK 的部分,这一块我们知道已经成功发送,所以不需要在缓冲区保留了。
-
第二部分是已发送但尚未收到 ACK 的部分。
-
第三部分是还没有发送,但是还在接收方通告窗口也就是处理范围内的数据,这块我们也可以称为可用窗口;第二、第三部分一起构成了我们的整个发送窗口。
-
最后一部分则是我们需要发送,但已经超过接收方通告窗口范围的部分,这一部分在没有收到新的 ACK 之前,发送方是不会发送这些数据的。通过这个限制,发送的数据就一定不会超过接收方的缓冲区了。
但如果发送方一直没有收到 ACK,随着数据不断被发送,很快可用窗口就会被耗尽。在这种情况下,发送方也就不会继续发送数据了,这种发送端可用窗口为零的情况我们也称为“零窗口”。
正常来说,等接收端处理了一部分数据,又有了新的可用窗口之后,就会再次发送 ACK 报文通告发送端自己有新的可用窗口(因为发送端的可用窗口是受接收端控制的)。
但是,万一要是 ACK 消息在网络传输中正好丢包了,那发送端还能感知到接收端窗口的变化吗?其实是不会的,在这个情况下,接收端就会一直等着发送端发送数据,而发送端也还会以为接收端仍然处于零窗口的状态,这样一直互相等待,就好像进入了死锁状态。
解决办法也很简单,我们可以再引入一个零窗口定时器,如果发送端陷入零窗口的状态,就会启动这个定时器,去定时地询问接收端窗口是否可用了,这也是在分布式系统中常见的处理丢包的方式之一。
接收窗口
相对发送端来说,接收端要简单的多,主要就分为已经接收并确认的数据和未收到但可以接收的数据,这一部分也就是接收窗口;剩下的就是缓冲区放不下的区域,也就是不可接收的区域。
如果进程读取缓冲区速度有所变化,接收端可能也会改变接收窗口的大小,每次通告给发送端,就可以控制发送端的发送速度了。这就是所谓的滑动窗口,也就是流量控制机制。
而之所以是滑动窗口,也很好理解,随着 ACK 或者进程读取数据,窗口也会顺次往后移动。比如在发送端的窗口中,如果我们在某次通信中收到了一条 ACK 消息,表示 36 之前的消息都已经被收到了,那么整个可用的窗口就会顺次往右移动。
总的来说,滑动窗口(流量控制机制)解决了发送端消息可能淹没接收端,导致处理跟不上的情况。
TCP 协议如何避免网络拥塞
慢启动和拥塞避免
拥塞控制的一般原理
出现资源拥塞的条件:对资源需求的总和 > 可用资源
拥塞控制是一个全局性的过程,设计到所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。
流量控制往往指在给定的发送端和接收端之间的点对点通信量的控制,它所要做的就是一直发送端发送读速率,以便是接收端来及接收。
拥塞控制所起的作用
慢开始和拥塞避免
发送发维持拥塞窗口cwnd(congestion window)。
发送方控制拥塞窗口的原则是:
- 只要网络没有出现拥塞,拥塞窗口就再增大一些,以便吧更多的分组发送出去;
- 只要网络出现拥塞,拥塞窗口就减小一些,以减小注入到网络中的分组数。
慢开始算法原理
开始时以慢速发送,然后逐渐加快发送速率。
设置慢开始门限状态变量 ssthresh
慢开始门限 ssthresh 的用法如下:
- 当 cwnd < ssthresh时,使用慢开始算法;
- 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法;
- 当cwnd = ssthresh 时,既可以使用慢开始算法,也可以使用拥塞避免算法。
拥塞避免算法的思路是让拥塞窗口 cwnd 缓慢地增大,即每经过 一个往返时间 RTT 就把发送发送方的庸俗窗口 cwnd 加 1,而不是加倍,使拥塞窗口 cwnd 按线性规律缓慢增长。
慢开始和拥塞避免算法的实现举例
开始发送数据时 拥塞窗口为 1,然后以指数形式增加至慢开始门限 ssthresh,达到慢开始门限 ssthresh 后每次增加 1。
当网络出现拥塞时,慢开始门限 ssthresh 值降为此时的一半,cwnd 值从 1 开始再次以指数形式增加至 慢开始门限 ssthresh,达到慢开始门限 ssthresh 后每次增加 1。依次往复…
- 当 TCP 连接进行初始化时,将拥塞窗口设置为1。图中的窗口单位不使用字节而使用报文段。
- 慢开始门限的初始值设置为 16 个报文段,即ssthresh = 16;
- 每经过 RTT(一个往返时间)cwnd 变化一次;
- 拥塞避免并非指完全能够避免了拥塞。利用以上的措施要完全避免网络拥塞还是不可能的;
- 拥塞避免是说在拥塞避免阶段把拥塞窗口控制为按线性规律增长,是网络比较不容易出现拥塞。
快重传和快恢复
快重传算法首先要求接收方每收到一个失序的报文后就立即发出重复确认。这样做可以让发送方及早知道有报文段没有到达接收方。
当发送端收到三个重复的确认时,就执行“乘法减小”算法,把慢开始门限 ssthresh 减半,但拥塞窗口 cwnd 现在不设置为 1,而是设置为慢开始门限 ssthresh 减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。
从连续收到三个重复的确认转入拥塞避免
发送窗口的实际上限值
发送方的发送窗口的上限值应当取为接收方窗口和拥塞窗口这两个变量中较小的一个,即按以下公式确定: 发送窗口的上限值 = M i n [ r w n d , c w n d ] 发送窗口的上限值 = Min[rwnd, cwnd] 发送窗口的上限值=Min[rwnd,cwnd]
5.3 TCP 的运输连接管理
传输连接有三个阶段:连接建立、数据传送和连接释放。
TCP 连接的建立都是采用客户端 + 服务器方式。
主动发起连接建立的应用进程叫客户端(client)。
被动等待连接建立的应用进程叫服务器(server).
三次握手建立 TCP 连接
- 第一次握手时的 seq 值不固定的值,取决于客户端;
- 第二次握手时的 seq 值不固定的值,取决于服务器;
- 重传机制:在Linux里,一般客户端重传此时设置在5次,并且超时时间第一次是1秒,第二次会变成2秒,第三次会变成4秒,一直到第五次16秒,且每次超时时间是上次超时时间的2倍。
为什么会有第三次握手?
-
情况一:
- 如果客户发送的握手数据包在网络中选用的是比较远的路由,很长时间还没到达服务器,那么客户端在超时时间内还没有收到服务器的确认,客户端会重发握手数据包给服务器。
- 假如第二次的数据包选用的是最近路由给到服务器,此时服务器会发出确认,当客户端接收到确认后就会建立连接。
- 如果在连接已经建立后第一次的数据包才被服务器接收到,服务器又会给客户端发送确认,客户端在接收到确认后由于连接已经确立所有就不会再处理这个确认,但服务器会认为这条连接时存在的,这样会占用服务器的资源;
- 如果有了第三次握手,当出现 3 的情况时,服务器会认为客户端没有收到确认,然后执行执行重传机制,最终认为通信断了,从而关闭连接避免资源占用。
-
情况二:
如果没有第三次握手,当服务器发送确认信号后,如果因为网络延迟等问题导致客户端在还没有接收到的确认时,服务器就开始给客户发送数据,这样会导致在没有建立连接的情况下客户端有可能就会接收端到服务器发来的数据。
建立 TCP 连接的状态转移
CLOSE:客户端或服务器处于关闭状态;
LISTEN:服务器打开监听端口,开始监听客户端的连接请求;
SYN-SENT:表示 SYN 已发送,客户端第一次握手发送后进入该状态,等待服务器发送确认;
SYN-RECD:表示ACK已发送,当服务器接收到第一条握手,发送确认给客户端后进入该转态,等待客户端发送确认;
ESTABLISHED:表示连接已经建立,可以进行数据交换。
TCP 连接释放
- 客户端 A 没有数据要发送时,先给服务器 B 发送连接关闭请求;
- 服务器 B 在接收到客户端 A 的关闭请求后会发送确认信号,告知客户端 A 关闭请求已经收到;
- 服务器 B 还可以继续将数据发送给客户端 A,当没有数据发送时,服务器 B 会向客户端 A 发送连接关闭请求;
- 客户端 A 收到服务器 B 的连接关闭请求后反馈一个确认给服务器 B,然后服务器 B 释放连接,客户端 A 进入TIME_WAIT状态;
释放 TCP 连接的转态转移
FIN-WAIT-1:主动关闭连接为主动方,在主动方发送关闭连接请求等待对方确认期间,进入该状态;
CLOSE-WAIT:被动方接收到关闭连接请求后发送确认,进入该状态,直到自己再次发送关闭连接请求;
FIN-WAIT-2:主动方在接收到确认后,进入该状态;
LAST-ACK:被动方发送连接关闭请求后,进入该状态;
TIME_WAIT:主动方接收到对方的关闭连接请求后发送确认,进入该状态。
- MSL 是 Maximum Segment Lifetime 英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。[RFC0793]中规定MSL为2分钟,然而在实际应用中可以为30秒、1分钟或者两分钟。在绝大多数的情况下,该值是可以修改的。Linux系统中net.ipv4.tcp_fin_timeout的数值记录了2MSL状态需要等待的超时时间(以秒为单位)。
为什么主动关闭方要进入TIME_WAIT:
-
情况一:
如果直接关闭连接,最后一次确认数据报要是丢了的话,服务器就会认为自己的发送请求对方没有收到,然后一直进行重传;
-
情况二:
如果在情况一的基础上,假如有个客户端用同样的 IP 地址和端口号与服务器建立了连接,那么就有可能收到服务器发送出来的关闭连接请求,从而误认为是发给自己的关闭连接请求;