> 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).

# 网络

计算机网络、HTTP协议、TCP协议相关

\[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>
