《计算机网络》笔记-第3章运输层

[TOC]

0. 前言

Chapter 3 Transport Layer

应用层处理应用之间的通信,而运输层则负责:把同台主机上各进程发送的数据收集起来交给网络层,并将从网络层收到的数据分发给各进程。

与此同时,运输层的TCP协议还向应用层提供了极其重要的可靠数据传输

1. Introduction and Transport-Layer Services(概述和运输层服务)

运输层接收应用层传来的应用报文,划分为较小的块,转换成运输层分组,称为运输层报文段(segment)

运输层 vs 网络层:

  • 运输层提供不同主机上进程之间的通信
  • 网络层提供主机之间的通信

将主机间交付扩展到进程间交付,被称为运输层的多路复用(transport-layer multiplexing)和多路分解(demultiplexing)

因特网运输层的主要协议和提供的服务如下:

  • UDP(用户数据报协议)
    • 进程到进程的数据交付
    • 差错检查
  • TCP(传输控制协议)
    • 进程到进程的数据交付
    • 差错检查
    • 可靠数据传输
    • 拥塞控制

2. Multiplexing and Demultiplexing(多路复用和多路分解)

2.1. 套接字

在第2章,我们知道,套接字是应用层与运输层之间的接口

发送数据时,应用层通过套接字将数据交付给运输层;运输层从网络层接收数据时,它需要将所接收的数据发给对应的套接字,从而到达应用层。

任一时刻,主机上可能有不止一个套接字,每个套接字都有唯一的标识符,其格式取决于它是UDP还是TCP。

将运输层报文段中的数据交付到正确的套接字的工作称为多路分解(demultiplexing);从不同套接字接收数据,并为每个数据块封装上首部信息从而生成报文段,然后将报文段传给网络层,这些工作称为多路复用(multiplexing)

pic

值得注意的是:一个进程通常有一个或多个套接字,例如:当今高性能Web服务器(HTTP服务器)只使用一个进程,但为每个新的客户连接创建一个具有套接字的新线程。

2.2. 无连接(UDP)的多路复用和多路分解

一个 UDP套接字(目的IP地址,目的端口号)组成的二元组 标识。

因此:如果两个UDP报文段有不同的源IP地址或源端口号,但具有相同的目的IP地址和目的端口号,那么这两个报文段将通过相同的目的套接字,定向到相同的进程。

例如:主机A和主机B,都向主机C的99端口发送UDP报文段,两个报文段将到达主机C上的同一个套接字。

2.3. 面向连接(TCP)的多路复用和多路分解

一个 TCP套接字(源IP地址,源端口号,目的IP地址,目的端口号)组成的四元组 标识。

因此:与UDP不同的是,两个具有不同源IP地址或源端口号的TCP报文段,即便目的IP地址和目的端口号相同,也将被定向到两个不同的套接字

以使用TCP服务的HTTP为例:同台主机上不同的HTTP会话(源IP地址相同,源端口不相同),将对应服务器上不同的套接字;不同主机上的HTTP会话(源IP地址不相同,源端口可能不相同),更对应服务器上不同的套接字。如下:

pic

3. Connectionless Transport: UDP(无连接运输:UDP)

UDP的工作:

  • 多路复用/分解
  • 少量的差错检测

UDP的特点:

  • 不可靠数据传输,不保证数据到达目的地
  • 将接收到的数据立即发送,不会因链路拥塞而等待
  • 无须建立连接,不会引入连接时延
  • 无连接状态,不需要额外存储状态数据
  • 分组首部开销小

使用UDP的运输层协议:DNS等。

3.1. UDP报文段结构

UDP报文段由 首部字段(8字节)数据 组成。

pic

  • 源端口号(Source port):源主机上发送UDP报文段的进程所在的端口。
  • 目的端口号(Dest port):目的主机上UDP服务器进程所在的端口。
  • 长度(Length):UDP报文段中的字节数。
  • 校验和(Checksum):用来检查该报文段是否出现差错。
  • 应用数据(Application data)

3.2. UDP校验和

