mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-30 07:33:45 +00:00
Merge pull request #1492 from CyC2018/update-network-notes
Update 网络相关笔记
This commit is contained in:
+153
-4
@@ -64,6 +64,8 @@
|
||||
* [A3 跨域](#a3-跨域)
|
||||
* [A4 TLS 握手](#a4-tls-握手)
|
||||
* [A5 XMLHttpRequest 发送 POST](#a5-xmlhttprequest-发送-post)
|
||||
* [A6 SSE 推送与断线重连](#a6-sse-推送与断线重连)
|
||||
* [A7 证书链与密码套件](#a7-证书链与密码套件)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -368,6 +370,11 @@ Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据
|
||||
|
||||
Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。
|
||||
|
||||
Cookie 与 Web Storage 的区别:
|
||||
|
||||
- Cookie 每次请求都会自动携带,单个大小约 4 KB(RFC 6265 要求浏览器至少支持 4096 字节),可以用 HttpOnly 禁止脚本读取;
|
||||
- localStorage 和 sessionStorage 不随请求发送,只能由脚本读写,每个源各约 5 MB;localStorage 一直保存到被删除,sessionStorage 按标签页隔离,关闭标签页就被清除。
|
||||
|
||||
#### 1. 用途
|
||||
|
||||
- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息);
|
||||
@@ -442,7 +449,7 @@ SameSite 属性控制跨站请求是否携带 Cookie,用于防范跨站请求
|
||||
|
||||
除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。
|
||||
|
||||
Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。
|
||||
Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。集群中的多台服务器如何共享 Session,见 [集群下的 Session 管理](集群.md#二集群下的-session-管理)。
|
||||
|
||||
使用 Session 维护用户登录状态的过程如下:
|
||||
|
||||
@@ -493,7 +500,7 @@ JWT 与 Session 的区别:
|
||||
|
||||
令牌在浏览器中的存放位置:放在 localStorage 中,页面上的任何脚本都能读取,一旦存在 XSS 漏洞,令牌就会被盗走;放在 HttpOnly 的 Cookie 中,脚本读不到,但浏览器会自动携带 Cookie,需要防范 [跨站请求伪造](攻击技术.md#二跨站请求伪造)。
|
||||
|
||||
需要注意的是,服务器验证时要固定使用的签名算法,不能接受头部中 alg 为 none 的令牌;HMAC 的密钥要随机生成,长度不少于 256 位。
|
||||
需要注意的是,服务器验证时要固定使用的签名算法,不能接受头部中 alg 为 none 的令牌;HMAC 的密钥要随机生成,长度不少于 256 位。签名也不能防止窃听和重放,令牌被截获后,在过期前任何人都能拿它冒充用户,所以必须通过 HTTPS 传输;签名只证明令牌是服务器签发的,服务器仍要检查令牌中的用户是否有权访问所请求的资源。
|
||||
|
||||
JWT 只是令牌的格式,而 OAuth 2.0 是授权框架,规定第三方应用如何获得访问令牌,这个访问令牌可以用 JWT 表示。
|
||||
|
||||
@@ -808,14 +815,48 @@ Sec-WebSocket-Accept: N5dfgp7eKvv+Po1vS+eAAHg5F1I=
|
||||
|
||||
Sec-WebSocket-Key 是客户端随机生成的 16 字节的 Base64 编码。服务器在它后面拼上协议规定的 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11,计算 SHA-1 后再做 Base64 编码,作为 Sec-WebSocket-Accept 返回,证明自己确实支持 WebSocket,而不是把请求当作普通的 HTTP 请求处理。之后连接上传输的不再是 HTTP 报文,而是 WebSocket 帧,帧头最短只有 2 字节,不用像 HTTP 那样每次都携带首部。
|
||||
|
||||
WebSocket 是基于 TCP 的应用层协议。帧头中有区分文本、二进制、关闭、Ping 和 Pong 的操作码,以及载荷的长度:不超过 125 字节时直接写在第 2 个字节中,更长时再用 2 或 8 个字节表示。
|
||||
|
||||
客户端发出的帧必须用 4 字节的随机掩码与数据逐字节异或,防止中间的代理把精心构造的数据当作 HTTP 请求,从而污染缓存。例如客户端发送文本 hello 的帧共 11 字节:81 85 两个字节、4 字节的掩码和 5 字节掩码后的数据;服务器发送同样的消息只需要 7 字节。
|
||||
|
||||
需要注意的是,长时间没有数据时,中间的代理或 NAT 可能会断开空闲的连接,所以通常定时发送 Ping 帧,对方回复 Pong 帧,以此保活并发现已经断开的连接。
|
||||
|
||||
SSE(Server-Sent Events)是另一种服务器推送方式:它是一个普通的 HTTP 响应,服务器不断地往响应中写入文本事件,只能由服务器发给客户端,浏览器断开后会自动重连。只需要服务器单向推送时(例如大模型逐字输出),SSE 更简单;客户端和服务器都需要随时发消息时(例如聊天、协同编辑),使用 WebSocket。
|
||||
SSE(Server-Sent Events)是另一种服务器推送方式:它是一个普通的 HTTP 响应,Content-Type 为 text/event-stream,没有 Content-Length,服务器不断地往响应中写入文本事件,每条事件以空行结束,只能由服务器发给客户端。例如推送订单 1024 的状态(curl 实测,见[附录 A6](#a6-sse-推送与断线重连)):
|
||||
|
||||
```text
|
||||
GET /orders/1024/events HTTP/1.1
|
||||
Host: 127.0.0.1:8781
|
||||
Accept: text/event-stream
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/event-stream
|
||||
Cache-Control: no-cache
|
||||
|
||||
id: 1
|
||||
event: status
|
||||
data: {"order": 1024, "status": "已下单"}
|
||||
|
||||
id: 2
|
||||
event: status
|
||||
data: {"order": 1024, "status": "已发货"}
|
||||
|
||||
```
|
||||
|
||||
浏览器的 EventSource 在连接断开后会自动重连,并在 Last-Event-ID 首部中带上最后收到的事件编号,服务器据此只发送之后的事件;重连的间隔可以用 retry 字段设置,服务器返回 204 时浏览器停止重连。心跳、续传和代理缓冲等实现细节见 [流式输出](大模型应用.md#流式输出)。
|
||||
|
||||
SSE 的局限:
|
||||
|
||||
- EventSource 只能发送 GET 请求,不能自定义首部和请求体。需要在 Authorization 首部中携带令牌,或者用 POST 提交一段对话时,通常改用 fetch 读取响应流,自己按空行切分事件,断线重连也要自己实现;
|
||||
- 在 HTTP/1.1 下,浏览器对同一个域名最多同时打开 6 个连接,每个 SSE 都一直占用一个,打开多个标签页时很快就会用完;HTTP/2 在一个连接上复用多个流,没有这个问题。
|
||||
|
||||
只需要服务器单向推送时(例如大模型逐字输出),SSE 更简单;客户端和服务器都需要随时发消息时(例如聊天、协同编辑),使用 WebSocket。
|
||||
|
||||
### 跨域
|
||||
|
||||
同源指的是协议、域名和端口都相同。浏览器的同源策略限制网页中的脚本读取其它源的响应,例如 https://shop.example.com 页面中的脚本请求 https://api.example.com/orders,请求能够发出去,但浏览器默认不把响应交给脚本。
|
||||
|
||||
同源策略保护的是用户浏览器中的身份和网络位置:没有它,用户打开一个恶意网页,网页中的脚本就能以用户的身份(带着 Cookie)请求其它网站,或者访问用户所在内网中的服务,并读取响应的内容。
|
||||
|
||||
跨域资源共享(Cross-Origin Resource Sharing, CORS)指的是服务器通过响应首部声明允许哪些源读取响应:
|
||||
|
||||
```text
|
||||
@@ -848,6 +889,12 @@ Access-Control-Max-Age 表示预检结果可以缓存的秒数,期间相同的
|
||||
|
||||
需要注意的是,CORS 只决定浏览器是否把响应交给脚本,并不是安全措施:服务器仍然执行了请求,curl 这类不经过浏览器的工具也完全不受限制,权限要靠认证和 [CSRF 防护](攻击技术.md#二跨站请求伪造) 保证。另一种解决跨域的办法是同源的反向代理,例如由 Nginx 把 https://shop.example.com/api 转发给后端,浏览器看到的就是同源的请求。
|
||||
|
||||
其它做法:
|
||||
|
||||
- JSONP:利用 script 标签加载脚本不受同源策略限制,让服务器返回一段调用回调函数的脚本。只能发送 GET 请求,并且要完全信任对方返回的脚本,已经被 CORS 取代;
|
||||
- WebSocket:握手请求不受同源策略限制,浏览器会带上 Origin 首部,服务器需要自己检查来源;
|
||||
- postMessage:不同源的页面之间(例如页面和其中嵌入的 iframe)通过它传递消息,接收方要检查消息的来源。
|
||||
|
||||
## 六、HTTPS
|
||||
|
||||
HTTP 有以下安全性问题:
|
||||
@@ -856,7 +903,7 @@ HTTP 有以下安全性问题:
|
||||
- 不验证通信方的身份,通信方的身份有可能遭遇伪装;
|
||||
- 无法证明报文的完整性,报文有可能遭篡改。
|
||||
|
||||
HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。
|
||||
HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信。HTTPS 的默认端口是 443,HTTP 是 80。
|
||||
|
||||
需要注意的是,SSL 已经被 TLS(Transport Layer Security)取代,SSL 3.0 在 2015 年由 RFC 7568 废弃,现在 HTTPS 实际使用的是 TLS,SSL 只是沿用的叫法。
|
||||
|
||||
@@ -897,6 +944,15 @@ HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)
|
||||
|
||||
需要注意的是,TLS 1.3 不再用公钥加密传输对称密钥,而是通过 ECDHE 密钥交换协商出会话密钥,非对称密钥只用于签名认证。
|
||||
|
||||
TLS 把用到的一组算法称为密码套件,例如访问 www.baidu.com 时协商出的 TLS 1.2 套件 ECDHE-RSA-AES128-GCM-SHA256(LibreSSL 3.3.6 实测,见[附录 A7](#a7-证书链与密码套件)):
|
||||
|
||||
- ECDHE:密钥交换,用椭圆曲线算法临时协商出会话密钥;
|
||||
- RSA:身份认证,服务器用证书的 RSA 私钥对自己发送的 ECDHE 参数签名;
|
||||
- AES128-GCM:对称加密,用 128 位的会话密钥加密数据,GCM 模式同时生成认证标签,保证完整性;
|
||||
- SHA256:由协商结果推导会话密钥时使用的哈希算法。
|
||||
|
||||
TLS 1.3 的套件名称只包含后两项,例如访问 www.jd.com 时为 TLS_AES_256_GCM_SHA384,密钥交换(X25519)和签名算法单独协商。另一种常用的对称算法是 ChaCha20-Poly1305,在没有 AES 硬件加速的设备上更快;DES、3DES 和 RC4 已经不安全,TLS 1.3 不再支持。
|
||||
|
||||
<div align="center"> <img src="pics/https-hybrid.png" width="740px"/> </div><br>
|
||||
|
||||
### 认证
|
||||
@@ -911,6 +967,10 @@ HTTPS 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)
|
||||
|
||||
<div align="center"> <img src="pics/cert-chain.png" width="760px"/> </div><br>
|
||||
|
||||
中间人截获了连接也无法冒充服务器:直接转发服务器的证书,它没有对应的私钥,无法完成握手;换成自己的证书,又拿不到 CA 为这个域名签发的证书,验证会失败。
|
||||
|
||||
Charles、Fiddler 等抓包工具能解密 HTTPS,是因为用户手动把它们的根证书加入了信任列表,工具再为访问的每个域名临时签发证书。App 可以在代码中只信任指定的证书或公钥,称为证书固定(Certificate Pinning),这时加入根证书也无法抓包。
|
||||
|
||||
### TLS 握手
|
||||
|
||||
TLS 握手完成三件事:协商加密算法、验证服务器的证书、协商出会话密钥。目前常用的密钥交换方式是 ECDHE:双方各自临时生成一对椭圆曲线密钥,把公钥发给对方,再用自己的私钥和对方的公钥算出相同的共享密钥。会话密钥本身从不在网络上传输,即使服务器的证书私钥日后泄露,之前录下的通信也无法解密,称为前向安全。服务器还要用证书的私钥对握手内容签名,证明自己就是证书的持有者。
|
||||
@@ -986,6 +1046,8 @@ HTTP/3 改为基于 UDP 的 QUIC 协议:
|
||||
- QUIC 内置了 TLS 1.3,传输层握手和加密握手合并,新连接 1 个 RTT 后就能发送请求,恢复之前的会话时可以 0 RTT;
|
||||
- 连接用连接 ID 标识,而不是 IP 地址和端口,手机从 Wi-Fi 切换到移动网络时连接不用重建。
|
||||
|
||||
UDP 本身不保证可靠,QUIC 在它之上自己实现了确认、重传和拥塞控制,见 [在 UDP 上实现可靠传输](计算机网络%20-%20传输层.md#在-udp-上实现可靠传输)。
|
||||
|
||||
浏览器通常先用 HTTP/2 访问,从响应首部 Alt-Svc 得知服务器支持 HTTP/3 后再切换,例如 www.example.com 的响应中有 alt-svc: h3=":443"; ma=86400。
|
||||
|
||||
## 八、HTTP/1.1 新特性
|
||||
@@ -1181,6 +1243,87 @@ $ openssl s_client -connect www.example.com:443 -servername www.example.com -tls
|
||||
|
||||
POST 的首部和请求体在同一次写入中。第一个请求的首部中有 Accept-Encoding: gzip, deflate, br, zstd。
|
||||
|
||||
### A6 SSE 推送与断线重连
|
||||
|
||||
服务器用 http.server 实现,GET /orders/1024/events 依次写入 4 条事件后关闭连接,请求带有 Last-Event-ID 时只发送它之后的事件:
|
||||
|
||||
```python
|
||||
last = int(self.headers.get("Last-Event-ID", 0))
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream")
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
self.end_headers() # 没有 Content-Length,连接保持打开
|
||||
for i in range(last + 1, len(EVENTS) + 1):
|
||||
self.wfile.write(f"id: {i}\nevent: status\ndata: {{\"order\": 1024, \"status\": \"{EVENTS[i-1]}\"}}\n\n".encode())
|
||||
self.wfile.flush(); time.sleep(0.5)
|
||||
```
|
||||
|
||||
```text
|
||||
$ curl -v -N -H "Accept: text/event-stream" http://127.0.0.1:8781/orders/1024/events
|
||||
> GET /orders/1024/events HTTP/1.1
|
||||
> Host: 127.0.0.1:8781
|
||||
> Accept: text/event-stream
|
||||
< HTTP/1.1 200 OK
|
||||
< Content-Type: text/event-stream
|
||||
< Cache-Control: no-cache
|
||||
id: 1
|
||||
event: status
|
||||
data: {"order": 1024, "status": "已下单"}
|
||||
...
|
||||
id: 4
|
||||
event: status
|
||||
data: {"order": 1024, "status": "已签收"}
|
||||
|
||||
$ curl -N -H "Accept: text/event-stream" -H "Last-Event-ID: 2" http://127.0.0.1:8781/orders/1024/events
|
||||
id: 3
|
||||
...
|
||||
id: 4
|
||||
...
|
||||
```
|
||||
|
||||
再用无头模式的 Edge 打开一个使用 EventSource 的页面:第 1 次连接先发送 retry: 500,再发送事件 1、2 后断开;第 2 次连接发送之后的 2 条事件后断开;第 3 次连接返回 204。服务器的日志和页面中记录的事件:
|
||||
|
||||
```text
|
||||
第 1 次连接 Accept='text/event-stream' Last-Event-ID=None
|
||||
第 2 次连接 Accept='text/event-stream' Last-Event-ID='2'
|
||||
第 3 次连接 Accept='text/event-stream' Last-Event-ID='4'
|
||||
|
||||
4ms id=1 已下单
|
||||
4ms id=2 已发货
|
||||
4ms error readyState=0
|
||||
504ms id=3 运输中
|
||||
504ms id=4 已签收
|
||||
514ms error readyState=0
|
||||
1014ms error readyState=2
|
||||
```
|
||||
|
||||
readyState 为 0 表示正在重连,为 2 表示已经关闭、不再重连。
|
||||
|
||||
### A7 证书链与密码套件
|
||||
|
||||
openssl 为 macOS 自带的 LibreSSL 3.3.6,LibreSSL 把 TLS 1.3 的套件 TLS_AES_256_GCM_SHA384 显示为 AEAD-AES256-GCM-SHA384:
|
||||
|
||||
```text
|
||||
$ openssl s_client -connect www.baidu.com:443 -servername www.baidu.com
|
||||
depth=2 OU = GlobalSign Root CA - R3, O = GlobalSign, CN = GlobalSign
|
||||
depth=1 C = BE, O = GlobalSign nv-sa, CN = GlobalSign RSA OV SSL CA 2018
|
||||
depth=0 C = CN, ST = Beijing, L = Beijing, O = "Beijing Baidu Netcom Science Technology Co., Ltd.", CN = baidu.com
|
||||
Server Temp Key: ECDH, P-256, 256 bits
|
||||
Protocol : TLSv1.2
|
||||
Cipher : ECDHE-RSA-AES128-GCM-SHA256
|
||||
Verify return code: 0 (ok)
|
||||
|
||||
$ openssl s_client -connect www.jd.com:443 -servername www.jd.com
|
||||
depth=2 OU = GlobalSign Root CA - R3, O = GlobalSign, CN = GlobalSign
|
||||
depth=1 C = BE, O = GlobalSign nv-sa, CN = GlobalSign RSA OV SSL CA 2018
|
||||
depth=0 C = CN, ST = Beijing, L = Beijing, O = "BEIJING JINGDONG SHANGKE INFORMATION TECHNOLOGY CO., LTD.", CN = *.jd.com
|
||||
Server Temp Key: ECDH, X25519, 253 bits
|
||||
Server public key is 2048 bit
|
||||
Protocol : TLSv1.3
|
||||
Cipher : AEAD-AES256-GCM-SHA384
|
||||
Verify return code: 0 (ok)
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [RFC 7519: JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519)
|
||||
@@ -1217,6 +1360,12 @@ POST 的首部和请求体在同一次写入中。第一个请求的首部中有
|
||||
- [RFC 9114: HTTP/3](https://www.rfc-editor.org/rfc/rfc9114)
|
||||
- [RFC 9000: QUIC](https://www.rfc-editor.org/rfc/rfc9000)
|
||||
- [RFC 6455: The WebSocket Protocol](https://www.rfc-editor.org/rfc/rfc6455)
|
||||
- [WHATWG HTML: Server-sent events](https://html.spec.whatwg.org/multipage/server-sent-events.html)
|
||||
- [MDN: EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource)
|
||||
- [MDN: Storage quotas and eviction criteria](https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria)
|
||||
- [MDN: Same-origin policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy)
|
||||
- [OWASP: Pinning Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Pinning_Cheat_Sheet.html)
|
||||
- [RFC 8439: ChaCha20 and Poly1305 for IETF Protocols](https://www.rfc-editor.org/rfc/rfc8439)
|
||||
- [RFC 9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110)
|
||||
- [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111)
|
||||
- [RFC 3986: Uniform Resource Identifier (URI): Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986)
|
||||
|
||||
+259
-181
@@ -15,6 +15,10 @@
|
||||
* [epoll](#epoll)
|
||||
* [工作模式](#工作模式)
|
||||
* [应用场景](#应用场景)
|
||||
* [附录:实测](#附录实测)
|
||||
* [A1 每次调用的耗时](#a1-每次调用的耗时)
|
||||
* [A2 LT 与 ET 的读取过程](#a2-lt-与-et-的读取过程)
|
||||
* [A3 select 与 poll 示例的运行结果](#a3-select-与-poll-示例的运行结果)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -32,7 +36,7 @@ Unix 有五种 I/O 模型:
|
||||
|
||||
- 阻塞式 I/O
|
||||
- 非阻塞式 I/O
|
||||
- I/O 复用(select 和 poll)
|
||||
- I/O 复用(select、poll 和 epoll)
|
||||
- 信号驱动式 I/O(SIGIO)
|
||||
- 异步 I/O(AIO)
|
||||
|
||||
@@ -40,7 +44,7 @@ Unix 有五种 I/O 模型:
|
||||
|
||||
应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。
|
||||
|
||||
应该注意到,在阻塞的过程中,其它应用进程还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其它应用进程还可以执行,所以不消耗 CPU 时间,这种模型的 CPU 利用率会比较高。
|
||||
需要注意的是,阻塞的只是当前进程,其它应用进程还可以执行,因此阻塞不意味着整个操作系统都被阻塞。被阻塞的进程不消耗 CPU 时间,CPU 可以去执行其它进程,因此这种模型的 CPU 利用率比较高。
|
||||
|
||||
下图中,recvfrom() 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。
|
||||
|
||||
@@ -60,7 +64,7 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *
|
||||
|
||||
### I/O 复用
|
||||
|
||||
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。
|
||||
使用 select、poll 或 epoll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。
|
||||
|
||||
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。
|
||||
|
||||
@@ -97,115 +101,64 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *
|
||||
|
||||
## 二、I/O 复用
|
||||
|
||||
select/poll/epoll 都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。
|
||||
I/O 复用(I/O multiplexing)指用一个系统调用同时等待多个描述符,其中任何一个就绪就返回。Linux 上的实现依次为 select、poll 和 epoll,epoll 解决了前两者在描述符数量很多时的性能问题。
|
||||
|
||||
这里的多路复用指内核对描述符的事件通知,与 HTTP/2 在一个 TCP 连接上并发多个流不是同一个概念,见 [HTTP/2.0](HTTP.md#七http20)。
|
||||
|
||||
下面用一个例子贯穿全节:服务器维护 10000 个空闲连接,任意时刻只有 1 个连接可读。
|
||||
|
||||
### select
|
||||
|
||||
select 的函数原型如下:
|
||||
|
||||
```c
|
||||
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
|
||||
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
|
||||
```
|
||||
|
||||
select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。
|
||||
select 等待 readfds、writefds 和 exceptfds 三个集合中的描述符就绪。timeout 为 NULL 时一直阻塞,为 0 时立即返回。返回值大于 0 表示就绪的描述符个数,0 表示超时,-1 表示出错。
|
||||
|
||||
- fd_set 使用数组实现,数组大小使用 FD_SETSIZE 定义,所以只能监听少于 FD_SETSIZE 数量的描述符。有三种类型的描述符类型:readset、writeset、exceptset,分别对应读、写、异常条件的描述符集合。
|
||||
fd_set 是位图,第 n 位表示描述符 n 是否在集合中。FD_SETSIZE 在 Linux 中为 1024,因此 select 只能监听编号小于 1024 的描述符([select(2)](https://man7.org/linux/man-pages/man2/select.2.html))。
|
||||
|
||||
- timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。
|
||||
|
||||
- 成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0。
|
||||
select 返回时会修改传入的集合,只保留就绪的描述符,因此每次调用前都要重新设置集合。下面的代码监听 sock1 和 sock2 的可读事件:
|
||||
|
||||
```c
|
||||
fd_set fd_in, fd_out;
|
||||
struct timeval tv;
|
||||
|
||||
// Reset the sets
|
||||
FD_ZERO( &fd_in );
|
||||
FD_ZERO( &fd_out );
|
||||
|
||||
// Monitor sock1 for input events
|
||||
FD_SET( sock1, &fd_in );
|
||||
|
||||
// Monitor sock2 for output events
|
||||
FD_SET( sock2, &fd_out );
|
||||
|
||||
// Find out which socket has the largest numeric value as select requires it
|
||||
int largest_sock = sock1 > sock2 ? sock1 : sock2;
|
||||
|
||||
// Wait up to 10 seconds
|
||||
tv.tv_sec = 10;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
// Call the select
|
||||
int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );
|
||||
|
||||
// Check if select actually succeed
|
||||
if ( ret == -1 ) {
|
||||
// report error and abort
|
||||
}
|
||||
else if ( ret == 0 ) {
|
||||
// timeout; no event detected
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( FD_ISSET( sock1, &fd_in ) ) {
|
||||
// input event on sock1
|
||||
}
|
||||
|
||||
if ( FD_ISSET( sock2, &fd_out ) ) {
|
||||
// output event on sock2
|
||||
}
|
||||
fd_set rset;
|
||||
FD_ZERO(&rset);
|
||||
FD_SET(sock1, &rset);
|
||||
FD_SET(sock2, &rset);
|
||||
int maxfd = sock1 > sock2 ? sock1 : sock2;
|
||||
struct timeval tv = {10, 0};
|
||||
int n = select(maxfd + 1, &rset, NULL, NULL, &tv);
|
||||
if (n > 0) {
|
||||
if (FD_ISSET(sock1, &rset)) { /* sock1 可读 */ }
|
||||
if (FD_ISSET(sock2, &rset)) { /* sock2 可读 */ }
|
||||
}
|
||||
```
|
||||
|
||||
### poll
|
||||
|
||||
```c
|
||||
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
|
||||
```
|
||||
|
||||
poll 的功能与 select 类似,也是等待一组描述符中的一个成为就绪状态。
|
||||
|
||||
poll 中的描述符是 pollfd 类型的数组,pollfd 的定义如下:
|
||||
poll 的函数原型和参数结构如下:
|
||||
|
||||
```c
|
||||
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
|
||||
|
||||
struct pollfd {
|
||||
int fd; /* file descriptor */
|
||||
short events; /* requested events */
|
||||
short revents; /* returned events */
|
||||
};
|
||||
int fd; /* 描述符 */
|
||||
short events; /* 关注的事件,由应用设置 */
|
||||
short revents; /* 实际发生的事件,由内核返回 */
|
||||
};
|
||||
```
|
||||
|
||||
poll 用结构体数组代替位图,数组长度由 nfds 指定,没有 FD_SETSIZE 的限制。timeout 以毫秒为单位,负数表示一直阻塞。revents 由内核在每次调用时重新写入,应用不需要清零。下面的代码监听的事件与 select 的例子相同:
|
||||
|
||||
```c
|
||||
// The structure for two events
|
||||
struct pollfd fds[2];
|
||||
|
||||
// Monitor sock1 for input
|
||||
fds[0].fd = sock1;
|
||||
fds[0].events = POLLIN;
|
||||
|
||||
// Monitor sock2 for output
|
||||
fds[1].fd = sock2;
|
||||
fds[1].events = POLLOUT;
|
||||
|
||||
// Wait 10 seconds
|
||||
int ret = poll( fds, 2, 10000 );
|
||||
// Check if poll actually succeed
|
||||
if ( ret == -1 ) {
|
||||
// report error and abort
|
||||
}
|
||||
else if ( ret == 0 ) {
|
||||
// timeout; no event detected
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we detect the event, zero it out so we can reuse the structure
|
||||
if ( fds[0].revents & POLLIN )
|
||||
fds[0].revents = 0;
|
||||
// input event on sock1
|
||||
|
||||
if ( fds[1].revents & POLLOUT )
|
||||
fds[1].revents = 0;
|
||||
// output event on sock2
|
||||
fds[0].fd = sock1; fds[0].events = POLLIN;
|
||||
fds[1].fd = sock2; fds[1].events = POLLIN;
|
||||
int n = poll(fds, 2, 10000);
|
||||
if (n > 0) {
|
||||
if (fds[0].revents & POLLIN) { /* sock1 可读 */ }
|
||||
if (fds[1].revents & POLLIN) { /* sock2 可读 */ }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -213,138 +166,263 @@ else
|
||||
|
||||
#### 1. 功能
|
||||
|
||||
select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。
|
||||
|
||||
- select 会修改描述符,而 poll 不会;
|
||||
- select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听少于 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 没有描述符数量的限制;
|
||||
- poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。
|
||||
- 如果一个线程对某个描述符调用了 select 或者 poll,另一个线程关闭了该描述符,会导致调用结果不确定。
|
||||
- select 会修改传入的集合,poll 把结果写入 revents,events 保持不变,因此 pollfd 数组可以反复使用;
|
||||
- select 只能监听编号小于 1024 的描述符,poll 没有这个限制,但 nfds 不能超过 RLIMIT_NOFILE(Linux);
|
||||
- select 只能按读、写、异常三个集合区分事件,poll 的 events 可以直接指定 POLLIN、POLLOUT、POLLPRI 等事件。
|
||||
|
||||
#### 2. 速度
|
||||
|
||||
select 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。
|
||||
select 和 poll 每次调用都要做三件事(Linux 6.12 的 fs/select.c,do_select 与 do_poll):
|
||||
|
||||
- 把描述符集合从用户空间复制到内核;
|
||||
- 对描述符逐个调用 poll 方法检查是否就绪,在找到就绪的描述符之前,把等待项挂到每个描述符的等待队列上;
|
||||
- 返回前把等待项从所有等待队列上摘下,再把结果返回给用户空间。
|
||||
|
||||
这些工作的开销与描述符数量 n 成正比,与就绪的数量无关。返回后,应用还要遍历 n 个描述符,才能找出就绪的那一个。以 10000 个描述符为例,poll 每次复制 10000 个 8 字节的 pollfd,约 78 KiB。
|
||||
|
||||
实测结果与此一致(Linux 6.10,见[附录 A1](#a1-每次调用的耗时)):只有 1 个描述符就绪时,监听 1000 个描述符的 select 和 poll 每次调用都约 34 µs,poll 监听 1 万个约 1.6 ms、10 万个约 22 ms;epoll_wait 无论注册了多少个描述符,每次调用都约 0.21 µs。
|
||||
|
||||
<div align="center"> <img src="pics/epoll-select-call.png" width="720px"/> </div><br>
|
||||
|
||||
#### 3. 可移植性
|
||||
|
||||
几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。
|
||||
select 几乎被所有系统支持;poll 是 POSIX 标准中的接口,类 Unix 系统都支持;epoll 只能在 Linux 上使用。
|
||||
|
||||
### epoll
|
||||
|
||||
epoll 的接口由三个系统调用组成:
|
||||
|
||||
```c
|
||||
int epoll_create(int size);
|
||||
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
|
||||
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
|
||||
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
|
||||
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
|
||||
```
|
||||
|
||||
epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。
|
||||
epoll_create 创建一个 epoll 实例;epoll_ctl 对描述符执行 EPOLL_CTL_ADD、EPOLL_CTL_MOD 或 EPOLL_CTL_DEL,即注册、修改关注的事件或删除;epoll_wait 返回就绪的事件。epoll_wait 与 read 一样会让调用线程睡眠,区别在于它等待的是注册的任意一个描述符就绪,并且带有超时参数。
|
||||
|
||||
从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。
|
||||
epoll 实例在内核中是 struct eventpoll,其中有两个结构(Linux 6.12 的 fs/eventpoll.c):
|
||||
|
||||
<div align="center"> <img src="pics/epoll.png" width="640px"/> </div><br>
|
||||
- 红黑树 rbr 保存所有注册的描述符(兴趣列表),插入、查找和删除都是 O(log n);
|
||||
- 就绪链表 rdllist 保存已经就绪的描述符,追加和取出都是 O(1)。
|
||||
|
||||
epoll 仅适用于 Linux OS。
|
||||
epoll_ctl 在 EPOLL_CTL_ADD 时把描述符插入红黑树,并在它的等待队列上挂一个回调 ep_poll_callback。之后描述符变为可读,内核调用回调,把它加入就绪链表,应用不需要做任何事。epoll_wait 只检查就绪链表:链表为空就阻塞;否则把其中的事件(最多 maxevents 个)复制到用户空间。
|
||||
|
||||
epoll 比 select 和 poll 更加灵活而且没有描述符数量限制。
|
||||
<div align="center"> <img src="pics/epoll-rbtree-rdllist.png" width="720px"/> </div><br>
|
||||
|
||||
epoll 对多线程编程更加友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。
|
||||
回到前面的例子:10000 个连接只有 1 个可读时,epoll_wait 只返回这 1 个事件,每次调用约 0.21 µs,而 poll 约 1.6 ms。与 select 和 poll 相比,epoll 有三点不同:
|
||||
|
||||
- 描述符只在 epoll_ctl 时复制一次,之后每次 epoll_wait 不再复制整个集合;
|
||||
- 就绪由回调通知,内核不需要在每次调用时检查所有描述符;
|
||||
- epoll_wait 只返回就绪的事件,应用不需要遍历全部描述符。
|
||||
|
||||
红黑树用于快速定位。epoll_ctl 的 MOD 和 DEL 操作只给出描述符编号,内核需要找到对应的节点,链表只能顺序查找,时间是 O(n),而树可以保证 O(log n)。就绪链表只做追加和取出,用链表即可。
|
||||
|
||||
epoll 没有 select 的 1024 限制。实际上限取决于三个因素:进程的文件描述符上限(RLIMIT_NOFILE,即 ulimit -n,很多系统的默认软限制较小,需要调大);每个用户能注册的描述符总数 max_user_watches,默认约为可用低端内存的 4%,再除以每个注册项的成本(epoll(7));以及内存。每个注册项在 64 位内核中约占 160 字节,因此 100 万个注册约占 150 MiB 内核内存,还不包括套接字本身的缓冲区。在 16 GiB 内存的机器上,max_user_watches 的默认值估算约为 430 万。
|
||||
|
||||
使用 epoll 时还有两点需要注意:
|
||||
|
||||
- 关闭描述符不会立即从兴趣列表中移除,只有引用同一个打开文件的所有描述符都关闭后才会移除(epoll(7))。描述符编号可能被新连接复用,事件缓存中残留的旧事件就会误指向新连接,因此应在关闭前用 EPOLL_CTL_DEL 移除,并在事件缓存中标记该连接已关闭;
|
||||
- 非阻塞 connect 返回 EINPROGRESS 后,可写事件只表示连接过程结束,成功与否要用 getsockopt(SO_ERROR) 判断,结果为 0 才表示成功(connect(2))。
|
||||
|
||||
### 工作模式
|
||||
|
||||
epoll 有两种触发模式:水平触发(level-triggered, LT)和边缘触发(edge-triggered, ET)。LT 只要描述符仍然可读,每次 epoll_wait 都会报告它;ET 只在有新数据到达时报告一次。
|
||||
|
||||
#### 1. LT 模式
|
||||
|
||||
LT 是默认模式,语义与 poll 相同(epoll(7))。epoll_wait 返回描述符后,内核会把它放回就绪链表,下一次调用时再检查一次。因此应用没有读完数据,下一次 epoll_wait 还会返回它。LT 模式下,描述符可以是阻塞的,也可以是非阻塞的。
|
||||
|
||||
关注写事件要按需进行。发送缓冲区通常是可写的,如果一直关注 EPOLLOUT,每次 epoll_wait 都会立即返回。应用只在 write 返回 EAGAIN(发送缓冲区已满)时注册 EPOLLOUT,数据发完后用 EPOLL_CTL_MOD 取消。
|
||||
|
||||
#### 2. ET 模式
|
||||
|
||||
ET 减少了重复通知,但要求应用一次把数据处理完。下面的例子中,客户端一次发送 3000 字节的请求,服务端每次 read 最多读 1024 字节:
|
||||
|
||||
| 步骤 | LT | ET,每次事件只 read 一次 | ET,循环 read 到 EAGAIN |
|
||||
| --- | --- | --- | --- |
|
||||
| 3000 字节到达,epoll_wait 返回,read 读 1024 字节 | 缓冲区剩 1976 字节 | 缓冲区剩 1976 字节 | 缓冲区剩 1976 字节 |
|
||||
| 下一次 epoll_wait | 立即返回,仍然可读 | 阻塞,没有新数据就不会产生事件 | 不会走到这一步,先继续读 |
|
||||
| 之后的读取 | 每次返回后读 1024、952 字节 | 无,1976 字节一直留在缓冲区 | 继续读 1024、952 字节,再 read 返回 EAGAIN |
|
||||
| 结果 | 全部读完,之后阻塞 | 客户端等待响应,连接停住 | 全部读完,然后回到 epoll_wait |
|
||||
|
||||
表中的过程与实测一致(Linux 6.10,见[附录 A2](#a2-lt-与-et-的读取过程))。
|
||||
|
||||
ET 模式下,描述符必须是非阻塞的,并且每次事件都要读到 EAGAIN(epoll(7))。EAGAIN 表示缓冲区已经空了,这时才能回到 epoll_wait。如果描述符是阻塞的,最后一次 read 在缓冲区为空时会一直等待,整个事件循环停在这里,其它描述符也得不到处理。下面是伪代码:
|
||||
|
||||
```c
|
||||
// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.
|
||||
// The function argument is ignored (it was not before, but now it is), so put your favorite number here
|
||||
int pollingfd = epoll_create( 0xCAFE );
|
||||
|
||||
if ( pollingfd < 0 ) {
|
||||
// report error
|
||||
/* 伪代码:ET 模式下处理一个可读事件 */
|
||||
for (;;) {
|
||||
n = read(fd, buf, sizeof(buf));
|
||||
if (n > 0) {
|
||||
handle(buf, n); /* 处理读到的数据 */
|
||||
continue;
|
||||
}
|
||||
if (n == 0) { /* 对端关闭 */
|
||||
close_connection(fd);
|
||||
break;
|
||||
}
|
||||
if (errno == EAGAIN) { /* 缓冲区已空,回到 epoll_wait */
|
||||
break;
|
||||
}
|
||||
close_connection(fd); /* 其它错误 */
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
// Initialize the epoll structure in case more members are added in future
|
||||
struct epoll_event ev = { 0 };
|
||||
ET 还可能造成饥饿:如果某个连接的数据源源不断,循环读取会长时间占用线程,其它连接得不到处理。常见的做法是每次事件只读固定次数,剩下的数据留给下一轮处理;epoll(7) 建议用就绪链表在描述符之间轮流处理。如果读取后没有读到 EAGAIN,剩下的数据不会再产生事件,连接会一直停住,这种情况更接近永久饿死。
|
||||
|
||||
// Associate the connection class instance with the event. You can associate anything
|
||||
// you want, epoll does not use this information. We store a connection class pointer, pConnection1
|
||||
ev.data.ptr = pConnection1;
|
||||
EPOLLONESHOT 可以与 ET 一起使用:描述符触发一次事件后会被禁用,直到用 EPOLL_CTL_MOD 重新启用。多个线程处理同一个连接时,可以避免两个线程同时处理该描述符的事件(epoll_ctl(2))。
|
||||
|
||||
// Monitor for input, and do not automatically rearm the descriptor after the event
|
||||
ev.events = EPOLLIN | EPOLLONESHOT;
|
||||
// Add the descriptor into the monitoring list. We can do it even if another thread is
|
||||
// waiting in epoll_wait - the descriptor will be properly added
|
||||
if ( epoll_ctl( pollingfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 ) {
|
||||
// report error
|
||||
### 应用场景
|
||||
|
||||
#### 1. select 应用场景
|
||||
|
||||
select 的 timeout 精度为微秒(struct timeval),poll 和 epoll_wait 为毫秒。需要更高精度时可以使用 pselect、ppoll,或 Linux 5.11 引入的 epoll_pwait2,它们的超时参数精确到纳秒。select 可移植性最好,几乎所有平台都支持,适合描述符编号较小、数量很少的跨平台程序。
|
||||
|
||||
#### 2. poll 应用场景
|
||||
|
||||
poll 没有 1024 的限制,在平台支持且描述符数量不多时可以代替 select。poll 每次调用仍要遍历全部描述符,描述符数量很多时应使用 epoll。
|
||||
|
||||
#### 3. epoll 应用场景
|
||||
|
||||
epoll 只能在 Linux 上使用,适合同时监听大量描述符、且多数是长连接的服务器。描述符只有几十、几百个时,select 和 poll 的开销很小,监听 100 个描述符的 poll 每次约 3 µs(见[附录 A1](#a1-每次调用的耗时)),epoll 的优势不明显。
|
||||
|
||||
如果描述符的状态变化很频繁、每次活跃的时间很短,epoll 的收益会被抵消:兴趣列表保存在内核中,每次修改都要一次 epoll_ctl 系统调用。
|
||||
|
||||
Reactor 模式(反应器模式)把事件循环和事件处理分开:线程通过 epoll 等待事件,就绪后调用对应的处理函数,处理函数不能长时间阻塞。Redis 的文件事件处理器基于 Reactor 模式,事件循环在主线程中运行,在 Linux 上封装 epoll,注册时只使用 EPOLLIN 和 EPOLLOUT,即水平触发,见 [Redis](Redis.md#十事件)。多路复用让一个线程同时处理多个连接,因此单线程的 Redis 也能处理大量连接;命令仍由主线程逐个执行。
|
||||
|
||||
Netty 的 NioEventLoop 基于 Java NIO 的 Selector,在 Linux 上由 JDK 的 epoll 实现,注册时不使用 EPOLLET,因此是水平触发(OpenJDK 的 EPoll 类)。Netty 的原生 epoll 传输(io.netty.channel.epoll)则使用 EPOLLET。Selector 的用法见 [Java IO](Java%20IO.md#选择器)。
|
||||
|
||||
多线程 Reactor 中,主 Reactor 通常只注册监听套接字,负责 accept;新连接按轮询或哈希分配给某个子 Reactor,由该子 Reactor 的 epoll 注册并处理读写。一个连接的描述符只注册到一个 epoll 实例中,读写都由同一个线程完成,连接的状态不需要加锁。Netty 的 BossGroup 与 WorkerGroup 就是这种主从结构。
|
||||
|
||||
如果多个线程的 epoll 同时监听同一个监听套接字,一个新连接会唤醒所有等待的线程,这称为惊群(thundering herd)。EPOLLEXCLUSIVE(Linux 4.5 起)让内核只唤醒其中的一个或部分等待者;SO_REUSEPORT(Linux 3.9 起)让每个线程绑定各自的监听套接字,由内核把新连接分配给其中一个(epoll_ctl(2)、socket(7))。
|
||||
|
||||
Proactor 模式与 Reactor 不同:Reactor 通知描述符已经就绪,由应用执行读写;Proactor 由内核执行 I/O,完成后通知应用,例如 Windows 的 IOCP。Linux 上的 epoll 属于 Reactor。
|
||||
|
||||
io_uring 与 epoll 的思路不同:epoll 只通知描述符就绪,之后仍要调用 read、write;io_uring 由应用把读写等操作放入提交队列(submission queue, SQ),内核完成后把结果放入完成队列(completion queue, CQ),两个队列都是用户空间与内核共享的环形缓冲区(io_uring_setup(2))。SQPOLL 模式由内核线程轮询提交队列,省去提交时的系统调用;内核线程空闲一段时间后会停止轮询,等待应用唤醒,负载不高时并不一定比 epoll 快。
|
||||
|
||||
Go 的运行时网络轮询器在 Linux 上也基于 epoll,并且使用 EPOLLET(Go 源码 runtime/netpoll_epoll.go)。协程在 I/O 未就绪时挂起,就绪后被唤醒,因此业务代码可以写成同步的形式,epoll 只是运行时底层的事件通知。
|
||||
|
||||
## 附录:实测
|
||||
|
||||
以下实验在 Docker Desktop 的 Linux 虚拟机中运行:Ubuntu 24.04 容器,内核 6.10.14-linuxkit(aarch64,宿主为 Apple M1 Pro),gcc 13.3.0,编译选项 -O2。
|
||||
|
||||
### A1 每次调用的耗时
|
||||
|
||||
方法:n 个描述符中,n - 1 个是各自独立的 UDP 套接字(没有数据,不可读),1 个是管道的读端(已写入 1 字节,可读)。超时为 0,每次调用返回 1 个就绪描述符。select 和 poll 每次调用都传入全部描述符;epoll 只在开始时用 epoll_ctl 注册一次,之后每次调用 epoll_wait 不传入描述符。迭代次数按每次测量至少 50 ms 校准,每个数量重复 5 次取中位数,整个程序运行两次。容器的描述符上限设为 200000(docker run --ulimit nofile=200000:200000)。
|
||||
|
||||
核心代码(省略了计时和错误处理):
|
||||
|
||||
```c
|
||||
struct pollfd *pfd = malloc(sizeof(*pfd) * n);
|
||||
for (int i = 0; i < n; i++) { pfd[i].fd = fds[i]; pfd[i].events = POLLIN; }
|
||||
for (int k = 0; k < iters; k++) poll(pfd, n, 0); /* 返回 1 */
|
||||
|
||||
int ep = epoll_create1(0);
|
||||
for (int i = 0; i < n; i++) {
|
||||
struct epoll_event ev = {.events = EPOLLIN, .data.fd = fds[i]};
|
||||
epoll_ctl(ep, EPOLL_CTL_ADD, fds[i], &ev); /* 只注册一次 */
|
||||
}
|
||||
for (int k = 0; k < iters; k++) epoll_wait(ep, &out, 1, 0); /* 返回 1 */
|
||||
```
|
||||
|
||||
// Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen)
|
||||
struct epoll_event pevents[ 20 ];
|
||||
select 的 FD_SETSIZE 为 1024,编号超过的描述符不能用 select,因此 n = 10000 和 100000 时没有 select 的结果。
|
||||
|
||||
// Wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array
|
||||
int ready = epoll_wait( pollingfd, pevents, 20, 10000 );
|
||||
// Check if epoll actually succeed
|
||||
if ( ready == -1 ) {
|
||||
// report error and abort
|
||||
}
|
||||
else if ( ready == 0 ) {
|
||||
// timeout; no event detected
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check if any events detected
|
||||
for ( int i = 0; i < ready; i++ )
|
||||
{
|
||||
if ( pevents[i].events & EPOLLIN )
|
||||
{
|
||||
// Get back our connection pointer
|
||||
Connection * c = (Connection*) pevents[i].data.ptr;
|
||||
c->handleReadEvent();
|
||||
}
|
||||
结果(单位 µs,每格为第一次运行 / 第二次运行):
|
||||
|
||||
| n | select | poll | epoll |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 0.258 / 0.258 | 0.224 / 0.257 | 0.211 / 0.213 |
|
||||
| 10 | 0.474 / 0.471 | 0.431 / 0.436 | 0.218 / 0.214 |
|
||||
| 100 | 3.110 / 3.069 | 3.198 / 3.164 | 0.212 / 0.213 |
|
||||
| 1000 | 33.894 / 33.867 | 34.078 / 35.432 | 0.217 / 0.212 |
|
||||
| 10000 | - / - | 1681.729 / 1548.770 | 0.211 / 0.211 |
|
||||
| 100000 | - / - | 22946.635 / 22109.146 | 0.211 / 0.211 |
|
||||
|
||||
select 和 poll 的耗时随描述符数量增长,1000 个以内每个描述符约 0.03 µs,更多时每个约 0.16~0.23 µs;epoll 与描述符数量无关。
|
||||
|
||||
### A2 LT 与 ET 的读取过程
|
||||
|
||||
方法:用 socketpair 建立一对 UNIX 域流套接字,读端设为非阻塞并注册到 epoll;对端一次写入 3000 字节,每次 read 最多 1024 字节,epoll_wait 的超时为 100 ms。
|
||||
|
||||
核心代码:
|
||||
|
||||
```c
|
||||
struct epoll_event ev = {.events = EPOLLIN | extra, .data.fd = sv[0]}; /* extra 为 0 或 EPOLLET */
|
||||
epoll_ctl(ep, EPOLL_CTL_ADD, sv[0], &ev);
|
||||
write(sv[1], buf, 3000);
|
||||
for (int round = 1; round <= 4; round++) {
|
||||
int n = epoll_wait(ep, &out, 1, 100);
|
||||
if (n == 0) break;
|
||||
for (;;) {
|
||||
ssize_t r = read(sv[0], buf, 1024);
|
||||
if (r < 0 || !loop) break; /* loop 为 0 时每次事件只读一次 */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行结果:
|
||||
|
||||
### 工作模式
|
||||
```text
|
||||
== LT,每次事件 read 一次
|
||||
epoll_wait #1 -> 1
|
||||
read -> 1024
|
||||
epoll_wait #2 -> 1
|
||||
read -> 1024
|
||||
epoll_wait #3 -> 1
|
||||
read -> 952
|
||||
epoll_wait #4 -> 0(100 ms 超时)
|
||||
结束时缓冲区剩余 0 字节
|
||||
== ET,每次事件 read 一次
|
||||
epoll_wait #1 -> 1
|
||||
read -> 1024
|
||||
epoll_wait #2 -> 0(100 ms 超时)
|
||||
结束时缓冲区剩余 1976 字节
|
||||
== ET,循环 read 到 EAGAIN
|
||||
epoll_wait #1 -> 1
|
||||
read -> 1024
|
||||
read -> 1024
|
||||
read -> 952
|
||||
read -> -1 EAGAIN
|
||||
epoll_wait #2 -> 0(100 ms 超时)
|
||||
结束时缓冲区剩余 0 字节
|
||||
```
|
||||
|
||||
epoll 的描述符事件有两种触发模式:LT(level trigger)和 ET(edge trigger)。
|
||||
### A3 select 与 poll 示例的运行结果
|
||||
|
||||
#### 1. LT 模式
|
||||
用两个管道的读端模拟 sock1 和 sock2,只向 sock2 写入 1 字节,然后运行正文中的 select 与 poll 代码(加上输出语句):
|
||||
|
||||
当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
|
||||
```c
|
||||
int p1[2], p2[2];
|
||||
pipe(p1); pipe(p2);
|
||||
int sock1 = p1[0], sock2 = p2[0];
|
||||
write(p2[1], "x", 1);
|
||||
```
|
||||
|
||||
#### 2. ET 模式
|
||||
运行结果:
|
||||
|
||||
和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。
|
||||
|
||||
很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
|
||||
|
||||
### 应用场景
|
||||
|
||||
很容易产生一种错觉认为只要用 epoll 就可以了,select 和 poll 都已经过时了,其实它们都有各自的使用场景。
|
||||
|
||||
#### 1. select 应用场景
|
||||
|
||||
select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。
|
||||
|
||||
select 可移植性更好,几乎被所有主流平台所支持。
|
||||
|
||||
#### 2. poll 应用场景
|
||||
|
||||
poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。
|
||||
|
||||
#### 3. epoll 应用场景
|
||||
|
||||
只需要运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。
|
||||
|
||||
需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。
|
||||
|
||||
需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试。
|
||||
```text
|
||||
select 返回 1
|
||||
select: sock2 可读
|
||||
poll 返回 1
|
||||
poll: sock2 可读
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- Stevens W R, Fenner B, Rudoff A M. UNIX network programming[M]. Addison-Wesley Professional, 2004.
|
||||
- http://man7.org/linux/man-pages/man2/select.2.html
|
||||
- http://man7.org/linux/man-pages/man2/poll.2.html
|
||||
- [select(2) — Linux manual page](https://man7.org/linux/man-pages/man2/select.2.html)
|
||||
- [poll(2) — Linux manual page](https://man7.org/linux/man-pages/man2/poll.2.html)
|
||||
- [epoll(7) — Linux manual page](https://man7.org/linux/man-pages/man7/epoll.7.html)
|
||||
- [epoll_ctl(2) — Linux manual page](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html)
|
||||
- [epoll_wait(2) — Linux manual page](https://man7.org/linux/man-pages/man2/epoll_wait.2.html)
|
||||
- [io_uring_setup(2) — Linux manual page](https://man7.org/linux/man-pages/man2/io_uring_setup.2.html)
|
||||
- [connect(2) — Linux manual page](https://man7.org/linux/man-pages/man2/connect.2.html)
|
||||
- [socket(7) — Linux manual page](https://man7.org/linux/man-pages/man7/socket.7.html)
|
||||
- [Linux 6.12 fs/eventpoll.c](https://github.com/torvalds/linux/blob/v6.12/fs/eventpoll.c)、[fs/select.c](https://github.com/torvalds/linux/blob/v6.12/fs/select.c)
|
||||
- [Redis 7.4.1 src/ae_epoll.c](https://github.com/redis/redis/blob/7.4.1/src/ae_epoll.c)
|
||||
- [Netty 4.1 AbstractEpollChannel](https://github.com/netty/netty/blob/4.1/transport-classes-epoll/src/main/java/io/netty/channel/epoll/AbstractEpollChannel.java)
|
||||
- [Go runtime netpoll_epoll.go](https://github.com/golang/go/blob/master/src/runtime/netpoll_epoll.go)
|
||||
- [OpenJDK EPoll.java](https://github.com/openjdk/jdk/blob/master/src/java.base/linux/classes/sun/nio/ch/EPoll.java)
|
||||
- [Boost application performance using asynchronous I/O](https://www.ibm.com/developerworks/linux/library/l-async/)
|
||||
- [Synchronous and Asynchronous I/O](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365683(v=vs.85).aspx)
|
||||
- [Linux IO 模式及 select、poll、epoll 详解](https://segmentfault.com/a/1190000003063859)
|
||||
- [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html)
|
||||
- [select / poll / epoll: practical difference for system architects](http://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/)
|
||||
- [Browse the source code of userspace/glibc/sysdeps/unix/sysv/linux/ online](https://code.woboq.org/userspace/glibc/sysdeps/unix/sysv/linux/)
|
||||
- [epoll(7) — Linux manual page](https://man7.org/linux/man-pages/man7/epoll.7.html)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 124 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
+232
-10
@@ -2,10 +2,13 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [计算机网络 - 传输层](#计算机网络---传输层)
|
||||
* [UDP 和 TCP 的特点](#udp-和-tcp-的特点)
|
||||
* [选择 TCP 还是 UDP](#选择-tcp-还是-udp)
|
||||
* [在 UDP 上实现可靠传输](#在-udp-上实现可靠传输)
|
||||
* [UDP 首部格式](#udp-首部格式)
|
||||
* [TCP 首部格式](#tcp-首部格式)
|
||||
* [TCP 的三次握手](#tcp-的三次握手)
|
||||
* [三次握手的原因](#三次握手的原因)
|
||||
* [握手报文丢失](#握手报文丢失)
|
||||
* [TCP 的四次挥手](#tcp-的四次挥手)
|
||||
* [四次挥手的原因](#四次挥手的原因)
|
||||
* [TIME_WAIT](#time_wait)
|
||||
@@ -17,6 +20,11 @@
|
||||
* [1. 慢开始与拥塞避免](#1-慢开始与拥塞避免)
|
||||
* [2. 快重传与快恢复](#2-快重传与快恢复)
|
||||
* [TCP 粘包与拆包](#tcp-粘包与拆包)
|
||||
* [附录:实测](#附录实测)
|
||||
* [A1 TCP 与 UDP 发送位置更新](#a1-tcp-与-udp-发送位置更新)
|
||||
* [A2 握手报文丢失](#a2-握手报文丢失)
|
||||
* [A3 三次挥手](#a3-三次挥手)
|
||||
* [A4 最后的 ACK 丢失](#a4-最后的-ack-丢失)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -28,6 +36,40 @@
|
||||
- 用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信;
|
||||
- 传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。
|
||||
|
||||
### 选择 TCP 还是 UDP
|
||||
|
||||
TCP 的可靠和按序是有代价的:一个报文段丢失后,后面已经到达的数据也要等它重传成功,才能按顺序交给应用。
|
||||
|
||||
- 数据必须完整、晚一点到也可以的场景用 TCP,例如网页(HTTP/1.1、HTTP/2)、文件传输、SSH、数据库连接;
|
||||
- 数据过时就没有用、宁可丢掉的场景用 UDP,例如实时对战游戏的位置同步、视频通话(WebRTC 优先使用 UDP);
|
||||
- 只有一问一答的场景用 UDP 可以省去握手,例如 DNS 查询。
|
||||
|
||||
直播常用的 HLS 基于 HTTP,也就是 TCP,观众能接受几秒的延迟,换来画面完整。
|
||||
|
||||
例如游戏客户端每 50 ms 上报一次位置,在 RTT 为 50 ms、丢包率为 5% 的网络中分别用 TCP 和 UDP 发送 600 次(Linux 实测,见[附录 A1](#a1-tcp-与-udp-发送位置更新)):
|
||||
|
||||
<div align="center"> <img src="pics/tcp-udp-latency.png" width="760px"/> </div><br>
|
||||
|
||||
5 次实测中,TCP 的 600 条更新全部到达,但有 24~53 条的延迟超过 100 ms,最长 307~939 ms,到达时位置早已过时;UDP 丢失了 23~30 条,到达的延迟都不超过 44 ms,丢失的那一条由 50 ms 后的下一次更新补上。
|
||||
|
||||
### 在 UDP 上实现可靠传输
|
||||
|
||||
游戏中的位置可以丢,购买道具、释放技能这类消息却必须送达。既要 UDP 的低延迟、又有一部分数据必须可靠时,就要在应用层按 TCP 的思路自己实现:
|
||||
|
||||
- 序号:给每个数据包编号,接收方据此去重和排序;
|
||||
- 确认和重传:接收方回复确认,发送方超时没有收到确认就重传,超时时间根据测得的 RTT 计算;
|
||||
- 流量控制和拥塞控制:限制已发送但未确认的数据量,避免压垮接收方和网络。
|
||||
|
||||
与直接使用 TCP 相比,好处在于可以按需取舍:只对需要的消息重传,不相关的消息互不阻塞,重传和拥塞控制的参数也可以按业务调整。
|
||||
|
||||
HTTP/3 使用的 QUIC 就是在 UDP 上实现的可靠传输,并改进了 TCP 的几处设计:
|
||||
|
||||
- 包号只增不减,丢失的数据放进一个新编号的包中重传。收到确认时能分清确认的是哪一次发送,RTT 测得更准;TCP 重传时沿用原来的序号,无法区分;
|
||||
- 数据按流(Stream)组织,每个流按帧中的偏移量单独排序,一个流丢包不影响其它流;
|
||||
- 一个确认帧可以携带很多个已收到的区间,TCP 的选择确认(SACK)受首部长度限制,通常最多携带 3 个区间。
|
||||
|
||||
QUIC 选择在 UDP 上重新实现,而不是改进 TCP,是因为 TCP 实现在操作系统内核中,改动要等操作系统升级才能普及;防火墙、NAT 等中间设备还会检查甚至改写 TCP 首部,不认识的新选项可能被丢弃。QUIC 运行在用户态,随浏览器和 App 一起更新,并且加密了大部分首部,中间设备无法干预。QUIC 的握手和连接迁移见 [HTTP/3](HTTP.md#http3)。
|
||||
|
||||
## UDP 首部格式
|
||||
|
||||
<div align="center"> <img src="pics/udp-header.png" width="600px"/> </div><br>
|
||||
@@ -53,38 +95,60 @@
|
||||
假设 A 为客户端,B 为服务器端。
|
||||
|
||||
- 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求;
|
||||
- A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x;
|
||||
- B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y;
|
||||
- A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1;
|
||||
- B 收到 A 的确认后,连接建立。
|
||||
- A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x,进入 SYN_SENT 状态;
|
||||
- B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y,进入 SYN_RCVD 状态;
|
||||
- A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1,A 进入 ESTABLISHED 状态;
|
||||
- B 收到 A 的确认后也进入 ESTABLISHED 状态,连接建立。
|
||||
|
||||
### 三次握手的原因
|
||||
|
||||
双方都要把自己的初始序号告诉对方,并得到对方的确认:A 发送 x,B 确认;B 发送 y,A 确认。这本来是四个动作,B 的确认和 B 的 SYN 合并在了一个报文中,所以只需要三次。只握手两次时,B 的初始序号没有得到确认,B 无法知道 A 是否收到了它。
|
||||
|
||||
第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。
|
||||
|
||||
客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。有了第三次握手,客户端收到服务器对滞留请求的确认时,发现确认号不是自己期望的,就回复 RST,服务器收到后放弃这个连接,不会多打开一个连接。
|
||||
|
||||
### 握手报文丢失
|
||||
|
||||
每个握手报文都可能丢失,丢失后靠超时重传恢复(Linux 6.10 实测,见[附录 A2](#a2-握手报文丢失)):
|
||||
|
||||
- 第一次握手的 SYN 丢失:客户端收不到 SYN+ACK,超时后重传 SYN。Linux 6.5 之前按 tcp_syn_retries(默认 6)重传,间隔从 1 秒起每次翻倍,约 127 秒后放弃;6.5 起前几次重传改为固定间隔 1 秒(tcp_syn_linear_timeouts,默认 4),实测共重传 10 次,134 秒后 connect() 返回超时错误;
|
||||
- 第二次握手的 SYN+ACK 丢失:客户端以为 SYN 丢了,重传 SYN;服务器收不到第三次握手,也重传 SYN+ACK(tcp_synack_retries 默认 5 次,间隔 1、2、4、8、16 秒),任何一方的重传到达,连接都能建立;
|
||||
- 第三次握手的 ACK 丢失:客户端发出 ACK 后已经进入 ESTABLISHED,服务器仍处于 SYN_RCVD,继续重传 SYN+ACK,客户端每收到一次就再回复一次 ACK。客户端发送的数据报文段也带有 ACK,服务器收到后同样会建立连接;如果客户端一直不发数据,服务器重传 5 次后放弃,之后客户端再发数据会收到 RST。
|
||||
|
||||
## TCP 的四次挥手
|
||||
|
||||
<div align="center"> <img src="pics/tcp-teardown.png" width="640px"/> </div><br>
|
||||
|
||||
以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。
|
||||
|
||||
- A 发送连接释放报文,FIN=1;
|
||||
- B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据;
|
||||
- 当 B 不再需要连接时,发送连接释放报文,FIN=1;
|
||||
- A 发送连接释放报文,FIN=1,进入 FIN_WAIT_1 状态;
|
||||
- B 收到之后发出确认,进入 CLOSE_WAIT 状态,A 收到确认后进入 FIN_WAIT_2 状态。此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据;
|
||||
- 当 B 不再需要连接时,发送连接释放报文,FIN=1,进入 LAST_ACK 状态;
|
||||
- A 收到后发出确认,进入 TIME_WAIT 状态,等待 2 MSL(Maximum Segment Lifetime,最大报文段寿命)后释放连接;
|
||||
- B 收到 A 的确认后释放连接。
|
||||
- B 收到 A 的确认后释放连接,进入 CLOSED 状态。
|
||||
|
||||
FIN 和数据一样,丢失后由发送方超时重传。A 在 FIN_WAIT_2 中等待 B 的 FIN,如果 A 的应用已经调用了 close(),而 B 迟迟不发 FIN,Linux 会在 tcp_fin_timeout(默认 60 秒)后直接释放这个连接。
|
||||
|
||||
### 四次挥手的原因
|
||||
|
||||
客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE_WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。
|
||||
|
||||
如果 B 收到 FIN 时已经没有数据要发送,应用立即调用 close(),B 的确认和 FIN 就可以合并在一个报文段中,挥手只有三次。Linux 收到 FIN 后,确认会延迟几十毫秒再发送,应用在这期间调用 close() 就会合并(Linux 6.10 实测,见[附录 A3](#a3-三次挥手)):
|
||||
|
||||
```text
|
||||
B 读到 EOF 后立即 close() B 读到 EOF 后 1 秒再 close()
|
||||
A → B FIN A → B FIN
|
||||
B → A FIN + ACK B → A ACK 47 ms 后
|
||||
A → B ACK B → A FIN + ACK 1 秒后
|
||||
A → B ACK
|
||||
```
|
||||
|
||||
### TIME_WAIT
|
||||
|
||||
客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:
|
||||
|
||||
- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生;
|
||||
- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。A 在 TIME_WAIT 中每收到一次重传的 FIN,就再确认一次,并重新开始计时(Linux 6.10 实测,见[附录 A4](#a4-最后的-ack-丢失));
|
||||
- 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。
|
||||
|
||||
2MSL 正好覆盖一个来回:A 的确认最多在网络中存活 1 MSL,B 超时重传的 FIN 最多再存活 1 MSL。Linux 中 TIME_WAIT 固定为 60 秒(TCP_TIMEWAIT_LEN)。
|
||||
@@ -229,6 +293,159 @@ static String receive(DataInputStream in) throws IOException {
|
||||
|
||||
需要注意的是,长度字段来自网络,必须限制上限,否则对方发来一个 2 GB 的长度,服务器就会为它分配 2 GB 内存。
|
||||
|
||||
## 附录:实测
|
||||
|
||||
以下实验都在 Docker Desktop(Apple M1 Pro)中的 Ubuntu 24.04 容器里进行,内核为 6.10.14-linuxkit,客户端和服务器都在 127.0.0.1 上,用 iptables 丢弃指定的报文,用 tc netem 模拟时延和丢包,tcpdump 的时间换算为相对第一个报文的秒数。容器中的默认参数:
|
||||
|
||||
```text
|
||||
net.ipv4.tcp_syn_retries = 6
|
||||
net.ipv4.tcp_synack_retries = 5
|
||||
net.ipv4.tcp_syn_linear_timeouts = 4
|
||||
net.ipv4.tcp_fin_timeout = 60
|
||||
```
|
||||
|
||||
### A1 TCP 与 UDP 发送位置更新
|
||||
|
||||
在回环网卡上加入 25 ms 的单向时延(RTT 50 ms)和 5% 的丢包,发送方每 50 ms 发送一条 12 字节的更新(序号和发送时间),TCP 关闭 Nagle 算法,接收方用收到的时间减去发送时间得到延迟:
|
||||
|
||||
```bash
|
||||
tc qdisc add dev lo root netem delay 25ms loss 5%
|
||||
```
|
||||
|
||||
```python
|
||||
# TCP:长度固定为 12 字节,接收方按 12 字节切分
|
||||
s = socket.create_connection(("127.0.0.1", 9100))
|
||||
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
for i in range(600):
|
||||
s.sendall(struct.pack("!Id", i, time.time())); time.sleep(0.05)
|
||||
|
||||
# UDP:每条更新一个数据报
|
||||
for i in range(600):
|
||||
s.sendto(struct.pack("!Id", i, time.time()), ("127.0.0.1", 9101)); time.sleep(0.05)
|
||||
```
|
||||
|
||||
连续运行 5 次,配图使用第 3 次的数据:
|
||||
|
||||
```text
|
||||
TCP: 到达 600/600,延迟 p50 32 ms,p99 654 ms,最大 939 ms,超过 100 ms 的 52 条
|
||||
UDP: 到达 572/600,延迟 p50 33 ms,p99 37 ms,最大 40 ms,超过 100 ms 的 0 条
|
||||
TCP: 到达 600/600,延迟 p50 30 ms,p99 152 ms,最大 307 ms,超过 100 ms 的 24 条
|
||||
UDP: 到达 570/600,延迟 p50 33 ms,p99 37 ms,最大 37 ms,超过 100 ms 的 0 条
|
||||
TCP: 到达 600/600,延迟 p50 32 ms,p99 253 ms,最大 416 ms,超过 100 ms 的 43 条
|
||||
UDP: 到达 577/600,延迟 p50 33 ms,p99 37 ms,最大 44 ms,超过 100 ms 的 0 条
|
||||
TCP: 到达 600/600,延迟 p50 30 ms,p99 363 ms,最大 466 ms,超过 100 ms 的 53 条
|
||||
UDP: 到达 570/600,延迟 p50 33 ms,p99 37 ms,最大 39 ms,超过 100 ms 的 0 条
|
||||
TCP: 到达 600/600,延迟 p50 32 ms,p99 191 ms,最大 363 ms,超过 100 ms 的 35 条
|
||||
UDP: 到达 571/600,延迟 p50 33 ms,p99 37 ms,最大 42 ms,超过 100 ms 的 0 条
|
||||
```
|
||||
|
||||
### A2 握手报文丢失
|
||||
|
||||
第一次握手的 SYN 丢失:丢弃所有发往 9000 端口的 SYN,客户端调用 connect():
|
||||
|
||||
```text
|
||||
$ iptables -A INPUT -p tcp --dport 9000 --tcp-flags SYN,ACK SYN -j DROP
|
||||
0.000 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799
|
||||
1.038 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799
|
||||
2.065 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799
|
||||
3.085 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799
|
||||
4.112 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799
|
||||
5.140 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799
|
||||
7.182 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799
|
||||
11.220 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799
|
||||
19.726 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799
|
||||
36.110 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799
|
||||
68.369 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799
|
||||
connect 失败: TimeoutError(110, 'Connection timed out'),耗时 134.4 秒
|
||||
```
|
||||
|
||||
第二次握手的 SYN+ACK 丢失:前 4 秒丢弃服务器发出的 SYN+ACK(tcpdump 在丢弃之前抓包,所以被丢弃的报文也会显示):
|
||||
|
||||
```text
|
||||
$ iptables -A INPUT -p tcp --sport 9000 --tcp-flags SYN,ACK SYN,ACK -j DROP
|
||||
0.000 127.0.0.1.56260 > 127.0.0.1.9000: Flags [S] 客户端的 SYN
|
||||
0.000 127.0.0.1.9000 > 127.0.0.1.56260: Flags [S.] 被丢弃
|
||||
1.062 127.0.0.1.9000 > 127.0.0.1.56260: Flags [S.] 服务器重传 SYN+ACK,被丢弃
|
||||
1.062 127.0.0.1.56260 > 127.0.0.1.9000: Flags [S] 客户端重传 SYN
|
||||
1.062 127.0.0.1.9000 > 127.0.0.1.56260: Flags [S.] 被丢弃
|
||||
...
|
||||
4.135 127.0.0.1.56260 > 127.0.0.1.9000: Flags [S] 4 秒时删除规则后,客户端再次重传 SYN
|
||||
4.135 127.0.0.1.9000 > 127.0.0.1.56260: Flags [S.]
|
||||
4.135 127.0.0.1.56260 > 127.0.0.1.9000: Flags [.], ack 1
|
||||
client: connect() 返回,4.1 秒
|
||||
```
|
||||
|
||||
第三次握手的 ACK 丢失:丢弃客户端发往 9000 端口的、不带数据的纯 ACK(长度不超过 52 字节),客户端连接成功后 5 秒发送 hello:
|
||||
|
||||
```text
|
||||
$ iptables -A INPUT -p tcp --dport 9000 --tcp-flags SYN,ACK,FIN,RST,PSH ACK -m length --length 0:52 -j DROP
|
||||
0.000 127.0.0.1.53690 > 127.0.0.1.9000: Flags [S]
|
||||
0.000 127.0.0.1.9000 > 127.0.0.1.53690: Flags [S.]
|
||||
0.000 127.0.0.1.53690 > 127.0.0.1.9000: Flags [.], ack 1 被丢弃
|
||||
1.044 127.0.0.1.9000 > 127.0.0.1.53690: Flags [S.] 服务器重传 SYN+ACK
|
||||
1.044 127.0.0.1.53690 > 127.0.0.1.9000: Flags [.], ack 1 被丢弃
|
||||
3.093 127.0.0.1.9000 > 127.0.0.1.53690: Flags [S.]
|
||||
3.093 127.0.0.1.53690 > 127.0.0.1.9000: Flags [.], ack 1 被丢弃
|
||||
5.003 127.0.0.1.53690 > 127.0.0.1.9000: Flags [P.], seq 1:6, ack 1, length 5
|
||||
5.003 127.0.0.1.9000 > 127.0.0.1.53690: Flags [.], ack 6
|
||||
|
||||
--- 3 秒时的连接状态(ss -tan)
|
||||
ESTAB 0 0 127.0.0.1:53690 127.0.0.1:9000
|
||||
SYN-RECV 0 0 127.0.0.1:9000 127.0.0.1:53690
|
||||
client: 5 秒后发送 hello
|
||||
server: accept() 返回
|
||||
server: 收到 b'hello'
|
||||
```
|
||||
|
||||
同样丢弃纯 ACK,但客户端 70 秒后才发送数据:服务器在 1、3、7、15、31 秒重传 SYN+ACK 后放弃,客户端的数据收到 RST:
|
||||
|
||||
```text
|
||||
31.822 127.0.0.1.9000 > 127.0.0.1.42608: Flags [S.]
|
||||
31.822 127.0.0.1.42608 > 127.0.0.1.9000: Flags [.], ack 1
|
||||
70.009 127.0.0.1.42608 > 127.0.0.1.9000: Flags [P.], seq 1:6, ack 1, length 5
|
||||
70.009 127.0.0.1.9000 > 127.0.0.1.42608: Flags [R], seq 4123133762, win 0, length 0
|
||||
client: ConnectionResetError(104, 'Connection reset by peer')
|
||||
```
|
||||
|
||||
### A3 三次挥手
|
||||
|
||||
客户端发送 bye 后 0.2 秒调用 close(),服务器循环读取直到读到 EOF,然后立即或 1 秒后调用 close():
|
||||
|
||||
```text
|
||||
=== 服务器读到 EOF 后立即 close()
|
||||
0.203 127.0.0.1.44056 > 127.0.0.1.9000: Flags [F.], seq 4, ack 1
|
||||
0.204 127.0.0.1.9000 > 127.0.0.1.44056: Flags [F.], seq 1, ack 5
|
||||
0.204 127.0.0.1.44056 > 127.0.0.1.9000: Flags [.], ack 2
|
||||
=== 服务器读到 EOF 后 1 秒再 close()
|
||||
0.210 127.0.0.1.44058 > 127.0.0.1.9000: Flags [F.], seq 4, ack 1
|
||||
0.257 127.0.0.1.9000 > 127.0.0.1.44058: Flags [.], ack 5
|
||||
1.220 127.0.0.1.9000 > 127.0.0.1.44058: Flags [F.], seq 1, ack 5
|
||||
1.220 127.0.0.1.44058 > 127.0.0.1.9000: Flags [.], ack 2
|
||||
```
|
||||
|
||||
### A4 最后的 ACK 丢失
|
||||
|
||||
连接建立后丢弃客户端发往 9000 端口的纯 ACK,客户端主动关闭,服务器读到 EOF 后立即关闭,3.5 秒后删除规则。ss -tano 的 timer 一列显示 TIME_WAIT 剩余的时间:
|
||||
|
||||
```text
|
||||
0.002 127.0.0.1.50966 > 127.0.0.1.9000: Flags [F.], seq 1, ack 1
|
||||
0.002 127.0.0.1.9000 > 127.0.0.1.50966: Flags [F.], seq 1, ack 2
|
||||
0.002 127.0.0.1.50966 > 127.0.0.1.9000: Flags [.], ack 2 被丢弃
|
||||
0.210 127.0.0.1.9000 > 127.0.0.1.50966: Flags [F.], seq 1, ack 2 服务器重传 FIN
|
||||
0.210 127.0.0.1.50966 > 127.0.0.1.9000: Flags [.], ack 2 被丢弃
|
||||
0.418 ... 0.826 ... 1.650 ... 3.314 重传间隔翻倍
|
||||
6.578 127.0.0.1.9000 > 127.0.0.1.50966: Flags [F.], seq 1, ack 2
|
||||
6.578 127.0.0.1.50966 > 127.0.0.1.9000: Flags [.], ack 2 规则已删除,到达
|
||||
|
||||
--- 2 秒时的连接状态
|
||||
LAST-ACK 0 1 127.0.0.1:9000 127.0.0.1:50966 timer:(on,1.254ms,3)
|
||||
TIME-WAIT 0 0 127.0.0.1:50966 127.0.0.1:9000 timer:(timewait,59sec,0)
|
||||
--- 8.5 秒时的连接状态
|
||||
TIME-WAIT 0 0 127.0.0.1:50966 127.0.0.1:9000 timer:(timewait,58sec,0)
|
||||
```
|
||||
|
||||
8.5 秒时 TIME_WAIT 还剩 58 秒,说明 6.578 秒收到重传的 FIN 时重新开始了计时,否则只剩约 51 秒。
|
||||
|
||||
## 参考资料
|
||||
|
||||
- 谢希仁. 计算机网络 [M]. 电子工业出版社, 2017.
|
||||
@@ -239,4 +456,9 @@ static String receive(DataInputStream in) throws IOException {
|
||||
- [RFC 6928: Increasing TCP's Initial Window](https://www.rfc-editor.org/rfc/rfc6928)
|
||||
- [RFC 9438: CUBIC for Fast and Long-Distance Networks](https://www.rfc-editor.org/rfc/rfc9438)
|
||||
- [BBR: Congestion-Based Congestion Control](https://queue.acm.org/detail.cfm?id=3022184)
|
||||
- [Linux ip-sysctl: tcp_tw_reuse、ip_local_port_range](https://www.kernel.org/doc/html/latest/networking/ip-sysctl.html)
|
||||
- [Linux ip-sysctl: tcp_tw_reuse、ip_local_port_range、tcp_syn_retries、tcp_synack_retries、tcp_fin_timeout](https://www.kernel.org/doc/html/latest/networking/ip-sysctl.html)
|
||||
- [RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport](https://www.rfc-editor.org/rfc/rfc9000)
|
||||
- [RFC 9002: QUIC Loss Detection and Congestion Control](https://www.rfc-editor.org/rfc/rfc9002)
|
||||
- [Langley A, et al. The QUIC Transport Protocol: Design and Internet-Scale Deployment. SIGCOMM 2017](https://dl.acm.org/doi/10.1145/3098822.3098842)
|
||||
- [RFC 8835: Transports for WebRTC](https://www.rfc-editor.org/rfc/rfc8835)
|
||||
- [RFC 8216: HTTP Live Streaming](https://www.rfc-editor.org/rfc/rfc8216)
|
||||
|
||||
+51
-2
@@ -16,6 +16,8 @@
|
||||
* [3. DNS 解析域名](#3-dns-解析域名)
|
||||
* [4. HTTP 请求页面](#4-http-请求页面)
|
||||
* [附录:实测](#附录实测)
|
||||
* [A1 访问网页各阶段的耗时](#a1-访问网页各阶段的耗时)
|
||||
* [A2 DNS 的 TTL 与 CNAME](#a2-dns-的-ttl-与-cname)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -32,11 +34,30 @@ DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转
|
||||
|
||||
主机到本地 DNS 服务器是递归查询,主机只等最终结果;本地 DNS 服务器到其它服务器是迭代查询,由它自己一步步问下去。每一级的结果都按 TTL 缓存,所以大多数查询在本地 DNS 服务器甚至主机上就能得到结果。
|
||||
|
||||
TTL 是每条记录自带的缓存时间,单位为秒,由权威服务器给出,缓存中的记录每过一秒剩余时间减 1。例如把公共 DNS 223.5.5.5 作为本地 DNS 服务器,查询 www.jd.com(dig 实测,见[附录 A2](#a2-dns-的-ttl-与-cname)):
|
||||
|
||||
```text
|
||||
www.jd.com. 74 IN CNAME www.jd.com.gslb.qianxun.com.
|
||||
www.jd.com.gslb.qianxun.com. 11 IN CNAME www.jd.com.s.galileo.jcloud-cdn.com.
|
||||
www.jd.com.s.galileo.jcloud-cdn.com. 20 IN CNAME wwwv6.jcloudimg.com.
|
||||
wwwv6.jcloudimg.com. 15 IN A 119.147.159.135
|
||||
```
|
||||
|
||||
第二列是剩余的 TTL,5 秒后再查询,四个值都减少了 5;直接问京东的权威服务器,第一条记录的 TTL 是 120。修改记录后,各级缓存中的旧记录要等 TTL 到期才会失效,所以更换服务器的 IP 地址之前,通常先把 TTL 调小。
|
||||
|
||||
这个结果也说明了 CDN 的调度方式:www.jd.com 通过 CNAME(别名记录)把解析交给 CDN 的调度系统,调度系统根据查询来源的位置返回较近的节点地址,TTL 只有几十秒,便于随时切换节点。DNS 负载均衡见 [DNS 域名解析](集群.md#2-dns-域名解析),CDN 见 [CDN](缓存.md#三cdn)。
|
||||
|
||||
DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传从而保证可靠性。在两种情况下会使用 TCP 进行传输:
|
||||
|
||||
- 如果返回的响应超过 512 字节(DNS 规定通过 UDP 传输的报文不超过 512 字节,使用 EDNS 扩展后可以更大);
|
||||
- 区域传送(主域名服务器把整个区域的数据传送给辅助域名服务器,称为 AXFR;只传送变化部分的增量传送称为 IXFR)。
|
||||
|
||||
DNS 报文默认不加密,也不验证来源,常见的攻击有两种:DNS 劫持指的是攻击者控制了用户使用的 DNS 服务器或路由器,把域名解析到自己的地址;DNS 污染指的是在查询途中抢先返回伪造的响应,解析器会接受先到达的那个。防范的方法:
|
||||
|
||||
- DNSSEC:权威服务器对记录签名,解析器验证签名后就能发现伪造的记录;
|
||||
- DNS over HTTPS(DoH)和 DNS over TLS(DoT):加密主机与解析器之间的查询,途中无法查看和篡改;
|
||||
- HTTPS:即使域名被解析到攻击者的地址,攻击者也拿不到这个域名的证书,浏览器会报错。
|
||||
|
||||
## 文件传送协议
|
||||
|
||||
FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件:
|
||||
@@ -116,14 +137,14 @@ IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删
|
||||
|
||||
在浏览器中输入 https://www.example.com/ 并回车后,主要经过以下步骤:
|
||||
|
||||
1. 浏览器解析 URL,检查缓存,有未过期的缓存时直接使用;
|
||||
1. 浏览器解析 URL,得到协议 https、主机名 www.example.com、端口(省略时 https 为 443)和路径 /,各部分的含义见 [URL](HTTP.md#url),然后检查缓存,有未过期的缓存时直接使用;
|
||||
2. DNS 解析域名,得到 IP 地址;
|
||||
3. 与服务器进行 TCP 三次握手,HTTPS 还要进行 TLS 握手;
|
||||
4. 发送 HTTP 请求,服务器处理后返回响应;
|
||||
5. 浏览器解析 HTML,再请求其中引用的 CSS、JavaScript、图片等资源,最后渲染出页面;
|
||||
6. 连接保持一段时间供后续请求复用,空闲后关闭。
|
||||
|
||||
以下是访问该网址时各阶段的耗时(curl 实测,见[附录:实测](#附录实测)):
|
||||
以下是访问该网址时各阶段的耗时(curl 实测,见[附录 A1](#a1-访问网页各阶段的耗时)):
|
||||
|
||||
<div align="center"> <img src="pics/url-timeline.png" width="760px"/> </div><br>
|
||||
|
||||
@@ -168,6 +189,8 @@ IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删
|
||||
|
||||
## 附录:实测
|
||||
|
||||
### A1 访问网页各阶段的耗时
|
||||
|
||||
macOS 自带的 curl,网络的 RTT 约 200 ms,连续访问 3 次。各字段都是从开始到该阶段结束的累计时间:time_connect 为 TCP 连接建立,time_appconnect 为 TLS 握手完成,time_starttransfer 为收到响应的第一个字节。第 3 次的 TLS 握手明显变慢,属于网络波动,配图使用第 1 次的结果:
|
||||
|
||||
```text
|
||||
@@ -177,6 +200,28 @@ namelookup 0.004086 connect 0.209781 appconnect 0.432419 starttransfer 0.6509
|
||||
namelookup 0.001873 connect 0.216569 appconnect 1.441755 starttransfer 1.667668 total 1.668500 http 2
|
||||
```
|
||||
|
||||
### A2 DNS 的 TTL 与 CNAME
|
||||
|
||||
macOS 自带的 dig 9.10.6,先向公共 DNS 223.5.5.5 查询两次,间隔 5 秒,再不经递归直接询问京东的权威服务器 ns1.jd.com:
|
||||
|
||||
```text
|
||||
$ dig @223.5.5.5 www.jd.com +noall +answer
|
||||
www.jd.com. 74 IN CNAME www.jd.com.gslb.qianxun.com.
|
||||
www.jd.com.gslb.qianxun.com. 11 IN CNAME www.jd.com.s.galileo.jcloud-cdn.com.
|
||||
www.jd.com.s.galileo.jcloud-cdn.com. 20 IN CNAME wwwv6.jcloudimg.com.
|
||||
wwwv6.jcloudimg.com. 15 IN A 119.147.159.135
|
||||
|
||||
$ (5 秒后)dig @223.5.5.5 www.jd.com +noall +answer
|
||||
www.jd.com. 69 IN CNAME www.jd.com.gslb.qianxun.com.
|
||||
www.jd.com.gslb.qianxun.com. 6 IN CNAME www.jd.com.s.galileo.jcloud-cdn.com.
|
||||
www.jd.com.s.galileo.jcloud-cdn.com. 15 IN CNAME wwwv6.jcloudimg.com.
|
||||
wwwv6.jcloudimg.com. 10 IN A 119.147.159.135
|
||||
|
||||
$ dig @ns1.jd.com www.jd.com +noall +answer +norecurse
|
||||
www.jd.com. 120 IN CNAME www.jd.com.gslb.qianxun.com.
|
||||
www.jd.com.gslb.qianxun.com. 60 IN CNAME www.jd.com.s.galileo.jcloud-cdn.com.
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [RFC 1035: Domain Names - Implementation and Specification](https://www.rfc-editor.org/rfc/rfc1035)
|
||||
@@ -187,3 +232,7 @@ namelookup 0.001873 connect 0.216569 appconnect 1.441755 starttransfer 1.6676
|
||||
- [RFC 1939: Post Office Protocol - Version 3](https://www.rfc-editor.org/rfc/rfc1939)
|
||||
- [RFC 9051: Internet Message Access Protocol (IMAP) - Version 4rev2](https://www.rfc-editor.org/rfc/rfc9051)
|
||||
- [RFC 6891: Extension Mechanisms for DNS (EDNS(0))](https://www.rfc-editor.org/rfc/rfc6891)
|
||||
- [RFC 7871: Client Subnet in DNS Queries](https://www.rfc-editor.org/rfc/rfc7871)
|
||||
- [RFC 4033: DNS Security Introduction and Requirements](https://www.rfc-editor.org/rfc/rfc4033)
|
||||
- [RFC 8484: DNS Queries over HTTPS (DoH)](https://www.rfc-editor.org/rfc/rfc8484)
|
||||
- [RFC 7858: Specification for DNS over Transport Layer Security (TLS)](https://www.rfc-editor.org/rfc/rfc7858)
|
||||
|
||||
Reference in New Issue
Block a user