Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added notes/pics/arp-hops.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/arp-request.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/bgp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/icmp-format.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/ip-classes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/ip-fragment.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/ip-header.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/nat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/route-lookup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/router-structure.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/tcp-cwnd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/tcp-fast-retransmit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/tcp-flow-vs-congestion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/tcp-handshake.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/tcp-header.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/tcp-sliding-window.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/tcp-sticky.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/tcp-teardown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/udp-header.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added notes/pics/vpn-tunnel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
100 changes: 86 additions & 14 deletions notes/计算机网络 - 传输层.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* [TCP 拥塞控制](#tcp-拥塞控制)
* [1. 慢开始与拥塞避免](#1-慢开始与拥塞避免)
* [2. 快重传与快恢复](#2-快重传与快恢复)
* [TCP 粘包与拆包](#tcp-粘包与拆包)
<!-- GFM-TOC -->


Expand All @@ -25,13 +26,13 @@

## UDP 首部格式

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/d4c3a4a1-0846-46ec-9cc3-eaddfca71254.jpg" width="600"/> </div><br>
<div align="center"> <img src="pics/udp-header.png" width="600px"/> </div><br>

首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。

## TCP 首部格式

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/55dc4e84-573d-4c13-a765-52ed1dd251f9.png" width="700"/> </div><br>
<div align="center"> <img src="pics/tcp-header.png" width="720px"/> </div><br>

- **序号** :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。

Expand All @@ -49,7 +50,7 @@

## TCP 的三次握手

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/e92d0ebc-7d46-413b-aec1-34a39602f787.png" width="600"/> </div><br>
<div align="center"> <img src="pics/tcp-handshake.png" width="600px"/> </div><br>

假设 A 为客户端,B 为服务器端。

Expand All @@ -67,11 +68,11 @@

第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。

客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。
客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。有了第三次握手,客户端收到服务器对滞留请求的确认时,发现确认号不是自己期望的,就回复 RST,服务器收到后放弃这个连接,不会多打开一个连接。

## TCP 的四次挥手

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f87afe72-c2df-4c12-ac03-9b8d581a8af8.jpg" width="600"/> </div><br>
<div align="center"> <img src="pics/tcp-teardown.png" width="640px"/> </div><br>

以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。

Expand All @@ -97,29 +98,51 @@

- 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。

2MSL 正好覆盖一个来回:A 的确认最多在网络中存活 1 MSL,B 超时重传的 FIN 最多再存活 1 MSL。Linux 中 TIME_WAIT 固定为 60 秒(TCP_TIMEWAIT_LEN)。

**大量 TIME_WAIT 与 CLOSE_WAIT**

TIME_WAIT 出现在主动关闭的一方,使用短连接的机器会积累大量 TIME_WAIT,例如每次请求都新建连接去访问下游服务。每个 TIME_WAIT 连接占用一个本地端口 60 秒,Linux 默认的本地端口范围是 32768~60999,共 28232 个,连接同一个目标地址和端口时,每秒新建超过约 470 个连接就会耗尽端口。

解决方案:

- 使用长连接或连接池,例如 HTTP 的 keep-alive、数据库连接池;
- 在发起连接的一方开启 net.ipv4.tcp_tw_reuse,允许新连接复用处于 TIME_WAIT 的端口,它依靠 TCP 时间戳选项识别旧连接的报文;
- 扩大本地端口范围 net.ipv4.ip_local_port_range。

另一个参数 tcp_tw_recycle 在 NAT 环境下会丢弃正常的连接,Linux 4.12 已将其移除。

CLOSE_WAIT 出现在被动关闭的一方,表示对方已经关闭,本方的应用还没有调用 close()。连接大量停在 CLOSE_WAIT 通常是程序的问题,例如异常分支没有关闭连接、从连接池借出的连接没有归还,需要排查代码,保证每个连接最终都会被关闭。

## TCP 可靠传输

TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。

一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下:

<div align="center"><img src="https://latex.codecogs.com/gif.latex?RTTs=(1-a)*(RTTs)+a*RTT" class="mathjax-pic"/></div> <br>
```text
RTTs = (1 - a) × RTTs + a × RTT
```

其中,0 ≤ a < 1,RTTs 随着 a 的增加更容易受到 RTT 的影响。

超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下:

<div align="center"><img src="https://latex.codecogs.com/gif.latex?RTO=RTTs+4*RTT_d" class="mathjax-pic"/></div> <br>
其中 RTT<sub>d</sub> 为偏差的加权平均值。
```text
RTO = RTTs + 4 × RTTd
```

其中 RTT<sub>d</sub> 为偏差的加权平均值。RFC 6298 建议 a 取 1/8,计算 RTT<sub>d</sub> 时的权重取 1/4,并要求 RTO 不小于 1 秒;Linux 的下限为 200 ms。

## TCP 滑动窗口

窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。

发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。

接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。
接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认,确认号为 32。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/a3253deb-8d21-40a1-aae4-7d178e4aa319.jpg" width="800"/> </div><br>
<div align="center"> <img src="pics/tcp-sliding-window.png" width="760px"/> </div><br>

## TCP 流量控制

Expand All @@ -131,18 +154,18 @@ TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文

如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/51e2ed95-65b8-4ae9-8af3-65602d452a25.jpg" width="500"/> </div><br>
<div align="center"> <img src="pics/tcp-flow-vs-congestion.png" width="700px"/> </div><br>

TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。

发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。
发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口,它取拥塞窗口和接收方通告的窗口中的较小值。

为了便于讨论,做如下假设:

- 接收方有足够大的接收缓存,因此不会发生流量控制;
- 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/910f613f-514f-4534-87dd-9b4699d59d31.png" width="800"/> </div><br>
<div align="center"> <img src="pics/tcp-cwnd.png" width="760px"/> </div><br>

### 1. 慢开始与拥塞避免

Expand All @@ -164,4 +187,53 @@ TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、

慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。

<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/f61b5419-c94a-4df1-8d4d-aed9ae8cc6d5.png" width="600"/> </div><br>
<div align="center"> <img src="pics/tcp-fast-retransmit.png" width="620px"/> </div><br>

## TCP 粘包与拆包

TCP 面向字节流,只保证字节按顺序到达,不保留应用每次写入的边界。发送方写入两次,接收方的一次读取可能读到两条消息拼在一起,称为粘包;也可能只读到一条消息的一部分,称为拆包(也叫半包)。

例如客户端依次写入 hello、world 和一条 200000 字节的消息,服务器等待 200 ms 后,用 1024 字节的缓冲区反复读取:

<div align="center"> <img src="pics/tcp-sticky.png" width="760px"/> </div><br>

产生的原因主要有以下几种:

- 数据先进入接收缓冲区,应用读取时缓冲区里有多少就读出多少,最多读满自己的缓冲区;
- 发送方的 Nagle 算法会把多次写入的小数据合并成一个报文段发送;
- 超过最大报文段长度(Maximum Segment Size, MSS)的消息会被分成多个报文段,可能分多次读到。

UDP 面向报文,一次读取得到的就是对方一次发送的完整数据报,不存在粘包。

解决方法是在应用层约定消息的边界,常用以下三种:

- 固定长度:每条消息长度相同,不足的部分补齐,浪费空间;
- 分隔符:用特殊字符标记消息结束,例如 Redis 协议的每一行以 `\r\n` 结束,消息中出现分隔符时需要转义;
- 长度字段:在消息头部写明消息体的长度,接收方先读长度,再读够这么多字节。

HTTP/1.1 同时用了后两种:首部以一个空行结束,消息体的长度由 Content-Length 给出,或者用分块传输编码在每一块前写明长度。Netty 提供了对应的解码器 FixedLengthFrameDecoder、DelimiterBasedFrameDecoder 和 LengthFieldBasedFrameDecoder。

用长度字段收发消息的最小实现如下,按这种格式重复上面的例子,接收方依次得到 hello、world 和 200000 字节的消息:

```java
static final int MAX = 1 << 20;

static void send(DataOutputStream out, String msg) throws IOException {
byte[] body = msg.getBytes(StandardCharsets.UTF_8);
out.writeInt(body.length); // 4 字节的长度
out.write(body);
out.flush();
}

static String receive(DataInputStream in) throws IOException {
int len = in.readInt(); // 读满 4 字节
if (len < 0 || len > MAX) {
throw new IOException("bad length " + len);
}
byte[] body = new byte[len];
in.readFully(body); // 读满 len 字节,不够就继续等待
return new String(body, StandardCharsets.UTF_8);
}
```

需要注意的是,长度字段来自网络,必须限制上限,否则对方发来一个 2 GB 的长度,服务器就会为它分配 2 GB 内存。
Loading
Loading