发送方:

  1. 首先将UDP报文段的校验和字段置为0。

  2. 将UDP报文段中所有的16比特字相加,求和时遇到任何溢出都要回卷(将溢出加到结果的低位上)。示例:

    pic

  3. 对和的结果进行反码运算。

  4. 将最后结果放在UDP报文段的校验和字段中。

接收方:

  1. 将UDP报文段中所有的16比特字相加。

  2. 如果没有出现差错,则和的结果为:1111 1111 1111 1111

虽然UDP提供差错检测,但它对差错恢复无能为力。

4. Principles of Reliable Data Transfer(可靠数据传输原理)

在介绍TCP之前,我们需要先了解可靠数据传输的原理——网络中最为重要的问题之一。

其服务模型和服务实现概况如下:

pic

实现这种服务抽象是可靠数据传输协议(reliable data transfer protocol, rdt)

注意:此处使用的术语为分组,而不是运输层的报文段

4.1. 构造可靠数据传输协议

由浅入深完善一个可靠数据传输协议,此书的独到之处,好评!!!

4.1.1. 经完全可靠信道的可靠数据传输:rdt1.0

最简单情况下,底层信道是完全可靠的。

发送方和接收方只需要发送和接收数据即可。

发送方的状态转换图(有限状态机)如下:

pic

接收方的状态转换图(有限状态机)如下:

pic

图释:

  • 圆表示一个状态,箭头表示状态变迁。
  • 横线上为引起变迁的事件,横线下为事件发生时所采取的动作。
  • rdt_send(data):发送高层传来的数据。其动作包括:make_pkt(data) 将数据封装为分组,udt_send(packet) 发送分组(udt表示不可靠数据传输)。
  • rdt_rcv(packet):接收底层接收一个分组。其动作包括:extract(packet, data) 从分组中取出数据,deliver_data(data) 将数据传递给高层。

4.1.2. 经具有比特差错信道的可靠数据传输:rdt2.x - 自动重传请求协议ARQ

此情况下,数据在信道中传输,有可能发生比特差错。

为了让接收方最终得到无差错的数据,我们可以如下操作:

  1. 差错检测。接收方检测接收的数据是否出现比特差错,通过分组中的校验和字段实现。
  2. 接收方反馈。如果无差错,则反馈 肯定确认ACK ;反之,则反馈 否定确认NAK
  3. 重传。如果发送方收到否定确认NAK,则重传该分组。

基于这样重传机制的可靠数据传输协议,被称为自动重传请求协议(Automatic Repeat reQuest protocols, ARQ)

4.1.2.1. rdt2.0 - 停等协议

发送方的状态图:

pic

  • checksum:用于差错检测的数据。
  • Wait for ACK or NAK:等待ACK或NAK。
  • isNAK(rcvpkt):接收到的为否定确认。
  • isACK(rcvpkt):接收到的为肯定确认
  • :不进行任何动作。

接收方的状态图:

pic

  • corrupt(rcvpkt):接受的分组存在差错。
  • nocorrupt(rcvplt):接收的分组不存在差错。

发送方在发送完一个分组后,并不会发送新的分组,除非发送方确信接收方已正确接收当前分组。由于这种行为,rdt2.0这样的协议又被称为停等协议

4.1.2.2. rdt2.1

rdt2.0看似完美,但它存在一个致命的缺陷:没有考虑ACK或NAK分组受损的可能性!!!

处理受损ACK或NAK有3种方法:

  1. 当发送方接收到“含糊不清”的ACK或NAK时,它将反问接收方:“你在说神魔?”。但是,如果发送方的“你在说神魔?”也发生了差错,那将出现更大问题!

  2. 增加足够的检验和比特,使发送方不仅可以检测差错,还可以恢复差错。但,这样将花费很多比特。

  3. 当发送方收到受损的ACK或NAK时,直接重传当前分组。但,问题在于接收方并不能区分:这是重传的分组,还是新的分组。

为了解决第3种方法产生的问题,有一个简单的办法:序号(sequence number)。让发送方对其分组编号,接收方只需要检查序号,即可知道这是重传还是新的分组。

而对于rdt2.0这种简单情况,只需要01两个序号就足够了。

发送方状态图:

