ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

按下回车到网页显示:DNS、TCP/IP、ARP与HTML渲染全链路拆解

按下回车到网页显示:DNS、TCP/IP、ARP与HTML渲染全链路拆解

很多人学网络协议都是分模块啃的,DNS是DNS,TCP是TCP,ARP是ARP,学完感觉都懂,但一旦被问到“小明在家按了下回车,网页是怎么出来的”,就卡住了。这篇文章就把这个经典场景完完整整拆一遍:从浏览器里敲下网址那一刻开始,到网页渲染完毕,中间涉及DNS缓存、DNS查询、TCP/IP协议、ARP协议、HTML渲染这几条线,全部串成一条可复现的链路。配合抓包思路和排障经验,适合正在学计算机网络、或者工作里经常要排查网络问题但知识不成体系的朋友。

1. 先看全局:按下回车那一刻,网络里有哪些角色在干活

1.1 家庭网络中的参与方

要理解一次完整的网页访问,第一步不是钻进协议细节,而是先把“当事人”都拉出来。小明的家用网络拓扑其实很简单,参与这次访问的角色包括:

  • 浏览器:负责发起HTTP请求、解析服务器返回的HTML资源、最终渲染页面。
  • 操作系统:提供Socket接口、维护DNS缓存、组织TCP报文,并让数据包进入网卡队列。
  • 网卡与设备驱动:把IP数据包封装成以太网帧,通过Wi-Fi或有线网线发出去。
  • 家庭路由器:充当网关,负责把家庭内网的数据包转发到运营商网络,同时承担NAT地址转换。
  • 运营商侧的接入设备:把来自用户家庭的数据汇聚进城域网和骨干网。
  • DNS解析服务:把域名翻译成IP地址,可以是运营商提供的DNS,也可以是用户手动配置的公共DNS。
  • 目标网站服务器:处理收到的HTTP请求,把网页资源返回给客户端。

每个角色只完成自己那一小段工作,然后把结果交给下一个环节。这就像工厂流水线,没人需要知道整个产品的全部工序,每个人只要把手上那份活干好就行。

1.2 一整套协议栈的分工

一次访问里涉及的协议远不止标题里那几个关键词。按TCP/IP分层模型来看,它们各司其职:

层次涉及协议这一层干的事生活类比
应用层HTTP/HTTPS、DNS定义客户端和服务器之间的语义,比如“我要获取某个资源”顾客和服务员沟通“我要点一份红烧肉”
传输层TCP把数据切成段、编号、确认,保证可靠有序交付快递公司保证包裹不丢、不破、按顺序送到
网络层IP、ICMP为数据包寻址,决定从源到目的走哪条路快递单上的地址,让包裹知道往哪个城市寄
链路层以太网、ARP把数据封装成帧,通过物理介质发送给下一个设备快递员按门牌号把包裹送到具体人家

这里面最容易被忽视的关系是:HTTP本身根本不关心网络通不通,TCP也不关心设备之间用Wi-Fi还是网线,只有到了链路层,ARP和以太网帧才管“物理上怎么把数据传出去”。所以标题把这几层协议放在一起讨论是有道理的,因为一次访问要打通的是整条链路,缺任何一环都不行。


2. DNS缓存的金字塔:每次打开网址,第一个环节怎么跑

2.1 浏览器和操作系统层面的缓存优先级

小明在地址栏输入www.demosite.net(虚构示例)并按回车以后,浏览器做的第一件事并不是发网络请求,而是先在自己的内存里找缓存。这个缓存就是DNS缓存,键值对形式,最常见的是:

www.demosite.net -> 203.0.113.88

浏览器有缓存,操作系统也有一份缓存,而且操作系统缓存往往是浏览器缓存之后的第二道防线。在Windows上,这是DNS Client服务在维护;在Linux上,通常是systemd-resolved或nscd在做这件事。

为什么要设计成两层缓存?因为DNS查询本质上是去别的机器上问问题,一次查询要跨越局域网、城域网甚至跨洋,哪怕只要几十毫秒,累积起来也很可观。而本地缓存把命中结果的时间压缩到微秒级——从内存里直接查表,比任何网络往返都快。

