# TCP/UDP

> TCP、UDP是传输层的服务，为应用进程提供通信。

## 重要概念

1. **传输层**为互相通信的应用**进程**提供**逻辑通信（端到端）**
2. **端口(port)**&#x4E0E;**套接字(socket)**
3. **无连接的UDP，不可靠信道**
4. **面向连接的TCP，全双工的可靠信道**
5. 在不可靠网络上实现**可靠传输**的原理，**停止等待协议**、**ARQ协议**
6. TCP的**滑动窗口**、**流量控制**、**拥塞控制**和**连接管理**

## 运输层的端口

运输层从IP层收到数据后，需要交付给特定应用进程，因此需要**标志**运行在应用层的各种应用进程。为了不同OS的应用进程可以互相通信，需要用统一的方法（与OS无关）标志。

**协议端口号（端口，port）**

TCP/IP使用16位的端口（65535个），TCP和UDP的首部都有**源端口**和**目的端口**，端口是标志**本计算机**应用层的各个进程与传输层交互的层间接口，只具有**本地意义**。通信时需要知道对方IP地址、端口

**端口分类**

1. **服务器使用端口**
   1. 熟知端口号、（系统端口号）：0-1023
   2. 登记端口号：没有熟知端口的应用程序使用，1024-49151
2. **客户端使用的端口号（短暂端口号）**
   1. 49512-65535，客户端进程运行时动态选择，暂时使用

![](/files/-LZxRJsmuD8pV7TubsCr)

## UDP

* 无连接的
* 尽最大努力交付
* **面向报文**，发送方的UDP对应用程序交下的报文，添加首部后就交付给IP层，不合并也不分拆。一次交付一个完整的报文。
* 没有拥塞控制，**拥塞不会降低发送速率**。对于实时的应用，要求主机以恒定速率发送数据，可以容忍丢失但是对延迟敏感，使用UDP。
* 支持**一对一、一对多、多对一、多对多的交互通信**
* 首部开销小（8个字节）

### UDP的首部格式

每个字段**2字节**

* 源端口，不需要对方回信时可全0
* 目的端口
* 长度，最小值为8（仅有首部）
* 校验和

接收方从IP层收到UDP数据报时，根据目的端口分发给应用进程。

若接收方UDP发现收到的报文中的目的端口不正确（不存在对应这个端口的应用进程），则丢弃报文，使用ICMP发送“端口不可达给发送方”

**校验和**

计算时要加入**伪首部**，伪首部仅用于计算校验和而不向上或向下传递，计算校验和的部分包括：伪首部、首部、数据部分。同时检查了包括IP数据报源地址、目的地址（伪首部），UDP源端口、目的端口（UDP首部），以及UDP数据部分。

![](/files/-LZxRJso4cMpU9hs4NEz)

## TCP

* 面向连接的传输层协议：应用使用之前必须先建立连接，传输完毕后必须释放已建立的TCP连接。
* 每条TCP连接只能有两个端点，是点对点的
* 提供可靠交付服务
* 全双工通信，设有发送、接收缓存
* **面向字节流**，TCP的“流”指的是流入到进程或者从进程流出的字节序列。**但TCP传输单元是报文段**。

> TCP并不关心应用进程一次将多长的报文发送到TCP缓存中，而根据对方给出的**窗口值**、**网络拥塞程度**决定一个报文段要包含多少个字节。

![](/files/-LZxRJsqciPdPfHrt9V3)

**TCP的连接**

TCP连接的端点是socket

```
套接字socket=(IP地址:端口号)
TCP连接 ::= {socket1,socket2} = {(IP1:port1), (IP2:port2)}
```

> 同一个IP地址可以有多个不同的TCP链接，同一个端口号也可以出现在多个不同的TCP连接中

### 可靠传输原理

TCP下层不提供可靠传输，需要协议实现：

* 出现差错时，让发送方**重传**出现差错的数据。（实际信道会产生差错）
* 接收方来不及处理数据时，告知发送方**降低发送速率**。（实际接收方处理速度有限）

#### 停止等待协议

发送完一个就停止并等待对方的确认，收到确认在发送下一个分组。

**超时重传**：超过一段时间没有收到确认，就认为丢失了，则重新发送分组。（需要设置超时器）

* 发送完毕后，需要保留**分组副本**
* **分组**和**确认分组**，都需要进行编号
* 超时器设置的重传时间，应该比平均RTT更长一些。

**确认丢失**：接收到分组后，又收到了重传的同一个分组。

* **丢弃**这个重复的分组
* 向对方**发送确认**（不能认为已发送过确认，就不再发送）

**确认迟到**：收到重复的确认，并丢弃

使用以上确认、重传机制，可以实现可靠的通信。（ARQ：Automatic Repeat reQuest）