pic

  • Wait for call 0 from above:等待高层对发送0号分组的调用。
  • sndpkt=make_pkt(0,data,checksum)中的0:分组序号。
  • (corrupt(rcvpkt) || isNAK(rcvpkt)) 表示:接收到受损的ACK/NAK分组,或者 接收方返回NAK
  • (nocorrupt(rcvpkt) && isACK(rcvpkt)) 表示:接收到无损的ACK/NAK分组,且 接收方返回ACK
  • Wait for ACK or NAK 0等待接收方返回0号分组的ACK/NAK分组时:
    • 如果接收到受损的ACK/NAK分组,或者 接收方返回的是NAK,则重传0号分组。
    • 如果接收到无损的ACK/NAK分组,且 接收方返回的是ACK,则转入Wait for call 1 from above状态。

接收方状态图:

pic

  • has_seq0(rcvpkt):分组序号是否为0。
  • sndpkt=make_pkt(ACK,checksum)中的checksum:用于ACK/NAK分组的差错检测数据。
  • 当在Wait for 1 from below状态等待1号分组时:
    • 如果接收到受损的分组,则返回NAK分组;
    • 如果接收到无损的0号分组,则返回ACK分组;
    • 如果接收到无损的1号分组,则返回ACK分组,并转入状态Wait for 0 from below
4.1.2.3. rdt2.2

在rdt2.1的基础上,我们考虑能否不需要NAK呢?

rdt2.1接收方Wait for 1 from below状态为例:

  • 接收到无损的0号分组,则返回对0号分组的ACK
  • 接收到无损的1号分组,则返回对1号分组的ACK,并转入下一状态;
  • 接收到受损的分组,则返回NAK。如果不发送NAK,而是对上次正确接收的分组发送ACK,我们也能实现与NAK相同的效果。在Wait for 1 from below状态中,即返回对0号分组的ACK

发送方Wait for ACK or NAK 1状态下,接收到0号分组的ACK,则相当于接收到NAK,将重传1号分组。

ACK编号,这就是rdt2.2的改进。

发送方如下:

pic

  • isACK(rcvpkt,1):接收到1号分组的ACK
  • isACK(rcvpkt,0):接收到0好分组的ACK

接收方如下:

pic

  • sndpkt=make_pkt(ACK,0,checksum):封装0号ACK分组。
  • sndpkt=make_pkt(ACK,0,checksum):封装1号ACK分组。

4.1.3. 经具有比特差错的丢包信道的可靠数据传输:rdt3.0 - 比特交替协议

在此情况下,信道不仅会发生比特差错,还会丢包。

那怎么解决丢包问题呢?重传呗。

发送方如果在一段时间后,还没有收到对应分组的ACK,则重传该分组。

发送方状态转换图如下:

pic

  • start_timer:表示开始计时器。
  • Wait for ACK 0状态下:
    • 如果接收到受损的ACK分组,或1号ACK分组,则啥都不干,坐等超时;
    • 如果timeout超时事件发生,则将重传0号分组,并重置计时器
    • 如果接收到正确的0号ACK分组,则暂停计时器,转入下一状态。
  • Wait for call 1 from above状态下,接收到任何分组都置之不理,因为可能是由于延时而产生的冗余分组。

接收方同rdt2.2。

rdt3.0在各种情况下的运行过程:

pic
pic

rdt3.0有时被称为比特交替协议(alternating-bit protocol)

至此,我们得到了一个可靠数据传输协议!!!

4.2. 流水线可靠数据传输协议

rdt3.0虽然是一个可靠数据传输协议,但性能并不令人满意。其性能问题的核心在于它是一个停等协议

我们定义发送方(或信道)的利用率为:发送方实际忙于将发送比特送进信道的那部分时间与发送时间之比:

$$
U_{sender} = \frac {L/R} {RTT + L/R}
$$

  • $L$:分组字节长度
  • $R$:发送方发送速率
  • $RTT$:往返传播时延

可以看出,当$RTT$较大时,利用率将会非常低。

为了解决这个问题,我们可以:不以停等协议运行,允许发送方同时发送多个分组而无须等待确认,即流水线技术。

pic
pic

