第三章 运输层

在上一章里面,我们明显看到TCP和UDP这两个运输层协议起到了非常重要的作用。在这一章中,我们会通过提出需求->不断复杂化情景以不断接近现实->根据不断复杂的情景优化方案->实现现实的运输层协议这个方法来介绍。

3.1 概述和运输层服务

运输层协议本质的功能就是提供了逻辑通信,简单来说就是忽略电缆之类的物理连接,只需知道它们是连着的。
作为网络边缘的一部分,运输层协议还是在端系统中实现的,在发送端,它把应用层的数据转化为运输层报文段并交给下层,在接收端,它从下层接受报文段并处理出应用层报文。
运输层的协议各种各样,最常用TCPUDP两种。

3.1.1 运输层和网络层的关系

我们设想两个家庭,一个在福建,一个在广东,每个家庭都有若干成员,都有一个人专门负责收集信件并发给另一个家庭。在这个类比下

  • 应用层报文 == 信封里面的信件
  • 进程 == 家庭成员
  • 主机 == 家庭
  • 运输层协议 == 专门负责收集信件并发给另一个家庭的人
  • 网络层协议 == 邮政

正如,寄件人只负责寄件,而不负责具体的邮政细节,理论上如果网络层有什么差错,运输层不能进行干预,但实际上,运输协议可以在不可靠的网络协议上实现可靠的数据运输。

3.1.2 因特网运输层概述