这里有个实际经验:改完DNS服务器地址以后,明明配置生效了,但域名解析还是老结果,八成就是缓存没刷新。Windows上用:

ipconfig /displaydns

可以看到操作系统里的完整DNS缓存表,确认你查的那个域名还挂着旧IP。

2.2 全部缓存未命中时,一次完整的DNS递归查询

如果浏览器、操作系统层面都没有缓存,查询就要被发到“外部世界”。小明的设备会把请求发给本地DNS服务器——这里的“本地”不是指小明的电脑,而是指运营商或用户配置的上游DNS服务。这个请求是递归查询,意思是:小明对本地DNS说“帮我彻底查清楚这个域名到底指向哪个IP,别让我再管后续了。”

本地DNS服务器拿到请求后,会分几步做迭代查询:

  1. 先问根DNS服务器:“www.demosite.net 的IP是什么?”根服务器不会直接给出答案,但会告诉它:“这个域名属于.net顶级域,你去问.net顶级域服务器。”
  2. 再去问**.net顶级域服务器**:“这个域名的权威服务器是谁?”得到答复:“权威服务器是 ns1.demosite.net。”
  3. 最后问权威DNS服务器:“www.demosite.net 的IP是什么?”这回拿到了真正的答案:203.0.113.88。

本地DNS拿到这个结果后,会先把结果缓存起来,同时把答案返回给小明。这个过程用文字描述显得很长,实际耗时通常只有几十到一两百毫秒——因为根和顶级域服务器的数据在本地DNS上一般是长期缓存的,不是每个域名都要完整走一遍。

2.3 缓存的保质期由TTL决定

为什么有时候换个域名解析记录,全球生效要等很久?关键在TTL(生存时间)。每条DNS记录都带着TTL,单位是秒,表示“这条记录可以被缓存多久”。如果权威服务器设置了600秒,那么途中所有缓存设备都会在10分钟内复用这个结果,而不重新询问。

TTL太短,权威服务器压力大;TTL太长,记录更新太慢。常规做法是:把基础记录的TTL设为3600秒左右,做迁移前24小时调低到300秒,迁移完再调回去。这个思路其实也适用于理解“为什么小明第二次打开同一个网站会明显变快”——因为缓存命中了。

2.4 路由器上的DNS逻辑:并不一定都有缓存

很多家用路由器本身并不缓存DNS响应,它们只做转发:把内网设备的DNS请求通过网络地址转换转发到上游运营商DNS。但也有不少路由器带有轻量DNS代理功能,会短暂记住前一次请求的结果。

不管路由器有没有缓存,小明电脑上DNS查询的目标默认都是路由器分配的DNS地址。这个地址通常来自运营商,但也可以手动改成公共DNS。需要留意的是:改电脑上的DNS指向,并不影响“从权威服务器到本地DNS”这条链路的解析质量。如果你的路由器本身已经做了某些重定向,那电脑改什么都没用,这个细节在排查DNS问题时经常是隐藏难点。


3. TCP连接:三次握手、一个请求,和断开时的四个动作

3.1 三次握手不是形式主义

DNS解析拿到目标服务器的IP地址后,浏览器就要发起HTTP请求了。HTTP/1.1时代,每个请求几乎都要新建一条TCP连接,而TCP连接的第一个动作就是三次握手。

假设服务器监听在80端口,小明这边发出第一个报文:

  1. 小明 → 服务器:SYN,seq = x。意思是:“我想建连接,我的初始序号是x。”
  2. 服务器 → 小明:SYN+ACK,seq = y,ack = x+1。意思是:“确认收到你的序号x,我的初始序号是y。”
  3. 小明 → 服务器:ACK,seq = x+1,ack = y+1。意思是:“确认收到你的序号y,连接正式建立。”

为什么一定要三次?核心原因是防止失效的连接请求突然到达服务器,导致服务器白白建立资源。如果一个历史遗留的SYN报文滞留在网络里,晚到服务器处,服务器回复SYN+ACK后,客户端如果发送的是RST而不是ACK,那这次握手就会中止——三次握手中的第三次不仅是确认,更是让服务器确认“客户端确实在线上”。