但新的技术总伴随着新的问题:

  • 必须增加序号范围,因为每个发送的分组必须有一个唯一的序号。
  • 发送方和接收方不得不缓存多个分组。
  • 连续发送的分组中,如何解决分组的差错恢复、丢包重传等问题。两种解决方法:
    • 回退N步(Go-Back-N,GBN)/ 滑动窗口协议
    • 选择重传(Selective Repeat,SR)

4.2.1. 回退N步(GBN)/ 滑动窗口

在回退N步协议中,发送方可以同时发送多个分组,但它受限于某个最大数N。

发送方所维护的GBN协议的序号空间和窗口如下:

pic

图示:

  • base:基序号,指向第一个发送但未收到确认ACK的分组。永远指向窗口头部。
  • nextseqnum:下一个序号,指向第一个待发送的分组。会在窗口中前后滑动。
  • Window size N:滑动窗口的长度N
  • 包含四种状态的分组:
    • Already ACK'd - 深蓝:已收到ACK确认的分组。
    • Sent, not yet ACK'd - 浅蓝:已发送,但还未收到ACK确认的分组。
    • Usable, not yet send - 灰色:待发送的分组。
    • Not Usable - 白色:还未准备好的分组。

随着协议的运行,窗口在序号空间中向前滑动。实际中,N的大小,受信道拥塞程度的影响。

在GBN协议中,发送方需要响应三种类型的事件:

  • 上层调用其发送数据。发送方首先检查发送窗口是否已满,即nextseqnum - base = N,是否有N个已发送但未收到ACK确认的分组:

    • 如果窗口未满,则产生一个分组并发送,更新相应变量;
    • 如果窗口已满,则将数据返回上一层,并指示窗口已满。
  • 收到一个ACK。
    -GBN协议采用累计确认的方式:收到序号为nACK分组(对n号分组的确认),表明接收方已正确接收到序号 $\leq n$ 的所有分组

    • 重启定时器,如果所有分组都已发送和确认,则停止该定时器
  • 超时事件。如果出现超时,发送方将重传所有已发送但未被确认的分组,这就像协议的名字“回退N步”所说的那样。

在GBN协议中,接收方的工作也很简单:

  • 如果一个序号为n的分组被正确接收,**并且按序,即上次接收到的分组的序号为n-1**,接收方则返回序号为nACK分组。
  • 而对于其它情况,接收方则丢弃接收到的分组,并发送最近按序接收到的分组ACK分组。比如,序号为n的分组被正确接收,但上次收到的分组的序号为n-2,则丢弃n号分组,并发送序号为n-2ACK分组。

下图是一个GBN协议运行的例子:

pic

4.2.2. 选择重传(SR)

GBN协议的缺点在于:单个分组的差错将会导致大量分组的重传,而许多分组根本没必要重传

而选择重传协议,通过让发送方仅重传那些出现差错的分组,而避免了不必要的重传。

发送方和接收方的序号空间和窗口如下:

pic

图示:

  • send_base:指向发送方第一个发送但未收到确认ACK的分组,永远指向发送方窗口头部。
  • rcv_base:指向接收方第一个期待接收的分组,永远指向接收方窗口头部。
  • 接收方四种状态的分组:
    • Out of order but already ACK'd - 深蓝:失序但已背确认的分组;
    • Expexted, not yet rec - 浅蓝:期待接收的分组;
    • Acceptable - 灰色:可接受的分组;
    • Not usable - 白色:不可用的分组;

发送方的事件和动作:

  • 上层调用其发送数据。与GBN协议一样。

  • 收到一个ACK。发送方将该ACK对应的分组标记为已接收,如果该分组的序号为send_base,则窗口向前滑动到具有最小序号的未确认分组处。

  • 超时事件。每个分组都必须拥有自己的逻辑定时器,超时发生后只能发送一个分组。

接收方的事件和动作:

  • 滑动窗口内的分组(序号在[rcv_base, rcv_base+N-1]内)被正确接收:

    • 如果该分组以前没收到过,则缓存该分组;
    • 如果该分组的序号等于rcv_base,则将该序号之后连续的已缓存分组交付给上层,并将窗口向前移动到具有最小序号的期待接受分组处。
  • 滑动窗口前的分组(序号在[rcv_base-N, rcv_base-1]内)被正确接收。产生一个ACK,即使接收方已经确认接收过该分组(防止ACK未到达发送方)

  • 其他情况,忽略该分组。