**信道利用率**：利用率很低，解决方法：**流水线传输**，**连续ARQ协议**和**滑动窗口协议**

#### 连续ARQ协议

**发送方**：维持**发送窗口**，每收到一个确认则把发送窗口前移一个分组的位置。

**接收方**：**累计确认**，不对每一个收到的分组都发送确认，**对按序到的的最后一个分组发送确认**。

> 基础版本：Go-back-N，发送方发5个分组，第三个丢失了，但他不知道后两个是否到达，必须重退到第3个开始发送

### TCP报文的首部

前20字节固定，后面有4n字节是根据需要而增加的选项。头部最长60字节（受限于数据偏移字段长度，20 + (2^4 - 1)\* 4Byte）

1. **源端口**、**目的端口**
2. **序号**，4字节，传输的字节流中，**每一个字节按顺序编号**（mod 2^32），首部中的值是**本报文段发送的数据的第一个字节的序号**（头部序号字段是301，携带100个字节，则最后一个字节序号是400）。初始序号，在连接建立时设置。
3. **确认号**，4字节，是**期望收到对方下一个报文段的第一个数据字节的序号**。（若收到301-400的报文段，则确认报文的确认号为401）
   1. > **确认号为N，则N-1为止的所有数据都已正确收到。**
4. **数据偏移**，即报文段起始距离数据的距离，也就是首部长度。
5. **保留**
6. **紧急URG**，置1表示有效
7. **确认ACK**（ACKnowlegment），连接建立后所有报文都置为1
8. **推送PSH**
9. &#x20;**选项复位RST**， RST == 1时，表明TCP连接出现严重错误，必须释放连接再重新建立连接。也用于拒绝非法报文段，或拒打开连接。
10. **同步SYN**，用于连接建立时同步序号，当SYN==1且ACK==0是连接请求报文段，若对方同意建立连接则响应报文使SYN==1和ACK==1。（SYN置为1则表明，是一个连接请求或连接接受报文）
11. **窗口**，2字节，范围为\[0, 2^16-1]，指发送本报文段的一方的**接收窗口**。告诉对方：从本报文段首部中的确认号起，接收对方目前允许对方发送的数据量。**接收方让发送方设置其发送窗口的依据。**
    1. > **指出现在允许对方发送的数据量，窗口值经常处于动态变化中**
12. **校验和**，2字节，校验范围包括首部和数据。跟UDP一样，添加12字节伪首部计算，不同的是TCP的伪头部第四个字段改为6，第五个字段改为TCP长度。
13. **紧急指针**
14. **选项**
    1. 窗口扩大（2^16 Byte =  64KB，对于目前网络而言太小）
    2. 时间戳（计算RTT，高速网络解决序号绕回）
    3. 选择确认SACK
    4. 最大报文段长度MSS，数据字段最大长度

### TCP的可靠传输实现

根据通信对方的确认报文段的窗口值调整**滑动窗口**（以字节为单位）。

![](/files/-L_1toWbnWB9yA6f9wEN)

假设A向B单向通信，窗口变化存在以下情况：

* 窗口向前移动，收到确认报文（且窗口值不会太小）
* 窗口不移动
  1. 没有收到新的确认，对方通知的窗口大小也不变。（窗口前沿不动）
  2. 收到新的确认，但窗口缩小了，使得窗口前沿正好不动

> 窗口不会往后移动，因为已确认接受的确认不可撤销

![](/files/-L_1toWdbZ7eZw4wa9De)

* **发送窗口**：图中的P3 - P1
* **可用窗口/有效窗口**：图中的P3-P2，允许发送但尚未发送的字节数

#### TCP缓存

发送方将字节流写入TCP发送缓存，接收方从TCP缓存接收数据

> 缓存空间、序号空间都有限，是循环使用的
>
> 窗口是缓存的一部分，发送程序不应该发送过多数据否则缓存不足，同样接收程序应及时接收数据。

![](/files/-L_1toWf-EsOjLV5maRq)

**发送缓存中的数据**：

* 发送程序传送给TCP准备发送的数据
* TCP已发出但没收到确认的数据

**接收缓存中的数据：**

* 按序到达、尚未被应用程序接收的数据
* 未按序到达的数据

#### 超时重传的时机选择

超时重传时间太短，则频繁重传；太长，则网络空闲时间增加，传输效率低。

TCP采用自适应算法，根据**报文段往返时间**$$RTT$$（报文段发送时间——接收响应确认的时间），并保留**加权平均往返时间**$$RTT\_s$$ 。权重$$\alpha$$的推荐值为$$1/8$$

$$RTT\_s = (1-\alpha)*RTT\_s + \alpha*RTT$$

