> For the complete documentation index, see [llms.txt](https://nixum.gitbook.io/note/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://nixum.gitbook.io/note/wang-luo.md).

# 网络

\[TOC]

## 基本

| OSI七层模型 | 对应网络协议                             | 作用            |
| ------- | ---------------------------------- | ------------- |
| 应用层     | HTTP、TFTP、FTP、NFS、SMTP、Telnet      | 应用程序间通信的网络协议  |
| 表示层     | Rlogin、SNMP、Gopher                 | 数据格式化、加密、解密   |
| 会话层     | SMTP、DNS                           | 建立、维护、管理会话连接  |
| 传输层     | TCP、UDP                            | 建立、维护、管理端到端连接 |
| 网络层     | IP、ICMP、ARP、RARP、AKP、UUCP          | IP寻址和路由选择     |
| 数据链路层   | FDDI、Ethernet、Arpanet、PDN、SLIP、PPP | 控制网络层与物理层间的通信 |
| 物理层     | IEEE 802.1A、IEEE 802.2到802.11      | 比特流传输         |

数据链路层：

* 数据包叫Frame，“帧”；
* 由两部分组成：标头和数据，标头标明数据发送者、接收者、数据类型；
* 用MAC地址定位数据包路径；
* 相关设备是交换机；

网络层：

* 数据包叫packet，“包”；
* IPv4：32个二进制，4字节\*8位；IPv6：1同一子网28个二进制，8字节\*16位；
* 子网掩码与IP的and运算判断是否为同一子网下；
* 路由：把数据从原地址转发到目标地址，同一局域网内，通过广播的方式找到，不同局域网内，原主机先将包根据网关添加路由器/主机地址，通过交换机的广播方式发给目标主机，原主机将数据包传输给目标主机，再由目标主机根据MAC广播交给对应目标
* ping ip，使用ICMP协议可以确认 IP 包是否成功送达目标地址、报告发送过程中 IP 包被废弃的原因和改善网络设置；
* ARP协议：IP与MAC地址的映射，ARP会在以太网中以广播的形式，获取IP对应的MAC地址；

  仅限IPv4，IPv6使用 Neighbor Discovery Protocol替代；
* 相关设备是路由器，网关

传输层：

* 数据包叫segment，“段”；
* Socket、UDP、TCP见下

应用层使用TCP传输数据时，会先将数据打到TCP的Segment中，然后TCP的Segment会打到IP的Packet中，然后再打到以太网Ethernet的Frame中，传输到目标主机后，再一层层解析，往上传递。

## 从浏览器输入URL之后都发生了什么

浏览器输入URL，按回车，

1. 浏览器根据输入内容，匹配对应的URL和关键词，校验URL的合法性，从历史记录、书签等地方，找到可能对应的URL，进行补全，使其符合通用URI的语法。
2. 发起请求，从URL中解析出域名，首先查看本地hosts文件，判断是否有这个域名对应的ip，如果没有，请求本地DNS服务器，先查询本地DNS服务的缓存，如果没有再向上级DNS服务器发送请求，递归查询，直到找到对应的IP地址，然后本地DNS服务器就把对应关系保存在缓存中

   注意根DNS服务器没有记录具体的域名和IP的对应关系，而是告诉本地DNS服务器可以到域服务器上继续查询，给出的是域服务器的地址。
3. 拿到域名对应的IP后，应用层程序准备好数据后，委托给操作系统，复制应用层数据到内核的内存空间中，交给网络协议栈（将其打包为tcp包(传输层)，帧(数据链路层)，并将数据从内核拷贝到网卡，后续由网卡负责数据的发送），与Web服务器建立 TCP/IP 连接（三次握手具体过程）。
4. 建立连接后，发起一个HTTP 请求，经过路由器的转发，通过Web服务器（CDN、反向代理之类的）的防火墙，该 HTTP 请求到达了Web服务器。
5. 服务器处理该 HTTP 请求，返回一个 HTML 文件。
6. 浏览器解析该 HTML 文件，解析HTML文件后，构建dom树 -》构建render树 -》布局render树 -》绘制 render树，自上而下加载，边加载边解析渲染，显示在浏览器端，对于图片音频等则是异步加载。

本质上是OSI七层模型 + 相应协议、组件实现；建立一次TCP后，在HTTP 1.1请求头配置keep-alive=true后，默认保持两小时的连接，可以一直进行http请求，但同时只处理一个http请求，HTTP 2.0才允许并行多个；

## HTTP方法

[菜鸟HTTP教程/HTTP请求方法](http://www.runoob.com/http/http-methods.html)

### Get和Post的区别

* 语义上的区别，Get一般表示查询、获取，Post是更新，创建
* Get具有幂等性，Post没有
* 参数传递方面，Get一般参数接在Url上，对外暴露，有长度限制（1024个字节即256个字符），只接收ASCII字符，需要进行url编码

  Post参数放在request body里，支持多种编码
* GET请求会被浏览器主动cache，而POST不会，除非手动设置
* GET产生的URL地址可以加入书签，而POST不可以
* GET请求参数会被完整保留在浏览器历史记录里，而POST中的参数不会被保留
* GET在浏览器回退时是无害的，而POST会再次提交请求

其实本质都是一种协议的规范，规定参数的存放位置，参数长度大小等，当然也可以反着来，只要服务器能够理解即可

幂等性：同样的请求被执行一次与连续执行多次的效果是一样的，服务器的状态也是一样的，每次返回的结果一样，不产生副作用；

根据语义，简单的把get看成查询，只要服务器的数据没变，每次查询得到的结果是一样的，而把post看成添加，每次post请求都会创建新资源，服务器状态改变

具有幂等性的方法：GET、HEAD、OPTIONS、DELETE、PUT

没有幂等性的方法：POST

安全性：安全的 HTTP 方法不会改变服务器状态，也就是说它只是可读的。

## 常见状态码

参考[HTTP状态码](http://www.runoob.com/http/http-status-codes.html)

* 301：永久移动请求的网页已永久移动到新位置，即永久重定向；返回信息会包括新的URI，浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替，新的URI会放在响应header的Location字段中；搜索引擎会抓取新内容同时也将旧地址修改为新地址。301调整默认会被浏览器cache，用户后续多次访问该url时会，浏览器会直接请求跳转地址。
* 302：临时移动请求的网页暂时跳转到其他页面，即暂时重定向；旧地址的资源还在，只是重定向到临时的新地址中，对SEO有利，搜索引擎会抓取新内容保存旧地址。如果在响应头中通过Cache-Control或Expires，也可以实现301中浏览器缓冲的效果。
* 405：请求的Method被禁止，比如Post的接口用成了Get
* 502：Bad Gateway，作为网关或者代理工作的服务器尝试执行请求时，从上游服务器接收到无效的响应
* 503：Service Unavailable，服务不可用
* 504：Gateway Time-out，充当网关或代理的服务器，未及时从上游服务器收到请求

## HTTP头

### Connection: keep-alive

保持长连接，连接复用，避免频繁建立连接带来的性能损耗。HTTP1.0默认是关闭的，HTTP1.1默认是开启的。对应的key是Connection。可以设置参数`Keep-Alive: max=5, timeout=120`表示一次keep-alive可以发送的请求次数是5，连接保持时间是120s，**默认是2小时**，可以一直进行HTTP请求，但**一个TCP连接同一时间只支持一个HTTP请求**，即一次连接中，请求是一个个处理的，如果前一个没处理完，没办法处理下一个，也就是下一个不返回。

不同的浏览器对同一Host建立的TCP连接数量的限制取决于浏览器本身，像Chrome最多允许同时建立6个TCP连接，假如一个HTML页面有很多图片需要加载，且这些图片都是同一Host，且HTTPs，那浏览器在TLS 后会和服务器确认是否启用HTTP2，如果启用就可以同一连接下同时下载多个，如果不启用，就仍然用那几个连接，排队去等了。

另外多个HTTP连接，是可以建立在一个TCP连接上的。

与TCP的keep-alive不同：

> * TCP 的 keep-alive 是由操作系统内核来控制，存在于内核态，通过 `keep-alive` 报文来防止 TCP 连接被对端、防火墙或其他中间设备意外中断，主要的作用是保活，和上层应用没有任何关系，只负责维护单个 TCP 连接的状态，其上层应用可以复用该 TCP 长连接，也可以关闭该 TCP 长连接。
> * HTTP 的 keep-alive是由应用层控制，存在于用户态， 机制则是和自己的业务密切相关的，浏览器通过头部告知服务器要**复用这个 TCP 连接，请不要随意关闭**。只有到了 `keep-alive` 头部规定的 `timeout` 才会关闭该 TCP 连接，不过这具体依赖应用服务器，应用服务器也可以根据自己的设置在响应后主动关闭这个 TCP 连接，只要在响应的时候携带 `Connection: Close` 告知对方

但是可见，两种keep-alive都是为了连接复用

### Content-Length

表示本次响应的数据长度，后面的字节就属于下一个响应的了，因为HTTP是基于TCP进行通信的，应用层需要解决粘包的问题，HTTP为了解决这个问题，有两种解决方案：1. 设置Content-Length作为响应的边界；2. 响应体设置回车符、换行符作为响应的边界，常见于`Transfer-Encoding=chunked`

### Upgrade

一般是用于协议升级，比如WebSocket，浏览器与服务器使用WebSocket通信时，会先通过普通的HTTP建立连接，在请求头里带上`Upgrade: WebSocket; Connection: Upgrade; Sec-WebSocket-Key: 随机生成的base64码`，发送给服务器。

如果服务器支持WebSocket协议，就会走WebSocket握手流程，根据客户端生成的basae64码，使用公钥变成另一个字符串，放在`Set-WebSocket-Accept`响应头中，并带上101响应码，表示协议切换，要建立起WebSocket连接。

### HTTP缓存

对于一些重复性的HTTP请求，比如每次请求得到的数据都是一样的，就可以把这对请求响应缓存在本地，从而提升性能。

#### 强制缓存

由两个响应头字段实现：

* `Cache-Control: 值可以是max-age=x秒，或者 s-maxage=x秒（代理缓存，如CDN），或者public、private表示缓存是否共享，或者no-cache表明不缓存，或者no-store表示禁止缓存`
* `Expires: 值可以是 max-age+请求时间，但需要配合Last-modified使用，或者直接是绝对时间，表示缓存什么时候到期`，

`Cache-Control`的优先级高于`Expires`

客户端接收到的响应头包括这两个字段中的一个时，在之后的时间里就不会去请求服务端获取数据，直接使用本地缓存，本地缓存一般是直接存在磁盘，响应码后面会直接标识 `200（from dish cache）`

#### 协商缓存

客户端与服务端协商，根据结果判断是否使用本地缓存，有两种头部

* `请求头中的If-Modified-Since，表示用来与响应资源比较，判断是否缓存是否被修改过，单位是秒`配合`响应头中的Last-Modified，表示响应资源的最后修改时间，单位是秒`字段实现，如果缓存被修改过，返回`HTTP 200`，如果没被修改过，返回`HTTP 304`
* `响应头中的ETag，表示缓存响应标识`配合`请求头中的If-None-Match，表示资源过期时，如果响应头里有Etag，则再次向服务器发起请求时，会设置If-None-Match的值位Etag`，服务器会对比`If-None-Match`的值，如果有缓存有修改，返回304，否则返回200

  `ETag`的优先级高于`Last-Modified`，ETag可以解决那种文件内容不变，但文件修改时间改变的场景；

## HTTPS

HTTPS主要解决HTTP的安全问题，如报文内容安全（加密解决），防篡改（签名解决），防冒充（CA认证解决）。

HTTPS = HTTP + SSL/TLS，SSL是介于HTTP之下TCP之上的协议层，提供 加密明文，验证身份，保证报文完整 的保障。TLS是升级版的SSL，作用类似，比SSL多了些其他功能，现在绝大多数浏览器都不支持SSL了，而是支持TLS。

HTTP 先和 TLS 通信，再由 TLS 和 TCP 通信。一般情况下，TLS需要先经过TCP三次握手，建立可靠连接之后，才能做TLS握手的事。

* 加密

  使用 对称加密 加密 报文（私钥加密私钥解密）

  使用 非对称加密 加密 对称加密的密钥，保证该密钥的传输安全（客户端公钥加密，服务端私钥解密；或者 服务端私钥加密，客户端公钥解密）
* 验证身份

  通过第三方（CA）发布TLS 证书，对通信方进行认证

  服务器的运营人员向 CA 提出公开密钥的申请，CA 在判明提出申请者的身份之后，会对已申请的公开密钥做数字签名，然后分配这个已签名的公开密钥，并将该公开密钥放入公开密钥证书后绑定在一起。

  所以这里涉及到两个公钥，一个是服务端用于 TLS 的公钥，一个是CA证书的公钥；

  所以，**TLS 证书主要是表明了该域名归属，日期等信息，还包含了用于报文加密的公钥和私钥以及数字签名，TLS 证书被服务端持有**。

  进行 HTTPs 通信时，服务器会把证书发送给客户端。客户端取得其中的公开密钥之后，先使用数字签名进行验证，如果验证通过，就可以开始通信了，通信时使用上述的加密机制保护报文；

  证书信任的方式：操作系统和浏览器内置；CA颁发；手动导入证书
* 保护报文完整

  TLS 提供**报文摘要**功能（即签名）结合加密和认证来进行完整性保护

**在HTTP的基础上，以TLS 1.2版本为例，RSA算法的握手流程：**

1. 客户端访问服务端的网页，首先经过浏览器内置的受信任的CA机构列表，查看该服务器是否向CA机构提供了证书；CA会对服务端提供的公钥和服务端的相关信息进行Hash，然后用CA的私钥进行签名，将签名的公钥和服务端提供的公钥和信息一起整成一份证书；
2. 如果服务器证书中的信息与当前正在访问的网站（域名等）一致，那么浏览器就认为服务端是可信的，并从服务器证书中取得服务器公钥；
3. 如果还没有取得服务器中的证书，则与服务器建立TCP连接，之后开始TLS的流程：
   1. 客户端向服务端发送请求，把自己支持的TLS版本，加密套件、一个随机数 发给服务端；
   2. 服务端收到请求后，确认客户端的TLS版本，加密套件，然后也生成一个随机数，与证书和公钥一起发给客户端；
   3. 客户端收到服务端的证书、公钥，需要验证该证书是否真实有效：先通过浏览器或操作系统内置的拿到CA证书，使用CA证书的公钥对CA证书进行解密，得到的CA证书的Hash值，然后与从服务端收到的证书的Hash值进行比较，判断是否一致，一致说明服务端可信；
   4. 客户端在验证服务端的证书是有效的之后，再生成一个随机数（也称预主密钥），使用刚刚收到的服务端公钥进行加密后发送出去；
   5. 服务端收到预主密钥后，用私钥进行解密，得到预主密钥的值；
   6. 最后，客户端用预主密钥，使用第1步和第2步的随机数计算出会话密钥；
4. 建立会话密钥：客户端通过服务器公钥加密会话密钥发送给服务端，服务端用自己私钥解密得到会话密钥，用于接收和发送数据，之后传输的http数据都是经过加密的。

   每个会话会生成一个会话密钥，每个会话的会话密钥均不同。

总结：非对称加密的手段传递密钥，然后用密钥进行对称加密传递数据，对称加密的密钥由客户端生成，每次会话密钥都不一样，CA会保存服务端提供的非对称加密的公钥并签名；

过程中，客户端会产生两个随机数，第一个用于制作会话密钥(对报文做对称加密时使用)，第二个用于验证公钥是否可用；服务端会生成一个随机数，给客户端制作会话密钥；

## RSA非对称加密

例子：

```
公钥为（7，33）
假设源数据翻译成十进制为：3，1，15
对其求7次幂为：2187，1，170859375
对其求33的余：9，1，27
得到密文：9，1，27

私钥为（3，33）
得到密文：9，1，27
对其求3次幂为：729，1，19683
对其求33的余：3，1，15
得到明文：3，1，15

设公钥为（e,n），私钥为（d,n）
即 明文^e%n=密文，密文^d%n=明文
```

**公钥和私钥的制作过程**

1. 生成两个质数：p和q
2. 两个质数相乘：N = p \* q
3. 使用欧拉函数计算：T = (p-1) \* (q-1)
4. 选出公钥，条件：质数 && 1 < 公钥 < T，不是T的因子，即E
5. 计算私钥，条件：（D \* E）% T = 1

当p和q特别大时，生成的T和N都非常大，即使公开N，也很难暴力算出p和q，所以破解非常困难

## HTTP 2.0

设备变好，内容形式多样，页面资源变多，实时性要求变高使得HTTP1.1延时变高，像在Chrome连接最大并发量是6个，且每一个连接都需要经过TCP和TLS握手耗时，HTTP1.1本身一个连接只能处理一个请求，才能继续处理，每次请求头部都巨大且重复，不支持服务端消息推送；

HTTP2.0建立在Https协议的基础上，支持二进制流而不是文本，支持多路复用而不是有序阻塞，支持数据压缩减少包大小，支持server push等特性，实现低延时，高性能。

### 头部压缩

http1.x的头带有大量信息，而且每次都要重复发送，字段是ASCII编码，效率较低。http/2使用HPACK算法来压缩header。

HPACK算法包含三个组成部分：静态字典、动态字典、哈夫曼编码(用于压缩)；通过字典长度较小的索引表示对于的字段、再使用哈夫曼编码进行压缩，可高达50%\~90%的压缩率。

**静态字典表**

存储高频的字段和对应的索引，比如一些常见的HTTP请求头字段和值以及对应的索引，索引从1开始自增，代表对应的字段；

HTTP2.0头部基于二进制编码（把索引值翻译成二进制数），不需要使用冒号、空格、或者\r\n作为分隔符，直接使用字符串长度来分割索引和值；

各自缓存之后，之后发送的请求如果不包含首部，就会自动使用之前请求发送的首部，如果首部发生变化，则只需将变化的部分加入到header帧中，改变的部分会加入到头部字段表中，首部表在HTTP2.0的连接存续期内始终存在，由客户端喝服务端共同渐进式更新。

**动态字典表**

> 静态表只包含了 61 种高频出现在头部的字符串，不在静态表范围内的头部字符串就要自行构建动态表，它的 Index 从 `62` 起步，会在编码解码的时候随时更新。

比如有新的不存在静态字典表的字段出现在双方的通信中，双方就会为这个新的字段更新一个对应的索引，记录到动态字典表中。动态字典表生效有一个前提是，必须在同一连接上，重复传输完成相同的HTTP头部，如果在一个连接上只发送了一次，或者重复传输时总是略有变化，动态字典表就无法充分利用了。

动态字典表会随着通信时间的累积，越变越大，占用的内存也越大，会影响服务器性能，因此一般服务器会提供类似`http2_max_requests`的配置，限制一个连接能传输的请求数量，避免动态字典表无限增大，当达到上限后，就会关闭连接来释放内存，等下次连接再重新建立动态字典表。

### 二进制分层帧

![](https://github.com/Nixum/Java-Note/raw/master/picture/HTTP2%E4%BA%8C%E8%BF%9B%E5%88%B6%E5%B8%A7%E7%9A%84%E7%BB%93%E6%9E%84.png)

* 帧类型有10种，分为数据帧和控制帧两大类；
* 标志位用于携带简单的控制信息，比如END\_HEADERS表示数据结束标志，相当于HTTP1.x里的空行或者/r/n；END\_Stream表示单向数据发送结束，后续不会有数据帧；
* 流标识符，用来标识帧属于哪个Stream，用于接收双方在乱序的帧里找到相同的StreamID，从而有序的组装信息；为了防止两端流ID冲突，客户端发起的流具有奇数ID，服务器端发起的流具有偶数ID；

  每个Stream是一个逻辑联系，一个独立的双向的frame存在于客户端和服务器端之间的HTTP2.0连接中。一个HTTP2.0连接上可包含多个并发打开的Stream，这个并发Stream的数量能够由客户端设置。
* 帧数据：由HPACK算法压缩过的HTTP头和包体；

消息：一个完整的请求或响应，由一个或多个帧组成。

> 在二进制分帧层上，http2.0会将所有传输信息分割为更小的消息和帧，并对它们采用二进制格式的编码将其封装，新增的二进制分帧层同时也能够保证http的各种动词，方法，首部都不受影响，兼容上一代http标准。其中，http1.X中的首部信息header封装到Headers帧中，而request body将被封装到Data帧中。

### 多路复用

HTTP2.0通过多路复用实现并发传输，多个Stream复用同一条TCP连接，达到并发的效果，解决HTTP1.1队头阻塞问题，避免频繁建立TCP、TLS握手的时间，减少TCP慢启动对流量的影响，提高HTTP传输的吞吐量。

> 多个 Stream 跑在一条 TCP 连接，同一个 HTTP 请求与响应是跑在同一个 Stream 中，HTTP 消息可以由多个 Frame 构成， 一个 Frame 可以由多个 TCP 报文构成。**不同 Stream 的帧是可以乱序发送的（因此可以并发不同的 Stream ）**，因为每个帧的头部会携带 Stream ID 信息，所以接收端可以通过 Stream ID 有序组装成 HTTP 消息，而**同一 Stream 内部的帧必须是严格有序的**；即多个Stream传输时是并行交错的，但是同一个Stream的帧是有序的。

连接是持久的，客户端和服务器之间只需要一个连接，每个数据流可以拆分成很多不依赖的帧，这些帧可以乱序发送，也可以分优先级，多个流的数据包能够混合在一起通过同样的连接传输，服务端在根据不同帧首部的流标识进行区分和组装。

### 请求优先级

将HTTP消息分为很多独立帧之后，就可以通过优化这些帧的交错喝传输顺序进一步优化性能，服务端也可以根据流的优先级，优先将最高优先级的帧发送给客户端。

### 服务端推送

服务端可以对一个客户端请求发送多个响应，而无需客户端明确地请求，省去客户端重复请求的步骤。

> 服务器推送资源时，会先发送 PUSH\_PROMISE 帧，告诉客户端接下来在哪个 Stream 发送资源，然后用偶数号 Stream 发送资源给客户端。

### 使用长连接需要考虑的点

* 客户端和服务端的数量，因为要保持连接，如果客户端的数量远超服务端的数量，服务端与每个客户端都维持一个长连接，对服务端来说负担比较大，此时需要设置一个合理的超时时间，在空闲时间过长时断开连接，释放服务端资源；
* 因为长连接的多路复用，连接一旦建立便不会断开，流量会被分到同一个服务端，会导致负载不均衡，需要连接池能分辨出服务端的多个实例，自己实现；

## TCP（Transmission Control Protocol 传输控制协议）

### 特点

* 面向连接的，提供可靠交付（只保证传输层可靠），丢包重传，有状态服务
* 有流量控制，拥塞控制
* 提供全双工通信
* 面向字节流（把应用层传下来的报文看成字节流，把字节流组织成大小不等的数据块）
* 头部20字节
* 每一条 TCP 连接只能是点对点的（一对一）
* 缺点：
  * TCP协议在内核中实现，升级起来比较麻烦，还需要client端和server端同时支持；
  * TCP建立连接有延时，启动时慢启动，拥塞避免，比如应用层HTTP要建立连接，需要先建立TCP连接，无法整合在一起；
  * TCP存在队头阻塞问题，因为要保证收到的字节数有序且完整，内核要保证收到连续的包才允许应用层读取，如果中间有一个包超时重传了，就会阻塞应用层;
  * IP变动，端口变动需要重新建立TCP连接，比如切换wifi和4G
* 关于TCP的Keep-Alive：

  连接的双方在物理层面没有连接的概念，连接也不是一直存在数据交互，所以双方是感知不到对方的连接有没有关掉的，TCP keep-alive 也是TCP的保活机制，其基本原理是，在一段时间内没有数据传输，就会给连接对端发送一个探测包，如果收到对方回应的 ACK，则认为连接还是存活的，在超过一定重试次数之后还是没有收到对方的回应，则丢弃该 TCP 连接，通过这种方式，来实现连接的概念。

### 应用场景

* HTTP、FTP文件传输、SSH、SMTP

### 包头

![TCP包头](https://github.com/Nixum/Java-Note/raw/master/picture/TCP%E5%8C%85%E5%A4%B4.jpg)

* **端口号**：用于找到目标主机上的应用程序进程，TCP包是没有IP地址的，因为IP数据是网络层的事，所以这一层只有端口号；
* **序号**：让包能顺序发送和接收，**解决乱序问题，也可以区分连接的阶段，区分不同的连接**。三次握手除了确立双方建立连接，最重要的是确立双方包的序列号，每一次连接都要有不同的序列号用于区别，序列号的起始通常是随时间，如果每次连接的序列号相同，可能会导致前一次连接的包发送到了下一次连接里；序号的增加和传输的字节数相关；
* **确认序列号**：发出去的数据包时进行确认的标记，**解决丢包问题**，接收方回复的ACK包的确认序号 = 发送方数据包的序号 + TCP的数据载荷字节数，注意此时的载荷数据可能是这一次报文的完整数据，也有可能是包含了上一次报文的部分数据和此次报文的部分数据；
* **状态位**：客户端和服务端连接的状态，即包的类型，操控TCP的状态机；
* **窗口大小**：**解决流量控制问题**，标识自己当前的处理能力；

**TCP通过四元组可以唯一的确定一个连接**，四元组：源地址、源端口、目的地址、目的端口，其中，源地址、目标地址在IP头部中，告诉IP协议发送报文给目标主机，源端口和目标端口在TCP头部中，告诉TCP协议要把报文发给哪个进程。

**TCP和UDP可以使用同一个端口**，因为TCP和UDP在内核中是两个独立的模块，**主机收到数据包后，可以在IP包头的协议号里判断数据包是TCP还是UDP**。

在客户端中，针对同一个端口，可以与多个不同的服务端建立TCP连接，原因是TCP是通过四元组确定一个唯一连接的，**对客户端来说，只要多个服务端的IP不同，就可以复用同一个端口**，不会导致连接冲突的问题。

同理，在服务端中，只要绑定的IP不是同一个，就可以复用同一个端口，一般针对服务端绑定IP来说，有几种可能，比如本身机器的IP，或者 127.0.0.1 或者 0.0.0.0（比较特殊，在没设置`SO_REUSEPORT`情况下，表示绑定该主机上的所有IP，就会出现冲突）

理论上，服务端单机TCP的最大连接数 = 客户端IP数 x 客户端端口数，即 2^32 \* 2^16 = 2^48，但会受操作系统资源影响，所以有一定的上限，以Linux为例，影响因素有：文件描述符限制、内存限制；

### 术语

SYN：发起一个新连接，并在其序号进行初始值的设定

ACK：回复，确认序号有效

ack：回复，确认序号，=发送方seq+1

FIN：释放一个连接

RST：复位标志，表示TCP连接中出现异常，需要重新连接

FIN：表示今后不再有数据发送，希望断开连接

MTU：一个网络包的最大长度，一般是1500字节

MSS：除去IP和TCP头部后，一个网络包能容纳的TCP数据的最大长度

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

![三次握手](https://github.com/Nixum/Java-Note/raw/master/picture/%E4%B8%89%E6%AC%A1%E6%8F%A1%E6%89%8B.jpg)

1. 第一次握手：Client将标志位SYN置为1，随机产生一个值seq=j，并将该数据包发送给Server，Client进入SYN\_SENT状态，等待Server确认。 每一端先发出去的都会有SYN，收到之后会发出ACK。

   如果client发送失败，会周期性进行超时重传，直到收到server的确认。
2. 第二次握手：Server收到数据包后由标志位SYN=1知道Client请求建立连接，Server将标志位SYN和ACK都置为1，ack=j+1，随机产生一个值seq=K，并将该数据包发送给Client以确认连接请求，Server进入SYN\_RCVD状态。

   如果server发送响应失败，会周期性进行超时重传，直到收到client的确认。
3. 第三次握手：Client收到确认后，检查ack是否为j+1，ACK是否为1，如果正确则将标志位ACK置为1，ack=k+1，并将该数据包发送给Server，Server检查ack是否为k+1，ACK是否为1，如果正确则连接建立成功，Client和Server进入ESTABLISHED状态，可以开始传输数据了。

   第三次握手可以顺便携带数据，前两次则不行。

   client第三次握手，此时client会认为自己已经ESTABLISHED，server还未收到，此时仍然为active状态：如果server一直没收到连接请求，server会重复第二次握手，直到自己收到第三次握手的请求，此时server才是ESTABLISHED；如果此时client发送了data数据且加上了ACK，server也会切换为established；如果server有数据发送却发送不出，也会重复第二次握手。

关于半连接队列、全连接队列，如果队列满了，后续的连接会被丢掉。

半连接队列是一个哈希表，因为队列里都是不完整的连接，便于在第三次握手时通过哈希表，快速取出对应的连接；

全连接队列是一个链表，因为存在此队列里的连接已经是可用的了，直接从头节点里获取即可；

#### 作用

**三次握手后初始化socket，序号和窗口大小，并建立TCP连接。**

* **确定双方TCP包的起始序号，保证连接后的可靠有序传输**：比如接收方可以去除重复的数据、接收方可以按序号进行顺序接收、或者用于标识已发送的数据包中，哪些已被对方接收。
* **确定了序号就可以区分新老连接，防止旧的重复连接初始化造成混乱**：由于可能出现client与server建立连接并进行通信后断开重连，如果每次连接没有新的起始序号，会导致server分辨不出收到的包是这次连接的还是上次连接的，这个号会作为以后的数据通信序号，以保证应用层接收到的数据不会因网络传输问题而乱序。

  另外，每个连接都会有不同的序号，序号的起始号随时间变化，每4微秒加一，如果有重复，需要4.55个小时后才会出现，因为IP包头里有TTL(生存时间)，超过4小时早过期了。

  序号有回绕问题，所以只能最大程度的避免，比如Linux**使用PAWS机制来解决序号回绕的问题**(需要开启`tcp_timestamps`)，其实就是包序号配合上时间戳，判断时间戳是否是递增的，来解决序号回绕问题，比如回绕时，新老序号不一样，新序号从0开始，老序号是最大值，此时配合上时间戳，就能检查出来包是否可以被接收。
* server端多出一个状态，用来检查client端是否接收到自己的报文，从而**避免客户端重复建立连接，造成不必要的资源浪费**。

#### 不使用两次握手进行连接的原因

* **因为两次握手，只保证一方的初始序号能被对方成功接收，双方无法确认对方收到了自己的序号**：

  如果两次握手确定连接，client发送连接请求给server，server接收后发送响应给client，此时连接建立，client可以正常的发送和接收，但是server并不知道自己发送的请求client有没有收到，此时server端无法确认自己的序号。
* **因为两次握手，server端没有中间状态给客户端来阻止历史连接**：

  如果client发送连接请求给server，server收到连接请求，响应回去后就建立连接，意味着这时双方可以互发数据，但如果此时客户端没有进入ESTABLISHED状态，重复发起连接，server端无法分辨是数据包还是连接包。（因为此时server端已经认为双方可以互发数据包了，server端只有在收到RST报文才会断开连接，本质上还是没有确认包序号导致的问题）
* **因为两次握手，如果server端每收到SYN就建立连接，会导致建立多个冗余的无效连接，造成资源浪费**：

  因为server端没有中间状态来检测client端是否接收到自己回复的ACK，如果client端重复发送多个SYN报文，而server端每次收到SYN就建立连接，就会建立多个冗余的无效连接，造成资源浪费。

另外，三次握手是因为第二次握手的时候，server收到client的请求和自己的请求一次性响应回了client，也可以把这一步拆出来，变成四次握手也是可以的，三次是至少的要求。四次挥手的第二和三次不能合并是因为此前连接已建立，贸然关闭会导致部分报文没有接受完成。

#### 传输时，TCP层使用MSS分包的原因

MSS用于TCP分片，描述一个网络包能容纳的TCP数据的最大长度，一般来说，TCP建立连接时，会协商双方的MSS值，如果数据包超过MSS值，就需要进行分片，由它形成的IP包长度也不会大于MTU（1500字节），以MSS为单位进行分片传输，这样的好处是重传时只需重传特定分片，不用重传所有分片，增加重传的效率。虽然本身IP层也能分包，但是由于IP层并没有超时重传机制，如果一个IP分片丢失，会导致整个IP报文的所有分片都需要重传。

#### 连接过程中超时或者握手丢失怎么办？

* 第一次握手超时或丢失，此时server端没有响应SYN-ACK回去，client端苦苦等待，就会触client端的超时重传机制，重传SYN报文，重传的SYN报文序号跟之前一样。Linux下重试次数是5次，第一次超时重传是1秒后，第二次两秒，**每次超时的时间是上一次的 2 倍**，如果5次都超时，总共要等等63s，TCP才会断开连接；
* 第二次握手超时或丢失，虽然server端响应了SYN-ACK给客户端了，但是client端收不到，此时client会跟上面的一样，进行超时重传SYN报文（继续第一次握手）；而server端因为迟迟收不到client端的ACK，也会触发超时重传，重传SYN-ACK报文（继续第二次握手）。Linux下也是重试5次，机制跟上面一样；
* 第三次握手超时或丢失，由于ACK不会重传，此时是server端触发超时重传，重传SYN-ACK，直到收到第三次握手或达到最大重传次数（继续第二次握手）。

SYN报文被丢弃可能的原因：

* PAWS机制 + NAT环境下，因为client端A和B通过NAT网关与server端建立连接，在server端看来，client端其实是同一个，此时如果client端A和B都开启了PAWS机制，数据包带有时间戳有先后顺序，就会导致server端丢弃client的数据包了；
* 半连接队列 / 全连接队列 满了，此时后来的SYN包都会被丢弃；

#### 已建立连接但重复接收SYN包会怎么样

当client端与server端建立连接后，client端宕机重启，再次发起SYN连接（四元组相同），但server端仍然处于上一个连接的ESTABLISHED状态，因为报文序号的原因，此时server端能知道此报文号不是自己期望的，那就会回复一个携带了正确序号和确认号的ACK报文（Challenge ACK，携带了server端下一次想要接收的序号），client端收到这个Challenge ACK后，发现确认号不是自己期望的，就会回复RST报文，server端收到后就会释放掉该连接。

Challenge ACK的作用是告诉发送方报文的序号是错误的，并给发送方正确的包序号。

#### 如何关闭连接

server端关闭连接时，如果贸然关闭整个server端，会导致所有连接到此server端的client端都断开，最佳实践是可以单独关闭每一条TCP连接，原理就是伪造一个SYN包发送给server端，获取Challenge ACK，反向制作出RST报文，分别发送给client端和server端，让双方断开连接。

TCP如果发现到达的报文序号对于当前的连接是不正确的，就会发送一个RST重置报文，从而使得连接断开，因此，TCP对RST重置报文的序号要求很严格，一定要等于下一个预期接收包的序号（可以利用challenge ACK实现），才能进行重置，断开连接。TCP重置攻击的原理，就是不断向TCP发送RST包，让正常的TCP连接断开，解决方案是IP层使用IPsec协议，通过加密和认证的方式判断数据来源。

#### 建立连接后出故障了怎么办？

* TCP设有一个保活计时器，每收到一次请求都会复位这个计时器，server端在一段时间内没有进行数据交互时，就会触发这套keep-alive机制，如果规定时间(2小时)内没收到，则发送探测报文测试对方是否出现故障，连续10次/75分钟，仍没反应，说明对方故障；

  如果没有开启这个机制，server端就还是一直保持在ESTABLESHED状态；

  这个机制主要是探测对端主机是否宕机重启的场景，如果是对端应用进程崩溃的，因为TCP连接是由操作系统内核维护的，能感知到并进行资源回收，进入四次挥手流程断开连接；
* 如果client端宕机后重启，还收到了之前TCP连接的报文，就会回复RST报文，断开连接；
* 如果client端宕机后没有重启，server端发送数据收不到ACK，会进行超时重传，直至达到上限，断开连接；

#### SYN Flood攻击的关系

> TCP三次握手时，客户端发送SYN到服务端，服务端收到之后，便回复**ACK和SYN**，状态由**LISTEN变为SYN\_RCVD**，此时这个连接就被推入了半连接队列。
>
> 当客户端回复ACK, 服务端接收后，三次握手就完成了。这时连接会等待被具体的应用取走，在被取走之前，它被推入ACCEPT队列，即全连接队列。
>
> SYN Flood攻击，在短时间内，伪造**不存在的IP地址**，向服务器大量发起SYN报文。当服务器回复SYN+ACK报文后，不会收到ACK回应报文，导致服务器上建立大量的半连接半连接队列满了，此时服务器就无法处理正常的TCP请求。
>
> 应对方案：
>
> * **调大`netdev_max_baklog`**，该参数用于当网卡接收数据包的速度大于内核处理速度时，用一个队列保存这些数据包，默认是1000。
> * **增大TCP半连接队列的长度**
> * **减少SYN+ACK重传的次数**：受到SYN Flood攻击，会有大量处于SYN\_REVC状态的TCP连接，处于这个状态会重传SYN+ACK，通过减少其重试次数上限，来快速关闭连接。
> * **开启`net.ipv4.tcp_syncookies`**：在收到SYN包后，服务器根据一定的方法，以数据包的源地址、端口等信息为参数计算出一个cookie值作为自己的SYNACK包的序列号，回复SYN+ACK后，服务器并不立即分配资源进行处理，等收到发送方的ACK包后，重新根据数据包的源地址、端口计算该包中的确认序列号是否正确，如果正确则建立连接，否则丢弃该包。
> * **SYN Proxy防火墙**：服务器防火墙会对收到的每一个SYN报文进行代理和回应，并保持半连接。等发送方将ACK包返回后，再重新构造SYN包发到服务器，建立真正的TCP连接。

### 关闭连接 - 四次挥手

![四次挥手](https://github.com/Nixum/Java-Note/raw/master/picture/%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B.jpg)

1. 第一次挥手：Client发送一个FIN、一个seq，用来关闭Client到Server的数据传送，Client进入FIN\_WAIT\_1状态。
2. 第二次挥手：Server收到FIN后，发送一个ACK给Client，确认序号ack为收到seq+1（与SYN相同，一个FIN占用一个序号），Server进入CLOSE\_WAIT状态。
3. 第三次挥手：Server发送一个FIN、一个ACK、ack为上面的seq+1、一个seq，用来关闭Server到Client的数据传送，Server进入LAST\_ACK状态。
4. 第四次挥手：Client收到FIN后，Client进入TIME\_WAIT状态（等待时间设为2MSL，即报文最大生存时间），接着发送一个ACK给Server，确认序号为收到序号+1，Server进入CLOSED状态，完成四次挥手，Server会比Client早一点关闭TCP连接。

由于TCP连接是全双工的，因此，每个方向都必须要单独进行关闭。这一原则是当一方完成数据发送任务后，发送一个FIN来终止这一方向的连接，**收到一个FIN只是意味着这一方向上没有数据流动了，即不会再收到新数据了（但仍然可能收到在途的数据）**，但是在这个TCP连接上仍然能够发送数据，直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭，而另一方则执行被动关闭。

#### 关闭连接需要四次挥手的原因

* 四次挥手：第一次挥手Client发送消息确定Client想关闭连接，第二次挥手Server发送消息确定Client可以关闭连接，第三次挥手Server发送消息确定Server想关闭连接，第四次挥手Client发送消息确定Server可以关闭连接，少了哪一次都可能导致没有完全关闭，造成一方可发送或者接收。
* 之所以要四次，是因为server收到FIN后，不能同时发送ACK确认信号和FIN，**有可能此时client有一些报文还没收完，如果client没有收到，server才有能重发**，所以第二、三次挥手不能合并。
* 关闭连接时，**当收到对方的FIN报文时，仅仅表示对方不再发送新数据**，但对方还能发送旧数据或接收数据，己方也未必把全部数据都发送给对方了，所以己方先发送ACK，告知对方我知道你不会再发送新数据，之后己方还仍然可以发送一些数据给对方，再发送FIN报文给对方来表示同意现在关闭连接，因此，己方的ACK和FIN一般都会分开发送。

正常情况下，是否发送第三次挥手的控制权不在内核，而在被动关闭方的应用层，取决于其什么时候调用关闭函数，调用了关闭函数，才会发第二个FIN，在关闭之前，还可以继续发送数据，所以二三次挥手的ACK和FIN是分开发的。

关闭函数有两种：

* `close()函数`，会使得socket不再有发送和接收的能力，当使用`close()函数`关闭连接时，由于不再具有发送和接收的能力，会直接返回RST报文给对方，然后内核释放连接，属于粗暴关闭。当客户端调用了`close函数`，服务端继续读操作，就会报`Connection reset by peer`，如果是写操作，则程序会产生`SIGPIPE`信号，交由应用层程序的信号处理器处理，默认是进程终止退出。
* `shutdown()函数`，会使socket只关闭发送方向而不关闭读取方向，此时会经历完整的四次挥手，属于优雅关闭。

只有在特定场景下，才会出现三次挥手：当被动关闭方在TCP挥手过程中，没有数据要发送，同时没有开启`TCP_QUICKACK, 即开启了TCP延时确认机制（该机制可以使得ACK和数据报文一起发送，提升效率）`，那么第二三次挥手可以合并传输，此时是三次挥手。

#### 断开连接过程中超时或者挥手丢失怎么办？

* 第一次挥手时超时或丢失，当重试到达上限后，client端会直接断开连接；
* 第二次挥手时超时或丢失，由于ACK不会重传，所以是client端进行第一次挥手的超时重传，当重试到达上限后，client端会直接断开连接；
* 第三次挥手时超时或丢失，此时client端处于FIN\_WAIT2状态，如果client端是调用close函数，则该状态最多持续60s，到时直接关闭；如果client端调用的是shutdown函数，则只关闭了发送方向，此时仍然可以接受数据，但状态会一直处于FIN\_WAIT2；而server端会重传FIN，直到到达上限，server断开连接；
* 第四次挥手时超时或丢失，server端会进行超时重传，当重试到达上限后，server端会直接断开连接；client此时的状态是TIME\_WAIT，在定时2MSL后（每次收到第三次挥手会进行重置），超时client端就会断开连接；

#### 主动关闭连接的那一方需要TIME\_WAIT状态的原因

* **保证被动关闭连接的那一方能正确的关闭连接**：client端发送的最后一个ACK报文给server端，这个ACK报文是可能丢失，此时server接收不到，那他就会重新发送FIN报文，client端就能在这2MSL内收到这个重传的报文，再次进入2MSL等待时间，发送ACK给server端，直到两者都进入closed状态。如果没有2MSL等待时间，而是client端发送完报文直接关闭，就会出现server端无法接收而导致无法进入closed状态；
* 如果server超过了2MSL时间依然没收到client的ACK，会再次重发第三次挥手，但此时client会发送RST标志，表示异常关闭连接；
* **防止历史连接中的数据，被后面相同的四元组连接错误的接收**：client端在发送完最后一个ACK报文段后，在经过2MSL，可以使本连接持续的时间内所产生的所有报文都从网络消失，用这个时间让这个连接不会和后面的连接混在一起，使得下一个新的连接不会出现旧的连接请求报文；

#### TIME\_WAIT等待时间是2MSL的原因

MSL（Maximum Segment Lifetime，报文最大生存时间，单位是时间），与IP头中的TTL的区别，TTL是指IP数据包可以经过的最大路由数，每经过一次处理它的路由，次数就减一，值为0时会进行丢弃，同时发送ICMP报文通知源主机，而MSL会设置一个大于等于TTL被消耗为0的时间，确保报文已被自然消亡。

TTL值一般是64，Linux将MSL设置为30秒，TIME\_WAIT设置为2MSL，表示一来一回的时间，相当于至少允许报文丢失一次，即server端可以在client端处于TIME\_WAIT状态下重试两次。

2MSL的时间从client端收到FIN后发送ACK开始计时，重复收到FIN报文则会进行重置。

#### TIME\_WAIT状态下，client端收到相同四元组的SYN包，会怎么样

此时要看SYN包是否合法（合法的SYN包，其序号和时间戳会比期望下一个收到的序号或时间戳要大）:

* 如果是合法的SYN包，就会重用此四元组连接，跳过2MSL进入SYN\_RECV状态，进入三次握手建立连接；
* 如果收到非法的SYN包，就会再回复一个第四次握手的ACK报文（Challenge ACK），server端收到后，发现不是自己期望收到的ACK，就会回复RST报文给client端；

#### 高并发下存在大量TIME\_WAIT状态的连接的原因 及 解决办法

TIME\_WAIT状态是 **主动发起连接关闭的那一方** 才会存在，可以是客户端，也可以是服务端

可能产生的原因：

* 短连接场景下，无论是客户端还是服务端，请求头里带`Connection: close`，都是服务端在处理完这次请求后，主动关闭连接；因为如果由客户端来关闭，服务端还得处理一次这次关闭的socket；
* 长连接超时，双方建立长连接之后，在一段时间没有数据通信，服务端就会关闭连接；
* HTTP长连接的请求数量达到上限，服务端就会关闭连接，比如在一些QPS比较高的场景

可能产生的影响：

* 如果在客户端，存在大量TIME\_WAIT状态的连接，可能导致端口占用（此时无法对同一四元组建立连接，对其他四元组建立连接还是没问题），连接未能及时回收，导致无法创建新连接；
* 如果在服务端，存在大量TIME\_WAIT状态的连接，可能导致系统资源占用，如文件描述符、内存资源、CPU资源、线程资源等。端口资源倒是不会受限，因为服务端只监听一个端口，此时只会导致无法与客户端同一四元组建立连接而已，对其他客户端连接没有影响，只要同一个客户端使用新端口，也不会有影响，所以更多是对系统资源的占用；

解决办法：

TIME\_WAIT的状态是必定存在的，所以一般来说，只能尽量减少（降低等待时间，降低出现的次数）TIME\_WAIT带来的危害，比如：

* 可以设置`tcp_tw_reuse（连接复用）`和`tcp_timestamps（报文带上 时间戳，防止回绕）`参数解决，该参数的作用是让client端能快速复用处于TIME\_WAIT的端口，如果在TIME\_WAIT期间，client端以相同四元组再次建立连接，会判断TIME\_WAIT是否超过一秒，超过则可以正常使用这个端口，但是可能会误接收上次连接的RST，导致连接断开，如果第四次挥手的ACK丢了，此时被动关闭的那一方可能不能被正常关闭；

  这种方案有个风险点，快速复用 TIME\_WAIT 状态的端口，导致新连接可能被回绕序号的RST报文断开，因为RST报文比较特殊，TCP在处理的时候，发生即使RST报文的时间戳过期了，序号正确，还是能被接收；如果没打开`tcp_tw_reuse`，停留2MSL，那这个RST报文就不会出现在这个新连接里了；

  另外，此方案只适用于客户端，即服务发起端；
* 修改`tcp_max_tw_buckets`参数，调小TIME\_WAIT的等待时间；
* 将短链接改成长连接，比如HTTP，把响应头的`Connection: close`改成`Connection: keep-alive`，告诉服务端不要关闭，尽量复用链接；
* 尽量不在服务端关闭连接；
* 增加可用端口的范围；

#### 服务端出现大量CLOSE\_WAIT状态的原因

* CLOSE\_WAIT状态是 **被动关闭连接的那一方** 才会存在，一般是在服务端，如果 被动关闭方 没有及时关闭连接，那就无法发出 FIN 报文，从而无法使得 CLOSE\_WAIT状态的连接转变为 LAST\_ACK 状态。
* 发生这种情况通常是代码问题，没有及时调用close方法关闭连接导致；

### 滑动窗口

TCP协议规定在建立连接后，会确定包的序号的起始ID，按照ID一个个发送，只有上一个数据包收到应答，才能发送下一个，这样就会导致效率比较低，为了解决这个问题，引入了滑动窗口，通过窗口来实现累计确认或累计应答的目的，应答某个ID的包就表示在这个ID之前的包都收到了，才允许发送下一个（可以理解成批量批量的ACK）。

**窗口大小即自己的数据接收缓冲池的大小，由接收端决定，窗口标识了无需等待确认应答，还可以继续发送数据的最大值。**

发送窗口和接收窗口所存放的字节数，都是放在操作系统内存缓冲区的，会受操作系统影响，如果接收端不能及时接收数据，可能导致窗口变小，发送端无法发送数据；如果先发生减少缓存，再收缩窗口，则可能出现丢包问题，因此TCP规定只能先收缩窗口，再减少缓存来避免丢包问题。

> 由于发送缓冲区大小决定了发送窗口的上限，而发送窗口又决定了「已发送未确认」的飞行报文的上限。因此，发送缓冲区不能超过「带宽时延积」，尽量靠近最好。带宽时延积 = RTT \* 带宽，如果发送缓冲区超过带宽时延积，此时网络过载，容易丢包，如果小于带宽时延积，带宽用不满，比较浪费。

![](https://github.com/Nixum/Java-Note/raw/master/picture/TCP%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3.png)

在发送端和接收端会分别使用缓存来保存这些包的记录，一般分为四个：1. 已发送并确认的、2. 已发送未确认的、3. 没有发送但准备发送的，且大小在接收方处理范围内的、4. 没有发送且暂时不会发送的，**滑动窗口就是处理第二、三部分的数据包**。

* 由发送方和接收方在三次握手阶段，互相通知自己的最大可接收的字节数。
* 当发送方窗口左部字节已发送且收到确认，窗口右滑直到左部第一个字节不是已发送并且已确认的状态，接受方窗口移动同理，此时可用窗口变大，表示可以继续发送数据；
* 接收窗口只会对窗口内最后一个按序到达的字节进行确认，确认之后表示之前的所有字节都接收到了；
* 发送窗口为0，表明可用窗口耗尽，在未收到ACK确认之前无法继续发送数据；

在处理过程中，当接收缓冲池的大小发生变化时，会给对方发送更新窗口大小的通知。

#### 粘包问题

**实际上因为TCP是面向字节流，而字节流本身没有粘包/拆包的概念，TCP层只管提供可靠性消息传输，不然为什么会对网络分层呢，粘包是应用层没有处理好导致**

发送方发送的若干包数据到接收方接收时粘成一包，从接收缓冲区看，后一包数据的头紧接着前一包数据的尾。

只有TCP会（通过窗口大小来接收数据，窗口大小又是动态的），而UDP因为有消息边界（头部有规定报文的大小），所以不会

* 产生的原因

  * 发送端粘包：发送端等到缓冲区满了才发送出去，造成粘包；

  有时为了提高发送数据的效率，服务端会把多个数据块合并成一个大的数据块后封包发送，但由于面向流的通信是无消息保护边界的，接收端就很难分辨出完成的数据包。

  当要发送的数据大于TCP发送缓冲区剩余大小，将会发生拆包；当待发送数据大于（最大报文长度），TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。

  * 接收端粘包：接收端不及时接收缓冲区的包，造成多个包接收；
* 解决方法

  粘包主要就是没有处理好数据包的边界，因此我们可以

  * 发送固定长度的消息；消息尺寸和消息一块发送；特殊标记标记消息区间；
  * 通信双方规定好协议 + 编解码器（比如规定定长的请求头、请求体），按协议的格式进行解析；比如将数据分成两部分，一部分是头部，一部分是内容体，其中头部结构大小固定，且有一个字段声明内容体的大小。
  * 程序控制发送和接收频率；

[TCP粘包问题分析和解决（全）](https://www.cnblogs.com/kex1n/p/6502002.html)

### 流量控制

TCP**需要解决可靠传输和包的乱序问题，就需要知道网络实际的数据处理带宽或数据处理速度，才不会引起网络拥塞，导致丢包，so需要做流量控制**。在TCP的包头中通过Window字段控制，这个字段是接收端每次ACK时会告诉发送端自己还有多少缓冲区可以接收数据，于是发送端就可以根据这个接收端的处理能力来发送数据，而不会导致接收端处理不过来，从而实现流量控制。

实现滑动窗口，发送端和接收端在进行数据交互时，会商定滑动窗口的大小，**在对于包的确认时会携带一个窗口的大小（通过ACK通知）**，通过该窗口大小来实现流量控制（或者时不时发送一个窗口探测的数据段来确认双方的窗口大小），当发送方和接收方的滑动窗口大小为0时，发送方会定时发送窗口探测数据包，来更新窗口大小。

* **窗口关闭**（Zero Window）：当滑动窗口的大小变成0，意味着发送端不发数据了，当接收方的滑动窗口可以更新时，会通过ACK通知发送端，但是这个窗口大小的ACK报文是可能丢失的，丢失会导致双方互相等待，导致死锁。

  所以收到Zero Window的那一方（即滑动窗口变为0），会启动一个持续计时器，如果计时器超时，就会发送Zero Window Probe报文进行探测，更新自己的窗口大小，这个值会被设置成3次，每次大约30-60s，如果3次过后还是0，可能就会断开连接了。
* **糊涂窗口综合征**（Silly Window Syndrome）：如果接收方太忙，来不及取走滑动窗口里的数据，就会导致发送方可发送的数据越来越小，如果每次发送的数据太小，带宽没有占用，实际上是很亏的，这种现象就叫Silly Window Syndrome。

  解决方法有多种，一种是如果这个问题是接收端引起的，当收到的数据导致滑动窗口的大小小于某个值，就直接回滑动窗口为0的ACK给发送端，把滑动窗口关闭，等接收方的滑动窗口大于某个值是，才把滑动窗口打开，让发送端发数据过来；另一种是如果这个问题是发送端引起的，就延时处理，攒多点数据一块发。

### 重传机制

\*\*丢包问题：\*\*发送方按顺序发送一系列的包，接收方接收这些包是中间一部分包没收到

* 产生原因
  * 中间这些包经过其他链路导致延时接收方还没收到；
  * 接收方收到且发送了ACK给发送方，但是发送方没有收到ACK；
  * 网卡丢包，数据链路有点问题，或者网卡性能不足，数据包真的丢了；
  * 因为顺序发送和接收问题，接收方后面的包收到了，但是前面的包还没收到，导致后面的包收到了也不能发送ACK给发送端；
  * 建立连接时，半连接队列 或者 全连接队列 满了，新来的包就会被丢掉；
  * 流量控制机制产生的丢包；
  * 接收数据时，数据会先暂存在内核缓冲区，如果缓冲区过小，发送速度又过快，产生溢出而丢包；

所以，为了保证可靠传输，解决丢包问题，就需要保证当报文丢失，在一定次数内持续重试，直到成功，又要确保如果重传都失败了，及时放弃传输，避免死循环的问题。

* 解决方法
  * **超时重传**（Retransmission Timeout，RTO），超时重传有一个计时器，在报文发送出去后开始计时，如果在时限内收到回复的ACK，计时器就清零，如果在时限内还没收到ACK，就触发重传。

    超时时间由自适应重传算法来决定，另外是每次重试的时间间隔会加倍，超过两次认为网络环境差，取消重传。**超时重传主要解决丢包时干等回复ACK的问题**。

    RTO初始值是1秒，建立连接后，RTO会被动态计算，上限是2min，下限是200ms。RTO的设置尤为关键，设置过长，重发就慢，效率低性能差；设置过短，就会导致没有丢就重发，重发过快，就会增加网络拥塞，导致更多的超时，从而又导致更多的重发，因此无法设置成一个写死的值，而是通过RTT算法动态调整。一般超时重传时间RTO会略大于报文往返时间RTT的值。

    由于ACK本身没有ACK了，如果ACK发生丢失，ACK会视情况重传，比如，如果发送方收到了后续的ACK，就说明前面的内容都接收到了，就不会重传；如果没收到后续的ACK，则触发超时重传机制。
  * **快速重传机制**，因为包是有顺序的，所以客户端可以检测出缺失的包，然后发送对应的冗余ACK（称为DupAck）给服务端，通过在服务端设置收到规定通过发送/接收冗余的ACK包的次数（一般是3个），如果达到了规定次数就把序号等于这个ACK号的包进行重传，不等超时时间，so **快速重传主要是解决超时等待过久的问题**。

    快速重传的触发条件是：收到3个或以上的重复ACK，即DupACK

    比如：接收端中间漏收了Seq2，后面又接收到了Seq3、4、5，那就会在接收时重复发送ACK2，发送端收到重复的ACK后就会重新发送Seq2了。

    ![](https://github.com/Nixum/Java-Note/raw/master/picture/TCP%E5%BF%AB%E9%80%9F%E9%87%8D%E4%BC%A0%E6%9C%BA%E5%88%B6.jpeg)

    但是快速重传有一个问题，ACK只向发送端告知最大的有序报文段，并不确定是哪一个报文丢失了，此时发送端不知道要重传哪些包，是重传一个，还是重传所有。

    以上图为例，由于报文的顺序性，接收端收不到Seq2，但Seq3、Seq4、Seq5都接收到了，但是ACK的时候只重复了ACK2，此时发送端只知道必定要重传Seq2，但是并不清楚是否要重传Seq3、Seq4、Seq5。

    SACK就是为了解决这个问题：

    > 在Linux中的SACK机制，它会在TCP头里加一个SACK（Selective ACK）的东西，SACK还是基于快速重传的ACK，只是SACK会把接收端收到的所有包的序列，都反馈给发送端，发送端根据遗漏的ACK序号，进行重传。SACK部分最多只能容纳4个块。
    >
    > 比如在上面的场景中，接收端在发送ACK时，带上SACK，SACK上带有Seq3、4、5的信息，这样发送端就知道只需重传Seq2了。

    > 另外，还有个D-SACK（Duplicate SACK），在SACK上做扩展，DSACK使用SACK的第一个段作为标识，这个段表示接收范围，通过比较这个段的值和SACK的回复范围，进行判断，让发送方知道是发出去的包丢了，还是接收方回应的ACK包丢了；还是发送方的超时太短，导致重传；还是先发出去的包后到的情况，还是数据包被复制了。

    SACK和D-SACK的区别在于次数和目的，SACK通过重复重传来告诉发送方要重传什么数据，D-SACK用来告诉发送方哪些数据被重复接收。

    ![](https://github.com/Nixum/Java-Note/raw/master/picture/Linux-SACK%E5%BF%AB%E9%80%9F%E9%87%8D%E4%BC%A0%E6%9C%BA%E5%88%B6.jpeg)

### 拥塞控制

* 产生原因：对资源的需求超过了可用的资源，网络吞吐量下降，如果网络出现拥塞，数据包将会丢失或延迟到达，发送方以为发送失败又会继续重传，从而导致网络拥塞程度更高。
* 判定方式：只要发送端没有在规定时间收到ACK应答报文，就会发生超时重传，此时会认定网络出现拥塞。

  **TCP通过数据包发送和确认的往返时间RTT，丢包率来判断是否拥塞，使用滑动窗口来进行拥塞控制**，控制发送方的发送速率，避免包丢失和超时重传。
* 解决方法：

  * **慢开始 + 拥塞避免，一开始慢慢的发送，逐渐增大发送速率（线性上升直至网络最佳值），再慢下来依次重复。**

    慢开始阶段，TCP连接每收到一个数据的ACK（不包括重复的ACK），拥塞窗口就翻倍增加一个MSS(最大报文大小)，当达到了慢开始的阈值，增长速度就会放缓，进入拥塞避免阶段，变成了每过一个RTT，拥塞窗口就会增长一个MSS；

    每一个TCP连接独立维护自己的拥塞窗口，拥塞窗口一般比MSS大，一般是MSS的某个倍数，MSS的上限一般是1460字节，一个窗口就是n个MSS；发送窗口大小 = min(拥塞窗口大小，接收窗口大小)；

    拥塞窗口由发送方维护，不放在TCP头部中；

    拥塞避免时会重复慢开始，每次收到一个数据的ACK，拥塞窗口增加量会进行减半，在再次进入慢开始+拥塞避免，所以，慢开始不止在TCP连接启动时发生，也有可能在传输过程中反复发生。

    ![慢开始+拥塞避免](https://github.com/Nixum/Java-Note/raw/master/picture/TCP_%E6%85%A2%E5%BC%80%E5%A7%8B+%E6%8B%A5%E5%A1%9E%E9%81%BF%E5%85%8D.png)
  * **快重传 + 快恢复，当拥塞发生时，减少超时重传的使用，而是使用快速重传机制**。

    TCP每发送一个报文，就会启动一个超时计时器，如果在限定时间内没有收到这个报文的ACK，发送方就会认为报文丢失，此时就会进行超时重传，一般最小的超时重传时间是200ms，为了解决每次丢包都要等待200ms或者更长的时间才会重传的问题，TCP就会采用快速重传，一旦发送方收到3次重复确认，就不用等超时计时器了，会直接重传这个报文。

    在Reno拥塞控制算法中，TCP在遇到拥塞点后，不会反复进行慢开始+拥塞避免，而是拥塞窗口减半，直接进入快速重传，不进入慢开始，保持跟拥塞避免一样的线性增长，直到下一个拥塞点。

    网络上的限速，原理其实就是设置拥塞窗口的上限，当超过限制，就会丢弃这些报文，主动进入拥塞避免阶段，确保传输速度。

    ![快恢复](https://github.com/Nixum/Java-Note/raw/master/picture/TCP%E6%8B%A5%E5%A1%9E_%E5%BF%AB%E9%80%9F%E6%81%A2%E5%A4%8D.png)

  为了实现上面两种机制，TCP使用BBR拥塞算法，来达到高带宽和低延时的平衡

**流量控制和拥塞控制的区别：**

流量控制针对接收方，为了让接收方能来得及接收，根据接收方自己的能力，控制发送速度，防止分组丢失；

而拥塞控制是为了防止过多的数据包注入到网络中，降低整个网络的拥塞程度，避免出现网络负载过载；

### 优化

参考：<https://xiaolincoding.com/network/3\\_tcp/tcp\\_optimize.html>

## UDP（User Datagram Protocol 用户数据报协议）

### 特点

* 不可靠的、无连接的，尽最大可能交付，只负责发送数据，无状态服务
* 没有拥塞控制，流量控制
* 面向报文（对于应用程序传下来的报文不合并也不拆分，只是添加 UDP 首部），一个一个地发，一个一个地收
* 头部只有8字节
* 支持一对一、一对多、多对一和多对多的交互通信。

### 应用场景

* 针对网络资源少，对丢包不敏感的应用，比如应用层的DHCP，在获取IP地址和子网掩码的使用，DNS、SNMP等
* 需要广播的应用，比如DHCP、VXLAN
* 需要处理速度快，时延低，容忍丢包、网络拥塞的应用，比如直播、视频，允许丢包，虽然丢包会导致丢帧，但影响不会很大；实时游戏、物联网终端的数据收集，其实大多数会基于UDP做一定的改进，减少UDP劣势的影响

### 包头

![UDP包头](https://github.com/Nixum/Java-Note/raw/master/picture/UDP%E5%8C%85%E5%A4%B4.jpg)

UDP包头比较简单，两端通信时，通过网络层里的IP，将数据包发送给对应的机器，IP头里有个8位协议，表明该数据是UDP协议的，解析到传输层，通过UDP包头提供端口号，让目标机器监听该端口号的应用程序进行处理

### 使用场景

* 网络环境较好，如内网应用，或者对于丢包不敏感的应用
* 需要广播或多播
* 需要处理速度快，时延低，容忍丢包和网络拥塞

## UDP如何实现TCP

建立连接，是为了在客户端和服务端维护连接，而建立一定的数据结构来维护双方交互的状态，通过这样的数据结构来保证面向连接的特性。

UDP属于传输层，协议已经定死了，要实现TCP的功能只能在应用层实现，模拟TCP有的那些功能：确认机制、重传机制、窗口确认机制、流量控制、拥塞控制等那些功能。

UDP实现可靠性，可以简单理解成，将TCP的三次握手发送数据全程用UDP去发送，模拟TCP的包头

* seq/ack机制，确保数据发送到对端
* 数据包 + 序号，确保有序性
* 数据包 + 确认序号，确保不丢包
* 添加发送和接收缓冲区，主要是用户超时重传。
* 定时任务实现超时重传机制。

发送端发送数据时，生成一个随机seq=x，然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存，并发送一个ack=x的包，表示对方已经收到了数据。发送端收到了ack包后，删除缓冲区对应的数据。时间到后，定时任务检查是否需要重传数据

[如何实现UDP的可靠传输](https://blog.csdn.net/AaronHyk/article/details/81505562)

比如有QUIC协议，就是基于UDP实现的可靠传输协议，也是在应用层实现的。

## 套接字Socket

Socket是对TCP或UDP协议的封装，本质上是一个调用接口，而非协议，工作在OIS模型的第五层（会话层）

### 基于TCP协议的Socket

![基于TCP的Socket](https://github.com/Nixum/Java-Note/raw/master/picture/%E5%9F%BA%E4%BA%8ETCP%E7%9A%84Socket.jpg)

* 服务端只调用bind()方法，不调用listen()方法，如果客户端直接根据这个ip和端口进行连接，此时无法联通，服务端会直接返回RST报文；
* 但是不调用listen()，是可以建立TCP连接的，而socket不行而已。在TCP中，存在自连接，即客户端自己连自己，不能有服务端参与，此时是可以建立连接的。虽然不调用listen()方法，不会创建半连接队列和全连接队列，但内核有个全局的hash表，可以存放socket连接的信息，连接信息通过回环地址从这个全局hash表中取出，最后成功建立连接；
* 不调用accept()方法，也能进行三次握手，建立连接，甚至，服务端在执行accept()方法前，如果客户端发送消息给服务端，服务端能正常回复ACK确认包的。因为accept()方法本身不参与握手，执行accept()只是为了从全连接队列里取出一条可用连接而已。

### 基于UDP协议的Socket

![基于UDP的Socket](https://github.com/Nixum/Java-Note/raw/master/picture/%E5%9F%BA%E4%BA%8EUDP%E7%9A%84Socket.jpg)

**服务端如何管理这些连接和资源？**

* 父进程使用子进程来管理连接和资源，子进程在完成连接和数据通信后告诉父进程进行回收
* 线程池 + 连接池，每个线程管理一个socket，连接池实现socket复用
* IO多路复用，一个线程维护多个socket，如Java NIO、Netty的网络模型

## 连接池实现

写得真不错，清晰易懂 <https://yusank.space/posts/conn-pool/>

## 参考

[TCP/IP参考1](https://blog.csdn.net/jungle_hello/article/details/51465119)

[TCP/IP参考2](https://blog.csdn.net/qq_18425655/article/details/52163228)

[极客时间 - 趣谈网络协议](/note/wang-luo.md)

[TCP 的那些事儿](https://coolshell.cn/articles/11564.html)

[面试必备！TCP协议经典十五连问！](https://juejin.cn/post/6983639186146328607)

[小林coding - 图解网络](https://xiaolincoding.com/network/)

<https://coolshell.cn/articles/11564.html>

<https://coolshell.cn/articles/11609.html>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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, and the optional `goal` query parameter:

```
GET https://nixum.gitbook.io/note/wang-luo.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