实际排障时,我用Wireshark看过太多次握手只完成一半的情况:客户端发出SYN,服务器没回SYN+ACK,或者回了以后客户端没有发出ACK。前者多半是防火墙拦截了SYN,后者则要考虑客户端连接池状态问题。

3.2 握手完成后:HTTP请求出门,数据开始分段

三次握手完成,小明就开始发HTTP请求:

GET /index.html HTTP/1.1 Host: www.demosite.net User-Agent: Mozilla/5.0 ... Accept: text/html

这串字符会作为TCP的载荷,被加上TCP头(源端口、目的端口、序列号)后,再交给IP层封装进IP数据包。数据太大时,TCP会按MSS(最大报文段大小)把它切成多段,每段都有自己的序号,接收方按序号重组,缺哪一段就重传哪一段。

很多人把TCP的可靠性简单理解成“丢了就重传”,但实际机制是:接收方每收到一个段,就返回一个ACK,发送方如果发现超时没收到ACK,会主动重传。这个超时时间不是固定的,而是根据RTT动态计算出来的。这也解释了一个场景:跨地域访问时,TCP握手时间变长、下载速度下降,不完全因为带宽不够,更可能是RTT变大导致拥塞控制窗口增长速度变慢。

3.3 IP和路由:数据包怎么一路走向目标服务器

这里必须补IP层的戏份。TCP报文交给IP层以后,IP层要做的不是打包“发送”,而是决定下一跳是谁。小明的电脑查看路由表,发现目标IP203.0.113.88不在本地网段,于是标记“下一跳是默认网关”,也就是家庭路由器的内网IP。

这个决策过程叫“最长前缀匹配”,每个路由器都用同一套逻辑不断接力,把包一路转发过去。IP协议本身不保证不丢包,不保证不乱序,这些事TCP负责;IP只保证一件事:数据包上有源地址和目标地址,沿途设备都知道该怎么尽量把它送到。

从家用路由器出去以后,数据会经过运营商接入设备、城域网节点、骨干网出口,再到服务器所在的数据中心交换机。这一路上的设备都在做类似的查表转发动作,只不过它们的路由表比小明家路由器的复杂几个数量级。

3.4 断开连接:四次挥手、RST与Keep-Alive的真相

小明拿到需要的页面内容以后,连接怎么断开?教科书里讲的是四次挥手:一方发FIN,另一方回ACK,然后反过来再FIN+ACK,最后一轮ACK关闭。实际情况远没有那么理想——大多数浏览器访问网页后会主动保持连接,因为同一个页面上还有CSS、JS、图片需要继续加载,这就引出了HTTP Keep-Alive。

但即使Keep-Alive,连接最终也会被某个机制关闭,常见的是服务器空闲超时断开,或者客户端主动RST。RST是个被低估的报文:它并不走四次挥手流程,而是直接异常终止。比如我遇到过一种典型情况:客户端请求的响应太大,服务器已经设置了max header size限制,直接发了RST,客户端只看到“Connection reset by peer”,这种情况靠抓包才看得出是应用层行为导致的复位。


4. ARP协议:同一个局域网里的“找MAC地址”广播

4.1 IP地址之外,网卡还需要什么

很多人在网络层和链路层的分界上栽过跟头:数据包明明有IP地址,为什么还需要额外的协议来确定“发给谁”?

因为IP地址是逻辑地址,它描述的是“这台机器在网络里的身份”,但实际在同一个局域网里传输数据时,网卡之间通信靠的是MAC地址。MAC地址是出厂时就烧在硬件上的物理唯一标识,局域网设备之间根本不认识IP,它们只认MAC。所以从一台设备向另一台设备发送以太网帧之前,必须先知道对方网卡的MAC地址,这一步就交给ARP协议。

4.2 家庭局域网里的两种典型ARP场景