下图是一个SR协议运行的例子:

pic

4.2.3. 有限序号范围的问题

当面对有限序号范围时,由于发送方和接收方窗口之间不可能同步,所以,接收方面临的困境就是:无法判断该序号的分组是一个新分组还是一次重传

包括4个分组序号、窗口长度为3的示例如下:

接收方收到的0号分组为一次重传

pic

接收方收到的0号分组为一个新分组

pic

解决方法:窗口长度必须小于或等于序号空间大小的一半。

4.3. 可靠数据传输机制及其用途的总结

pic

5. Connection-Oriented Transport: TCP (面向连接的运输:TCP)

TCP/IP协议(传输控制协议/网际协议,Transmission Control Protocol/Internet Protocol),是当今因特网的支柱性协议。

TCP概述:

  • 面向连接:在发送数据之前,客户端需要与服务端建立一个连接。三次握手、四次挥手。
  • 可靠传输:TCP提供可靠数据传输。
  • 全双工:TCP连接提供全双工服务
  • TCP报文段(TCP segments):TCP报文的称呼。

5.1. TCP 报文结构

TCP报文段由两部分组成:

  • 首部字段:一般20字节
  • 数据部分:数据部分大小受限于最大报文段长度(MSS,maximum segment size)。MSS又受限于最大链路层帧长度/最大传输单元(MTU,maximum transmission unit),MTU = TCP/IP首部(一般40字节)+ MSS。以太网和PPP链路层协议的MTU都为1500字节,因此MSS典型值为1460字节。当TCP发送一个大文件时,例如某Web页面的一个图像,TCP会将该文件划分长度为MSS的若干块。

pic

  • 源端口号(Source Port)目的端口号(Dest Port):用于标识源主机和目的主机上的进程。

  • 序号(Sequence Numbers)确认号(Acknowledgment Numbers):用于实现可靠数据传输。

    • 序号:TCP将数据看成有序的字节流,序号是TCP报文段中数据部分首字节的字节流编号。如下图,500000字节的文件,MSS为1000字节,文件被划分成500个TCP报文段,第一个报文段序号为0,第二个报文段序号为1000,以此类推。示例中初始序号为0,实际上初始序号是随机产生的——由于网络中有可能存活着旧连接的TCP报文段,这样可以防止新连接阴差阳错地接收到旧连接残留的报文。

      pic

    • 确认号:由于TCP是全双工的,因此主机A在向主机B发送数据时,也会接收来自主机B的数据。主机A报文段中的确认号,就是主机A期望从主机B收到的下一个字节的序号。同时,表明主机A已经成功收到确认号之前的数据,这样可以实现可靠数据传输中累计确认的功能。

    • 示例(Seq:序号,ACK:确认号):

      pic

  • 4比特的首部长度(Header length):TCP首部的长度,以4字节为单位。

  • 8比特位的标志字段

    • URG:紧急数据标志位
    • ACK:确认标志位
    • PSH:请求推送位,接收端应尽快把数据传送给应用层
    • RST:连接复位,通常,如果TCP收到的一个分段明显不属于该主机的任何一个连接,则向远程发送一个复位包
    • SYN:建立连接时使用
    • FIN:释放连接时使用
  • 接收窗口(Recieve window):用于流量控制。

  • 检验和(Internet checksum):与UDP检验和字段一样。

  • 紧急数据指针(Urgent data pointer):只有当紧急标志置位时URG,该16位的字段才有效。

  • 可选与变长的选项字段(Options)

  • 数据(Data)

5.2. TCP 可靠数据传输

TCP在IP不可靠的尽力而为服务之上创建了一种可靠数据传输服务,TCP可靠数据传输服务 = rdt3.0 + 流水线

