§ 4.3 · Section

建立 TCP 连接

The Handshake in Context · RFC 9293

上一步 DNS 交回了一个 IP 地址。但知道门牌号不等于能说上话——你还得敲门、对方还得应门、你还得说一声"好我进来了"。这一节讲的就是这三句话(三次握手)在真实访问里怎么发生、你手机上那个源端口号是谁给的,以及为什么"连接复用"是现代网页速度的最大功臣之一。

生活场景
📞 你终于查到号码,开始拨过去

上一节你从前台那儿拿到了老李的号码 8848。号码在手,但事情还没办成——你得真的拨过去,而且得确认双方都听得见

你拨号 → 听到"嘟"声(你发出了请求
对方接起:"喂?"(对方应了,说明他在,而且他听得到你
你说:"能听到吗?"对方说:"能,你呢?"你说:"我也能。"(双向都确认过了

只有走完这几句,你才敢开口讲正事。为什么不能拨通就直接讲?因为你不知道对方是不是真接起了、也不知道他那边听不听得清。万一你噼里啪啦讲了三分钟,对方一句没听见,那三分钟全白费。

TCP 三次握手就是这几句"喂喂喂"。它的原理你在 § 3.2 已经学过了,这一节要看的是:它在一次真实的网页访问中,具体发生在哪个时刻、花多少时间、失败了会是什么表现。

上一步刚完成了什么,这一步要干什么

接力棒交到这一节手上时,浏览器手里已经有了一套完整的"拨号信息":

这一节要干的事:在你的设备和那台服务器之间,建立一条双方都认账的可靠通道。注意"双方都认账"这四个字——这是理解 TCP 连接最关键的一点,后面会反复回到它。

干完之后交给下一节的东西是:

协议原理不在本节重复。三次握手为什么必须是三次、四次挥手怎么走、序号确认号怎么算、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(快速打开,允许在握手时就带上数据)说白了就是"拨通的同时把话说出去,不等对方喂完"熟客进门边走边喊"老样子一份",不用先寒暄

这张表里最需要记牢的是四元组这个概念,因为它解释了一件很多人想不通的事:为什么你能同时开几十个标签页访问同一个网站,而服务器不会把它们搞混。答案是每个标签页的请求用的源端口不同,四元组因此各不相同,服务器眼里它们是几十通独立的"电话"。

Analogy · 连接是"两边各记一笔",不是"路上拉了一根线"

这是全节最重要的一个观念纠正。很多人以为"建立连接"是在你和服务器之间物理上拉通了一条线路,就像老式电话交换机那样把两根铜线插在一起。其实完全不是。

打个比方,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 毫秒——在你看到任何内容之前,已经过去将近一秒,而这一秒里没有传输任何有用数据。

换成大白话:这就像你要问外地一家店"某款货有没有"。查号一趟、拨通确认一趟、验明身份一趟、真正问货一趟——四个来回。如果每趟通话建立都要等两百毫秒,光是"准备说话"就花掉了大半秒。

这个观察直接推出了现代网络优化的三条主线,本章后面都会碰到:

三次握手在真实网络包层面长这样。注意这里的 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 是 SSH110、119、120 这类全国统一的号码,人人都知道
1024 – 49151注册端口(Registered)各类软件申请登记的,如数据库、消息队列各行业的服务热线,需要备案但不是人人皆知
49152 – 65535动态 / 临时端口就是你发起连接时用的这一类停车场的临时号牌,用完即还

注意上面这个范围划分是规范推荐值,各操作系统实际使用的临时端口范围不完全一致,而且可以配置。所以不要背死具体数字,要记住的是三层结构:固定的公共服务号 / 登记过的服务号 / 用完就还的临时号

这里有一个真实存在的容量上限值得知道:因为端口号只有 16 位,一台机器对同一个目标最多只能开约六万多条连接。正常上网远远用不到,但在两种情况下会真的撞上天花板:

换成大白话:停车场只有六万个临时号,而且每辆车走了之后号还要冻结几分钟才能重发。平时绰绰有余,但如果有人一分钟内进出上万辆车,号就真的发不出来了。这也是为什么高并发服务的调优里,总有"扩大临时端口范围""缩短 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 时代的常见做法是每个域名六条左右,各浏览器不完全一致)。为什么要限制?两个理由:

这个上限催生了一个曾经流行、如今已过时的优化手法叫域名分片(Domain Sharding):把资源分散到 img1.a.comimg2.a.comimg3.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 讲过(拥塞控制四阶段),这里只说它在真实访问里造成的一个后果。

后果是:新建的连接在最开始那一小段时间里,跑不出你的实际带宽。这就带来一个很反直觉的现象——对于小文件,你的宽带有多快几乎不重要,重要的是来回有多快。

打个比方,这相当于新手司机上高速:不管这条路限速多少,他一开始都只敢慢慢开,跑一段觉得没问题才敢加速。如果这趟路本来就只有两公里,那他全程都没机会开到限速——路的宽度对他毫无意义,决定用时的是他的加速过程。

这解释了两件事:

这也是为什么现代协议如此重视"少建连接、复用连接"。说白了:每建一条新连接,你都要重新经历一遍"打招呼 + 试探速度"这两笔开销,而这两笔开销跟你要传的内容多少毫无关系。

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 上,但把握手与加密协商合并成一次,自己在上层重新实现了可靠性。换句话说——它没有放弃那些保障,只是把"取得保障"的成本从两个来回压到了一个。这正是协议演进的典型形态:不是砍掉功能,是把同样的功能做得更省。

Recap · 收束

这一节干的活儿:把一个 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 握手要干的事。

☰ 主页
Xue Hai Wu Ya · Network · § 4.3 · 建立 TCP 连接