**超时重传时间RTO（Retransmission Time-out）**：

$$RTO = RTT\_s + 4\*RTT\_D$$

其中$$RTT\_D$$ 是RTT的偏差的加权平均值，与$$RTT\_s$$ 和新的$$RTT$$样本之差有关，权重$$\beta$$ 推荐为0.25

$$RTT\_D = (1-\beta)*RTT\_D + \beta*|RTT\_s - RTT|$$

然而，上面方法存在一个问题。当出现重传后，收到确认，如何知道确认是对此前哪一个发送的报文的确认？

**Karn提出的改进**：只要报文段重传了，就不采用它的RTT样本。

这个方法同样存在问题，当延迟突然增加很多的时候，重传时间内无法收到确认，报文段都需要重传，但是由于重传报文不用作更新，超时重传时间不被更新。

**改进**：只要重传了，就增大RTO。

#### 选择确认SACK

在TCP报文段首部增加SACK选项，告知收到的不连续字节快的边界（一个字节块边界32bit，4字节），以选择性地重传缺少的数据。

### TCP的流量控制

流量控制（flow control）：让发送方发送速率不要太快，以便接收方可以及时接收。

**rwnd，窗口值（字节为单位）**

> 可能存在**死锁**：B向A发送零窗口报文段，过一段时间后由于B有缓存空间，再次发送rwnd=400的报文段，但这个报文段丢失。A一直没收到，一直保持0窗口。
>
> 解决：**持续定时器（persistence timer）**，只要一方收到零窗口通知，就启动定时器，设置时间到期就发送1字节的**探测报文段**，对方则给出目前窗口值。

实际上，程序将数据交给TCP缓存之后，由TCP控制，这里有几种发送报文段的时间节点：

* 数据达到MSS（最大报文段长度）字节后，组成TCP报文段发送
* 应用程序指明要发送，即**PUSH**操作
* 发送方的计时器到期，将缓存装入报文段（不超过MSS）发送出去

> 存在的情况是，例如**交互式**TELNET连接，发送一个字节数据，TCP+IP头部共41字节，对方回复40字节（无数据）。仅传输1个字节，要发送2个报文段，共81个字节。应适当推迟发回确认报文，使用**捎带确认**的方法。
>
> 另一个情况是，**糊涂窗口综合症**，接收方TCP缓存已满，交互式进程每次只读取1个字节，再向发送发发送确认，对方将窗口设为1个字节。而后发送方又发来1个字节数据（IP数据报长41字节），如此循环。网络效率很低。解决方法：接收方等待一段时间，使得接收缓存有空闲（能容纳一个最长报文段，或空闲总共一半的空间）

总的来说，发送方不发送很小报文的同时，接收方也不要急于有一些空间就将很小的窗口通知发送给对方。

### TCP的拥塞控制

![](/files/-L_1toWh4K8H_YB__-zU)

与流量控制的联系和差别

* **拥塞控制**：是全局性的过程，目的是防止过多数据注入到网络中，避免网络中路由器或者链路过载。前提：网络能够承载现有的负荷。假定：迟迟不收到对方的确认，认为网络中某处可能发生拥塞。
* **流量控制**：控制点对点通信量，是端到端的问题。

两者联系：

* 有的拥塞控制算法，是向发送端发送控制报文，让其放慢发送速率。
* 发送方的窗口上限，由流量控制、拥塞控制共同决定：
  * `发送方窗口上限值= Min[rwnd, cwnd]`

几种拥塞控制的方法：

#### 慢开始 & 拥塞避免

发送方维持**拥塞窗口**`cwnd` ，并动态变化，让发送窗口等于拥塞窗口

![](/files/-L_1toWjgeBvihZ5drwr)

**慢开始**，从很小的值开始，并非增长得慢。（先将`cwnd`设置为一个MSS的数值）。经历一轮传输后（即，收到对之前已发送的最后一个字节的确认），则将拥塞窗口增大一倍。

为了防止增长过大，设置状态量**慢开始门限**`ssthresh`

1. 当`cwnd<ssthresh`，使用慢开始
2. 当`cwnd>ssthresh`，使用**拥塞避免算法**
3. 当`cwnd=ssthresh`，使用上述两种均可。

所谓拥塞避免算法，是降低增大速率，从指数增长降低为线性增长。

无论是慢开始还是拥塞避免算法，只要检测到**拥塞**（即没及时收到确认），就将`ssthresh`设置为拥塞时发送方窗口值的一半（不小于2），并将`cwnd`设置为1，执行慢开始算法。

将拥塞窗口减少为一半，又称为**乘法减小**； 线性增加的拥塞窗口，又称为**加法增大**

#### 快重传、快恢复