场景一:小明访问的目标是外面的服务器。这时ARP要解析的不是远端服务器,而是小明和路由器之间的那个网关。小明的电脑通过路由表得知下一跳是家里的路由器(比如192.168.1.1),但它不知道路由器的MAC地址,于是发出ARP广播:“192.168.1.1的MAC地址是什么?”路由器收到广播后单播回复:“我就是,我的MAC是xxx。”随后小明就把数据帧的目标MAC填成路由器的MAC,往出扔。

场景二:小明访问的是路由器管理页面(比如192.168.1.1)。这时ARP解析的目标就是路由器本身。如果网络里有一台打印机的IP被手动改成和路由器的IP冲突,ARP会解析到哪个MAC就不一定了,这也是偶尔出现“打开路由器管理页却出来一台打印机设置页”的原因——因为ARP缓存表里那个IP对应的MAC已经变了。

4.3 ARP缓存和局域网安全防护

ARP广播不能太频繁,所以每台设备内部都有ARP缓存表。在Linux上查看:

ip neigh show

在Windows上是:

arp -a

缓存表里的条目也会过期,所以如果换了路由器、改了网关IP,局域网设备短时间内可能解析到旧地址,这就是“交换机/路由器都换了,但设备还是连不上”的常见原因:清一下ARP缓存就好了。

另外一个不能回避的话题是ARP安全问题。ARP协议本身没有身份校验,局域网里任何一个设备都可以声称“我就是网关”,把发给网关的数据包骗走——这就是ARP欺骗。我家里就把路由器的静态ARP绑定做过一遍,这是最简单的防护手段。在企业网络里还会有专门的动态ARP检测,但家用网关上绑定MAC和IP,对付绝大多数情况已经够了。


5. 服务器响应之后:浏览器如何把HTML渲染成页面

5.1 从响应字节流到DOM树

服务器收到小明的GET请求后,返回一坨text/html字节流。浏览器拿到数据后,连字符编码都没法定死,因为HTML里可能写明charset,但也可能依赖HTTP头。确定编码后,字节流被切分成一个个token,再按HTML语法规则组装成节点,最后构造成一棵DOM树。

这里有一个新手不太理解的点:DOM不是因为页面好看才存在,它是浏览器内部对HTML文档的结构化表示,JS脚本能通过DOM API操作页面,浏览器的渲染引擎也是基于它工作的。这个阶段一旦遇到语法错误,浏览器并不会直接崩溃,而是通过“错误恢复机制”尽力解析下去——HTML规范里专门写了各种错误怎么处理,这一点和XML“报错就不生”的哲学完全不同。

5.2 CSS和DOM结合:渲染树、布局和绘制

CSS字节流经过类似的过程,变成CSSOM树,描述每个元素的样式。DOM树和CSSOM树合并成渲染树,但渲染树不是简单把两者拼起来,它只包含“会真正显示到页面上的节点”——比如display: none的元素就不会进渲染树,而visibility: hidden的元素虽然看不见,但还占着空间,会进渲染树。

接下来是布局,浏览器要计算每个节点在视口里的几何坐标。这里有个容易误导人的概念:你以为CSS写的是“宽度10px”,实际布局引擎处理的是大量相互影响的约束计算,特别是flex、grid这种现代布局,在计算过程中可能要多次回溯调整。

然后进入绘制阶段,浏览器把每个节点绘制成位图层。这里要点名一个性能关键概念:绘制不是全局重头来一遍,而是分层的。每个图层有自己的纹理,合成器再把所有图层像叠相片一样拼到屏幕上。这也是为什么transform和opacity动画往往比修改top、left动画更流畅——因为前者直接在合成阶段处理,不需要重新布局和绘制。

5.3 JavaScript对渲染路径的干扰

渲染流程中最容易被“卡住”的环节是JavaScript。浏览器解析HTML时碰到<script>标签,如果是普通的同步脚本,会立刻停下来,先下载并执行完脚本再继续解析后续HTML。所以脚本放<head>里会导致页面首屏的时间明显变长。

解决办法不外乎两种:把script标签放在body底部;或者给脚本加上defer或async。但两者有区别,defer保证执行顺序与文档顺序一致,适合有依赖的多个脚本;async则下载完就执行,谁先到谁先跑,适合独立的统计脚本、广告脚本。我自己实测过,同样的页面结构,把三个同步脚本改成defer,首屏时间能缩短几百毫秒,在慢速移动网络上差距更明显。


