传输层

传输服务

完成传输层工作的硬件或软件称为 传输实体 transport entity,可以在不同位置实现(比如操作系统内核、进程、网卡)。网络层、传输层、应用层的关系如下:

network-transport-and-application-layers-n

网络层使用数据报或虚电路来为端到端(host to host)通信提供数据包交付服务,而传输层则提供两台计算机上进程与进程(end to end)的数据传输服务。

传输层与网络层很类似,都可以分为面向连接的服务与无连接的服务;连接都要经过三个阶段:连接建立、连接传输、连接释放。

为什么要设立两个相似的层,因为网络层主要由运营商来控制,用户无法进行错误处理,所以需要设置传输层让用户能够控制服务质量。另一个好处是,网络层的原语(原语=api)在不同网络中是不同的,但传输层的原语则是统一的,这样对于应用开发更为友好。

另外,传输层与数据链路层也有相似之处,两者都会有差错控制、数据排序,不同之处在于传输环境,一个在通信子网上,一个在物理信道上。

传输服务原语

传输层为应用程序提供一些操作(接口),如下:

原语 发出的包 含义
LISTEN   阻塞,直到某个进制试图与之连接
CONNECT CONNECTION REQ 主动尝试建立连接
SEND DATA 发送信息
RECEIVE   阻塞,直到收到DATA包
DISCONNECT DISCONNECTION REQ 请求释放连接

传输层传输的数据单元称为 段 SegmentTPDU,传输协议数据单元,Transport Protocol Data Unit。网络层的数据单元则叫包,数据链路层的叫帧,三者关系如下:

原语的基本使用过程如下:

  • 客户 CONNECT,并发送 CONNECTION REQ,如果此时服务器处于 LISTEN,则建立连接
  • 一方 SEND,一方 RECEIVE。发送的每个数据包都要确认!(包括上面的 CONNECT 和下面的 DISCONNECT)
  • 如果是连接是非对称的,那么一方 DISCONNECT,另一方收到 DISCONNECTION REQ,整个连接就被释放。如果连接是对称的,那么单独关闭每个方向的连接,一方发送 DISCONNECT 后,依然可以收数据。

传输协议

寻址

为了与指定的应用程序建立连接,我们将应用程序定义到一个 端点(port) 上,专业术语称为 TSAP,Transport Service Access Point,传输服务访问点。网络层也有类似的 NSAP,IP 就是一个NSAP.

那么怎么知道应用与那个TSAP关联呢?有两种方法,一种是某些应用约定俗成地绑定在某些端口上(如邮件服务器在 25);另一种是,先与端口映射器连接(它总是在某个知名端口上),然后通过端口映射器获取应用的端口,再释放连接,重新连接到那个端口。

一台机器上有多个服务器进程,如果让每个服务都监听一个TSAP地址,则很浪费资源。一般是只有一个特殊的 进程服务器 负责监听一组端口,当有请求时,再将服务器与端口连接,这种方案叫 初始连接协议

连接建立

建立连接的基本过程包括发送 CONNECTION REQUEST 和等待 CONNECTION ACCEPTED 应答,此外,还要考虑网络可能丢失、延迟、重复数据包。想象用户与银行建立了连接,并发起了转账,结果数据包延迟了,这样数据会重传,但不幸的是,延迟的数据包又冒了出来,导致有两笔转账。为了避免这种错误,有几种方案:

  1. ❌每个连接有一个唯一地址,当连接释放时,地址被丢弃且不重复使用。这种坏处是,会使得首次建立连接变得超级困难
  2. ❌每个连接有唯一标识符。坏处是,源机器和目标机器上的标识符要保持一致,并且要无限期维护历史信息,万一哪一天机器崩溃导致信息丢失,那就完jb蛋。
  3. ⭕不允许数据包在网络中无限存在,而是有一定生存期,生存期可以通过下面几种技术设定:
    • 限制网络设计(避免数据包进入循环,并限制延迟)
    • 使用跳计数器
    • 为数据包打上时间戳

    Internet 的生存期是 120 秒。由于要保证数据包和确认都死亡,所以实际要等待生存期的倍数时间 $T$ 后,才可认为该数据包的所有痕迹都没了。在这个基础上,发送方给每个段一个序号,并且该序号在 $T$ 内不被重用。这样收方就能丢弃延迟或重复的包。

在第三种方案的基础上,Tomlinson 提出一种编号的方法:每个主机有一个独立的日时钟(time-of-day clock),该时钟用二进制计时,时钟的低 k 位用于初始化序号。序号要有足够位数。假设时钟速率为 $C$,序号空间为 $S$,那么,$S/C>T$。另外,当主机停机时,时钟不会停下。

