建立 TCP 连接
上一步 DNS 交回了一个 IP 地址。但知道门牌号不等于能说上话——你还得敲门、对方还得应门、你还得说一声"好我进来了"。这一节讲的就是这三句话(三次握手)在真实访问里怎么发生、你手机上那个源端口号是谁给的,以及为什么"连接复用"是现代网页速度的最大功臣之一。
上一节你从前台那儿拿到了老李的号码 8848。号码在手,但事情还没办成——你得真的拨过去,而且得确认双方都听得见。
你拨号 → 听到"嘟"声(你发出了请求)
对方接起:"喂?"(对方应了,说明他在,而且他听得到你)
你说:"能听到吗?"对方说:"能,你呢?"你说:"我也能。"(双向都确认过了)
只有走完这几句,你才敢开口讲正事。为什么不能拨通就直接讲?因为你不知道对方是不是真接起了、也不知道他那边听不听得清。万一你噼里啪啦讲了三分钟,对方一句没听见,那三分钟全白费。
TCP 三次握手就是这几句"喂喂喂"。它的原理你在 § 3.2 已经学过了,这一节要看的是:它在一次真实的网页访问中,具体发生在哪个时刻、花多少时间、失败了会是什么表现。
上一步刚完成了什么,这一步要干什么
接力棒交到这一节手上时,浏览器手里已经有了一套完整的"拨号信息":
- 目标 IP比如
93.184.216.34,由 § 4.2 的 DNS 查询提供 - 目标端口443(https 的默认值),由 § 4.1 的 URL 解析确定
- 要用什么协议https 意味着底下走 TCP,且上面还要套一层 TLS
这一节要干的事:在你的设备和那台服务器之间,建立一条双方都认账的可靠通道。注意"双方都认账"这四个字——这是理解 TCP 连接最关键的一点,后面会反复回到它。
干完之后交给下一节的东西是:
- 一条可用的连接四个数字唯一确定它:源 IP、源端口、目标 IP、目标端口
- 双方的起始序号握手时交换的编号,之后所有数据都按它接着排
- 协商好的参数最大报文段长度、窗口大小、支持哪些可选功能
协议原理不在本节重复。三次握手为什么必须是三次、四次挥手怎么走、序号确认号怎么算、SYN Flood 攻击原理、TIME_WAIT 在等什么、拥塞控制四阶段——这些在 § 3.2 已经完整讲过。本节只做一件事:把这套机制放回真实访问的时间轴上,看它在哪一格、占多少时间、出问题时长什么样。
先把这一节的术语翻译成人话
这一节的术语大部分在 § 3.2 出现过,但有几个是"实战视角"才会碰到的新词。一起过一遍:
| 术语 | 换成大白话 | 生活里对应的东西 |
|---|---|---|
| 连接(Connection) | 说白了就是"双方各自在自己本子上记着这笔业务" | 银行合同:你一份、他一份,中间的马路可不知道你俩签过约 |
| 三次握手(Three-way Handshake) | 说白了就是"喂—能听到—我也能听到"这三句 | 打电话开头那三句确认 |
| 源端口(Source Port,你这边用的门号) | 说白了就是"我这次用哪个话机打的" | 公司总机下面的一个内线号,方便对方回话找到你 |
| 临时端口(Ephemeral Port,操作系统临时分给你的那个号) | 说白了就是"随手拿一个空号用,用完就还" | 停车场取的临时车位号,走的时候号就还回去了 |
| 四元组(Four-tuple,源IP+源端口+目标IP+目标端口) | 说白了就是"这四个数一起,才唯一确定一通电话" | 一通通话记录:谁的哪个话机 → 谁的哪个话机 |
| 连接复用(Connection Reuse / Keep-Alive) | 说白了就是"这通电话别挂,还有事要问" | 打电话问了三件事都在一通里问完,而不是挂了再拨三次 |
| 连接池(Connection Pool) | 说白了就是"手里攥着几条已经通了的线,谁要用谁拿" | 办公室那几台一直连着的对讲机,谁要联系分店就抄一台用 |
| 三次握手的 RTT 代价 | 说白了就是"打招呼这件事本身,就得白花一个来回的时间" | 喊一嗓子到听见回音的时间(§ 1.6 讲过 RTT) |
| 连接超时(Connection Timeout) | 说白了就是"敲了半天没人应,我不敲了" | 门铃按了两分钟没人开,你转身走了 |
| 连接被拒(Connection Refused) | 说白了就是"有人应门,但明确说了不接待" | 敲门后里面喊一声"今天不营业" |
| TCP Fast Open(快速打开,允许在握手时就带上数据) | 说白了就是"拨通的同时把话说出去,不等对方喂完" | 熟客进门边走边喊"老样子一份",不用先寒暄 |
这张表里最需要记牢的是四元组这个概念,因为它解释了一件很多人想不通的事:为什么你能同时开几十个标签页访问同一个网站,而服务器不会把它们搞混。答案是每个标签页的请求用的源端口不同,四元组因此各不相同,服务器眼里它们是几十通独立的"电话"。
这是全节最重要的一个观念纠正。很多人以为"建立连接"是在你和服务器之间物理上拉通了一条线路,就像老式电话交换机那样把两根铜线插在一起。其实完全不是。
打个比方,TCP 连接更像银行开户:你和银行各自在自己的本子上,记下"这笔业务的编号是 8848、当前进行到第几步、还有多少额度"。这两份记录都在两端的内存里,中间那些马路、分拣中心、传送带压根不知道你俩签过约。
这个理解能解释三件否则很难理解的事:
第一,为什么路由器不需要"知道"任何连接。它只看每个包头上的目标 IP 就往前送(§ 1.5 讲过),它既不知道也不关心这个包属于哪条连接。所以同一条连接里的不同包,完全可能走不同的路——就像同一个客户的三份材料被三个不同的快递员分别送到。
第二,为什么会有"连接明明还在,实际却已经断了"这种怪事。如果对方的机器直接断电,它内存里那份记录瞬间消失,但你这边的记录还在——你还以为合同有效,对方那边压根没这回事了。这种状态叫"半开连接",只有等你真去发数据或者心跳探测超时,才会发现。相当于你手上的合同还在,可对方公司昨晚已经人去楼空。
第三,为什么"三次"握手是必要的最小次数。因为要让两个本子上都记下"对方确认收到了我的记录"。一次不够(只有你记了),两次也不够(对方确认了你,但你还没确认他的确认),三次刚好双向闭环。这一点 § 3.2 已经详细论证过。
三次握手在真实时间轴上的位置
先看它在整段旅程里占哪一格。把 § 4.1 到 § 4.5 的时间轴摊开:
时间 →
[本机处理] URL 解析、查缓存 ← § 4.1,微秒到毫秒级,不占来回
│
[等来回 1] DNS 查询(若未命中缓存) ← § 4.2,通常一个或多个来回
│
[等来回 2] TCP 三次握手 ← § 4.3,正好一个 RTT ★本节
│
[等来回 3] TLS 握手(1.3 是一个 RTT) ← § 4.4
│
[等来回 4] 发请求 + 等响应 ← § 4.5
│
[本机处理] 渲染 ← § 4.6,不占来回,占 CPU
★ 关键观察:在你看到页面第一个像素之前,
至少要白花三到四个来回,纯粹用在"打招呼"和"商量规矩"上。
内容本身一个字节都还没开始传。
这就是为什么跨国访问格外慢。假设一个来回是 200 毫秒(跨洲的典型量级),那么四个来回就是 800 毫秒——在你看到任何内容之前,已经过去将近一秒,而这一秒里没有传输任何有用数据。
换成大白话:这就像你要问外地一家店"某款货有没有"。查号一趟、拨通确认一趟、验明身份一趟、真正问货一趟——四个来回。如果每趟通话建立都要等两百毫秒,光是"准备说话"就花掉了大半秒。
这个观察直接推出了现代网络优化的三条主线,本章后面都会碰到:
- 减少来回次数。TLS 1.3 把两个来回压成一个(§ 4.4);连接复用干脆把握手这一步整个省掉(本节后面讲)。
- 缩短每个来回的距离。这就是 CDN——把服务器搬到离你几十公里的地方,来回从 200 毫秒变成 10 毫秒(§ 1.6 讲过"传播时延无法优化,只能缩短距离")。
- 把该等的和不该等的分开。能提前做的提前做(预连接)、能并行的并行。
三次握手在真实网络包层面长这样。注意这里的 SYN 和 ACK 只是包头上的一个标志位,它们不携带任何业务数据——你要看的整个页面,此刻一个字节都还没上路:
你的手机 服务器
(源端口 51234) (目标端口 443)
│ │
│ ① SYN,我的起始序号 seq=x │
│ ─────────────────────────────────────────→ │
│ "我想跟你建连接,我从 x 号开始编号" │
│ │
│ ② SYN+ACK,我的起始序号 seq=y,ack=x+1 │
│ ←───────────────────────────────────────── │
│ "行,我也开个连接,我从 y 开始; │
│ 另外你那个 x 我收到了" │
│ │
│ ③ ACK,ack=y+1 │
│ ─────────────────────────────────────────→ │
│ "你那个 y 我也收到了。" │
│ │
连接建立(双方本子上都记好了) 连接建立
│ │
│ ← 从这一刻起,才能开始传真正的数据 → │
耗时:第 ① 到第 ② 是一个完整来回(一个 RTT)。
第 ③ 这个 ACK 通常可以跟第一条真实数据一起发出去,
所以工程上常说"三次握手的成本是一个 RTT",而不是一点五个。
最后那句注释值得留意,它是很多人容易记错的地方:三次握手虽然有三个包,但代价是一个 RTT 而不是一点五个——因为第三个包(你发的 ACK)不需要等对方回应,可以跟后面的数据顺手一起发出去。相当于你说完"我也听得到"就直接接着讲正事,不用等对方再应一声。
源端口是谁给的:那个你从没注意过的号码
握手的第一个包要填四个字段,其中三个你已经有了:源 IP(你设备自己的地址)、目标 IP(DNS 查来的)、目标端口(443)。第四个——源端口——是哪来的?
答案是:由操作系统临时分配的。你的系统里维护着一个"临时端口范围",每次要发起新连接时就从里面挑一个还没被占用的号给你。这类端口有个专门的名字叫临时端口(Ephemeral Port,"ephemeral"就是"短暂的")。
打个比方,这相当于停车场的临时车位号。你开车进商场,保安给你一张写着"B2-137"的卡;你办完事取车走人,这个号立刻还回池子,下一辆车可能就用它。号码本身不属于你,只是这段时间归你用。
为什么必须有这个号?因为回话得知道找谁。服务器要把响应发回来,它填的目标端口就是你的源端口。你的操作系统收到包后,看端口号才知道"这份数据该交给哪个程序、哪个标签页"。
| 端口范围 | 叫什么 | 谁在用 | 生活里对应 |
|---|---|---|---|
| 0 – 1023 | 知名端口(Well-Known) | 标准服务:80 是 HTTP、443 是 HTTPS、53 是 DNS、22 是 SSH | 110、119、120 这类全国统一的号码,人人都知道 |
| 1024 – 49151 | 注册端口(Registered) | 各类软件申请登记的,如数据库、消息队列 | 各行业的服务热线,需要备案但不是人人皆知 |
| 49152 – 65535 | 动态 / 临时端口 | 就是你发起连接时用的这一类 | 停车场的临时号牌,用完即还 |
注意上面这个范围划分是规范推荐值,各操作系统实际使用的临时端口范围不完全一致,而且可以配置。所以不要背死具体数字,要记住的是三层结构:固定的公共服务号 / 登记过的服务号 / 用完就还的临时号。
这里有一个真实存在的容量上限值得知道:因为端口号只有 16 位,一台机器对同一个目标最多只能开约六万多条连接。正常上网远远用不到,但在两种情况下会真的撞上天花板:
- 压力测试或爬虫。一台机器疯狂对同一个服务器发起连接,很快就把临时端口耗尽,表现是"再也建不了新连接"。
- 大量连接停留在 TIME_WAIT 状态。连接关闭后端口不会立刻释放,要等一小会儿(§ 3.2 讲过 TIME_WAIT 在等什么)。短时间内建立又关闭大量连接,就会积压一堆占着端口不放的记录。
换成大白话:停车场只有六万个临时号,而且每辆车走了之后号还要冻结几分钟才能重发。平时绰绰有余,但如果有人一分钟内进出上万辆车,号就真的发不出来了。这也是为什么高并发服务的调优里,总有"扩大临时端口范围""缩短 TIME_WAIT"这些项。
四元组:为什么服务器不会把几十个标签页搞混
现在把四个数字凑齐,你就得到了一条连接的唯一身份证:
一条 TCP 连接 = (源 IP,源端口,目标 IP,目标端口)
你在手机上开三个标签页访问同一个网站:
标签页 1: (192.168.1.5, 51234, 93.184.216.34, 443)
标签页 2: (192.168.1.5, 51235, 93.184.216.34, 443)
标签页 3: (192.168.1.5, 51236, 93.184.216.34, 443)
↑
只有这一个数字不同,但足以区分
服务器眼里这是三条毫不相干的连接,各自独立收发,绝不混淆。
相当于同一家公司的三个员工分别打给同一个客服中心。客服看到的是三通来电,虽然主叫单位相同,但内线号不同,三份记录各自独立。只要四个数字里有任何一个不同,就是另一条连接。
这个机制还解释了另一个常被问的问题:服务器只有一个 443 端口,怎么同时服务几万个用户?因为区分用户靠的不是"服务器这边的端口",而是整个四元组——每个用户的 IP 和源端口都不同。说白了,客服中心只有一个热线号码,但每通来电的主叫号码不同,所以几百个坐席可以同时接听而不乱。
顺便解决一个更容易混的误解:"一个端口只能被一个程序占用"这句话只对"监听"成立,对"连接"不成立。服务器程序在 443 端口上监听(这是独占的),但由此建立起来的成千上万条连接,服务器这一侧的端口全都是 443。相当于一个营业厅只有一个门牌号,但门里可以同时接待几百位客人。
握手时还顺手商定了几件事
三次握手不只是"打个招呼",它还是一次参数协商。那三个包的头部里带着一些叫"选项"(TCP Options)的字段,双方借这个机会把后面通信要用的规矩商定好。主要有这么几项:
| 协商项 | 它在定什么 | 换成大白话 | 不协商会怎样 |
|---|---|---|---|
| MSS(Maximum Segment Size,最大报文段长度) | 一个包最多能装多少字节数据 | "一个纸箱最多装多重" | 包太大会在路上被切开(分片),丢一片废全包(§ 1.5 讲过) |
| 窗口缩放(Window Scale) | 让"我还能收多少"这个数字能表示更大的值 | "我家仓库其实很大,别按小格子算" | 高带宽长距离的链路上跑不满速(§ 1.6 的 BDP 问题) |
| SACK 允许(Selective ACK) | 丢包时能精确说"缺的是第几个" | "别整批重发,就缺 7 号那一箱" | 丢一个包要重传一大片,浪费带宽 |
| 时间戳(Timestamps) | 更准确地测量往返时间 | "每个包上盖个发出时刻的章" | 超时重传的时间算不准(§ 3.2 讲过 RTO 怎么算) |
本质上就是:三次握手这一个来回,同时办了两件事——确认对方在,以及把后面的规矩说清。相当于两家公司签合作协议时,见面握手的同一场会议上,顺便把"单批最大发货量、我方仓储上限、缺货怎么补、怎么算交货时效"全部写进合同。既然反正要花一个来回见面,那就一次把该谈的都谈完——这是协议设计里非常常见的思路。
这里有一个值得知道的细节:这些选项只能在握手时协商,连接建立后就不能改了。所以如果握手包被中间某个设备"好心"修改或剥离了选项(这在一些老旧的网络设备上真的会发生),后面整条连接的性能都会受影响,而且很难排查。说白了,合同一签定死,中途发现条款不合适也只能忍到这笔业务做完。
连接复用:省掉握手这一整步
现在讲这一节最有实战价值的部分。既然握手要花一个来回,那么最好的优化就是——不握手。怎么做到?复用已经建好的连接。
回想一下打开一个网页要下载多少东西:一个 HTML、几个 CSS、十几个 JS、几十张图片。如果每一个资源都单独建一次连接、握一次手、再断开,那开销大到不可接受。
糟糕的做法(HTTP/1.0 早期的默认行为):
资源 1:握手(1 RTT) → 请求响应(1 RTT) → 断开
资源 2:握手(1 RTT) → 请求响应(1 RTT) → 断开
资源 3:握手(1 RTT) → 请求响应(1 RTT) → 断开
...50 个资源 → 50 次握手 → 光握手就白花 50 个 RTT
好的做法(连接复用 / Keep-Alive,HTTP/1.1 起的默认行为):
握手(1 RTT) → 请求响应 → 请求响应 → 请求响应 → ...一直用同一条线
50 个资源 → 只握手 1 次
打个比方,这相当于打电话问事情。糟糕的做法是:拨号、问一句、挂断;再拨号、再问一句、再挂断——问五十件事拨五十次号。好的做法是拨通了别挂,把五十件事一口气问完。而"拨号"这个动作本身的时间,跟你问几件事完全无关——所以能省的次数越多,省下的时间越多。
浏览器把这件事做得更进一步:它维护一个连接池,把已经建好的连接攥在手里一段时间,随时给新请求用。这就是 § 4.1 讲的"浏览器联网前查连接池"那一步。相当于办公室里那几台常年通着的对讲机——谁要联系分店,抄一台就用,不用重新架线。
但这里有个约束值得知道:浏览器对同一个域名的并发连接数有上限(HTTP/1.1 时代的常见做法是每个域名六条左右,各浏览器不完全一致)。为什么要限制?两个理由:
- 保护服务器。如果每个浏览器都开几百条连接,服务器立刻被压死。
- 保护网络。连接开太多会加剧拥塞,反而整体变慢(§ 3.2 讲过拥塞控制)。
这个上限催生了一个曾经流行、如今已过时的优化手法叫域名分片(Domain Sharding):把资源分散到 img1.a.com、img2.a.com、img3.a.com 几个域名上,好让浏览器能开更多条连接。换成大白话:既然每个窗口最多排六个人,那我就多开几个窗口。
但在 HTTP/2 时代这个手法反而变成了负优化,因为 HTTP/2 支持在一条连接上并行跑很多个请求(叫多路复用)。这时候拆成多个域名,等于强迫浏览器多建好几条连接、多握好几次手,纯粹是浪费。同一个技巧在不同协议版本下可以从"最佳实践"变成"性能陷阱"——这正是网络优化里最容易踩的一类坑:照抄过时的经验。
握手失败的三种表现,以及怎么区分它们
这一节的排查价值集中在这里。建连接失败有三种截然不同的表现,对应三种完全不同的原因,而绝大多数人都笼统地说成"连不上"。
| 表现 | 底层发生了什么 | 说明什么 | 生活里对应 |
|---|---|---|---|
| 连接被拒(Connection Refused)——立刻失败,很快 | 你发了 SYN,对方回了一个 RST(重置)包 | 机器在、网络通,但那个端口上没有程序在监听 | 敲门,里面有人喊"今天不营业"——你至少知道有人在 |
| 连接超时(Connection Timeout)——转圈很久才失败 | 你发了 SYN,一直没有任何回应,重试几次后放弃 | 包被丢了或被静默拦截,可能是防火墙、路由不通、机器不在 | 按门铃,里面一点动静都没有——你不知道是没人还是门铃坏了 |
| 连上了但立刻断 | 握手成功,但随后收到 RST 或连接被关 | 服务过载、被限流、或应用层主动拒绝 | 门开了让你进,刚坐下就被告知"今天接待满了" |
前两种的区分极其实用,记住这一句就够:"被拒"是快的,"超时"是慢的;快说明有回应,慢说明没回应。
为什么"没回应"比"明确拒绝"更麻烦?因为你无法区分"机器不存在""端口被墙""路上丢包""对方太忙"这几种情况——它们的表现完全一样,都是一片寂静。而防火墙的默认行为恰恰常常是"静默丢弃"而不是"明确拒绝",因为静默能让攻击者连"这台机器存不存在"都探测不到。
换成大白话:明确说"不接待"至少泄露了"这里有人"这条信息。装作没人在家,连有没有这户人家都不让你确定。安全性更好,代价是排查更难。
验证连接能否建立,最简单的命令是这样:
方法一:telnet(很多系统需要手动启用)
telnet 93.184.216.34 443
· 屏幕清空、光标闪烁 → 连上了,三次握手成功
· 立刻报「拒绝」 → 端口上没有程序在听
· 一直卡着不动 → 超时,包被丢或被拦
方法二:PowerShell(Windows 自带,更方便)
Test-NetConnection example.com -Port 443
它会告诉你 TcpTestSucceeded 是 True 还是 False
★ 用这一步做定位极其高效:
· IP 能连通但域名不行 → 问题在 § 4.2 的 DNS
· IP 也连不上 → 问题在本节(路由、防火墙、端口)
· 连得上但页面不出来 → 问题在 § 4.4 的 TLS 或 § 4.5 的 HTTP
这三行判断是本章最实用的一段内容。它把"网站打不开"这个笼统的抱怨,切成了三个可以分别处理的问题。而这正是学这一章的意义所在:知道流程的顺序,你才能二分定位;不知道顺序,就只能一遍遍重启路由器。
慢启动:刚连上时其实故意跑得很慢
握手成功了,是不是就能全速传数据了?不是。TCP 有个规矩叫慢启动(Slow Start):连接刚建立时,它只敢发很少几个包,收到确认后才敢翻倍,一步步试探到底能跑多快。这套机制的原理 § 3.2 讲过(拥塞控制四阶段),这里只说它在真实访问里造成的一个后果。
后果是:新建的连接在最开始那一小段时间里,跑不出你的实际带宽。这就带来一个很反直觉的现象——对于小文件,你的宽带有多快几乎不重要,重要的是来回有多快。
打个比方,这相当于新手司机上高速:不管这条路限速多少,他一开始都只敢慢慢开,跑一段觉得没问题才敢加速。如果这趟路本来就只有两公里,那他全程都没机会开到限速——路的宽度对他毫无意义,决定用时的是他的加速过程。
这解释了两件事:
- 为什么"把宽带从 100M 升到 1000M"对刷网页几乎没有感觉。因为网页由大量小文件组成,每个都在慢启动阶段就传完了,压根用不到那么大的带宽。真正有感觉的是下载大文件和看高清视频——那些能跑到全速。
- 为什么连接复用的收益比想象的更大。复用不光省掉了握手那一个来回,还继承了这条连接已经试探出来的速度。一条跑热了的连接,比新建一条要快得多。相当于那个司机已经加速到限速了,你接着用这辆车就直接是快的。
这也是为什么现代协议如此重视"少建连接、复用连接"。说白了:每建一条新连接,你都要重新经历一遍"打招呼 + 试探速度"这两笔开销,而这两笔开销跟你要传的内容多少毫无关系。
TCP Fast Open 与 QUIC:想把这一步也省掉
既然握手这一个来回是纯开销,工程师们自然想把它也干掉。有两条路线:
路线一:TCP Fast Open(TFO,快速打开)。思路是——如果你以前连过这台服务器,服务器给过你一张"凭据",那下次你可以在发 SYN 的同时就把数据带上,不用等对方应答。相当于熟客推门就喊"老样子一份",不用先寒暄再点单。
这个机制在原理上很漂亮,但实际部署一直不顺利。原因很现实:路上那些中间设备(防火墙、运营商的加速盒子、NAT 网关)看不懂"带数据的 SYN 包",有些会直接把它丢掉。换成大白话:你想抄近路,但半路那些收费站的老式闸机只认标准流程,见到不标准的就一律拦下。
这是网络协议演进里一条极重要的教训:一个技术上正确的改进,如果需要路上所有中间设备配合,那它几乎推不动。这也解释了为什么 IPv6 普及了二十多年才过半(§ 1.3 讲过)——凡是要求"大家一起换"的方案,都极难落地。
路线二:QUIC / HTTP3——干脆换掉 TCP。既然改 TCP 这么难,那就在 UDP 上重新实现一套可靠传输,把 TCP 握手和 TLS 握手合并成一次。这样"建立连接"和"商定加密"一个来回就完成了。
为什么在 UDP 上做就绕开了那个问题?因为中间设备对 UDP 的处理很简单,不会像对 TCP 那样"自作聪明地插手"。相当于那些老式闸机只严格检查"标准货运车",而对"私家车"基本放行——于是新方案索性伪装成私家车通过。
关于 QUIC 的详细机制不在本章展开(那是协议层面的话题)。这里要你记住的只有一句:整个网络协议近二十年的演进主线,就是"把这些串行的来回一个个压掉"。本节的握手、下一节的 TLS 握手,都是被压缩的目标。
整节小结:一次建连接的完整时间线
把本节内容按真实顺序串起来,接在 § 4.2 的第 ⑥ 步之后:
接过 IP 地址 93.184.216.34 与端口 443
│
├─① 先查连接池:跟这个「IP:端口」还有活着的连接吗?
│ 有 → 直接拿来用,本节全部跳过 ★真实世界最常走
│ 没有 → 继续
│
├─② 操作系统分配一个临时源端口(如 51234)
│ 至此四元组凑齐:(本机IP, 51234, 93.184.216.34, 443)
│
├─③ 发出第一个包:SYN(带上 MSS、窗口缩放、SACK 等协商选项)
│ │
│ ├─ 收到 SYN+ACK → 对方在,且同意建连接
│ ├─ 收到 RST → 连接被拒(快速失败,端口上没程序)
│ └─ 什么都没收到 → 重试几次后超时(慢速失败,包被丢或被拦)
│
├─④ 回一个 ACK(这个包通常跟第一份真实数据一起发出去)
│
├─⑤ 连接建立。双方各自在内存里记下这条连接的状态
│ 注意:中间的路由器完全不知道这件事
│
├─⑥ 进入慢启动阶段:一开始只敢发少量数据,逐步加速
│
└─⑦ 通道就绪 →
若是 http → 直接发请求,跳到 § 4.5
若是 https → 还要先在这条通道上做 TLS 握手 → § 4.4
耗时量级:
命中连接池 ≈ 0(本机查表,微秒级)
同城建连接 通常十毫秒级
跨国建连接 可达百毫秒级(纯粹由物理距离决定,§ 1.6 讲过)
(握手的时间成本几乎完全等于一个 RTT,与你的带宽无关)
最后强调那句"与你的带宽无关"。建连接花的时间只跟距离有关,跟你家宽带多少兆完全没关系。本质上就是:宽带是"路有多宽",握手是"跑一趟要多久"——把路修宽十倍,跑一趟的时间一点都不会少。这是 § 1.6 那条"带宽与时延是两件独立的事"在真实流程里最清楚的一次体现。
路上那些"看不见的第三方":NAT 与代理
前面说"连接是两端各记一笔,中间设备不知道"。这句话在纯粹的 IP 网络里成立,但现实中有两类设备偏偏要插手,而它们造成的现象你几乎天天遇到。
第一类:NAT(Network Address Translation,网络地址转换)。你家路由器就在干这件事(§ 1.3 讲过 NAT 的原理)。它必须记住每条连接,因为它要在包出门时把你的私网地址换成公网地址,回来时再换回去。
你的手机 家用路由器(做 NAT) 服务器
192.168.1.5:51234 → 公网 IP 1.2.3.4:60001 → 93.184.216.34:443
路由器内部维护一张表:
内网 192.168.1.5:51234 ←→ 外网 1.2.3.4:60001
回包到达时,它查这张表,才知道该转发给哪台内网设备。
★ 关键后果:服务器看到的源地址是路由器的,不是你手机的。
全家几十台设备在服务器眼里共用同一个 IP。
相当于一整栋写字楼共用一个总机号码。外面打进来只能拨总机,总机前台手上有一本"哪个外线通道对应哪个内线"的对照表,靠它把电话转到正确的房间。总机必须记住每一通正在进行的通话,否则回话就不知道该转给谁。
这个"必须记住"带来一个真实的问题:NAT 表里的记录不能永远留着,它有超时。如果一条连接长时间一句话不说,路由器会认为它结束了,把记录删掉。此后服务器发来的包,路由器就不知道该给谁,只能丢掉。换成大白话:前台看到某条通话线路半小时没声音,以为早挂了,把对照表那一行擦掉了——结果对方再说话,前台不知道该转给谁。
这正是为什么很多应用要发"心跳包":每隔一段时间发一个没什么内容的小包,纯粹是为了告诉路上的设备"这条线还活着,别删我的记录"。相当于长时间不说话时,隔一会儿"喂"一声,让前台知道通话还在继续。你手机上的聊天软件一直在悄悄做这件事。
第二类:代理服务器(Proxy)。它比 NAT 更进一步——它不是转换地址,而是代替你去连。你连的其实是代理,代理再连服务器,中间是两条独立的 TCP 连接。
打个比方:NAT 相当于总机转接(还是你在跟对方直接对话),代理相当于找了个中间人代为传话(你跟中间人说,中间人再跟对方说)。后者的中间人当然知道你们谈了什么内容——除非内容本身是加密的,这正是下一节 TLS 要保障的事。
公司网络、学校网络里常见的"上网要走代理"就是这类。它带来的一个常见排查现象是:同一个网站在公司打不开、用手机流量就正常。原因往往是代理拦了它,而不是网站本身有问题。说白了,问题不在对方,在中间那个替你传话的人。
为什么"连上了"和"能用"是两件事
这一节最后要澄清一个非常常见的混淆:三次握手成功,只证明"这个端口上有程序在听",完全不证明"这个服务是好的"。
三次握手是操作系统内核层面完成的动作。服务器程序甚至可能已经卡死、数据库已经挂了、业务逻辑全是错的——只要它的进程还在、还在那个端口上监听,握手就能成功。
| 状态 | 握手能成吗 | 页面能打开吗 | 生活里对应 |
|---|---|---|---|
| 服务器一切正常 | 能 | 能 | 门开着,人在,能办事 |
| 程序在监听但内部卡死 | 能 | 不能(会一直等到超时) | 门开着、窗口有人坐着,但那人睡着了叫不醒 |
| 数据库挂了,但网页服务在跑 | 能 | 能连上但报 500 错误 | 柜员在,但后台系统坏了,只能告诉你"今天办不了" |
| 程序崩了、端口没人听 | 不能(立刻被拒) | 不能 | 窗口拉着卷帘门,明确告诉你没人 |
| 机器断电或被防火墙拦 | 不能(一直超时) | 不能 | 整栋楼黑着,敲门也没任何回应 |
第二行那种"能连上但没反应"的状态最难查,因为所有的连通性测试都会显示"正常",而用户体验是"完全打不开"。换成大白话:你打电话过去通了、也没占线、也没人说不接待,但对面就是一直不说话。从"线路"的角度看毫无问题,从"办事"的角度看什么都办不了。
这引出一条很实用的运维观念:监控一个服务是否健康,绝对不能只测"端口通不通"。必须真的发一个请求、看它返回的内容对不对。相当于检查一家店是否正常营业,不能只看卷帘门开着没有,得真进去点一份东西试试。这类检查叫"健康检查",而只测端口的那种叫"存活检查"——两者的可靠性差得很远。
一次真实的连接建立:从开发者工具里看它
这一节讲的东西全部可以在浏览器里亲眼看到,而且看的位置很具体。
操作步骤:
① 按 F12 打开开发者工具,切到 Network(网络)面板
② 勾上 "Disable cache"(禁用缓存),这样能看到完整流程
③ 刷新页面
④ 点击列表里第一条请求(通常就是那个 HTML 文档)
⑤ 切到 Timing(时序)标签页
你会看到一组分解的时间条,其中和本章前三节对应的是:
Queueing / Stalled 排队等待(浏览器内部调度,§ 4.1 那些本机检查)
DNS Lookup ← § 4.2 这一整节的耗时
Initial connection ← 本节:TCP 三次握手 ★
SSL / TLS ← § 4.4 下一节的耗时
Request sent 发送请求(通常极短)
Waiting (TTFB) ← § 4.5:等服务器处理并返回第一个字节
Content Download 下载响应内容
★ 这张时序图就是本章前五节的完整可视化。
值得反复看几次不同的网站,感受各段占比的差异:
· 访问国内近距离的站:各段都很短
· 访问跨国站点:Initial connection 与 SSL 明显变长(距离决定)
· 第二次刷新(不禁用缓存):Initial connection 常常直接消失
—— 因为连接被复用了,这正是本节讲的连接池在起作用
最后那一条是本节最值得亲手验证的现象:第一次刷新有 Initial connection 这一段,第二次就没有了。那一段消失的时间,就是连接复用省下来的。"复用连接"这四个字从抽象概念变成一个消失的时间条,理解会牢固得多。
相当于你亲眼看到第一趟去开了个户、第二趟直接就办上业务了——那省下的开户时间,就是复用的价值。
为什么这一步必须存在:无连接方案的代价
最后回答一个很自然的疑问:既然握手要白花一个来回,为什么不干脆省掉,直接把数据发出去?——这个问题是有答案的,而且答案就是 UDP(§ 3.3 讲过)。
UDP 确实不握手:想发就发,一个包扔出去,不管对方在不在、收没收到。那为什么网页不用它?因为省掉握手的同时,你也一并放弃了四样东西:
| 握手带来的东西 | 没有它会怎样 | 对网页意味着什么 |
|---|---|---|
| 确认对方在、且愿意收 | 你可能对着一台不存在的机器一直发 | 用户等半天才发现"其实压根没连上" |
| 双方约定起始序号 | 没有编号,收到的数据不知道该怎么排序 | HTML 文本可能乱序拼接,页面全是乱码 |
| 协商 MSS 等参数 | 只能按最保守的值来,或者一路踩坑 | 包被分片、丢一片废全包,效率极差 |
| 建立"这是一条连接"的共识 | 无法做重传、流控、拥塞控制 | 网络一堵就雪崩,谁都下不动 |
换成大白话:省掉那三句"喂喂喂",你就得接受"可能对着空气讲了半小时"。对于打游戏发一个位置坐标,这个代价可以接受(丢了下一帧就补上);对于要完整无误地传一份网页文本,这个代价完全不可接受——少一个字节,整个页面可能就渲染不出来。
所以真实的取舍是这样:不是"要不要那一个来回",而是"这一个来回换来的保障值不值"。对网页来说值,所以付;对实时语音来说不值,所以不付。相当于寄一份合同要挂号带回执(多花钱多花时间但必须确认送到),发一句问候用普通短信就行(丢了再发一遍,无所谓)。
而 HTTP/3 走的是第三条路:它建在 UDP 上,但把握手与加密协商合并成一次,自己在上层重新实现了可靠性。换句话说——它没有放弃那些保障,只是把"取得保障"的成本从两个来回压到了一个。这正是协议演进的典型形态:不是砍掉功能,是把同样的功能做得更省。
这一节干的活儿:把一个 IP 地址变成一条双方都认账的可靠通道。
最重要的观念纠正:"建立连接"不是在路上拉了一根线,而是两端各自在内存里记了一笔。中间的路由器完全不知道你俩连着——所以同一条连接的不同包可以走不同的路,也所以会出现"我这边以为还连着、对方早已断电"的半开连接。
三次握手的原理见 § 3.2(为什么必须三次、序号怎么算、SYN Flood、TIME_WAIT、拥塞控制),本节只讲它在流程里的位置:它正好花一个 RTT(不是一点五个,因为第三个 ACK 可以跟数据一起发),而且顺手协商了 MSS、窗口缩放、SACK、时间戳这几件事——这些选项只能在握手时定,之后不能改。
源端口由操作系统从临时端口池里分配,用完就还,相当于停车场的临时号牌。加上目标 IP 与端口,凑成四元组——这是一条连接的唯一身份证,也是"几十个标签页不会混"和"一个 443 端口能服务几万人"的答案。
最有实战价值的两条:① 连接复用(Keep-Alive)能把 50 次握手压成 1 次,而且复用还继承了已经试探出来的速度(慢启动的成果);② 失败分两种,快慢就能区分——"被拒"是快的(有 RST 回应,端口上没程序),"超时"是慢的(一片寂静,包被丢或被静默拦截)。
下一步交出什么:一条可用的通道。如果是 http,现在就可以开口说话了(跳到 § 4.5)。但我们访问的是 https——所以在开口之前,还要先在这条通道上验明对方身份、商定一把只有你俩知道的钥匙。这就是 § 4.4 TLS 握手要干的事。