实际中，若每次检测到拥塞，就将`cwnd`调整为1，可能有点**过激**，因为有可能网络环境较好，只丢失了一个分组，这样调整窗口对网络利用率不高。

![](/files/-L_1toWlNuPbIw_aK5l4)

**快重传**算法，是指**接收方**一旦收到一个**失序的报文段**，就马上发出重复确认。目的是及时让发送方知道，中间有报文没有及时到达。规定发送方一旦连续受到三个重复确认，就认为丢失了报文段并**快速重传**，而不等待定时器到期。

**快恢复**算法，是在**发送方**连续受到三个重复确认时使用，这种情况下认为网络**很可能没有发生拥塞**，因为接收方已经收到了连续几个报文。但是为了避免网络拥塞，依然需要乘法减半，但接下来并不执行慢开始：

1. 将慢开始门限`ssthresh`设置为`cwnd`的一半（乘法减小）
2. 将`cwnd`设置为新的`ssthresh`值，并执行拥塞避免算法（加法增大）

#### 随机早期检测

为了避免路由器队列满时，丢弃所有分组导致大量TCP连接同时进入慢开始状态。在队列满之前（队列较长时）采取随机丢弃分组的方法。

* **平均队列长度**小于最小门限`TH_min`，新到达分组放入队列尾部
* **平均队列长度**大于最大门限`TH_max`，丢弃新到的分组
* **平均队列长度**介乎两者之间，按照概率`p`随机丢弃到达分组

### TCP运输连接管理

**三个阶段**：

1. 连接建立
2. 数据传输
3. 连接释放

TCP采用C-S方式建立连接

#### 连接建立：三次握手

![](/files/-L_1toWnxQ1SGt7s8FIS)

1. 服务端B监听（Listen）
2. 客户端A发生连接请求报文，SYN=1，ACK=0，序号seq=x（SYN报文段不携带数据，但消耗一个序号）。**客户端**进入`SYN_SENT`状态
3. B收到请求，若同意则发送确认。确认报文中SYN和ACK置1，确认号为ack=x+1，同时设置自己的序号seq=y（同样不携带数据，但消耗一个序号）。**服务端**进入`SYN_RCVD`状态。
4. A收到B的确认后，还需发送一次确认报文（第三次握手）。确认报文ACK置为1，确认号ack=y+1，而自己序号seq=x+1。【若这个确认报文不携带数据，则不消耗序号，下一个数据报仍然为x+1】。此时，TCP连接建立，**客户端**进入`ESTABLISHED`状态。
5. B收到A的确认，也进入`ESTABLISHED`状态

> 为什么要三次握手？个人理解如下：
>
> * 因为TCP是面向连接的，至少两次握手（没收到对方确认，无法认为连接已建立）
> * 若只握手两次，那可能存在**客户端**发送过两次请求，第一次遇到网络拥塞，而第二次先到达并建立连接。此后第一次迟到，而**服务端**此时再建立一个连接，但是这个连接是无效的，因为客户端认为自己没有发送连接请求。所以在服务端耗多了一个**无效连接**

#### 连接释放：四次挥手

![](/files/-L_1toWpuI1uzcgA-1qv)

1. A发送FIN=1，seq=u的释放报文，进入`FIN_WAIT-1`状态（FIN报文不携带数据也要消耗一个序号）
2. B收到后，发出确认，确认号ack=u+1，seq=v，并进入`CLOSE-WAIT`状态。（此时TCP通知应用，A到B方向的连接释放了，而B可能还有数据要发，B到A的连接没释放）
3. A收到B的确认，进入`FIN_WAIT-2`状态，等待B的释放报文段
4. B没有数据需要发送了，则应用通知TCP释放连接。发出FIN=1，ACK=1的释放报文，B的序号seq=w，还需要发送确认号ack=u+1。B进入`LAST-ACK`状态
5. A收到B的释放报文后，还需要发出确认，将ACK=1，确认号ack=w+1，自己序号为seq=u+1，A进入`TIME-WAIT`状态。等待**时间等待计时器**设置的时间2MSL之后，才进入`CLOSED`状态。

> 为什么A要等待？
>
> 1. 保证A最后一个确认报文到达B，因为它可能丢失那么B会超时重传FIN+ACK报文段，A收到再次确认并重启定时器。否则B可能无法进入`CLOSED`状态
> 2. 防止“已失效的连接请求报文段”，保证本连接的数据已经从网络中消失而不会出现在下一次新的连接中

此外，还有**保活计时器（keepalive timer）**，不让服务器白白等待客户端。每次收到客户端消息会重设这个定时器。

### TCP状态机

![](/files/-L_1toWr_8Upmy2J8B7t)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://hdnotes.gitbook.io/ns/computer-network/networktcp-udp.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