下图(a)中,从 0 开始的粗线表示时钟,为了避免前面的序号延时 $T$ 后与初始序号重复,在一定时间内不能使用禁止区域中的序号。所以,为了避免发送速度过快导致序号落在禁止区域,段发送速度不能太快,最大速率为时钟速率。同时不能太慢,太慢会像图(b)那样进到后面的禁止区域。

传输层 Tomlinson

但这种方法依然不能用于建立连接。因为收方无法根据收到的初始序号判断出是不是重复连接。所以我们引入了三次握手,假设 A 向 B 发起连接:

  1. A 发送序号 x 的 CONNECTION REQUEST,即 seq=x
  2. B 回应一个 ACK,并宣告自己的序号 y,即 seq=y,ack=x+1
  3. A 对 B 选择的 y 进行确认,即 seq=x+1,ack=y+1

这样一来,如果收到重复连接请求,那么 B 会发送应答,A 收到应答后发现这是个重复连接请求,就会拒绝 B,这样 B 也就知道这是个重复连接。(见下图)

三次握手重复请求

更复杂的情况下,连接请求和确认都重复了(如下图),B 会在第三次握手的时候发现连接请求重复而丢弃改连接,而同时,B 在收到重复确认时,也能根据 ACK 的不同发现这个重复了。

三次握手重复请求2

连接释放

有两种连接释放:非对称释放和对称释放(前面原语说过了),非对称有一个缺点是,当 A 发送 DISCONNECTION REQUEST 后,有一个 B 发出的数据到达了 A,但此时 A 的连接已经关闭,这样就会导致数据丢失。所以一般要使用对称释放,两个方向的连接独立释放。

但对称释放也有个问题,俗称 两军对垒问题。简短来说就是:

  1. A 要释放连接(停止发数据),但 A 停止发数据的前提是,B 知道 A 停止发数据(避免 B 一直在等待数据,浪费资源)。反过来,B 停止收数据的前提是,A 知道 B 停止收数据。
  2. 上面两个前提导致 B 对 A 发送的每个信息都要回应(让你知道我收到了信息);反过来,A 也要对 B 的每个信息回应
  3. 这样导致 A,B 一直相互回应,无法真正释放。

这个问题无法解决,所以干脆也用三次握手:

  1. A 发送 DR,并启动计时器
  2. B 发送 DR 并启动计时器
  3. A 收到 B 的 DR,释放连接,回送 ACK
  4. B 收到 ACK,释放连接

如果定时器超时了,那么即使没收到回应也释放连接。

UDP

UDP(User Date Packet)用户数据报协议,是一个无连接的传输层协议,类似于 IP 的分组传输(为啥还要 UDP:可使用端口在进程间进行数据传输;用户可以控制)。提供端点标识,端到端的数据传输,不提供差错检测和可靠传输,但简洁高效。

UDP 数据段包括 8字节的头部和数据两部分,数据段头又包括:

  • 2 bytes 源端口
  • 2 bytes 目的端口
  • 2 bytes UDP 长度(头部和数据总共的字节数)
  • 2 bytes 校验和(checksum)是可选的,如果不计算校验和,则该域置为 0

端口为 16bit,共 $2^{16}$ 个,范围是 0~65545,分成三类:

  1. $<1023$:知名端口,用于公共应用(保留,全局分配,用于标准服务器),IANA分配;
  2. $1024\sim 49151$:用户端口,注册端口;
  3. $>49152$:动态端口,私人端口。

还有一种端口号叫自由端口,一般是由操作系统随机分配的大于 49512 的端口。当我们流量网页时,服务器上的端口为 80,而我们电脑上的就是自由端口。

UDP 数据段的校验和,并不只校验 UDP 数据段,还要加上伪首部:

  • 源IP地址:发送方将UDP数据包通过源IP发送出去。
  • 目的IP地址:接收方从目的IP接收UDP数据包。
  • 协议类型1字节(UDP 17,TCP 6),需要补充1字节长度。
  • UDP长度:UDP头部长度(8字节)加上数据包的长度

UDP伪首部

然后按如下过程求校验和:

  1. 按每16位求和得出一个32位的数;
  2. 如果这个32位的数,高16位不为0,则高16位加低16位再得到一个32位的数;
  3. 重复第2步直到高16位为0,将低16位取反,得到校验和。