我们知道有提供不可靠无连接服务的UDP(用户数据报协议)和提供可靠,面向连接服务的TCP(传输控制协议)
在进行讲解前我们需要明确,书中TCP和UDP的分组统称报文段,而网络层分组统称数据报。以进行区别,实际上它们经常被混用(
因为IP的服务模型是尽力而为交付服务,它不保证完成任务,只是尽最大的努力,是不可靠服务,它在主机间交付报文段
而我们的运输层协议则通过运输层的多路复用和多路分解将主机间的报文段扩展到进程间,相当于之前那个专门负责寄信的人,一边收集不同进程的信,一边把不同的信分配给不同的进程。
我们又都知道UDP也是管杀不管埋的主,而TCP则不仅提供可靠数据运输,而且还提供拥塞控制,这就是我们接下来要了解的。

3.2 多路复用和多路分解

我们记得,进程有自己的套接字作为分组进出的门户,而多路分解就是把运输层报文段的数据交付到的正确套接字,而多路复用就是在从套接字拿到数据并封装上首部信息的过程。
那么我们不难推理

  1. 套接字有独特的标识符
  2. 报文段有特殊字段指示正确的套接字

实际上,这些字段是源端口号字段目的端口号字段,端口号是一个16比特的数,在0~65535之间,其中0~1023为周知端口号,已经被保留给指定的服务,其他可以自由为新的应用程序分配。

对于UDP套接字,它是由一个二元组全面标识的,只包含目的IP地址和目的端口号,在发送端,数据被运输层封装上源端口号,目的端口号和其他的首部字段,假设,它幸运地到达目的地,目的主机会检查它的目的端口号,将其分解到对应的套接字,并保留源端口号作为“返回地址”。
而对于TCP套接字,它是由四元组标识的,即源IP地址,源端口号,目的IP地址,目的端口号,同理它的分解就是按照报文段里面的对应字段来分解的。不同的主机对同一个进程发送的报文段也会进入不同的套接字。

3.3 无连接运输:UDP

我们知道,TCP可以确保数据正确抵达,那为什么我们的UDP还没有被淘汰呢?

  1. 关于发送什么数据和何时发送的应用层控制更为精细:其实就是可以大胆抢夺带宽
  2. 无需连接建立
  3. 无连接状态
  4. 分组首部开销小

现在就让我们看看这种轻量级的运输层协议

3.3.1 UDP报文段结构

UDP的首部只有4个字段,每个2字节,分别是

  • 源端口号
  • 目的端口号
  • 长度:UDP报文段的字节数
  • 检验和:检验报文段是否出现差错

3.3.2 UDP检验和

UDP检验和提供了差错检验功能,虽然UDP对差错无能为力。
它通过将UDP报文段里面所有的16字节数据相加,最后取反码得到。这样接受方就可以把所有数据和检验和加起来,通过最后是不是16个1来判断数据用没有差错。你可能好奇为什么提供差错检测,这是为了满足端到端原则,因为我们不能保证下层是否是可靠的,而且在下层提供可靠性的成本大于在上层提供。

3.4 可靠数据传输原理

可靠数据运输为上层提供了一种假象,数据似乎是通过可靠的信道进行运输的,实际上,是可靠数据运输协议加上不可靠的信道(
接下来我们用rdt协议简称可靠数据运输协议

3.4.1 构造可靠数据传输协议

rdt1.0 经可靠信道的可靠数据运输

在这个情况下发送端的行为只有

  • 被上层调用,发送报文

接收端的行为

  • 收到报文,交付

因为信道是可靠的,所以不需要确认,也不需要害怕差错

rdt2.0 经具有比特差错信道的可靠数据运输

现在我们的信道不再可靠了,它的运输的分组会出现比特受损的情况。
试想你在信号不好的时候和别人聊天,是不是会向别人进行确认,即发起“OK”和“没听清”两种确认。这种使用肯定确认否定确认的重传机制的可靠数据运输协议叫自动重传请求(ARQ)协议
它需要三种协议功能

  • 差错检测 判断出现了比特差错
  • 接收方反馈 进行肯定确认(ACK)和否定确认(NAK
  • 重传 出现差错后重传

发送端现在有两种行为:

  • 被上层调用,发送报文
  • 等待NAK和ACK
    • NAK -> 重传
    • ACK -> 等待调用

接收端现在也是

  • 拿到数据进行差错检测
    • 没有差错 -> 返回ACK 交付
    • 有差错 -> 返回NAK 等待

在目前分组被正确运输前,发送方会一直等待,这就是停等协议
现在2.0看起来很完美,但是忽略了ACK和NAK受损的情况,我们不可能用新的NAK和ACK不断套娃,只能对从其他方面下手
其实很简单,引入冗余分组就行,拿到不清楚的ACK和NAK也大胆重发,但这又对于接收方提出了挑战,它怎么知道拿到的分组是新的还是旧的呢?(比如发的ACK被差错了,拿了一个重复的)
于是我们为分组引入了序号,对于停等协议,我们只需要1和0两种序号,循环使用就好。
于是我们发明了rdt2.1
发送端:

  • 被上层调用,发送有序号的报文
  • 等待反馈
    • (正确的NAK || 错误的NAK或ACK)-> 重传
    • 正确的ACK -> 等待调用

接收端:

  • 拿到数据进行检测
    • 没有差错 && 序号正确 -> 返回ACK 交付
    • 没有差错 && 序号错误 -> 返回ACK 丢弃
    • 有差错 -> 返回NAK 等待

接下来我们优化掉NAK,只需给ACK加上序号,让发送端进行序号的读取进行优化,得到rdt2.2
发送端:

  • 被上层调用,发送有序号的报文
  • 等待反馈
    • 差错的ACK -> 重传
    • 无差错但序号错误ACK -> 忽略
    • 无差错且序号正确ACK -> 等待调用

接收端:

  • 拿到数据进行检测
    • 没有差错 && 序号正确 -> 返回对应序号ACK 交付
    • 没有差错 && 序号错误 -> 返回对应序号ACK 丢弃
    • 有差错 -> 返回错误序号ACK 等待
rdt3.0 经具有比特差错的丢包信道的可靠数据运输

现在我们的信道更真实了,有时候不只是比特出问题,而是分组整个没有了,这个情况下我们怎么处理呢?
我们借用最通用的方法->倒计数定时器,只要过了指定的时间还没有拿到反馈,发送方也进行一个重传,于是,我们发明了rdt3.0
发送端:

  • 被上层调用,发送有序号的报文,启动定时器
  • 等待反馈
    • (差错的ACK || 定时器超时)-> 重传
    • 无差错但序号错误ACK -> 忽略
    • 无差错且序号正确ACK -> 关闭定时器,等待调用

接收端:

  • 拿到数据进行检测
    • 没有差错 && 序号正确 -> 返回对应序号ACK 交付
    • 没有差错 && 序号错误 -> 返回对应序号ACK 丢弃
    • 有差错 -> 返回错误序号ACK 等待

因为分组序号在0和1之间交替,我们的rdt3.0有时被叫做比特交替协议

现在,我们至少得到了一个功能正确的协议,但它的性能却低得离谱,在大部分时间里,作为停等协议的它只是等待反馈而不进行发送。

3.4.2 流水线可靠数据传输协议

发送方可以同时发送多个分组一起等待确认,这种类似流水线法方法来提速。现在我们只是需要解决性能加强带来的新的功能问题

  • 必须增加序号范围,保证可靠数据传输中序号的作用
  • 发送方和接收方必须缓存多个分组
  • 新的差错恢复方法 回退N步选择重传

3.4.3 回退N步

回退N步(GBN)协议中,我们用一个长度为N的窗口包含分组,将窗口内的分组进行发送,定义窗口内第一个分组的序号为基序号,定义窗口内第一个没被发送的分组的序号为下一个序号
窗口内只有两种分组,从基序号到下一个序号-1的分组为已发送但没有确认的分组,而下一个序号到窗口末尾的是可发送但还没被发送的序号。
一旦基序号的分组被确认,窗口就向前滑动,因此我们也叫GBN协议滑动窗口协议
发送端:

  • 被上层调用,告诉上层有无空闲,按流水线发送有序号的报文,启动定时器
  • 等待反馈
    • (差错的ACK || 定时器超时)-> 从基序号开始重传
    • 无差错但序号错误ACK -> 忽略
    • 无差错且序号正确ACK -> 关闭定时器,窗口前滚

接收端:

  • 拿到数据进行检测
    • 没有差错 && 序号正确 -> 返回对应序号ACK 交付
    • 没有差错 && 序号错误 -> 返回基序号ACK 丢弃后面所有分组
    • 有差错 -> 返回错误序号ACK 等待

我们可以发现,GBN协议会丢弃所有失序分组,这虽然浪费,但好在简单易操作。

3.4.4 选择重传

GBN协议丢弃所有失序分组的方式会造成大量浪费,就像你因为第一个字错误重新写一个1000字的文章(尽管你已经写到999个字了)
选择重传(SR)协议通过缓存已经正确发送的分组,仅处理需要重传分组的方法进行优化 发送端:

  • 被上层调用,告诉上层有无空闲,按流水线发送有序号的报文,启动每个分组自己的定时器
  • 等待反馈
    • (差错的ACK || 定时器超时)-> 重传对应分组
    • 无差错但序号大于发送基序号ACK -> 关闭定时器,确认对应分组
    • 无差错且发送基序号ACK -> 关闭定时器,发送窗口前滚至第一个未确认分组

接收端:

  • 拿到数据进行检测
    • 没有差错 && 接收基序号 -> 返回对应序号ACK 按序号交付所有连续已缓存分组,接收窗口前滚至第一个期待(还没到)分组
    • 没有差错 && 大于接收基序号 -> 返回对应序号ACK 缓存
    • 没有差错 && 小于接收基序号 -> 返回对应序号ACK 丢弃
    • 有差错 -> 返回错误序号的ACK 等待

对于SR协议,接受窗口的大小必须小于最大序号的一半,因为如果窗口过大,无法确认是新的分组还是重传的分组,

3.5 面向连接的运输:TCP

3.5.1 TCP连接

TCP是面向连接的,而且这个连接是双全工(双向)的。
TCP会把数据放到发送缓存里面,在方便时取出受限于最大报文段长度(MSS)的数据,而MSS受限于最大链路层帧长度(最大传输单元(MTU))。 TCP为每个客户数据加上TCP首部,形成TCP报文段

3.5.2 TCP报文段结构

TCP字段也是首部字段和数据字段,我们细说首部字段

  • 源端口号 16比特
  • 目的端口号 16比特
  • 序号字段 32比特 实现可靠数据传输
  • 确认号 32比特 实现可靠数据传输
  • 接收窗口字段 16比特 流量控制
  • 首部长字段 4比特 首部长度
  • 检验和 16比特
  • 选项字段 可变 多功能
  • 标志字段 6比特
    • ACK 标志报文有效
    • RST SYN FIN 连接的建立和拆除
    • CWR ECE 明确拥塞通告
    • URG 指示紧急数据存在
    • PSH 指示立刻交付数据
  • 紧急数据指针字段 16比特 指向紧急数据

总之我们开始介绍,首先是我们的序号和确认号
序号就是我们的每个字节的序号(x
举个例子,MSS是1000字节,我们的文件有500_000字节,那么我们的第一个报文段就是序号是x,第二个是x+1000,第三个就是x+2000。
而确认号就是主机需要的下一个字节的序号,比如我的电脑已经拿到了0-555字节了,那么我的电脑发的下一个报文段里面的确认号就是556。
因为累积确认的存在,TCP确认号是第一个顺位的字节,比如我拿到了0-900 1000-1100,那么我的确认号会是901。

Telent这个应用层协议就很有意思,它一般被拿来登陆,但是因为它不加密,已经淘汰了。
简单来讲,它会通过回显来确认你打的字被处理了。

  1. 客户端发送你打的字,里面有序号,确认号,和你打的字
  2. 服务器收到你的字,回发你的字,序号,确认号
  3. 客户端最后回复一个确认