发送方

  • 上层调用其发送数据。生成具有序号的TCP报文段,并启动定时器(只有一个,如果已经启动则不需重启)。

  • 收到一个ACK。

    • 采用累计确认的方式:收到确认号n的ACK分组,表明接收方已正确接收到序号n之前的所有分组;
    • 重启定时器。如果所有分组都已发送和确认,则停止该定时器。
  • 收到3个冗余ACK(冗余ACK是对已确认报文段的再次确认)。一旦收到3个冗余ACK,发送方则快速重传冗余ACK报文确认号对应的报文段。

  • 超时事件。如果出现超时,发送方将重传第一个(序号最小)已发送但未被确认的分组(与滑动窗口不同的是:只重传一个!),并启动定时器。

pic

pic

接收方

  • 报文段按序到达。延迟的ACK,对下一个按序报文段等待500ms,如果没有到达,则发送一个ACK;如果到达,则立即发送累积ACK

  • 比期望序号大的报文段到达。立即发送冗余ACK,即期望序号的ACK。

  • 中间缺失的报文段到达。立即发送ACK。

TCP是回退N步还是选择重传呢?

TCP更类似于回退N步(滑动窗口),但不同点在于:GBN中,如果超时,则重传所有已发送但未被确认的报文段;但TCP中,超时只重传第一个已发送但未被确认的报文段。

pic

5.3. TCP 流量控制

问题:如果某应用读取数据时相对缓慢,而发送方又发送得太多、太快,接收方的接受缓存就会出现溢出。

为此,TCP为它的应用程序提供了 流量控制(flow-control) 服务。

发送方维护一个 接收窗口的变量 rwnd 来实现流程控制,该变量表示接收方还有多少可用的缓存空间。发送方已发送但未被确认报文段的总字节数,不能超过接收窗口的值。

接收方会将当前剩余的缓存空间,通过TCP确认报文中的接收窗口字段告诉发送方。

新的问题:假如接收方缓存空间已满,它通过TCP确认报文告诉发送方,发送方接收窗口的变量变为0,发送方将不再发送报文段。此时,若接收方缓存出现空余,它将不能告诉发送方。

解决:接收窗口变量为0时,发送方将继续发送只有一个字节的报文段,如果接收方缓存开始清空,则会返回确认报文,并将接收窗口字段设为非0值。

5.4. TCP 拥塞控制

众所周知,网络是存在拥塞的。如果拥塞时,还向网络发送数据,那将加剧拥塞。这就像交通堵塞一样。

那么,TCP是如何进行交通管制的呢?它首先要解决三个问题

  1. TCP发送方如何限制其发送速率?
  2. TCP如何感知路径上存在拥塞的呢?
  3. 当感知到拥塞时,采用何种算法来改变发送速率呢?

跟流量控制一样,发送方也维护着一个**拥塞窗口的变量 cwnd**,发送方已发送但未被确认报文段的总字节数,不能超过min{rwnd, cwnd}

但在讨论拥塞控制时,我们假设接收方缓存是无限大的,即发送方的已发送但未被确认报文段的总字节数,只取决于拥塞窗口变量。

并且,我们需要知道网络中没有明确的拥塞状态信号,TCP通常通过隐式地感知拥塞:超时事件3个冗余ACK

接下来,我们将介绍广受赞誉的TCP拥塞控制算法(TCP congestion-control algorithm),它包含3个主要部分:

  • 慢启动
  • 拥塞避免
  • 快速恢复

其中,慢启动和拥塞避免最为关键。有时,也算上快速重传算法,即4个部分。

5.4.1. 慢启动(Slow start)

思想:从一个较小值开始,逐渐增加拥塞窗口值。

具体:

  • 初始拥塞窗口值设置为 1~4 个MSS(最大报文段长度);
  • 每收到一个按序的确认报文后,则将拥塞窗口值增加 1 MSS:cwnd = cwnd + 1MSS

pic

由于每次接收到的报文数,即为拥塞窗口值/MSS,所以,拥塞窗口值呈倍数增长,一点也不慢!

考虑慢启动中的3种特殊情况:

  • 当拥塞窗口值cwnd达到慢启动阈值ssthresh时,将进入拥塞避免状态;
  • 当遇到超时事件时,慢启动阈值ssthresh将被设置为cwnd/2,再将拥塞窗口值cwnd重置为 1 MSS
  • 当遇到3个冗余ACK时,慢启动阈值ssthresh也将被设置为cwnd/2,但拥塞窗口值cwnd被设为ssthresh,并执行快速重传,然后进入快速恢复状态。

