TCP 握手只有三种结局:接通、被拒、超时。这三种要先分清,再谈加密。 HTTPS 多的那一层在握手之后——代价是额外的往返,不是额外的字节。
1.2.6 讲的是「什么都在联网」,这一节讲「联网之后怎么保证不被偷看」。 1.2.8 会收束到「为什么偏偏是 TCP/IP 赢了」,给整个 1.2 节做个总结。
一、握手只有三种结局
任何一次 TCP 连接尝试,结局只有三种,分清楚这三种是排障的第一步。
| 结局 | 对端做了什么 | 本机拿到什么 |
|---|---|---|
| 接通 | 回了 SYN+ACK | 连接建立 |
| 被拒 | 回了 RST | ConnectionRefusedError |
| 超时 | 什么都没回 | TimeoutError |
第二行那个 RST 是「明确的拒绝」:包到了、机器活着、但那个端口没人听。 第三行那个「什么都没回」可能是三种情况:路径不通、被防火墙静默丢弃、或者对端太忙。 三种情况从本机看完全一样,所以只能靠「换目标试」来缩小范围。 这个区别在排障时非常关键:被拒说明方向对、服务没开;超时说明方向可能就不对。
二、为什么「被拒」也可能很慢
这一点反直觉:RST 本该是毫秒级的事,但实测常常要等一两秒。 原因是 SYN 重传:第一个 SYN 丢了(或被拦),要等超时重传,第二个才拿到 RST。
| 情形 | 耗时量级 | 原因 |
|---|---|---|
| 一次就拿到 RST | 毫秒 | 对端立即回 |
| 重传后才拿到 | 1~3 秒 | 第一个 SYN 丢了 |
| 一直被静默丢弃 | 到超时上限 | 防火墙 DROP |
第二行就是本机 Demo 里出现的那个数:连本机没监听的端口,等了 2 秒才被拒。 所以「被拒」与「超时」不能靠耗时区分, 必须看错误码——这也是写网络代码时不要「靠时间猜结果」的原因。
三、HTTPS 多出来的那几步
TCP 握手完成时,通道是通的,但内容是明文。 HTTPS 在这之后再加一段 TLS 握手:
| 协议 | 总往返数 | 多出来的是什么 |
|---|---|---|
| HTTP | 1 | —— |
| HTTPS(TLS 1.2) | 3 | 2 个往返:密钥协商 + 完成 |
| HTTPS(TLS 1.3) | 2 | 1 个往返:密钥协商与完成合并 |
这两行数字是规范规定的,不是本机实测——要数准得抓完整的握手过程。 TLS 1.3 把两次往返并成一次, 省的不是字节,是往返次数——而往返次数乘上单程时延,就是用户感知到的「打开慢」。 所以跨洋访问时,TLS 1.3 的体感差别比 TLS 1.2 明显得多: 一次往返 150 毫秒,省一次就是省 150 毫秒。
四、动手 Demo:从【单纯建立连接】到【安全建立连接】
把三种结局都量出来:本机已监听的端口(接通)、本机没监听的端口(被拒)、外网 443(接通)。 然后对比 HTTP 与 HTTPS 的往返次数——那部分是规范值,不是实测值,正文里写清楚了。
import socket, threading
import _netsim as N
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.bind(('127.0.0.1', 0))
srv.listen(4)
okport = srv.getsockname()[1]
def acc():
while True:
try:
c, _ = srv.accept()
except OSError:
return
c.close()
threading.Thread(target=acc, daemon=True).start()
tmp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tmp.bind(('127.0.0.1', 0))
deadport = tmp.getsockname()[1]
tmp.close()
for tag, host, port in (('本机已监听', '127.0.0.1', okport),
('本机没监听', '127.0.0.1', deadport),
('外网 443', 'www.python.org', 443)):
dt, st = N.tcp_connect_ms(host, port, timeout=4.0)
print('%-10s %-14s %.1f ms' % (tag, st, dt))
srv.close()
实跑输出:
--------------------------------------------------------------------
从「单纯建立连接」到「安全建立连接」
--------------------------------------------------------------------
**同一个端口,三次握手只有三种结局:接通、被拒、超时。**
**先把这三种结局都量出来,再谈加密。**
对象 结局 耗时ms
--------------------------------------
本机已监听 接通 0.4
本机没监听 被拒(RST) 2023.0
外网 443 接通 158.4
**先看一个反直觉的数**:连「本机没监听」的端口,也等了 2023 毫秒才被拒。**
**毫秒级拒绝只是理想情况**:这次的 SYN 重传了一次才拿到 RST,所以时间一下子涨到两秒。
**所以「被拒」与「超时」不能靠耗时区分,必须看错误码。**
**被拒拿到的是 `ConnectionRefusedError`,超时拿到的是 `TimeoutError`。**
**这两个错误码对应两种完全不同的现场**:
**被拒说明包到了、对面没开服务(或防火墙主动回 RST);超时说明包根本没到,或被静默丢弃。**
**方向相反,处理方式也相反。**
**上面量到的都只是 TCP 握手**,此时数据还是明文。HTTPS 比 HTTP 多的那一层在握手之后:
协议 建立连接要几个往返
-------------------------------------------
HTTP 明文 1 个(TCP)
HTTPS + TLS 1.2 3 个(TCP + 2 个 TLS)
HTTPS + TLS 1.3 2 个(TCP + 1 个 TLS)
**这两个数是规范规定的往返次数,不是本机实测**——要数准得抓完整的握手过程,本机没有抓包工具。
**「安全建立连接」的代价就是这几下额外的往返。**
**所以 TLS 1.3 把两次并成一次,不是为了省字节,是为了省往返**——
**往返本身有传播时延,跨洋一次就是一百多毫秒。**
表里「本机没监听」那一行是 2 秒级的,比「接通」慢了两个数量级——这就是 SYN 重传的代价。 外网 443 那行是几十到一百多毫秒,主要花在两段链路的传播时延上,不是协议栈的开销。 把这三个数放在一起看,能得出一个实用结论: 「慢」有三种完全不同的原因——重传、传播、排队;对应三种完全不同的处理方式。
30 秒实验
- 把
timeout从 4 秒改成 1 秒,看「本机没监听」那一行会不会变成超时。 - 试着连一个不存在的外网 IP(如
192.0.2.1), 看是超时还是被拒——这能验证「静默丢弃」与「明确拒绝」的区别。 - 查一下 TLS 1.3 的握手过程,想想为什么它能比 1.2 少一个往返。
常见误区
- 以为「被拒」一定是毫秒级。 SYN 重传会让它变成秒级, 所以不能用耗时区分「被拒」与「超时」,要看错误码。
- 以为 HTTPS 慢是因为「加密计算多」。 主要成本是额外的往返, 不是加解密运算——AES 在现代 CPU 上有硬件指令,很快。
- 以为 TLS 1.3 只是「更快一点」。 它砍掉了一整个往返,跨洋访问时这是实打实的体感差别。
自测三问(点开对答案)
1. TCP 握手的三种结局分别拿到什么错误? —— **接通**:连接建立;**被拒**:`ConnectionRefusedError`(收到 RST); **超时**:`TimeoutError`(什么都没收到)。 2. 为什么连本机没监听的端口也要等 2 秒? —— **因为第一个 SYN 丢了,触发了重传**,第二次才拿到 RST。 **说明「被拒」的耗时不可靠,不能用来判断原因。** 3. HTTPS 比 HTTP 多出来的成本主要是什么? —— **主要是额外的往返次数**(TLS 1.2 多 2 次、1.3 多 1 次)。 加解密本身有硬件加速,**不是主要瓶颈**。下一节收束整个 1.2 节:为什么最后活下来的是 TCP/IP,而不是别的协议栈。
- ☐ (本节完,下一节 1.2.8 手握金刚钻的TCP/IP)