一个数据包的旅行?
按下 Enter 之后
浏览器地址栏里,躺着这样一行字:
1 | https://www.example.com |
手指按下 Enter。
不到一秒,页面就出现了。
大多数人从来不会想这一秒里发生了什么。网页跳出来,就跟呼吸一样自然。
但如果你愿意慢下来,把这一秒钟拉长,拉得足够长,你会发现:这背后有一场真正的旅行。
旅行的主角,不是浏览器,也不是服务器。
是一个数据包。
数据包出生了
数据包刚刚诞生的时候,知道的事情少得可怜。
它只知道一件事:
“我要把这个请求,送到 www.example.com。“
它不知道 www.example.com 在哪里,不知道该走哪条路,不知道路上会遇到谁。它甚至不知道”IP地址”是什么。
它只有一个任务,和一路上不断冒出来的问题。
第一个问题,几乎立刻就出现了。
第一站:你到底在哪里?
数据包盯着”www.example.com"这几个字,发现一个很尴尬的事实:
这不是一个地址,这只是一个名字。
网络里传输数据,靠的是地址,不是名字。数据包没法拿着”www.example.com"去敲门——它需要一个像门牌号一样的东西。
于是,查询开始了。
这项工作交给了 **DNS(域名系统)**。可以把它理解成一个巨大的、分布式的电话簿系统:你把域名交给它,它告诉你对应的 IP 地址。
DNS 不是某一台服务器,而是一整套查询系统。这次旅行不需要知道它内部具体怎么工作——那是以后的事情。现在,数据包只需要知道结果:
www.example.com 对应着某个 IP 地址,比如
203.0.113.10。
问题解决了。数据包终于有了一个可以出发的方向。
它以为自己终于可以上路了。
结果很快发现,没那么简单。
第二站:知道了地址,却不知道交给谁
数据包现在手里握着一个 IP 地址。它以为这就够了。
但它马上意识到一件更基础的事:
它现在还没离开自己所在的这一小片局域网。
在这一小片网络里,设备之间靠的不是 IP,而是另一种身份——MAC 地址,也就是这一跳链路层上的身份证。
这里要区分两件容易被混在一起的事:
- IP 地址,描述的通常是最终目的地;
- MAC 地址,描述的是当前这一跳该交给谁。
知道目的地是谁,和知道这一步该交给谁,是两个完全不同的问题。
于是,一个叫 ARP 的机制登场了。它的工作,就是在本地网络里问一句:”这个 IP,对应的下一跳链路层地址是什么?”——然后拿到答案。
问题解决了。数据包知道了这一跳该交给谁。
但它立刻发现了第三个问题。
第三站:目标根本不在这里
数据包一路问下去才发现一个更麻烦的事实:
它要去的服务器,根本不在自己所在的局域网里。
它所在的这个小网络,只是整个互联网里极小的一角。目标服务器,在很远很远的地方。
这时候,默认网关出现了。
默认网关可以理解成这片局域网的大门——当数据包发现目的地不在本地时,它知道:该把数据交给这扇门,由它负责往外送。
数据包被交到网关手上。
它第一次真正地,离开了自己出生的地方。
第四站:进入真正的互联网
出了本地网络,数据包看到了一个比它想象中大得多的世界。
互联网,并不是一整块统一的东西。它是无数个网络,通过一台又一台路由器连接起来的体系。数据包每到一个路口,路由器都要重新决定一次:
“这个目的 IP,下一步该往哪个方向走?”
这个决定的依据,叫做路由——每个路由器手里都有一张”路线地图”,告诉自己不同的目的地该往哪个方向转发。
数据包不需要知道完整的路径,也没人告诉它完整的路径。它只是被一站一站地转发:
1 | 本地网络 → 默认网关 → 路由器 → 路由器 → …… → 目标网络 → 服务器 |
每一跳,链路层的”外包装”其实都在变化——数据包并不是从头到尾裹着同一层皮肤走完全程,变的是链路层这一层,而不是它真正要送达的那个 IP 目的地。
这一段旅程,数据包走得晕头转向,但幸运的是,它并不需要认路——每一站的路由器都替它做好了决定。
终于,它看到了目标网络的边界。
第五站:到了服务器门口,却发现里面还有很多房间
数据包终于抵达了目标服务器所在的网络,离目的地只有一步之遥。
但一个新的问题冒了出来:
这台服务器上跑着很多个程序,我该交给哪一个?
只知道 IP,只能找到”这台机器”。但一台机器上,可能同时开着网页服务、邮件服务、其他各种服务。
于是,**端口(Port)**出现了。它就像这台主机里的门牌号——每个门牌号后面,对应着一个正在监听的服务。
而在操作系统内部,真正负责把网络数据和某个具体程序对接起来的,是一个叫 Socket 的东西。Socket 不是一种协议,而是应用程序和操作系统之间,用来做网络通信的窗口和抽象。
数据包知道了目的地、也知道了该找哪扇门。
但它还面临一个问题:这一路要传的数据,该用什么方式送过去?
第六站:数据该怎么传?
网络传输并不是只有一种方式。
数据包了解到,自己脚下正走的是这样一条路:如果这次请求需要可靠、有序地送达——比如网页请求这种,不能丢字节、不能乱序——通常会走 TCP。
TCP 提供的是一种面向连接、可靠的字节流传输方式。至于它具体怎么做到”可靠”——那背后还有一整套机制,是以后的旅程要慢慢揭开的。
而如果一份数据不需要这种严格保证,比如追求更低延迟、可以容忍偶尔丢包的场景,走的可能是另一条路——UDP。UDP 是无连接的、以数据报为单位传输的方式,它不像 TCP 那样提供有序和可靠的保证。
需要说明的是,UDP 并不是”更快的 TCP”——两者根本是为不同目标设计的,不能简单地用”快慢”去比较。
这次的请求,走的是 TCP。
但还有一件事没解决——这一路上,会不会有人偷看这次通信的内容?
第七站:我不想让路上的人看到内容
从浏览器地址栏那个小小的 https 就能看出,这次通信,数据包不希望被沿途的人看到内容。
负责这件事的是 TLS。它像一个加密信封,为这次通信提供机密性、身份认证和完整性保护——既让内容不被偷看,也确保数据包真的在和它以为的那个服务器说话,而不是被冒充了。
这里有一个小小的补充:并不是所有 HTTPS 通信都固定走在 TCP 之上。较新的 HTTP/3 使用的是 QUIC,而 QUIC 是建立在 UDP 之上的。这次旅程走的是最经典的路线——TCP 之上的 TLS——但值得记住,路并不是只有一条。
数据包裹上了这层安全的外壳,继续前进。
它终于,踏进了服务器所在的那台机器。
第八站:进入 Linux 的世界
数据包以为,到了服务器,故事就该结束了。
它没想到,真正复杂的一段旅程,才刚刚开始。
服务器运行着 Linux。数据包从网卡进入,经过驱动,一路走进操作系统内部的网络协议栈——这是 Linux 内核里专门处理网络数据的一整套机制。在这里,数据包会经过 TCP/UDP 层的处理,最终被交到前面提到的 Socket,再由 Socket 转交给对应的应用程序。
粗略地画出来,大概是这样一条路径(注意,这只是一个便于理解的简化示意,真实情况要复杂得多):
1 | 网卡 → 驱动 → Linux 网络协议栈 → TCP/UDP → Socket → 应用程序 |
在这条路径上,数据包第一次隐约察觉到还有一个”检查站”的存在——Linux 内核里有一整套叫 Netfilter 的数据包处理与过滤机制,负责决定哪些数据包可以通过、哪些会被拦下、要不要被改写。
这次旅程还不需要深入这个检查站具体怎么运作。数据包只是路过,知道它的存在。
任务完成——却还没有结束
应用程序终于收到了这次请求。
数据包松了一口气。它以为,自己的任务到这里就结束了。
但很快,一份新的数据从服务器那头产生出来——这是响应,它要顺着某条路,送回最初发出请求的那台电脑。
数据包这才意识到:
去程,只是这次旅行的一半。
一张地图
走到这里,不妨停下来,把这一路看到的东西画成一张地图:
1 | 浏览器 客户端所在局域网 |
这张图,以后会反复被用到。数据包每一次新的旅行,都会在这张图的某个位置上,继续深入。
回家的路,会一样吗?
数据包抬头看了看自己走过的这条路——DNS、ARP、网关、一路的路由器、端口、TCP、TLS,还有刚刚在 Linux 内部瞥见的那一小段。
响应已经生成,该往回走了。
它下意识地以为,回去的路,应该就是来时这条路的原样倒放。
但它很快犹豫了一下——真的是这样吗?
这个问题,它自己也回答不上来。它决定留到下一次旅行,再亲自去验证。
而在那之前,还有一件更基础的事,它其实一直没弄明白:
www.example.com, 到底是怎么变成一个 IP 地址的?
它只知道结果,却从没看过这个过程究竟发生了什么。
下一站,它要回头,弄清楚这第一步,究竟是怎么走出来的。