6. 从这条链路反推排障思路:页面打不开时,问题最可能出在哪

6.1 沿着协议链路快速定位故障环节

搞懂整条链路以后,最大的收益不是“会背协议”,而是“遇事知道先查什么”。

故障现象最可能出问题的环节第一反应
输入IP可以访问,但输域名不行DNS解析本机DNS缓存、本地DNS服务器、域名托管商的解析记录
域名解析正常,但TCP一直转圈网络层连接防火墙拦截、路由不可达、目标服务器拒绝连接
浏览器提示证书错误TLS握手证书链不完整、日期不对、域名不匹配
首屏白屏但curl能拉到HTML渲染资源加载JS执行错误、CSS阻塞、资源路径404
打开很慢但最终能打开内核协议栈参数或链路质量RTT太高、MTU分片、MSS不一致

这个表格是我平时最快的一个排查框架。比如“输入IP能开、域名不行”这件事,直接用nslookup看解析结果就能确认是不是DNS层面的问题:

nslookup www.demosite.net

如果返回了正确的IP,但浏览器还是打不开,那问题可能在本机缓存或HSTS(浏览器强制HTTPS)上,这时候就要换个端口/换一个隐身窗口来排除浏览器端干扰。

6.2 一个典型的“能打开但很慢”的排查过程

我遇到过类似情况:小明访问网页能成功,但首屏要等好几秒。打开浏览器开发者工具的Network面板,发现一个CSS文件耗时2.8秒。问题出现在哪?

链路反过来排查:curl直接拉这个CSS文件,本地测很快;那问题就不在服务器。再看Network面板,浏览器对该资源的加载是排队状态——原来是页面里有两三百张小图片,每个都单独发HTTP请求,同一时刻浏览器对同一域名只能开有限的N条连接,大头全被图片请求占住了。

这种问题常见的解法是域名分片或者开启HTTP/2多路复用。如果服务器支持HTTP/2,同一个连接上可以并发传输多个资源,排队问题就能缓解大半。我当时的处理是把静态资源移到某个支持HTTP/2的CDN前面,问题直接消失。

也有另一种情况:curl能秒开,浏览器很慢,但Network面板里资源全都正常。这时要把注意力转到渲染链路上,看是不是JS在DOMContentLoaded之后做了大量同步计算,导致渲染进程被阻塞。打开Performance面板录制一次加载,是最快的定位方式。

6.3 排障过程中最容易忽略的三个细节

第一,IPv6优先级。如果你访问的域名同时有A记录和AAAA记录,而家庭网络的IPv6连通性有问题,操作系统可能一直尝试IPv6、超时后再退回IPv4,白白浪费几秒。排查时可以直接关掉系统IPv6,看问题是否消失。

第二,路由器的MTU分片陷阱。家用宽带默认MTU通常控制在1500,但如果PPPoE拨号让链路MTU变小了,而两边没有正确协商MSS,就可能出现小包通、大包不通的诡异现象:网页能开,但下载大文件卡死。这时候可以手动调低路由器WAN口的MTU到1492,顺带打开MSS Clamp。

第三,别一上来就上抓包工具。排查大部分问题,先用浏览器开发者工具和curl -v就够了。Wireshark适合抓本地环境的问题,比如ARP欺骗、TCP重传、异常RST,但看到一堆乱糟糟的报文之前,你得先确定自己到底在查哪一层的问题。我见过太多人开着Wireshark不知道该过滤什么,等我把问题定位到HTTP层时,他还在逐条翻TCP重传。

从DNS缓存到HTML渲染,这条链路我拆完以后,再回过头看那个按了一次回车的“小明”,感觉就像看了一场默剧——每个角色都在自己的位置上安静地工作,没有谁特别重要,但没有谁可以缺席。我自己在工作里排查网络问题时,最依赖的就是这套整体视角,建议你也试着把知识按“数据从哪来到哪去”的主线重新串一遍,会比死记协议字段有用得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进