1. 运输层协议概述
1.1 进程之间进行通信
运输层的功能
:运输层向它上面的应用层提供通信服务
,属于面向通信部分的最高层以及用户功能中的最低层。
关于运输层的作用
看图说话:
为什么需要运输层?
如图所示,主机A和主机B通过互联的广域网WLAN进行通信,IP
协议的作用是能够将主机A发送出来的分组,按照分组首部中的地址,通过路由节点
发送到主机B,这是网络层
为我们提供的服务,它能够保证从一个主机上发送出来的数据能够被目标主机所接收。
但是这样就足够了吗?我们知道主机上有很多进程,实际上这些数据包的请求与响应都是不同的进程发起的,因此如果要实现主机上不同进程之间的精准匹配,仅依赖于网络层
的功能是远远不够的,也就是说两台主机进行通信就是两台主机中的应用进程在交换数据,是应用进程在互相通信
。
端到端的通信是应用进程之间的通信。
那么在这个图中主机A上运行了AP1
和AP2
,主机B上运行了AP3
和AP4
,但是我们发现数据从网络层交付到应用进程的时候,经过的是同一个过程,而在最终的数据分发的时候,又产生了分叉。
复用:
发送方
不同的应用进程都可以使用同一个运输层协议传送数据分用:
接收方
的运输层在剥去报文的首部之后能够把这些数据正确交付到目的应用进程。怎么理解分用和复用?
应用层所有的应用进程产生的数据都可以通过运输层再传送到IP层
网络层
,这就是复用运输层从
IP
层收到发送给各应用该进程的数据后,必须分别交付指明的各应用进程,这就是分用
网络层为主机之间的通信提供服务,而运输层则在网络层的基础上,为应用进程之间的通信提供服务
运输层还具备的一个功能是对收到的报文进行差错检测
当运输层采用面向连接
的TCP
协议的时候,尽管网络层采用的是例如分组交换
技术等不可靠的转发技术,TCP的这种逻辑通信信道就相当于是一条全双工的可靠通信(指的是建立连接后,两个进程既能收数据,又能够发数据)
当采用无连接
的UDP
协议的时候,这种逻辑通信是一条不可靠的信道。
1.2 两个主要协议
用户数据报协议UDP(
User Datagram Protocol
)传输控制协议TCP
(Transmission Control Protocol)
运输协议数据单元
两个对等运输实体在通信的时候传送的数据单元叫做运输协议数据单元,
TPDU
TCP传送的数据单位协议是TCP报文段
segment
UDP传送的的数据单元叫做UDP用户数据报
UDP
在传送数据之前不需要先建立连接,远地主机在收到UDP报文之后,不需要给出任何的确认
,虽然UDP
不提供可靠的交付,但是在某些情况下,UDP
是一种最有效的工作方式
TCP
提供面向连接的服务,在传送数据之前必须建立连接,数据传送结束之后要释放连接,而TCP是不提供广播或者多播服务的,由于TCP
要提供可靠的、面向连接的运输服务,因此会有额外的开销。
1.3 端口
为了使得运行不同操作系统的计算机的应用进程能够互相通信,就必须使用统一的方法,而这种方法必须与特定的操作系统无关,对TCP/IP
体系下的应用进程进行标志。
应用层中的应用进程要通过运输层发送到互联网,必须通过某个门,这个门被设置为通信的抽象终点,这些抽象终点的正式名称就是协议端口,一般就称为端口,每一个端口用一个称为端口号。
这种端口属于是软件端口,软件端口是应用层的各种协议进程与运输实体进行层间交互的地点
当应用层要发送数据的时候,就把数据发送到合适的端口,然后运输层从该端口读取数据,进行后续的处理工作。
当运输层收到对方主机发来的数据的时候,就把端口发送到合适的端口,然后应用进程就从该端口读取这些数据。
端口必须有一定容量的缓存来暂时存放数据
TCP/IP
的运输层用一个16
位的端口号来标志一个端口,但是,端口号只有本地意义,它只是为了标志本计算机应用层中各个进程在和运输层交互的时候层间接口。
进程要互相通信,首先要通过主机IP
号找到对方的主机,然后通过端口号
,找到进程数据交互的接口进行数据的发送与接收。
2. 用户数据报协议UDP
2.1 UDP概述
UDP是无连接的
:发送数据之前是不需要建立连接的,发送结束之后也不需要释放连接。
UDP是尽最大努力进行交付的
:就是不保证可靠交付,主机不需要维持复杂的连接状态表
UDP是面向报文的
:发送者发送下来的报文,在添加首部后就向下交付IP
层下放到网络层,它对于应用层下来的报文首部,它不合并也不拆分,应用层给UDP多长的报文,UDP就直接发送,一次只发送一个报文,
而对于接收者的UDP而言,在去除了首部之后就就直接交到应用层了。
也就是说UDP一次将会交付一个完整的报文,因此应用进程需要选择合适大小的报文,如果报文太长,
IP
协议可能会将其进行分片,这是由于数据链路层中,CSMA/CD的MTU规定了是46~1500个字节的大小,如果UDP的报文段太短了,就可能导致IP数据报的首部相对长度太大,降低了IP层的效率。
UDP是没有拥塞控制的
:网络中出现的拥塞不会使得源主机的发送速率降低,这对某些实时应用是很重要的。一些应用场景是要求发送方以恒定的速率发送数据,如果中途丢包也可以接受。
UDP
支持一对一、一对多、多对一、多对多的交互通信
UDP的首部开销小
:只有8个字节,比TCP的首部要求要短
如图所示,假设主机H1和主机H2通信关系如下:
P1->P4,P2->P4,P3->P4
主机H1的os为这三个进程指派了网络通信端口,分别是a,b,c,同理H2,
在知道了对方的通信端口后,就可以组装UDP数据报的首部了,格式如源端口,目标端口
那么分别就是(a,x)、(b,x),(c,y)
组装了相关数据之后就下发到网络层
,这时候它们三个应用进程的下发报文被组装成不同的IP
数据报后,就会共用一个网络层协议
,这就是复用的核心。
然后通过IP数据报
协议,在互联网上根据各种路由选择协议进行传输,最终将IP数据报
发送到对方的主机,分别传送到相应的端口,这个过程中,原本共用一个传输协议的报文被分发到了不同的端口,这个过程就是分用,便于上层的应用进程到端口读取数据。
2.2 UDP的首部格式
用户数据报UDP
有两个字段,数据字段和首部字段,首部字段比较简单,只有8个字节,4个子字段,每个子字段的长度都是2字节,各字段的意义如下
- 源端口:源端口号,在需要对象回信的时候选用,不需要的时候全部填入全0
- 目的端口:目的端口号,这在终点交付报文的时候必须使用
- 长度:UDP用户数据报的长度,其最小值是8
- 检验和:检测UDP用户数据包在传输过程是否有差错。
如果接收方UDP发现收到的报文中的目的端口号不是正确的,也就是说接收方中找不到对应该端口号的应用进程,那么就会将该应用报文给丢弃掉,并且在网络层中启动ICMP
网际报文协议,发送端口不可达
的差错报文到发送方。
校验和的检验过程
在计算检验和的时候,要在UDP
用户数据报之前增加12个字节的伪首部
,这个伪首部
并不是UDP数据报真正的首部,只是在计算检验和的时候,临时添加到UDP
用户数据报前面,得到一个临时的UDP用户数据报,检验和就是依照这个临时的UDP
用户数据报来计算的,它的作用仅仅只是用来计算检验和
UDP
的检验和是把首部和数据部分一起都检验
- 第一步,先将检验和字段设置为全0
- 第二步,将伪首部以及UDP看作是由许多16位的字串拼接起来的。
这个怎么理解呢,你看图,153.19.8.104,这个是点分十进制法的一个IP地址,然后我们知道它一个.之前的那个数字是8个bit表示的嘛,所以的话就是说,你把这一整段东西,分成是16bit一个单位的报文
,然后你把这个报文按照这个规则去拆,拆出来像图那样子。既然是16个bit一个单位,也就是2B一个单位,所以的话UDP用户数据报如果不是偶数个字节的话,那么就要填入一个全零的字节
发送方或接收方根据IP报文首部获得8字节的源地址+目的地址、2字节的0字段+UDP协议字段、2字节的报文长度(首部+数据),得到12字节伪首部,临时添加在首部前面。
- 第三部,对上述处理完毕后的报文段按照二进制反码求这些16位字的和,当没有差错的时候结果应该全部都是1
问题:UDP的数据报的检验是在哪里
由于UDP的数据报检验中利用到了IP数据报中的关键数据源地址和目的地址
,而如果报文上到了传输层,那么就肯定没有IP数据报的内容了,因此推测这个过程应该是在网络层完成的。
3.传输控制协议TCP
3.1 TCP的特点
TCP是面向连接的传输层协议
:必须先建立TCP
连接,在传输数据完毕之后,必须要释放已经建立的TCP连接每一条TCP连接只能有两个端点
,每一条TCP连接只能是点对点的TCP
提供可靠交付的服务TCP
是全双工通信的,它允许通信双方的应用进程在任何时候都能发送数据。
TCP连接的两端都设计有发送缓存
和接收缓存
,这是他们实现双向通信的基础,在发送的时候,应用程序在把数据传给TCP的缓存之后,就可以做自己的事情,然后TCP
会根据网络情况在合适的情况将数据发送出去,在接收的时候,TCP
把收到的数据放入缓存,上层的应用进程在合适的时候读取缓存中的数据
面向字节流
这个所谓流是是指流入到进程
或者从进程流出
的字节序列,TCP和应用进程的交互一次是一个数据块,但是TCP把应用程序交付下来的数据仅仅看作是一连串无结构的字节流
。TCP协议不保障接收方应用程序所接收到数据块和发送方应用程序所发送的数据块具有相同的大小,但是它能够保证这一次数据交换中,发送方所发送的字节流和接收方所接收的字节流是完全一样的。
TCP和UDP在发送报文的时候所采用的方式完全不同
TCP并不关心应用进程一次把多长的报文发送到TCP的缓存中,而是根据对方给出的窗口值和当前网络拥塞的程度,
来决定一个报文段到底应该包含有多少个字节
而对于UDP而言,UDP报文段的长度是应用进程规定的
同时,如果应用进程传送到TCP缓存的数据块太长了,TCP就可以把它划分成短一些的数据块再传送出去。如果应用进程一次只发来一个字节,TCP也可以等待积累足够多的字节再组装为报文段发送出去。
3.2 TCP的连接
TCP把连接
作为最基本的抽象。而用来描述这种端到端的连接可以使用套接字进行描述
套接字socket - (Ip地址:端口号)
每一条TCP连接唯一地被通信两端的两个端点所确定。
4. 可靠传输的原理
4.1 停止等待协议
我们假设A是发送方而B是接收方,我们将要传送的数据单元都称为分组,停止等待就是每发送完一个分组就停止发送,等待对方的确认,在收到确认之后再发送下一个分组
。
4.1.1 无差错的情况
A发送分组M1
发送完就阻塞等待B发来一个确认,唤醒A,待A被唤醒后,继续发送M2
,发送完M2
后阻塞等等待,等待B发来一个ACK
对A进行唤醒。
4.1.2 产生差错的情况
在出现差错的时候,B不会向A发送ACK
,而出现差错通常有两种情形
- A发送分组的时候,M1在运输过程中因为网络问题而丢失了
- A发送分组的时候,M1在运输到了B方,但是未通过检验,也就是数据包是问题的,这时候B也就会直接将数据包给丢掉,而不会向A做任何的反馈处理。
这两种情况下,B都不会向A发送ACK
,那么双方也并不会干耗着,TCP协议中存在着超时重传机制
这种机制是如何实现的呢?
A为每一个已发送的分组都设置了一个超时计时器,如果A只要在超时时间到之前收到了关于这个分组的确认,那么就会撤销这个计时器,而如果到了超时时间还没有收到确认,这时候A就会认为B没有收到A的数据包,然后启动超时重传。
- A在发送完一个分组之后,必须暂时保留已发送分组的副本,这个副本的作用是在发生超时重传的时候,能够重新发送该分组,只有在收到相应的确认之后才会清除暂时保留的分组副本
- 分组和确认分组都必须进行编号的,编号的目的是为了明确哪一个发送出去的分组收到了确认,而哪一个分组还没有收到确认
- 超时计时器的启动是在TCP报文的最后一个bit从端口中发送完毕之后才启动的
- 超时计时器的设置的重传时间应该要比数据在分组传输的平均往返时间更长一些。
4.1.3 确认丢失和确认迟到
所谓确认丢失,就是B在向A发送ACK
信息的时候,这时候ACK
丢掉了,然后这时候的情形是这样的
- A发送的分组,B正确地收到了
- 如果A收到了ACK,那么继续发送下一个数据包
- 可是现在A没有收到ACK,那么A就会认为是A发送的分组丢失了,这时候超时时间一到,就会马上开始超时重传
然后这时候B就会收到两个重复的分组,这时候B的操作是
- 将这个重复的数据包丢弃,不向上面进行交付
- 再向A发送一次
ACK
所谓确认迟到,就是B向A发送ACK
信息,这时候ACK
没有丢掉,也没有出问题,但是可能局限于网络的限制,它迟迟没有到达B,而是到了超时重传到的时间之后,A才收到一个ACK
,但是这时候重传的M已经出去了,这时候也可以分两种情况
- A重传分组后,还没有收到ACK,这时候A收到M的ACK之后,就会着手开始发下一个包
- A重传分组后,已经收到了ACK,而第一个ACK奇慢无比,在重传的ACK之后才到,这时候A就会把这个ACK收到,但是啥也不干。
这是一个关于重复收到确认和重复收到分组的处理策略。
像这样的可靠传输协议通常称为是自动重传请求ARQ
,接收方不需要请求发送方重传出某个出错的分组
4.1.4 信道利用率
TD
是发送时延,我们将横轴看作是时间轴,假设我们是发送一个分组,分组的第一个比特发送出去的时间点就是TD的左端点,分组的最后一个比特发送出去的时间点就是TD的右端点,那么TD就是分组的总比特数/数据发送率(每个时间单位发送出去的比特数),比如说有100个比特,然后发送率是10bit/s
,那么也就是说发送用的时间就是10s
假设B处理A发来的分组的处理时间可以忽略不计,同时马上发回ACK
确认分组,RTT是数据往返时长,也就是从一端开始发送到一端被接收的时间,A在经过时间(TD+RTT+TA)
之后就可以发送下一个分组了。而对于信道的使用率来说,仅仅是TD的时间才是用来传送有用的数据的.
这个信道指的是发送数据的信道
而不是传送数据的信道
。
4.2 连续ARQ协议
为了提高传输效率,发送方可以不使用低效率的停止等待协议,而是使用流水线传输。
流水线传输就是发送方可以连续发送多个分组,而不需要发完一个分组就停下来等对方的确认,这样可以使得信道上一致有数据在不间断地进行传送。
- 发送窗口:发送方会维护一个发送窗口,位于发送窗口内的分组都可以被连续的发送出去,而不需要等待对方的等待
- 发送窗口滑动:发送方每收到一个确认,就把发送窗口向前滑动一个分组的位置
- 累计确认:接收方对按序到达的最后一个分组发送确认,表示:到这个分组为止的所有分组都已经正确收到了。
在启用滑动窗口的时候,我们可以将发送窗口中的分组都连续发送出去,而不需要等待对方的确认。
累计确认能够解决确认丢失的问题,在确认丢失的前提下能够不重传。
这个是啥意思呢?就是说,原本确认丢失的单位是单个确认,而对于累计确认机制而言,它会对按序到达的最后一个分组做确认,举个例子来说,本来发送一个确认号为10的,但是这个确认丢失了,如果后边有个确认号是12的发过来了,那么这时候就认为12以前的全部都收到了,这时候不需要重发10的确认了。
缺点主要还是为了维护TCP的可靠性而带来的副作用。
假设我们发送方已经发送了前5个分组,而中间的第三个分组丢失掉了,这时候只能对前两个分组发出确认,但是这时候接收方其实是已经收到了4和5的分组的,但是发送方就会认为你345都没收到,然后就会连带着把后边的3个分组又重新发过一次,这就叫做GO-back-N
,它表示需要再退回重传已经发送过的N个分组。
5.TCP报文段的首部格式
TCP虽然是面向字节流的,但是TCP传送的数据单元是报文段,一个TCP报文段中包含有首部和数据。
TCP报文段首部的前20个字节是固定的,后面有4n个字节是根据需要而增加的选项,n是整数,因此TCP首部的最小长度是20个字节
- 源端口和目的端口:各占两个字节,分别写入的是端口号
- 序号:占有4个字节,也就是说序号范围是
[0,2^32-1]
,序号增加到2^32-1之后就又会回到0,猜测底层定义这个数据的时候应该用的是无符号整形或者是对其做了mod2^32 的运算。因为TCP面向字节流的特性
,因此序号描述的是报文段中传输的字节流的属性,序号字段指的是本报文段所发送的数据的第一个字节的序号,比如说一个报文段的序号字段值是301,而携带的字段一共有100个字节,这就说明,这个报文段的下一个报文段的序号是401。 - 确认号:占4个字节,是期望收到对方下一个报文段的第一个数据字节的序号。比如说B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200个字节,这也就是说这个报文段发送的
501~700
的字节,这就表明了B期望收到A的下一个数据序号数据是701,因此在发送给A的确认报文段中把确认号设置为701
你应该要记住:如果确认号是N,那么也就是说到序号N-1为止的所有数据都已经正确收到了
- 数据偏移:占4位,指的是TCP报文段的数据起始处距离TCP报文段的起始处有多远。说人话就是
首部的长度
,这是因为首部是变长的,是20+4n个字节。数据偏移的单位
是32位字,也就说4个bit,能代表的十进制数是15,然后你单位是4B,那么也就是说数据偏移量的最大值是60个B - 保留
- 紧急URG:当URG为1的时候,代表的是紧急指针有效,它告诉系统报文段中有紧急数据。发送方TCP就把紧急的数据插入到本报文段数据的最前面,而在紧急数据后面的数据依然是普通数据,这时候就要与首部中的紧急指针配合使用了。
- 确认ACK:仅当ACK=1的时候才是有效的
- 推送PSH:当两个应用进程进行交互式通信的时候,希望收到实时回应。当这个字段设置为1的时候,TCP就会马上创建一个报文段并push出去而不会等到缓存满了才发送
- 复位RST:当RST=1的时候.这时候必须释放连接并重新建立连接
- 同步SYN:在连接建立的时候用来同步序号,当SYN=1而ACK=0的时候,这表示是一个连接请求报文段,如果对方同一建立连接,则应该在响应的报文段中使用
SYN=1ACK=1
表示接收请求。从这就可以得出,SYN=1
的时候,这是一个连接请求或者是连接接受的报文。 - 终止FIN:用来释放一个连接,当FIN=1的时候,表示此报文段的发送方的数据已经发送完毕,并且要求释放运输连接
窗口
:占2个字节,窗口指的是发送本报文段的接收窗口
,窗口值是用来告诉对面,从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量,以字节为单位。
之所以要有这个字段,是因为任何接收方的缓存都是优先的,这也就决定了发送方发送的数据长度不能乱来,提供此依据让其设置发送窗口
例如,发送了一个报文段,它的确认号是701,而窗口的字段算出来是1000,这就是说,从701这个字节开始, 我可以一直收701~1700的字节,你可以给我发送这范围内的数据。
窗口字段明确指出了现在允许对方发送的数据量,窗口值是动态变化的
- 检验和:占两个字节,与UDP的检验逻辑一致,但是协议字段有所不同,UDP是17,而TCP是6
- 紧急指针:指出本报文段中的紧急数据字节数,从开头算起,有多少个字节是紧急数据,当所有紧急数据都处理完了,才告诉程序恢复正常执行。
- 选项:长度可变,根据数据偏移可以计算出其长度最长是40个字节
TCP最初只规定了一个选项:最大报文长度MSS(Maximum Segment Size)
MSS是每一个TCP报文段中的数据字段的最大长度,然后数据字段+首部才是报文段
MSS应该尽可能大一些,只要在IP层传输的时候不需要再分片就好了,在连接建立的过程中,双方都把自己能够支持的MSS写入到这个字段,以后就按照这个数值传输就可以了,两个传输方向可以有不同的MSS值(比如说从A到B,一开始不知道MSS,然后B那边确定好了,通知A从A到B的MSS是多少,然后A填进去就行了,从B到A同理),这是由在不同的通信线路上是否需要分片来决定的,如果没有填写,那么默认就是536个字节
6. 可靠传输的实现
6.1 以字节为单位的滑动窗口
现在假定A已经收到了来自B的确认报文段,如下图所示
其中窗口大小是20个字节,而B发来的确认号是31,这也就是说30包括它自己以及之前都被正确收到了。
- 发送窗口:在没有收到B的下一个确认之前,A可以把窗口内的所有的数据连续地发送出去,而不需要一个字节一个字节的确认交替发送。凡是已经发送过的数据,在未收到确认之前都必须暂时保留,以便在超时重传的时候使用。这里面的序号表示的是允许发送的序号。
要注意的是:A的发送窗口一定不能够超过B的接收窗口的数值,这个数值的约定是在TCP报文段中进行约束的,在窗口字段中约定了:作为接收的一方,它的窗口的大小。
发送窗口的决定性因素有两个:窗口的前沿和后沿,而窗口的大小也主要体现在这两个参量上
- 当不动的时候:也就是没有收到新的确认
- 当前移的时候:代表收到了新的确认,这时候窗口大小就可以变化了,表示可以发送后续的字段出去。
发送窗口的前沿是有可能向后收缩的,这一般发生在对方通知的窗口缩小了。
但是这样做的话可能会引发错误,比如说原本发送方可以发送46~50的字节序列,然后发送出去了,后来通知窗口缩小了,这时候就会产生错误,尽管对方已经收到了相关数据,但是现在向后收缩了,对于发送方而言,它就会认为自己没有发送过这样的数据。
- case1:按序发送并且全部接收
这时候A发送了3141序号的字节序列,已经发送出去了,但是对方还没有通知我收到,然后4250序号的字节序列是允许发送,但是没有发送,51~…就是窗口以外的数据,这些数据是不允许发送的,在窗口的前沿滑动到前面之前。
因此,描述一个窗口至少需要三个指针来确认此时的状态
P1指针:表示这个字节(不包含它自己)之前的所有的字节都已经确认收到了,在窗口的后沿之后
P2指针:表示这个字节(不包含它自己)之前到P1(包含P1)这个区间的字节都已经发送出去了,但是还没有确认收到
P3指针:表示这个字节(包含它自己)以及其之后的所有字节都不能被发送,因为没有归到滑动窗口中。
P3-P1:发送窗口
P2-P1:已经发送但尚未收到确认的字节数
P3-P2:允许发送但尚未发送的字节数,可用窗口
case2:中途丢包
这时候转换角度,我们从接收方的角度来看,假设B某一时刻收到了32序号和33序号的字节序列,但是没有收到31序号的字节
按序到达:B只能对按序收到的数据中的最高序号作出确认,B发送的确认报文段中的确认号依然是31,因为31还没有收到。
那么对于32和33怎么处理呢?这时候接收方会先把32和33存到窗口中,然后等待A给我发31,直到31 32 33 这个这三个序号都按序到达了,这时候B就会发34,说它期待的下一个序号是34
在这个过程中,可用窗口扩大了3个字节数据单位。这个扩大是这样做的:
首先B会把31~33的数据交付到主机,然后删除缓存中的这些数据,然后窗口移动三个单位
接着会给A发送确认号,这个序列号34告诉它下一个期望的序列号是谁谁谁,同时还告诉它,之前的都已经收到了,可以不用再发送这些数据给我了。
允许发送和允许接收:这个机制是滑动窗口保证双方通信可靠性的基础,主要体现在发送方的滑动窗口内的数据是允许发送的,而接收方的窗口内的约定的序号是允许接收的,同时这个机制也提高了效率,它对于之前接收到的数据起到了一个缓存作用,而不是简单地丢弃,在丢失的数据到达之后,结合之前收到的数据,窗口在收到丢失数据包的时刻能一下子扩充许多,而不是一个个数据单位地进行扩大。
PPT上的这张图是这样表示的,对于全双工的工作机制而言,作为数据发送方,每一个数据包都带有自己的数据包序号,同时也会携带确认号,这个确认号和窗口存在的前提的是双方同是发送和接收方,这时候双方就可以通过TCP报文互相交互信息。
A在发送完42~53这些数据之后,P2指针就和P3指针重合了,这时候必须停止发送数据包,等待来自B的ACK
,这时候可能会存在一种情况,A数据全部发出去了B数据也全部收到了,然后发确认号的时候出了岔子,确认全部迟到或者丢失掉了
这时候A没有接收到来自B的ACK,它只会认为数据在传输过程中丢失掉了,这时候**ARQ(自动重传请求)**就会起作用,之前说过,每个数据包在发送完毕(最后一个比特发出之后)的时候都会在系统内部启动计时器,一旦超时又没有收到ACK
,这时候就会重传数据包,直到收到ACK为止。
6.2 窗口和缓存的关系
- 缓存空间和序号空间都是有限的,并且都是循环使用的,也就是说是一个动态的环状数组,当首尾相遇认为为满
- 缓存或窗口中的实际字节数可能会很大
- 发送方缓存暂时存放的数据有
- 发送应用程序传送给发送方TCP准备发送的数据
- TCP已经发送但是还没有收到确认的数据
- 发送窗口通常只是发送缓存的一部分,已被确认的数据应当从发送缓存中进行删除,因此发送缓存和发送窗口的后沿是重合的
- 而对于发送窗口的前沿和发送缓存的前沿来说,是不一样的,通常来说发送缓存会预写入一些字节,有一部分字节是不在发送窗口之中的。
关于接收缓存
- 按序到达的、但尚未被应用程序读取的数据
- 未按序到达的数据
如果收到的分组被检测出有差错,那么就要丢弃,如果接收的应用程序来不及读取到的数据,接收缓存最终就会被填满,使得接收窗口减小到0,扩大来说,最大不能够超过接收缓存的大小。
可见:下一个期望收到的字节是接收窗口首部的第一个字节。
第一,虽然A的发送窗口是根据B的接收进行设置的,但是在同一时刻,并不能保证A的发送窗口总是和B的接收窗口一模一样,这是因为这些信息都是通过TCP报文首部进行解析的,而传输TCP报文是需要时间的。
第二,对于未按序到达的数据,TCP并没有做出明确的规定,直接丢弃不利于网络资源的利用,一种比较好的方法是存储在接收窗口中,等到字节流中所缺少的字节收到后,再按序交付上层的应用程序
第三,TCP要求接收方必须有累计确认的功能,接收方可以在合适的时候发送确认,也可以在给对方发送数据包的时候,把确认号的窗口号写入TCP报文首部。
6.3 超时重传时间的选择
ARQ
规定了TCP发送方在规定的时间内没有收到确认就要重传已发送的报文段
一个关键的问题是:超时重传报文段之后,如何判定此报文段是对原来的报文段的确认,还是对重传报文段的确认?
这几乎是一个无解的问题,任意一种情况的错误估计都会导致RTT的计算不合理。(实际上在TCP的时间戳选项可以用来精确的测量RTT,发送方在发送报文段的时候把当前的时钟的时间值发放入时间戳,接收方在该报文段中把时间戳字段值复制到时间戳会送回答字段中)
还有一种是重传队列中数据包的TCP控制块
在TCP发送窗口中保存着发送而未被确认的数据包,数据包skb中的TCP控制块中包含着一个变量tcp_skb_cb ->when,它记录了这个数据包的第一次发送时间,当收到该数据包确认,就可以计算RTT,RTT = 当前时间-when,这就意味着发送端收到一个确认,就能够计算新的RTT
Karn
提出的算法是这样说的,在计算加权平均RTTS
的时候,只要报文段重传了,就不采取这个样本,这样的话就不要导致传回的往返时间有歧义。设想这样的情况, 如果报文段的实验突然增大了很大,而且不是一时半会的事情,很有可能的以后的都是这样,而在这样的情况下,必然会导致重传,但是一直重传,根据这个算法重传时间就一直不会采集样本更新重传时间,这样的话重传时间就一直无法更新,就会一直导致重传。
修正的方法是报文段每重传一次,就把超时重传时间RTO增大一些,类似于PID的闭环控制,每次根据环境状态变量修改当前的自变量参数,典型的做法是取新的重传时间为旧的重传时间的两倍,这样的话能够实现动态调整。
6.4 选择确认SACK
如果收到的报文段没有差错,只是没有按照序号进行接收,中间还缺少一些序号的数据,能否只传送缺少的数据而不重传已经正确到达接收方的数据?
对于这种问题提出了选择确认SACK技术,这种技术在TCP报文段首部中的选项添加了SACK选项,这个选项规定一个字节作为SACK的开关标志位,一个字节作为SACK所占用选项部分的长度,如果启用了SACK,那么就会在SACK选项中添加这几个字节块的边界,这个几个字节块边界以及TCP首部的确认号形成了选择确认SACK的核心,通过这种不定区间的信息,对方就知道哪些字节序列是收到的了,哪些字节序列是没有收到的了
7. 流量控制
7.1 利用滑动窗口实现流量控制
流量控制
:就是让发送方的发送速率不要太快,要让接收方来得及接收,这是因为接收方的缓存的大小是有限制的,如果超过了这个限制,接收方只能选择将其丢弃。
这是一个利用可变窗口进行流量控制的例子
发送方是A,接收方是B
- 连接建立阶段,B发出的TCP报文首部中包含有窗口大小的信息,一开始是
400
,假定 - 然后连接建立成功,开始全双工的TCP数据通信,由于窗口是400个字节,这时候A就可以发送
1~100
、101~200
、201~300
、301~400
的数据过去到B那里 - 然后A发出了4个数据包出去,然后
201~300
的数据包丢掉了,这时候B采取应答(注意:应答时机并不是特定的,而是与当前的网络环境相关)
,然后B发出一个ACK,确认号是201
,然后调整了窗口大小是300
个字节 - 然后A收到了ACK之后,解析报文首部,然后知道了B那边的接收窗口大小,相应的调整自己这边的发送窗口
考虑一种情况,B向A发送了零窗口的报文段不久,B的接收缓存又空出来了,于是B向A发送了
rwnd=400
的报文段,然而这个报文段在传输的时候走丢了,现在就会形成这样的局面A一直在等待关于B的非零窗口通知,而B呢一直在等待A发送的数据,注意哦,B这里是接收方,ACK丢了就丢了,ACK是没有超时重传机制的。那也就是产生了死锁,互相等待。
为了应对这样的情况,TCP为每一个连接设计有一个
持续计时器
,只要TCP连接的一方收到对方的零窗口通知,就会启动这个持续计时器,如果持续计时器设置的时间到了,它就会去发送一个零窗口探测报文段(仅携带1个字节的数据),而对方就在确认这个探测报文段时确认了当前的窗口值,如果窗口值依然是0,那么收到这个报文段的一方就重新设置持续计时器,如果窗口不是零,那么死锁的僵局就可以打破了
7.2 TCP的传输效率
传输数据的三种控制方式
MSS阈值法
:TCP维护一个变量,这个变量是连接建立时,发送双方互相通知对方注意不是协商出来的
的一个变量,这个变量值等于最大报文段长度MSS
,只要缓存中的字节数量到达MSS
的时候,它就组成成一个最大报文段发送出去PUSH机制
:由发送方的进程指明要求发送的报文段,也就是Push操作。当Push字段启用的时候,直接创建报文段并且发送出去计时器机制
:发送方的一个计时器时限到了,这时候就会把当前所有的缓存数据装入报文段,但是长度不能超过MSS
Nagle算法:若发送应用进程把要发送的数据逐个字节地送到TCP
的发送缓存,则发送方就把第一个数据字节先发送出去,把后面到达的数据字节都缓存起来,直到对方给我发来关于第一个数据字节的ACK
之后,我才把缓存中的所有数据字节组成成一个TCP
报文段发送出去,当到达的数据达到发送窗口的大小的一半或者达到报文段的最大长度的时候,就马上发送一个报文段,这样做就可以提高网络的吞吐量
糊涂窗口综合征
这种情况是当TCP的接收方的缓存已经是满的了,而交互式的应用进程一次也只会从接收缓存中读取1个字节,然后向发送方发送确认,并把窗口设置为1个字节,发送方收到ACK之后,依然只是将自己的发送窗口设置为1个字节。这样进行下去,就变成了停止等待协议。
要解决这个问题,可以让接收方等待一段时间,使得接收缓存有一定空间了,发送方就可以发送更多的数据了,这时候才发出确认报文,然后更新新的窗口大小。
此外发送方也不要发送太小的报文段,而是将数据累计成足够大的报文段,或者达到接收方缓存的空间一般大小。
8. 拥塞控制
8.1 一般原理
拥塞
:在某段时间内,如果网络中的某一资源的需求超过了该资源所能够提供的部分,网络的性能就会变坏。
问题的实质往往是整个系统的各个部分是不匹配的才会导致拥塞,拥塞导致的重传会使得网络情况更加糟糕
拥塞控制:所谓拥塞控制就就是防止过多的数据注入到网络中,这样可以使得网络中的路由器或者链路不至于过载,拥塞控制所要做的都有一个前提,就是网络能够承受目前的网络负荷。这是一个全局的调配过程
而基于滑动窗口实现的流量控制,控制的是双方通信的窗口,是针对通信端对端的两个主机的调配,是局部的调配。
简单来说,就是一个手段,但是要解决的是不同的问题,同样是使得发送端的速率减小,但是流量控制是因为接收方的缓存太小了,你继续发这么快,他就继续丢这么快。发了也白发,不如慢一点,这样的话不至于白发数据。
而拥塞控制则是接收方有空间有能力收你发过来的东西,但是这时候网络出问题了,搞到你数据一直过不去,接收方也收不到,你继续这个大速率的发数据过去,网络更堵,总之,这样也是发了也白发,不如慢一点,然后等到网络畅通的时候再快也不迟。
总而言之,流量控制和拥塞控制都是为了解决你发了也是白发,然后超时重传还干扰网络的这个问题
拥塞控制所起的作用
8.2 拥塞控制方法
TCP执行拥塞控制的算法有四种
- 慢开始
- 拥塞避免
- 快重传
- 快恢复
为了理解的的方便,先规定数据是单方向传送的,对方只传送确认报文,接收方总是有足够大的缓存空间因而发送窗口的大小总是由拥塞程度来决定的。下面将要讨论的是基于窗口的拥塞控制
8.2.1 慢开始和拥塞避免
发送方维护着一个叫做拥塞窗口(cwnd)
的状态变量,拥塞窗口的大小决定于网络的拥塞程度,并且是动态变化的,发送方让自己的发送窗口等于拥塞窗口。
发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就可以再增大一些,以便把更多的分组发送出去,这样就能够提高网络的利用率了,但是只要网络出现拥塞或者可能将要出现拥塞,就必须把拥塞窗口减小一些,以减少注入到网络中的分组数。
检测拥塞的标志:只要发送方没有按时收到对方的确认报文,也就是说,只要出现了超时,就可以估计产生了拥塞。
慢开始是如何工作的?
由于刚建立起TCP
连接,这时候并不知道网络中的负荷情况,这时候一个比较好的做法是由小到大逐渐注入到网络中的数据字节,也就是说从小到大慢慢扩大发送方的拥塞窗口。
它规定,在每收到一个新的报文段的确认之后,就可以把拥塞窗口增加最多一个SMSS
的数值
拥塞窗口cwnd每次的增加量 = min(N,SMSS)
SMSS就是Sender Maximum Segment Size
N是原先未被确认的,但是现在被刚收到的确认报文所确认的字节数,要注意增加量的单位是字节
发送方每收到一个对新报文段的确认之后,就把发送方的拥塞窗口+1,如上图所示,初始拥塞窗口是1,然后接着收到了一个确认,窗口就变成了2,如果两个确认报文都回来了,那么窗口就会再+2变成4。
为了放置拥塞窗口cwnd
增长过大而引起网络拥塞,还需要设定一个慢开始的门限
当cwnd<慢开始的门限:使用上述的慢开始算法
当cwnd>慢开始的门限:停止使用慢开始算法而改用拥塞避免算法
当cwnd=慢开始的门限:既可以使用慢开始算法也可以使用拥塞避免算法
拥塞避免算法的目的是让拥塞窗口cwnd
缓慢地增大,执行算法大约是这样说的
每经过一个往返时间RTT,发送方的拥塞窗口cwnd
的大小就+1,而不是成倍增加,因此在拥塞避免阶段就称为加法增大
- 初始阶段:执行慢开始算法, 一开始拥塞窗口只有0,然后依据慢开始算法,以2的指数级增长
- 第一次执行拥塞避免算法:当慢开始达到门限之后,就执行拥塞避免算法,这时候每经过一个往返时延RTT,窗口就增加1而不是收到ACK就+1
- 第一次遇到超时,超时之后标志网络发生了拥塞,于是调整门限值,门限值调整为24/2=12也就是发生超时的拥塞窗口的一半大小
- 第二次执行慢开始算法,这时候将拥塞窗口设置为0,然后也是成倍的增加,但是门限值是12
- 第二次执行拥塞避免算法:这时候达到门限值,然后每经过一个往返时延RTT,窗口大小就会增加1
- 第一次遇到收到三个重复确认,这是也认为出现了拥塞,对于3-ACK的处理策略是执行快恢复的算法,这时候门限值设置为16/2=8,然后执行拥塞避免算法。
8.2.2 快重传和快恢复
快重传
:让发送方尽早知道发生了个别报文段的丢失,快重传要求接收方不要等待自己发送数据时才进行捎带确认而是应该立即发送确认,即使是收到了失序的报文段
也要立即发出已收到的报文段的重复确认。
现在假定接收方没有收到M3但是却收到了M4,按照快重传算法,必须发送对M2的重复确认,这时候的确认号是M3,也就是说要求要M3,快重传只要一连收到3个重复确认,就可以知道现在并未出现网络拥塞,只是接收方少到收到一个报文段而已,然后立即重传M3
因此在上图中的(4),这时候并不执行慢开始算法,而是执行快恢复算法,使得当前的门限值为当前的窗口的一半,然后开始执行拥塞避免算法。
拥塞避免的算法我们称为加法增大AI,而快恢复的算法我们称为是乘法减小,二者合在一起就是AIMD
算法
然后我们再对实际情况做进一步讨论,实际上接收窗口的大小总是 有限制,因此在发送窗口除了拥塞窗口之外,还需要考虑接收者的窗口大小, 这时候的策略是两者取最小值即可
8.3 主动队列管理AQM
路由器的队列通常都是按照先进先出
的规则处理到来的分组,由于队列的长度总是有限的,因此当队列已满的时候,以后再到达的所有分组都将被丢弃,这种策略是尾部丢弃策略
全局同步
路由器尾部丢弃往往会导致一连串分组的丢失,这就使得发送方出现了超时重传,使得TCP进入拥塞控制的慢开始状态,网络中是有许多
TCP
连接的,这就可能导致许多TCP连接在同一时间突然都进入到慢开始的状态
主动队列管理(ActiveQueueManagement)
:所谓主动就是不要等到路由器的长度已经达到最大值的时候才丢弃后面的分组,应该在队列长度达到某个值的时候就丢弃分组。
RED算法
- 若平均队列长度小于最小门限,则把新到达的分组放入队列
- 若平均超过最大门限,那么就直接丢弃分组
- 若平均队列长度在最小门限和最大门限之间,则按照概率丢弃分组
9 TCP的运输连接管理
TCP
是面向连接的协议,运输连接是用来传送TCP报文的,TCP的连接和释放是你每一次面向连接通信中不可缺少的过程,因此,运输连接就有三个阶段:连接建立,数据传送,连接的释放
- 要使得每一方都能确知对方的存在
- 要允许双方协商一些参数
(如窗口最大值、是否使用窗口扩大选项)
- 能够对运输实体资源
(如缓存大小,连接表中的项目)
进行分配
主动发起连接请求的我们叫做客户端,而被动等待连接的应用叫做服务器
9.1 TCP的连接建立
TCP连接建立的过程被叫做握手
,握手需要在客户和服务器之间交换三个报文段。
如图所示,A主机的是TCP客户端应用进程,B主机运行的TCP服务器端程序
最初两端的进程都是CLOSED
的状态,然后客户端请求服务端的服务,A主动打开连接,B被动打开连接
B的TCP服务器进程先创建传输控制块TCB
,准备接收客户进程的连接请求,然后服务器进程就处于LISTEN(收听)
等待客户的连接请求,如果有连接请求则马上响应
传输控制块TCB
存储了每一个连接中的一些重要信息,如TCP连接表,指向发送和接收缓存的指针,指向重传队列的指针,当前的发送和接收序号
一开始,A的TCP客户进程也是首先创建TCB,然后再打算建立TCP连接的时候,向B发出连接请求的报文段,格式是(SYN=1,seq=x),TCP规定了SYN报文段是不能携带数据的,但是要消耗掉一个序号。然后,A进入到SYN-SENT(同步已发送)
的状态
B收到连接请求报文段之后,如果同意连接的建立,那么就会向A发送确认。确认格式是(ack = x+1,SYN = 1,ACK = 1,)
,同时为了建立全双工的连接,它自己也会选择一个初始的序号seq = y
,注意,这个报文段也不能携带数据,但是也要消耗掉一个序号,然后B就进入了SYN-RCVD
同步收到的状态
TCP客户进程收到B的确认之后,还要向B给出确认,确认报文段的ACK设置成1,确认号是y+1,而自己的序号是x+1.
TCP的标准规定ACK报文段可以携带数据,但是如果不携带数据则不消耗序号
这种情况下,下一个数据报文段的序号依旧是
seq=x+1
这时TCP连接已经建立,A进入ESTABLISHED
,而B收到A的确认之后,也进入一个ESTABLISHED
为什么A最后还要发送一次确认?
考虑这样一种正常的情况,A发出连接请求,但是因为连接请求报文的丢失,或者是确认丢掉了,于是A根据超时计时器的指导再重传一次连接请求,但是后面收到了确认,建立了连接,数据传输完毕后就释放了连接,A一共发送了两个连接请求的报文段,其中第一个丢失,第一个到达了B,没有已经失效的连接请求报文段
异常情况
A发出的第一个连接请求报文段没有丢失,而是在某些网络节点长时间滞留了,以延误到连接释放以后的某个时间才到达B,这是一个没用的报文段,但B认为这是一个新的请求连接,然后同意建立连接,我们假设不需要报文握手,那么只要发出确认,新的连接就建立了。
但是现在A不会理睬B的ACK,也不会向B发送数据,但是B却认为你的连接已经建立了,这时候就会浪费许多资源。
小注意点:B发送给A的报文段也可以拆成两个报文段,也就是一个确认报文段和一个同步报文段
9.2 TCP的连接释放
数据传输结束后,通信的双方都可以释放连接,现在A和B处于ESTABLISHED
状态, 首先是A的应用进程先向TCP发出连接释放报文段,并停止发送数据,主动关闭TCP
连接
A把连接释放报文段首部的终止控制为FIN
设置为1,它的序号是上一次发送数据的最后一个字节的序号+1,这时候A就进入了FIN-WAIT-1(终止状态1)
等待B的确认,TCP规定需要消耗一个序号,即便不携带任何数据
B收到连接释放报文段就发出确认,ack = u+1
,seq = v
,等于B前面已传送过的数据的最后一个字节的序号+1,然后B就进入了CLOSE-WAIT(关闭等待)
的状态,这时候TCP
服务器进程这时候通知高层的应用程序。
注意,这个时候A已经没有数据要发送了,但是B可能还要发送数据,比如发送cookie等
A收到来自B的确认后,就进入了FIN-WAIT2(终止状态2)
,再等待B发出的释放报文段。
这时候B可能还要收尾数据要发,发完了之后其应用进程就通知TCP
释放连接,这时B发出的连接释放报文段使得FIN = 1
,现在规定经过收尾数据的发送后,B的序号是w,B必须重复上次已经发送过的确认u+1
,这时候B就进入了LAST-ACK
的状态等待A的确认
理一下,w是B发送数据的序号,而u是A发送给B的序号,根据TCP传输的规则,需要维持两者的关系。
A在收到B的连接释放报文后,必须对此发出确认,在确认段中把ACK置为1,确认号是w+1,而序号是u+1。
然后最后A还会进入一个TIME-WAIT
的状态,这个状态下TCP连接还没有释放掉,必须等待两个2MSL后才能释放,时间MSL叫做最长报文段寿命。经过了2MSL之后,撤销TCB,就结束了这次的连接
2MSL的作用
假设现在网络出现了波动,A发送的最后一个
FIN+ACK
的确认没有过去,这时候B就不会关闭连接,而是等待时间超时后就会重传FIN+ACK
这个报文段,然后A就能在2MSL中收到这个重传的FIN+ACK
报文段,接着A又重传一次确认,重新启动计时器。这样可以避免B没有收到报文A就关掉了.防止已失效的连接请求报文段,A在发送完最后一个ACK报文段后,再经过2MSL后,就可以使得本连接中产生的所有报文段都从网络中消失,下一个新的连接中就不会出现这种旧的连接请求报文段
保活计时器:服务器每收到一次客户的数据,就重新设置保活计时器,时间的设置通常是两小时。如果两个小时没有任何应答,服务器就发送一个探测报文段,以后每隔75s就发送一次,如果发送10次都没有任何应答,直接终止连接。