如果收方的校验和为全1,传输无错。因为校验和使用了 IP 地址,所以 UDP 破坏了分层的原则。

一些比较著名的 UDP 服务(端口)有:DNS(53)传输控制协议、TFTP(69)、SNMP(161)

TCP

TCP (Transmission Control Protocol) 传输控制协议 是专门为了在不可靠的互联网络上提供可靠的端到端字节流而设计的

要想获得TCP服务,发送方和接收方必须创建一种称为套接字( sockets )的端点( end points),每个套接字包含一个IP地址和一个16位的端口( port )

通信进程的全球唯一标识:

  • 三元组:协议、源端点、目的端点
  • 五元组:协议、本地地址、本地端口号、远端地址、远端端口号

支持TCP的机器都有一个 TCP 实体,可以是用户进程或者是操作系统内核,都可以管理TCP流和跟IP层的接口,它完成如下工作:

  • 封装:接收本地进程的用户数据流,将其分割成不超过64kB的分片(实践中,通常分割成1460字节,以通过以太网传输)
  • 解封装:当包含TCP数据段的报文到达某台机器的时候,被提交给传输实体,传输实体将其重构出原始的字节流

TCP 协议

收发双方的TCP实体以数据段的形式交换数据,TCP 数据段的长度要考虑 IP分组网络载荷限制下层网络的MTU。数据段包括:

