计算机网络(第6版) WriteBy 谢希仁 —— 传输层
计算机网络(第6版) WriteBy 谢希仁 —— 传输层
-
前言
主机与主机之间的通信,实际上是两个主机应用进程之间的通信,网络层只负责寻找对目标主机,而寻找对应的通信进程,是传输层来做的。传输层主要有两个协议,TCP和UDP。
使用TCP协议发送的数据称作TCP报文段(segment),使用UDP协议发送的数据称作UDP数据报。
UDP在发送之前不需要建立连接,远程主机收到UDP报文后,不需要给出响应。
TCP在提供面向连接的服务,发送数据之前需要建立连接。
在传输层,使用端口来区分进程,每个进程需要通信的话,会占用不同的端口号,源站点发送的TCP报文的首部中会携带目标进程的端口号的。
传输层为上面的应用层提供数据的传输服务(当然,这只是站在应用层的角度来看待的这个问题)
-
UDP协议
-
特点
-
无连接 => 减少了发送时延和传输时延
-
尽最大努力交付 => 尽力就好,即不可靠的交付
-
面向报文 => 不对应用层交付下来的报文做任何修改(比如分片),添加UDP首部后,交给网络层;对网络层交付上来的报文,也是去掉首部之后,原封不动的交给上层应用。
-
没有拥塞机制 => 网络出现繁忙时,源主机不会降低发送速率,允许一些报文的丢失
-
支持一对一、一对多、多对一和多对多的通信
-
首部开销小 => 只有8个字节
-
-
首部格式
-
源端口 => 在需要对方回信时选用。不需要时可用全0
-
目的端口 => 这在终点交付报文时必须要使用到
-
长度 => UDP用户数据报的长度,其最小值是8(仅有首部)
-
检验和 => 检测UDP用户数据报在传输中是否有错。有错就丢弃
-
伪首部 => 在进行校验的时候临时生成的,校验和就是按照这个临时生成的“伪首部”进行计算的
-
-
-
**TCP协议 **
-
特点
-
面向连接 => 使用TCP协议发送数据之前,必须先建立连接;发送完数据之后,必须释放连接
-
点对点通信(一多一)
-
可靠交付 => 无差错、无乱序、无丢失、无重复
-
全双工通信 => 可以同时进行数据的发送和接受,TCP的两端都设有发送缓存和接收缓存。应用将要发送的数据写入发送缓存后,就可以不用管了;TCP将收到的数据放入接受缓存,等待上层应用的读取。
-
面向字节流 => TCP中的“流”指的是流入进程或者流出进程的字节序列。TCP并不关心上层应用到底放了多大的内容到发送缓存中去,它只关心接收方的窗口值和当前网络的拥塞程度。如果上层应用写入的数据块过大,那么就分小一点嘛;如果发送的数据块较小(比如一字节),那就等多一点在发出去嘛。
-
-
TCP的连接
上文叙述过,TCP的连接是“点到点的”,这个“点”被抽象称为 socket(套接字 \ 插口) => 很形象嘛,插头插入插座不就建立了一条连接了嘛,哈哈。
socket被表示为**{ip地址:端口号}**
可以将这个socket理解为通信双方维护的一个状态,通信的过程就是去改变双方的这个状态。
-
TCP可靠传输的工作原理
-
停止等待协议
所谓的“停止等待”就是每次发送一个分组之后,就会等待接收方的响应,确保该分组准确的到达,然后发送下一个分组。=> 当然,这是理想的情况。
现在我们来想一下这一种情况:如果接收方经过校验后,发现数据包有误,丢弃了;或者说在路由的过程中就已经被丢弃了。那么是不是发送方就一直阻塞在那里呢?
为了解决这个问题,采用了一种叫做超时重传的机制。=> 每发送出去一个分组,就开启计时,时间到了还没有收到对方的响应,就重发;到期之前收到了,继续发送下一个,并且刷新定时器。
实际上,要超时重传机制需要考虑以下三点:
- 发送方发送一个分组之后,必须保留之前发送分组的副本(触发该机制时使用),只有在收到对方的确认后,才将其清除
- 分组和确认分组都要进行编号,这样发送方才能知道哪一个发送出去的分组收到了确认信号
- 超时时间的设置应当比数据在分组传输的平均往返时间更长一些。当然,对于时间的设定,是一个很复杂的问题。
当然,还有考虑另一种,接收方返回了确认分组,但是可能由于网路阻塞等原因,导致该分组没有按时到达接收方。
此时,发送方触发了重发的机制,又向目标主机发送了同样的分组,目标主机接收到了,丢弃掉这个重复的分组,重传确认分组。
对于发送方来说,也是会出现去人分组重复的情况,这就很粗暴了,直接丢弃掉。
上述机制称为自动重传请求ARQ (Automatic Repeat reQuest) => 重传的请求是自动进行的。接收方不需要请求发送方重传某个出错的分组。
很显然,停止等待协议不能很好的利用信道资源,为了提高传输效率,发送方可以采用流水线传输。
=> 发送方可以连续不断的发送多个分组,而不必等待上一个分组确认到达后,再发送下一个分组。
-
-
TCP报文段首部格式
-
**源端口:**发送方的进程所占用的端口号
-
**目的端口:**接收方的进程所占用的端口号
-
序号:TCP是面向字节流的。TCP传输的过程中,每一个字节都需要按顺序进行编号。起始序号在连接建立时设置。序号是用来标识数据段的。 => 比如某个TCP报文的数据段长度为100字节,首部中的序号值为301,那么就表示该数据段的第一个字节的序号是301,最后一个是400。如果后续还有数据要接上,那么下一个报文首部的序号值应为401。
-
确认号:期望收到对方下一个报文的第一个数据字节的序号。
-
**数据偏移:**表示TCP报文段的数据里TCP报文的起始处有多远。相当于指出了TCP报文首部的长度。该字段所能表示的十进制数是15,所以,TCP报文的首部最长是60字节,也就是选项字段最多40字节。
-
**保留:**就保留了嘛,现在也不知道用来干嘛。
-
URG:该字段位值为1时,表示这是一个紧急指针字段有效,相当于告诉系统这个报文要优先发送。
-
ACK:该字段位置为1时,表示确认号有效;为0,表示确认号无效。 => 连接建立后,所有的报文的ACK字段都应该置为1。
-
**PSH:**该字段置为1时,表示需要快速得到对方的响应,在网络协议栈中向上交付的时候,无需等待缓存填满了再向上交付。=> 很少有进程会这样使用
-
**RST:**该字段置为1时,表示出错啦,需要释放当前连接,重新建立一个新的连接。这个还可以用来拒绝连接一些非法报文和打开一个连接。
-
**SYN:**在连接建立时用来同步序号。SYN = 1 并且 ACK = 0 时,表示这是一个连接请求报文。对方同意连接,应该将响应报文的 SYN 和 ACK 都置为1。
-
**FIN:**该字段值为1时,表示发送方的数据已经发送完毕,要求释放连接资源
-
窗口:这个就是在给发送方说,目前能够接受的数据量大小。窗口值的大小不是固定的,是动态的,随时间的变化而变化,以适应网络条件和接收端的缓冲容量大小。
-
**检验和:**校验首部和数据这两部分。
-
**紧急指针:**提高优先级嘛,先发送我!!!只有在URG字段为1时,才有意义
-
选项:这里主要介绍MSS(最大报文段长度)字段,实际上是每一个报文段中数据部分的最大长度限制。=> 为什么会有这么一个限制呢?如果数据部分太小了,实际上的网络利用率很低;太大了,也不行,如果太大了,会在IP层进行分片处理,重要的是,如果其中一个分片在传输的过程中出现了问题,你说怎么搞?IP层只负责最大努力交付,分组发送就发送了,又不会有缓存,那是不是得传输层在进行分片呢?然后全部重传?很显然,这样很蠢。。。
-
-
TCP可靠传输的实现
-
以字节为单位的滑动窗口
现在,假设A主机收到了B主机的确认分组,构造了一个大小为20的滑动窗口,当然主机B那里同样是这样的。后沿之前的序号代表已经发送并收到了确认;前沿之前的数据包不在窗口范围内,不能发送。其实也不会将窗口中的数据包全部发送出去:
我们来看A的发送窗口中,有三个指针,图都很好看懂的吧。。。
在B的接收窗口中,我们可以看见,32和33序号的数据提前到达了,但是也只能等待31序号的数据达到之后,在向上进行交付。这也可以称作“队头阻塞” 。
此时,31 ~ 33序号的包已经收到了,双方的窗口移动:
观察发送方的窗口,我们发现:P3指针移动了三个位置,因为收到了之前窗口内前三个包的确认分组。
TCP的缓存与窗口的关系:
发送缓存用来暂时存放应用程序放入缓存中待发送的数据、已发出但未确认的数据。 => 应用程序应当控制写入缓存的速度和数据量大小,否则可能会造成缓存溢出。
接收缓存用来暂时存放按序到达,但尚未被程序读取的数据、为按序到达的数据。 => 如果应用程序不能及时处理缓存中的数据,那么接收缓存最终会被填满,窗口大小缩减为0,不在接收数据。
这里还有三点需要注意的:
-
大多时候,在同一时刻,通信双方的窗口大小并不是统一的,因为网络存在各种各样的时延。
-
对于不按序到达的问题,处理策略就是等待第一个确认序号的包到达后,再统一交付上层应用。
-
接收方可以延迟累积(不用每收到一次数据,就发送一次响应)确认,但是时间不宜过长,不然会触发发送方的超时重传机制。
-
-
-
超时重传的时间选择
这里的话,数学好的去看一下吧,我数学不好。。。。那些数学名字都不知道是干嘛的。。。
-
选择确认SACK
这个东西出现的背景主要是由于网络传输中会出现乱序的问题。
出现乱序问题后只重传没有收到的部分就好了,但是,这需要在TCP报文的首部信息中添加一个“允许SACK”的选项,然后指定边界(选项字段长度是有限的,指定的边界数量也是有限的,算下来也很少)。
目前大多数的实现还是超时后,全部重传。
-
-
TCP的流量控制
-
利用滑动窗口实现
所谓的流量控制就是让发送方的发送速率不要太快,让接收方来得及接收。
这是一个接收方使用滑动窗口控制发送速率的例子,接收方给发送方设置的窗口初始值为400。这个图,应该没问题吧。。。(rwnd表示给发送方设置的窗口值大小)
这里会涉及到一个问题,假设B这边的接收窗口大小为0了,给发送方发送了一个报文,发送方也将自己的窗口大小改为0。然后,当接收端的缓存空间有空余的时候,给发送方发送了一个重新设置窗口大小的报文,但是丢掉了!!!于是乎,发送方在等,“我什么时候能再发送啊”;接收方也在等,“这小子怎么还不发不过来,睡着啦?”。这就是出现了死锁现象。
为了解决这个问题,每一个TCP连接都会有一个计时器,在收到零窗口报文的时候,开始计时。到时还未收到发送通知的话,就会主动发送一个探测报文。如果还是零窗口,那么就刷新定时器;如果不是,正常发送数据即可。
-
传输效率的考虑
这个东西在于不多不少,刚刚好。
在发送的时候,先发送一字节的数据,将后续的数据进行缓存起来,等待收到那个一字节数据的确认消息后,在发送整段报文(Nagle算法 => 它还规定如果缓存中待发送的数据到达窗口的一半或者MSS时,就立马发送一个报文段)。
接收的时候可能出现应用进程一次只读取很少数据量(几字节)的情况,那么给发送方设置的窗口大小就会比较小,传输利用率大大降低。于是乎,接收端可以“延时”嘛,等待缓存可以容纳一个较长的报文段或者缓存有一半的空间空闲时,再向发送放发送窗口设置的报文。
-
-
TCP的拥塞控制
拥塞指的是对于网络资源的需求(带宽、路由处理能力等)大于其供给能力,导致数据传输延迟增加甚至数据丢失的现象。
常见的拥塞控制的算法有:慢开始、拥塞避免、快重传、快恢复
发送方维护一个叫做拥塞窗口,该窗口的大小取决于当前网络的拥塞程度,可以动态的变化。结合之前的滑动窗口,我们可以得出:滑动窗口的大小(发送端)小于等于拥塞窗口。
但是这里摆在我们面前的一个问题是,我们怎么知道当前的网络是否拥堵呢?
“慢开始算法”的设计思路是刚开始发送少量的数据包去探测网络是否拥堵,响应到达后,逐渐扩大拥塞窗口的大小。
但是不可能无限的去扩大,当达到慢开始门限的时候,就采用拥塞避免算法。
拥塞避免算法的思路是每收到一个响应,就将拥塞窗口的大小扩展一个MSS。
无论是是在慢开始阶段还是在拥塞避免阶段,只要发送判断出了当前网络拥塞,就会将慢开始门限设置为拥塞时发送窗口值的一半,然后重新执行慢开始算法。 => 减轻路由器的压力,让路由器能够处理积压的任务
接下来呢,我们在来说一下“快重传”和“快恢复”。
之前我们提到过失序报文段的问题,这可能是网络拥塞导致的,之前的做法就是超时重传。
如果应用“快重传”的话,发送方就能及早的知道哪个报文段没有到达对方,然后做出相应的处理。
如图,接受方一直在等M3,但是就是等不到,所以每收到一个不是M3的报文段,就向发送方发送对于M2的重复确认。
发送方连续三次接收到对于M2的重复确认的时候,就将慢开始门限减半。之后不是执行慢开始算法,而是执行拥塞避免算法。(既然发送方都能收到接收方发送的重复确认请求,那么可能目前的网络没有发生拥堵)
不仅要考虑网络的拥塞程度,还要考虑接收方**接收窗口(通知窗口)**的大小,发送方的发送窗口大小一定不能超过接收方的接收窗口大小。
随机早期检测RED:
路由器队列长度是有限的,当队列满了的时候,后续的数据包就会被丢弃掉,这就是尾部丢弃策略。发送方收不到响应就会导致超时重传。更为严重的,会导致许多的TCP连接一起进入拥塞控制的慢开始状态,称为全局同步,大幅度影响通信质量。
RED算法的核心思想是在路由器的输出队列即将满之前,随机地丢弃一些数据包,以此来避免拥塞的发生和减少数据包的延迟。
-
TCP的运输连接管理
-
TCP的连接建立
没错,这就是你朝思暮想的三次握手。
最开始,双方处于closed状态。
客户端A,安耐不住寂寞,向服务器B发送了一个连接请求报文(SYN字段的值为1,消耗掉一个序号,不能携带数据),然后进入【SYN-SENT(同步已发送)】状态。
服务器B呢,处于一个监听状态,传输层可以将报文交付给应用程序。
服务器B收到了客户端A发送的连接请求报文后,给客户端A响应了一个报文,表名服务端有收发的能力。
客户端A收到了服务器B发送的响应报文后,又发送了一个报文给服务端,表名客户端有收发的能力。
TCP协议规定,ACK报文段如果不携带数据则不消耗序号。=> 所以客户端下一个发送给服务端的报文的seq的值还是【x+1】
-
TCP连接的释放
没错,这就是你熟悉的四次挥手。
客户端要飞了,于是向服务端发送一个请求连接关闭的报文,seq的值是最后一次传输字节的序号+1。客户端进入【FIN-WAIT-1】状态。
服务端收到之后,回给客户端一个“知道了”响应,seq的值是最后一次传输字节的序号+1。通知上层应用,然后进入【CLOSE-WAIT】状态。 => 客户端收到之后,进入【FIN-WAIT-2】状态
如果服务端有需要给客户端的数据的话,在这个时候可以进行发送。
数据发送完毕后,服务端再最后向客户端发送一个ACK包,seq的值是服务端处于【CLOSE-WAIT】状态是发送的最后一个字节的序号+1。=> 客户端收到该数据包后,向服务端发送一个ACK包之后,进入【TIME-WAIT】状态。
【TIME-WAIT】状态等待的时间较长,其一是怕客户端最后的ACK包不能到达服务端,好进行超时重传;其二是为了防止旧的数据报文影响新的连接。
当然,TCP还设有一个保活计时器,防止一方突然出现故障,不能正常的收发数据。一般来说是2个小时,如果一方2个小时内没有响应的话,每个一段时间会发送一个探测报文,若连续10个报文段都没有响应,那么强制关闭该连接。
-
TCP的有限状态机
能看懂就看,看不懂就算,无所谓的。。。
所谓的“连接”,本质上就是通信双方在内部维持的一种状态嘛。。。
-
**结语:**懂得越多越好,但是不建议去死磕,毕竟没什么意义。。。可以结合热门的面试题看一下。。。