5.4.2. 拥塞避免(Congestion Avoidance)

思想:缓慢增加拥塞窗口值。

具体:

  • 当每一轮发送的所有报文段,都收到确认报文时,拥塞窗口值加 1 MSS:cwnd = cwnd + 1MSS

pic

考虑拥塞避免中的2种特殊情况:

  • 当遇到超时事件时,慢启动阈值ssthresh将被设置为cwnd/2,拥塞窗口值cwnd将被重置为 1 MSS,并进入慢启动状态;
  • 当遇到3个冗余ACK时,慢启动阈值ssthresh将被设置为cwnd/2,拥塞窗口值cwnd被设为ssthresh,并快速重传冗余ACK指定的报文段,然后进入快速恢复状态。

5.4.3. 快速恢复(Fast Recovery)

思想:收到3个冗余ACK说明网络并不像超时那么糟糕。

具体:

  • 在快速恢复状态中,也会发送报文段。
  • 如果收到冗余ACK,那么拥塞窗口值增加 1 MSS:cwnd = cwnd + 1MSS。由于进入快速恢复状态时,已经收到 3 个冗余ACK,所以进入快速恢复状态的初始拥塞窗口值为:cwnd = ssthresh + 3MSS

考虑快速恢复中的2中特殊情况:

  • 当遇到超时事件时,慢启动阈值ssthresh将被设置为cwnd/2,拥塞窗口值cwnd将被重置为 1 MSS,并进入慢启动状态;
  • 当遇到新的ACK时,拥塞窗口值cwnd被设为ssthresh,并进入拥塞避免状态。

5.4.4. 总结

TCP拥塞控制算法概括:加性增,乘性减

状态图:

pic

拥塞窗口值cwnd变化示例(在TCP Reno版本中加入了快速恢复状态):

pic

5.5. TCP 三次握手与四次挥手

TCP是面向连接的,那么TCP是如何建立、释放连接的呢?

5.5.1. 三次握手

  • 第一步:客户端TCP首先向服务端TCP发送一个特殊的TCP报文段,不包含应用层数据,报文段首部的一个标志位 SYN 被置为 1 ,序号字段 seq 被置为一个随机值 client_isn。这个特殊报文段被称为SYN 报文段

  • 第二步:服务端收到 SYN 报文段后,也返回一个特殊的TCP报文段,不包含应用层数据,首部的标志位 SYN 被置为 1 ,确认号字段 ack 被置为 client_isn + 1,序号字段 seq 被置为一个随机值 server_isn。这个特殊报文段被称为SYNACK 报文段

  • 第三步:客户端收到 SYNACK 报文段后,可以返回普通的TCP报文段,可以包含应用层数据,首部的标志位 SYN 被置为 0 ,确认号字段 ack 被置为 server_isn + 1,序号字段 seq 被置为 client_isn + 1

pic

为什么需要三次握手呢?

TCP连接是双向的,第一次和第二次的成功能够保证服务端听得到客户端的声音,第二次和第三次的成功能够保证客户端听得到服务端的声音。这可以类比打电话:

1
2
3
“喂,你听得到吗?”
“我听得到呀,你听得到我吗?”
“我能听到你,今天 balabala……”

为什么不是两次握手呢?

因为,握手阶段结束后,服务端将会为新连接分配变量和缓存。如果两次握手中第二次的 SYNACK 报文段丢失,客户端接收不到确认报文段,不会发送数据,这导致服务器会白白分配变量和缓存、苦苦等待,浪费空间和时间。

为什么不是四次握手呢?

多余。通过第二次握手,服务端不仅可以告诉客户端自己听得到,也可以验证客户端是否听得到自己。

5.5.2. 四次挥手

天下没有不散的宴席,TCP通过四次挥手断开连接:

  • 客户端/服务端发送 FIN 报文段,首部 FIN 字段被置为 1 ,表明自己已经发送完所有数据。

  • 服务端/客户端返回 ACK 报文段,表明自己知道对方已经发送完数据了。

  • 服务端/客户端发送 FIN 报文段,首部 FIN 字段被置为 1 ,表明自己已经发送完所有数据。

  • 客户端/服务端返回 ACK 报文段,表明自己知道对方已经发送完数据了。