TCP数据段

  1. 16 位的源端口和 16 位的目的端口
  2. 32 位序列号:初始序列号(ISN)随机产生,并且每个字节对应一个序列号
  3. 32 位确认号:期望接收的字节号
  4. TCP 段头长度,单位是 ×4字节
  5. 保留字段
  6. 特殊位:
    • URG:URG=1时,表明有紧急数据,必须首先处理。配合紧急指针使用(指向紧急数据开始的位置)。
    • ACK:1 表示确认号有效,0 标明确认号无效。
    • PSH:接收方收到这样的数据,应该立刻送到上层,而不需要缓存它。
    • RST:被用来重置一个已经混乱的连接
    • SYN 用在连接建立的过程。当SYN=1,ACK=0,连接请求;当SYN=1,ACK=1,连接接受。
    • FIN 被用来释放连接,它表示发送方已经没有数据要传输了,但是可以继续接收数据。
  7. Window size:(滑动窗口协议)告诉对方可以发送的数据字节数(从确认字节号开始(决定于接收方)
  8. Checksum:校验的范围包括头部、数据和概念性的伪头部。伪头部与 UDP 类似,协议类型从 17 改为 6.
  9. 紧急指针

TCP 连接建立(三次握手)

和前面说的三次握手一样:

TCP三次握手

  1. A 随机生成一个序列号 SEQ=x,并且 SYN=1,ACK=0
  2. B 收到后,也随机生成一个序列号 SEQ=y,并且 SYN=1,ACK=1,ACK=x+1(表示期望收到 x+1)
  3. A 收到 B 的应答,A 发最后确认:SYN=0,ACK=1,SEQ=x+1,ACK=y+1

三次握手可能受到DoS 攻击(deny of service),就是控制很多机器,伪造 IP 地址,向服务器(泛洪)发送第一次握手信息 SYN。由于 IP 地址是伪造的,所以服务器发送第二次握手后,无法等到第三次握手,浪费大量资源,导致服务器瘫痪。

TCP 连接释放

  1. 任何一方在没有数据要传送的时候,都可以发送一个FIN置位了的 TCP 数据段;
  2. 当FIN被确认的时候,该方向的连接被关闭;
  3. 当双向连接都关闭了的时候,连接释放。

为了避免两军队(two-army)问题,使用定时器

  1. 如果一方发送了 FIN 数据段出去却在一个设定的时间没有收到应答,释放连接
  2. 另一方最终会注意到连接的对方已经不在了,超时后连接释放

具体流程见下图(三次握手和四次握手)

TCP连接释放

TCP连接释放四次握手

理论上讲,如果初始DR的和重传都丢了,协议失败,发送者将放弃发送且释放连接,但是,另外一端却不知道这些情况,仍然处于活跃的状态,这种情形导致 半开放连接(half- open)。要杀死半开放连接,可以规定如果在一定的时间内,没有TPDUs 到达的话,连接自动释放。如果这样,传输实体在发送一个TPDU的时候必须启动定时器,定时器超期,将发动一个哑TPDU(dummy TPDU),以免被断掉。

TCP 传输策略

TCP 传输数据的方式类似于数据链路层,使用了肯定确认重传与滑动窗技术。大概过程如下图:

TCP传输策略

主要注意的点:

  • SEQ 是最开始的字节的编号,返回的 ACK 等于最后一个字节的编号+1
  • WINDOW SIZE用于告知发送方最多可以发多少数据
  • 当 WINDWO SIZE=0时,发送者不能发正常数据段,但可以发:
    • Urgent 数据
    • 一个字节的数据段,以便让接收者再次发送期待接收的字节号和窗口数(避免死锁)

为了优化传输,有几种方法:

  • 发送方(Nagle’s algorithm)
    • 尽量不发送数据含量小的数据段
    • 缓存应用层的数据,达到一定量再发送
  • 接收方(Clark’s solution,解决傻瓜窗口综合症)
    • 不请求对方发送短数据段(window size)
    • 延迟窗口变更信息,使接收缓冲区足够大

TCP拥塞控制

虽然网络层也试图管理拥塞,但是,大多数繁重的任务是由 TCP 来完成的,因为针对拥塞的真正解决方案是减慢数据率。TCP 主要做两件事:

  • 拥塞检测Congestion detection:监视超时的情况来判断是否出现拥塞
  • 拥塞控制Congestion prevention:发送方维护两个窗口:
    • 接收者窗口大小反映了目前窗口的容量 (容易控制)
    • 拥塞窗口大小反映了网络目前的容量(难于控制)
    • 发送者发送的数据字节数是两个窗口中小的那个窗口数

为了决定窗口大小,TCP 使用慢启动算法,使得窗口不断增大直到接近拥塞窗口。

慢启动算法(Slow Start):

  1. 初始:当连接建立的时候,发送者用当前使用的最大数据段长度初始化拥塞窗口,然后发送一个最大的数据段。除了使用接收者窗口和拥塞窗口,TCP拥塞控制还是用了第三个参数,阈值(threshold),初始化为64K
  2. 指数增长:如果在定时器超期之前收到确认,则将拥塞窗口翻倍,然后发送两个数据段……
  3. 线性增长:拥塞窗口增长到阈值时停止指数增长,从这个点开始,每次成功的传输都会让拥塞窗口线性增长(即每次仅增长一个最大的数据段长度)
  4. 直至超时(或达到接收方窗口的大小)
  5. 当一个超时发生的时候,阈值降为当前拥塞窗口的一半,同时将拥塞窗口设为一个最大数据段的长度
  6. 重新开始慢启动过程,从一个数据段开始,重复指数增长、线性增长等过程

TCP慢启动算法.jpg

为了加快重新启动的过程,有一种方法叫 快速恢复,就是重启后,从阈值开始线性增长(而不是从 1开始)。

TCP快速恢复算法

如果收到一个ICMP抑制分组( ICMP source quench)并被送给TCP传输实体,则这个事件被当作超时对待

TCP 定时器

除了前面的释放连接定时器和哑帧定时器外,还有几个很重要的定时器:

  • 重传定时器(retransmission timer,Positive ackn. with retransmit:当没收到 ACK 时,重传分组。
  • 持续定时器(persistence timer):用来避免如下的死锁( deadlock )发生:
    • 接收方发送了一个窗口数为零的确认(窗口更新),告诉发送方等待
    • 稍后,接收方空出了缓冲,发送更新窗口的数据段,但是,很不幸,该分组丢失
    • 收发双方都在等待对方发送数据段过来,但永远等不到!死锁产生
    • 发送方可以在win size=0时启动持续定时器,当计到0时,发送探测数据段,引发对方重传窗口
  • 保活定时器(keep-alive timer):(就是前面释放连接说的那个)用来检查连接是否存活,当一个连接空闲的时间超过保活定时器的时间,该连接将被杀掉
  • TIMED WAIT 状态中使用的定时器,它运行两倍的最大分组生存时间,以确保连接关闭之后,该连接上的所有分组都完全消失

TCP 与 UDP 对比

性能 TCP UDP
可靠性 可靠 不可靠
传输延迟 不确定 网络延迟
拥塞控制 控制 不控制
  • TCP
    • 可靠传输方式
    • 可让应用程序简单化,程序员可以不必进行错误检查、修正等 工作
  • UDP
    • 为了降低对计算机资源的需求,如DNS
    • 应用程序本身已提供数据完整性的检查机制,勿须依赖传输层的协议来保证
    • 应用程序传输的并非关键性的数据,如路由器周期性的路由信息交换
    • 一对多方式,必须使用UDP(TCP限于一对一的传送),如视频传播