pic

为什么四次挥手呢?

很简单,因为TCP是全双工的,一方发送完数据,不代表另一方也发送完数据。

6. socket 套接字编程

  • UDP 客户端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from socket import *

serverName = 'localhost' # 服务端地址
serverPort = 12000 # 服务端端口

# 创建客户端套接字。AF_INET: 使用IPv4协议, SOCK_DGRAM: 使用UDP协议
clientSocket = socket(AF_INET, SOCK_DGRAM)

message = input('Input lowercase sentence: ')

# 向服务端发送消息。UDP发送的每条消息,都必须附上服务端地址
clientSocket.sendto(message.encode(), (serverName, serverPort))

# 接收服务端的消息
recvMessage, serverAddress = clientSocket.recvfrom(2048)
print('From Server:', recvMessage.decode())

clientSocket.close()
  • UDP 服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from socket import *

serverName = 'localhost' # 服务端地址
serverPort = 12000 # 服务端端口

# 创建服务端套接字。AF_INET: 使用IPv4协议,SOCK_DGRAM: 使用UDP协议
serverSocket = socket(AF_INET, SOCK_DGRAM)
serverSocket.bind((serverName, serverPort)) # 将套接字绑定到之前指定的端口

print("The server in ready to receive")
# 服务器将一直接收UDP报文
while True:
message, clientAddress = serverSocket.recvfrom(2048) # 接收客户端信息,同时获得客户端地址
print("receive: " + str(message) + " [from" + str(clientAddress) + "]")
retMessage = message.upper() # 将客户端发来的字符串变为大写
serverSocket.sendto(retMessage, clientAddress) # 通过已经获得的客户端地址,将修改后的字符串发回客户端
  • TCP 客户端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from socket import *

serverName = '47.110.32.215' # 服务端地址
serverPort = 8082 # 服务端端口

# 创建客户端套接字。AF_INET: 使用IPv4协议, SOCK_STREAM: 使用TCP协议
clientSocket = socket(AF_INET, SOCK_STREAM)

# 向服务端发起连接
clientSocket.connect((serverName, serverPort))

message = input('Input lowercase sentence: ')

# 将信息发送到服务器
clientSocket.send(message.encode())

# 从服务器接收信息
recvMessage = clientSocket.recv(1024)
print('From Server:', recvMessage.decode())

# 关闭套接字
clientSocket.close()
  • TCP 服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from socket import *
import time

serverName = 'localhost' # 服务端地址
serverPort = 12000 # 服务端端口

serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind((serverName, serverPort))
serverSocket.listen(1)

print('The server is ready to receive')
while True:
# 服务端接收到客户端连接请求后,为新客户创建一个特定的套接字。单线程只支持单个用户
connSocket, clientAddress = serverSocket.accept()
message = connSocket.recv(1024).decode()
print("receive: " + str(message) + " [from" + str(clientAddress) + "]")
retMessage = message.upper()
connSocket.send(retMessage.encode())
connSocket.close()

time.sleep(20)
if input('press q to quit or other to continue:') == 'q':
break

7. 补充

7.1. IP 分片 与 TCP 分段

由于受链路层 MTU(Maximum Transmission Unit,最大传输单元) 的影响,网络层IP会将数据报分片传输,而这对运输层是透明的,当这些数据报的片到达目的端时有可能会失序,但是在IP首部中有足够的信息让接收端能正确组装这些数据报片。

尽管IP分片过程看起来透明的,但有一点让人不想使用它:即使只丢失一片数据也要重新传整个数据报。因为TCP报文段,对应于一份IP数据报(而不是一个分片),TCP没有办法只重传数据报中的一个数据分片。

因此,TCP试图避免IP分片。TCP是如何避免IP分片的呢?一旦TCP数据过大,超过了MSS(MSS = MTU - TCP/IP首部),则在运输层就会对TCP数据进行分段,这样到了IP层的数据报,自然不会超过MTU,也就不用分片了。

而使用UDP很容易导致IP分片。