输入网址与 URL 解析
整段旅程的第一步,发生在数据还一个字节都没出你的手机之前。浏览器要先看懂你输入的那串字:它是网址还是搜索词?协议写全了吗?主机是哪一段?端口是几号?——这一节讲的就是这场"读单子"的过程,以及它为什么比你想象的复杂得多。
你写了一张地址条塞给快递小哥:"朝阳区幸福路 88 号 3 栋 1201 室 张三 收 · 易碎"。
小哥不会一眼就"看到一个地址",他是一段一段拆的:先看城市(决定发哪个分拨中心)、再看区和路(决定哪个网点)、再看栋和室(决定哪个快递员的哪一趟)、最后看备注(决定要不要加气泡膜)。
拆错一段,包裹就跑偏:城市看错,货直接飞去另一个省;室号看错,敲了半天邻居家的门。
浏览器处理网址完全是这个套路。https://shop.example.com:443/item/8848?color=red#reviews 这一串在你眼里是"一个链接",在浏览器眼里是七个必须分开处理的字段——而其中有一段(#reviews)压根不会发给服务器,就像"易碎"这个备注只给快递员看、收件人根本不知道。
上一步刚完成了什么,这一步要干什么
这是全章的第一节,所以"上一步"就是你的手指:你点了链接,或者在地址栏敲完字按了回车。此刻浏览器手里只有一样东西——一串字符。除此之外它什么都没有:不知道对方在哪、没有任何连接、没发出任何一个字节。
这一节要干的事是:把这串字符变成一份"可执行的行动计划"。干完之后,浏览器手里会多出下面这几样东西,它们是后面每一节的入场券:
- 用哪种协议是 HTTP 还是 HTTPS?——这决定了 § 4.4 的 TLS 握手要不要做
- 要找哪台机器主机名(如
shop.example.com)——这是 § 4.2 DNS 查询的输入 - 敲哪个门端口号(默认 80 或 443)——这是 § 4.3 建立 TCP 连接的目标端口
- 要什么东西路径 + 查询字符串——这是 § 4.5 请求行里那一段
- 拿到后滚到哪片段标识(
#reviews)——只有本机用得上,跟服务器无关
一句话概括这一节的产出:把"一串字"变成"协议 + 主机 + 端口 + 路径"这四件确定的事。后面五节全是在这四件事的基础上往下走。
先把这一节的术语翻译成人话
这一节会冒出十来个术语,多数长得很像、很容易混。先用一张表把它们对齐到你熟悉的东西上,后面再遇到就不慌了。
| 术语 | 换成大白话 | 生活里对应的东西 |
|---|---|---|
| URL(Uniform Resource Locator,统一资源定位符,读作"U-R-L") | 说白了就是"一件东西在网上的完整地址" | 一张写全了的快递面单:省市区路号栋室,一样不少 |
scheme(方案 / 协议部分,如 https) | 说白了就是"用哪种方式送" | 邮局窗口的选项:平邮、挂号、EMS、同城闪送——同一个地址,送法不同 |
| host(主机,域名或 IP) | 说白了就是"哪一台机器" | "城东那家老李杂货店"这个店名 |
| port(端口号) | 说白了就是"这栋楼的哪一个门" | 小区门牌里的"几号房"(§ 1.3 讲过) |
| path(路径) | 说白了就是"进门之后往哪走" | 图书馆的索书号:三楼 → B 区 → 第 7 排 → 第 3 格 |
| query(查询字符串,问号后面那串) | 说白了就是"附加的要求" | 点菜时说的"少盐、不要葱、加个蛋" |
| fragment(片段标识,井号后面那段) | 说白了就是"拿到之后翻到第几页" | 借到书之后自己翻到第 88 页——图书馆压根不知道你要看哪一页 |
百分号编码(Percent-Encoding,用 % 加两位十六进制表示特殊字符) | 说白了就是"把不能直接写的字,换成一个代号写" | 填表时姓名栏写不下生僻字,就在备注里注音说明 |
| IDN(Internationalized Domain Name,国际化域名——非英文的域名) | 说白了就是"中文、日文、阿拉伯文写的域名" | 银行的中英文双名招牌:门口挂中文,营业执照上是英文全称 |
| HSTS(HTTP Strict Transport Security,严格传输安全——网站声明"以后只准用 HTTPS 来找我") | 说白了就是"这家店贴了张条:只收刷卡,别递现金" | 商场门口的"本店谢绝自带饮食"告示,你还没进门就已经生效 |
| omnibox(多功能地址栏——既能输网址也能输搜索词的那一栏) | 说白了就是"一个既能查号又能拨号的框" | 医院的一体化窗口:挂号、缴费、查报告都在这一个窗口办 |
看完这张表,你大概已经能猜到这一节的味道了:URL 这个东西,本质上就是把"一份写得足够清楚的地址"标准化成机器能严格解析的格式。标准化到什么程度?连"哪个字符算分隔符"都写进了国际规范——那份规范叫 RFC 3986(2005 年发布,URI 的通用语法标准)。后面凡是提到"规范怎么规定的",指的都是它。
打个比方,把 https://shop.example.com:443/item/8848?color=red#reviews 摆到快递面单上,是这样一一对应的:
https —— 面单最上面勾选的"服务方式":这次要走加密专线(顺丰保价件)而不是普通平邮。勾错这一栏,整个运输方式都不同。
shop.example.com —— 收件单位名称。注意:这是名字不是地址,快递系统还得拿它去查真实门牌(这就是 § 4.2 的 DNS)。
:443 —— 门牌里的房间号。多数时候不用写,因为"走加密专线默认送 443 室"是行业惯例。
/item/8848 —— 进门之后的内部路线:"货架区 → 8848 号格位"。
?color=red —— 面单备注栏的附加要求:"要红色那款"。
#reviews —— 这一栏最特别:它不写在面单上,只记在你自己的小本子上。意思是"东西到手后,我先翻到评价那一页看"。快递公司完全不知道你要看哪一页,也不需要知道。
为什么要强调最后这一条?因为它有非常实际的后果:你在网址后面加 #reviews,服务器日志里压根不会出现这几个字。所以靠服务器统计"多少人看了评价区"是统计不到的——这一段从来没离开过你的手机。
URL 的七个部分:逐个拆开看
现在正式动手拆。RFC 3986 定义的通用语法,写成一行是这个样子:
scheme ":" ["//" [userinfo "@"] host [":" port]] path ["?" query] ["#" fragment]
把它套到一个真实网址上:
https://alice:s3cret@shop.example.com:8443/item/8848?color=red&size=42#reviews
└─┬─┘ └─────┬────┘ └───────┬──────┘ └┬─┘└────┬───┘ └───────┬──────┘ └──┬──┘
①scheme ②userinfo ③host ④port ⑤path ⑥query ⑦fragment
协议 用户信息 主机 端口 路径 查询串 片段
七个部分,只有 scheme 和 host 是访问一个网站时几乎必须有的,其余五个都可以省略。下面这张表把每一部分的职责、默认值、以及"省略了会怎样"一次讲清。
| # | 部分 | 例子 | 它决定什么 | 省略时会怎样 |
|---|---|---|---|---|
| ① | scheme 协议 | https | 用哪套规矩通信,以及要不要加密 | 浏览器地址栏里会帮你猜(通常猜 https);在网页源码里省略则表示"跟当前页一样" |
| ② | userinfo 用户信息 | alice:s3cret | 直接把用户名密码写在网址里 | 绝大多数情况都省略。现代浏览器对含密码的 URL 极为警惕,因为这是钓鱼常用招数 |
| ③ | host 主机 | shop.example.com | 找哪台机器——DNS 查询的输入 | 不能省。省了浏览器不知道该找谁 |
| ④ | port 端口 | 8443 | 敲这台机器的哪个门 | 省略则用协议的默认端口:http → 80,https → 443 |
| ⑤ | path 路径 | /item/8848 | 要这台机器上的哪份资源 | 省略等价于 /,也就是网站首页 |
| ⑥ | query 查询串 | color=red&size=42 | 给这次请求附加参数 | 省略就是"没有附加要求" |
| ⑦ | fragment 片段 | reviews | 拿到内容后,本机滚动到哪个位置 | 省略就是从头显示。这一段永远不发给服务器 |
这张表里有三处最容易被误解,值得单独说清。
第一处:// 那两个斜杠不是装饰,它是"后面跟着一个主机名"的信号。对比一下就明白了:https://example.com/a 有双斜杠,表示"去 example.com 这台机器上取 /a";而 mailto:zhang@example.com 没有双斜杠,因为它压根不需要"连到某台机器",冒号后面直接就是内容。tel:10086 同理。说白了,双斜杠的意思是"这是个网络地址,得先找到机器"。
第二处:端口的默认值是"协议自带的常识",不是网址里省略了什么。你写 https://example.com,浏览器实际连的是 443 端口——这个 443 不是从网址里读出来的,是"https 默认 443"这条规矩带来的。所以如果某个网站部署在 8443 端口,你不写端口号就永远连不上,会一直卡在建连接那一步(§ 4.3 会讲这种卡法长什么样)。
第三处:userinfo 这一段是历史遗留,而且是钓鱼的经典道具。看这个网址:https://www.yourbank.com@evil.example/。粗看以为是去银行,其实 @ 前面全是"用户信息",真正的主机是 evil.example。换成大白话:这就像有人递给你一张名片,正面印着"中国银行",背面小字才写着真实地址是城郊某民房。现代浏览器会警告或直接拒绝这类地址,但你自己也该记住这条读法——看主机名只看最后一个 @ 之后、第一个 / 之前的那一段。
浏览器在联网之前先干了什么
很多人以为按下回车的下一件事就是"发数据出去"。其实在任何一个字节离开你的设备之前,浏览器已经忙完了好几轮。这些活儿全在本机完成,快得你察觉不到(微秒到毫秒级),但它们会实质性地改变后面的走向——甚至可能让整段旅程直接跳过大半。
按真实顺序,浏览器大致做这七件事:
| 顺序 | 动作 | 说白了就是 | 可能的后果 |
|---|---|---|---|
| 1 | 判断输入意图 | 你敲的到底是网址还是搜索词? | 判成搜索词就直接送去搜索引擎,整个 URL 流程另起一遍 |
| 2 | 补全与规范化 | 你少写的部分我替你补,写乱的部分我理顺 | Example.COM → example.com;/a/./b/../c → /a/c |
| 3 | 查 HSTS 名单 | 这个域名有没有声明过"只准用 HTTPS"? | 在名单上就把 http 直接改成 https,不发出任何明文请求 |
| 4 | 查本地缓存(HTTP 缓存) | 这份东西我是不是刚拿过、还没过期? | 命中且新鲜 → 整段旅程结束,直接进渲染,一个包都不发 |
| 5 | 查 Service Worker | 这个站有没有装过"本地小代理"? | 装了的话它可能直接返回离线内容,同样不联网 |
| 6 | 查 DNS 缓存 | 这个域名的 IP 我记着没? | 记着就跳过 § 4.2 的大部分工作 |
| 7 | 查连接池 | 跟这台机器我是不是还连着一条线? | 连着就跳过 § 4.3 和 § 4.4,直接发请求 |
看出规律了吗?这七步里有四步的目标都是"能不联网就不联网"。本质上就是四道由近到远的关卡,每一道都在问同一句话:"这活儿我在本地能不能办了?"办得了就当场结束,办不了才往外走一步。
打个比方,这相当于你想做个菜要用酱油:先看灶台边的调料罐(浏览器内存缓存)→ 再看厨房吊柜(磁盘缓存)→ 再看阳台储物间(本机 DNS 记录)→ 都没有才穿鞋下楼去超市(真正联网)。没人会为了一勺酱油每次都下楼,浏览器也一样。这就是为什么第二次打开同一个网站往往快得像本地文件——因为它根本就是本地文件。
第一件事:你输的是网址还是搜索词
现代浏览器的地址栏叫 omnibox(多功能地址栏),既收网址也收搜索词。这带来一个每天发生几十亿次的判断题:用户敲的这串字,该当地址解析,还是该丢给搜索引擎?
各家浏览器的具体规则不完全一样、也在持续调整,但共同的判断线索是清楚的:
- 有明确的 scheme(
https://...、ftp://...)→ 当网址,没有二话。 - 看着像域名(含点、且最后一段是已知的顶级域如
com、cn、org)→ 倾向当网址。 - 含空格(如
牛肉面 做法)→ 几乎必然当搜索词,因为域名里不能有空格。 - 单个词且能解析成主机名(如公司内网的
wiki)→ 有些浏览器会先试着当主机名连,连不上再搜。 - 纯数字(如
8848)→ 通常当搜索词,但形如93.184.216.34的四段数字会被当成 IP 地址。
这里有个真实会踩到的坑:公司内网的单词主机名和搜索词长得一样。你在办公室敲 wiki 想上内部百科,浏览器可能直接给你搜了一堆维基百科的结果。换成大白话:这就像你在办公室喊一声"小李",前台不知道你是要找同事小李,还是要她帮你查一下"小李"是谁。解决办法很朴素——敲 wiki/ 加个斜杠,或者干脆写全 http://wiki/,把意图明确掉。
还有一个安全后果值得知道:你在地址栏里敲的每一个字,都可能被实时发给搜索引擎做联想补全。这就是为什么有些公司会关掉地址栏联想功能——因为你打了一半的内部系统地址,可能已经飘出去了。相当于你在窗口前刚说出半句话,旁边的广播已经把它播出去了。
第二件事:URL 规范化——把你写歪的地址理顺
人手输入的网址往往不规矩:大小写混乱、多余斜杠、带点的相对路径、末尾少个斜杠。浏览器会先做一轮规范化(Normalization,把等价但写法不同的 URL 统一成一种标准写法)。这不是洁癖,是必须——因为缓存查找、同源判断都要靠"字符串是否相同"来做,写法不统一就查不中。
| 规范化动作 | 处理前 | 处理后 | 为什么要这么做 |
|---|---|---|---|
| scheme 和 host 转小写 | HTTPS://Shop.Example.COM/A | https://shop.example.com/A | 域名不区分大小写;但路径区分,所以 /A 保持不动 |
| 去掉默认端口 | https://a.com:443/ | https://a.com/ | 443 是 https 默认值,写与不写等价 |
| 补上空路径 | https://a.com | https://a.com/ | 空路径等价于根路径 |
| 解析点段 | /a/b/../c/./d | /a/c/d | .. 是上一级,. 是当前级,跟文件夹一个道理 |
| 百分号编码大写化 | %3a | %3A | 规范建议十六进制用大写,便于字符串比较 |
| 解开不必要的编码 | %7Euser | ~user | ~ 属于"无需编码字符",编了反而不规范 |
其中"域名不区分大小写、路径区分大小写"这一条是最容易出事的。https://Example.com/Photo.JPG 和 https://example.com/photo.jpg——前半段等价,后半段在多数 Linux 服务器上是两个不同的文件,后者很可能直接 404。本质上就是:域名这一段由 DNS 管,DNS 规定不分大小写;路径这一段由服务器上的文件系统管,而 Linux 的文件系统认大小写。相当于寄快递时"BEIJING"和"beijing"邮局都认,但收件人姓名写错一个字,前台就是找不到人。
另有一个尾斜杠的坑:https://a.com/blog 和 https://a.com/blog/ 在规范上是两个不同的 URL。多数网站会用重定向(§ 4.5 讲)把其中一个跳到另一个,但那要多花一个来回。说白了,多打一个斜杠有时真的会慢一点点——虽然肉眼感觉不到。
第三件事:HSTS——有些站你连明文都发不出去
假设你在地址栏敲了 http://shop.example.com,明明写的是不加密的 http。但如果这个域名启用了 HSTS(HTTP Strict Transport Security,严格传输安全),浏览器会在发出任何请求之前,自己把 http 改成 https。
为什么要有这个机制?因为不这么做,会留下一个致命缝隙。设想没有 HSTS 的情况:
你敲 http://bank.example
↓ 明文请求已经发出去了(这一步就是缝隙)
服务器回:301 跳转到 https://bank.example
↓
浏览器改用 HTTPS 重新访问,从此加密
问题在于:第一个明文请求已经在路上被人看见了。
更糟的是——如果路上有坏人,他可以拦下这个明文请求,
假装自己是服务器,永远不告诉你"该用 HTTPS",
你就会一直在明文里跟他聊,密码照样输给他。
这种攻击叫 SSL Stripping(降级剥离)。
HSTS 的做法是让网站在响应头里声明一句"以后 N 秒内只准用 HTTPS 来找我",浏览器记下来,之后本机自行改写,那个明文请求根本不产生。打个比方,这相当于一家银行给所有老客户发过通知:"以后来我们这儿办事只走后门那条监控走廊,前门那条巷子一律不走。"客户记住了,下次即使随口说"从前门进吧",自己走到路口也会自动拐弯。
但这里还有一个"第一次"的问题:你从来没访问过这个网站,怎么会知道它要求 HSTS?解法是 HSTS 预加载列表(Preload List)——一份内置在浏览器里的域名清单,凡在清单上的域名,哪怕你是第一次访问,浏览器也一律强制 HTTPS。换成大白话:这份清单相当于印在员工手册里的"以下这些客户一律走加密流程",新员工上岗第一天就知道,不需要客户再说一次。
这带来一个很实用的知识点:你在浏览器里访问某些站点,明明输的是 http,地址栏却瞬间变成 https,中间连一次网络请求都没有。这不是网站跳转的,是你自己的浏览器改的。查看方法是在浏览器的网络面板里看——你会发现根本没有那条 301 记录。
第四件事:四级缓存查找——最快的请求是没发出去的请求
接下来浏览器要问一句非常实在的话:"这东西我手上是不是已经有了?"这一问不是问一次,而是由近到远问四次,因为缓存不是一个地方,是好几层。
| 层级 | 存在哪 | 速度量级 | 生活里对应 | 清空方式 |
|---|---|---|---|---|
| 内存缓存(Memory Cache) | 浏览器进程的内存里 | 纳秒到微秒级 | 灶台边那个盐罐——伸手就够 | 关掉标签页就基本没了 |
| Service Worker | 网站自己注册的本地脚本 | 微秒到毫秒级 | 你雇的私人采买——他自己有小仓库 | 在开发者工具里注销 |
| 磁盘缓存(Disk Cache) | 硬盘上的缓存目录 | 毫秒级(随机读) | 厨房吊柜里那袋备用的 | 清除浏览数据 |
| 推送缓存 / 连接级缓存 | 当前连接的生命周期内 | 微秒级 | 刚拆开还摊在案板上的那份 | 连接一断就没了 |
关键在于"命中"分两种,后果完全不同,这是很多人混淆的地方:
- 强缓存命中(还在保质期内):浏览器一个字节都不发,直接用本地那份。开发者工具里显示
from disk cache或from memory cache。整段旅程的第 2 到 5 步全部跳过。 - 协商缓存命中(过期了但内容没变):浏览器还是要发一次请求去问"我这份还能用吗",服务器回一个很短的
304 Not Modified(未修改)。省的是"传内容"的时间,省不了那一个来回。
本质上就是两种不同的做法:前者是"冰箱里的酸奶还没到保质期,直接喝";后者是"保质期到了,但打电话问店家说这批延期了还能喝"。后一种还是得打个电话,前一种连电话都不用打。所以做网站优化时,把资源设成长期强缓存的收益远大于依赖协商缓存——省掉的是整个来回。
顺便说清一个常见误解:按 F5 刷新和按 Ctrl+F5 强制刷新,走的路完全不同。普通刷新一般还会用缓存(但可能带上协商);强制刷新则会告诉浏览器"当我从来没来过,全都重新拿"。相当于普通刷新是"再看一眼冰箱",强制刷新是"把冰箱清空重新采购一遍"。这就是为什么改了网页样式看不到效果时,强制刷新往往有用——这也正是本站每次改共享样式都要给文件加版本号的原因。
路径与查询串:一条分界线两种性质
路径(/item/8848)和查询串(?color=red)都在描述"我要什么",但它们的性质有一条重要区别,理解了这条区别,你会一下看懂很多网站的地址设计。
路径描述的是"东西在哪",查询串描述的是"怎么处理它"。换成大白话:路径是图书馆的索书号,指向书架上确定的那一本;查询串是你递给馆员的附加条件——"我要复印版""帮我按出版年排序""只给我第二章"。索书号变了就是另一本书;附加条件变了还是那本书,只是给你的形式不同。
| path 路径 | query 查询串 | |
|---|---|---|
| 写法 | 斜杠分层:/a/b/c | 问号开头,键值对用 & 连:?k1=v1&k2=v2 |
| 层级关系 | 有——像文件夹一层套一层 | 无——键值对之间是平的,顺序原则上无所谓 |
| 典型用途 | 定位资源:哪个商品、哪篇文章、哪个用户 | 筛选、排序、分页、追踪来源、翻译语言 |
| 大小写 | 区分(由服务器/文件系统决定) | 区分(由后端程序决定) |
| 对缓存的影响 | 不同路径必然是不同缓存条目 | 不同查询串通常也算不同条目——这点常被忽略 |
| 会不会进日志 | 会 | 会——所以绝对不要把密码放在查询串里 |
最后那一条是这一节里最该被记住的安全常识。查询串会出现在服务器访问日志、浏览器历史、代理日志、甚至第三方统计里。如果登录接口写成 /login?user=alice&pass=123456,那这个密码就同时被记在了至少四个地方。打个比方,这相当于你在银行大厅把密码大声念给柜员——柜员听清了没错,但排队的人、监控录像、大堂经理的记录本全都收到了。正确做法是把敏感数据放进请求体(POST 的 body),这一点 § 4.5 会详说。
还有一个查询串带来的实际问题:它会撑爆缓存。很多分享链接后面会挂一串追踪参数(比如 ?from=wechat&share_id=xxxxx),每个人分享出来的参数都不一样,于是同一个页面在缓存里变成了成千上万个不同条目。说白了,就好比同一款商品因为每张标签上的经手人编号不同,仓库被迫按编号分格存放,一款货占了一万个格子。这也是为什么专业的 CDN 配置里会有"忽略指定查询参数"这一项。
片段标识:那个永远不出门的 #
七个部分里,fragment(片段标识,井号后面那段)最特殊,因为它从来不会被发送给服务器。这不是某个浏览器的实现选择,是 RFC 3986 明确规定的:片段的解释责任在客户端。
它的原始用途很朴素:页内跳转。https://a.com/doc#chapter3 的意思是"把 /doc 这份文档拿来,然后滚动到 id 为 chapter3 的那个元素"。前半段是网络的事,后半段是渲染的事(§ 4.6 讲)。
这个"不出门"的性质带来三个很实际的后果:
- 改片段不会重新请求。你在页内点几个目录链接,地址栏的
#后面不断变化,但网络面板里一条新请求都没有。相当于你借了一本书回家,翻到第几页是你自己的事,图书馆不会因为你翻页而重新给你一本。 - 服务器统计不到片段。想知道"多少人看了页面的评价区"?靠服务器日志是查不出来的,只能靠前端脚本上报。
- 片段可以被当作"前端路由"用。早期的单页应用(打开一次就不再整页刷新的网页)就靠改
#后面的内容来切换"页面",因为这样切换不触发网络请求,切得飞快。
另外补一条容易混的:# 后面的内容和 ? 后面的内容顺序不能颠倒。规范规定 query 在前、fragment 在后。写成 /a#b?c=d 的话,?c=d 会整个被当成片段的一部分——因为片段一旦开始,后面所有字符都归它。本质上就是"井号是最后一道闸门,闸门一落,后面全归自己管"。
百分号编码:为什么网址里会出现一堆 %E4%B8%AD
你一定见过这种网址:https://a.com/search?q=%E7%89%9B%E8%82%89%E9%9D%A2。那一串百分号看着像乱码,其实是百分号编码(Percent-Encoding)——把一个不能直接写进 URL 的字符,换成"% + 该字符字节的两位十六进制"来表示。
为什么需要这套东西?因为 URL 里有些字符已经被派了"结构性差事":/ 分层、? 开查询、& 分参数、# 开片段、= 连键值。如果你要搜的内容本身就含这些字符,就必须先把它"伪装"起来,否则解析器会当成结构符号处理,整个网址被拆错。
打个比方,你去邮局填一张汇款单,"附言"那一栏只有 20 格,而且格与格之间用竖线分开。现在你的附言里本身就含一个竖线符号——直接写进去,柜员会以为那是分格线,附言被劈成两截。
正规做法是:约定一个"转义写法",比如规定"竖线一律写成 #124 这样的代号"。柜员看到 #124 就知道"这不是分格线,这是内容里的一个竖线字符"。
URL 里的百分号编码就是这个代号系统。/ 写成 %2F、空格写成 %20、? 写成 %3F、& 写成 %26。至于中文,因为 URL 规范只允许 ASCII 字符,所以中文得先按 UTF-8 编码变成几个字节,每个字节再各自写成一个 %XX。"中"这个字在 UTF-8 里是三个字节,所以写出来就是三段:%E4%B8%AD。
这也解释了一个常见现象:为什么复制一个含中文的网址,粘出来会变得巨长。因为你在地址栏看到的中文是浏览器"翻译回来给你看"的样子,真正传输的一直是那串百分号。相当于柜员在单据上写的是代号,但打印给你的回执上贴心地还原成了你能读的字。
常见字符的编码值,记几个最有用的就够:
| 字符 | 编码 | 为什么必须编码 | 常见踩坑场景 |
|---|---|---|---|
| 空格 | %20(表单里也可能是 +) | URL 里不允许出现裸空格 | 文件名带空格的下载链接,复制到聊天软件被截断 |
/ | %2F | 它是路径分隔符 | 参数值里带日期 2026/08/08,不编码会被当成三层路径 |
& | %26 | 它是参数分隔符 | 公司名叫"张三 & 李四",不编码会被切成两个参数 |
= | %3D | 它是键值连接符 | Base64 编码的值结尾常有 =,不处理会解析错乱 |
# | %23 | 它是片段起始符 | 参数值里带话题标签,不编码则后面全被吞进片段 |
% | %25 | 它自己就是转义符 | 要表示"打折 50%",不编码会被误当成一个编码的开头 |
| 中文(如"中") | %E4%B8%AD | URL 只允许 ASCII | 不同系统按不同字符集编码,导致对方看到乱码 |
最后一行藏着一个真实的经典 bug:如果一边按 UTF-8 编码、另一边按 GBK 解码,中文就会变成乱码。这不是"网络问题",是两头对同一串字节的理解不一致。说白了,就好比你按公制写了"3 尺",对方按英制读成了"3 英尺"——数字没错,单位错了,结果差得远。所以现代规范一律要求 URL 中的非 ASCII 字符按 UTF-8 编码。
中文域名:那个 xn-- 开头的怪东西
域名部分的非英文字符处理,跟路径部分完全是另一套机制,这也是最容易混淆的地方。路径里的中文用百分号编码,域名里的中文用一套叫 Punycode 的转换法。
原因在于 DNS 系统(§ 3.4 会详讲 DNS 协议本身)诞生于 1980 年代,它的设计里只认得 ASCII 里的字母、数字和连字符。要在这套老系统上支持全世界的文字,又不能推翻重来,工程师们想出的办法是"把非英文字符可逆地翻译成一段合法的 ASCII"。这套翻译法叫 Punycode,翻译出来的域名一律以 xn-- 开头。
你看到的(浏览器显示给人看): 中国.cn
实际在 DNS 里查询的(机器用): xn--fiqs8s.cn
你看到的: 例子.测试
实际查询的: xn--fsqu00a.xn--0zwm56d
规律:xn-- 是固定前缀,意思是"后面这段是被翻译过的"。
所以在地址栏看到 xn-- 开头的域名,说明它原本是非英文的。
这套机制叫 IDN(Internationalized Domain Name,国际化域名)。它带来一个非常严重的安全隐患,叫同形字攻击(Homograph Attack):世界上有些字母长得几乎一样但编码完全不同——比如西里尔字母的 а 和拉丁字母的 a,肉眼几乎无法区分。攻击者可以注册一个用西里尔字母拼的"apple.com",域名在你眼里跟真的一模一样,实际却是完全不同的域名,指向完全不同的服务器。
换成大白话:这就像有人开了一家店,招牌上"永和大王"四个字里的"和"用的是另一个长得几乎一样的异体字。你站在门口看不出差别,工商登记里却是两家毫不相干的公司。浏览器的应对办法是:当检测到一个域名混用了多种文字体系时,直接把地址栏里的显示切换成 xn-- 原形——让你一眼看出"这域名不对劲"。所以下次看到地址栏出现 xn--,而你并没有故意访问中文域名,那就要提高警惕了。
URL、URI、URN:三个词到底什么关系
你会在文档里同时见到 URL 和 URI 两个词,有时还会冒出 URN。它们的关系其实很简单,一张表加一句话就说完了。
| 缩写 | 全称 | 中文 | 它回答的问题 | 例子 |
|---|---|---|---|---|
| URI | Uniform Resource Identifier | 统一资源标识符 | "这是哪个东西?"——只要能唯一标识就算 | 以下两者都是 URI |
| URL | Uniform Resource Locator | 统一资源定位符 | "这东西在哪、怎么拿?" | https://a.com/book/1 |
| URN | Uniform Resource Name | 统一资源名称 | "这东西叫什么?"——不管在哪都是它 | urn:isbn:9780132350884 |
一句话记住:URI 是大类,URL 和 URN 是它下面的两种,区别是 URL 告诉你"去哪拿",URN 只告诉你"它叫什么"。
打个比方,一本书的 ISBN 书号就是 URN——不管这本书在北京图书馆还是在你家书架上,书号永远是那一串,但书号本身不告诉你去哪儿能借到。而"市图书馆三楼 B 区第 7 排第 3 格"就是 URL——它告诉你确切位置,能照着去拿,代价是书一挪位置,这个地址就失效了。
这个区别解释了网络上一个永恒的痛:链接失效。你收藏的网址过两年打不开了,因为 URL 绑定的是"位置"而不是"东西本身"。网站一改目录结构,所有旧链接全断。说白了,URL 记的是"东西放在哪个格子",格子一动就找不着了。这也是为什么正规的学术资料会用 DOI(数字对象唯一标识符)这类偏 URN 性质的编号,而不是直接给网址。
实践中该用哪个词?规范文档里一律用 URI(RFC 3986 的标题就是 URI),日常和代码里习惯说 URL。两者在讲"一个网址"这件事上,日常语境里可以当同义词用,不必纠结。
同源策略:为什么 URL 的前三段决定了"谁能访问谁"
URL 的前三段——协议 + 主机 + 端口——合在一起有个专门的名字叫源(Origin)。这三段只要有一个不同,就算不同源。浏览器有一条铁律:不同源的页面之间,默认不许互相读对方的数据。这条规矩叫同源策略(Same-Origin Policy)。
和 https://a.com/x 比 | 同源吗 | 为什么 |
|---|---|---|
https://a.com/y/z | 是 | 只有路径不同,路径不参与判断 |
http://a.com/x | 否 | 协议不同(一个加密一个不加密) |
https://b.com/x | 否 | 主机不同 |
https://sub.a.com/x | 否 | 主机不同——子域名也算不同主机,这点最常被误解 |
https://a.com:8443/x | 否 | 端口不同 |
为什么要这么严?想一个场景就明白了:你在一个标签页登录了网银,同时另一个标签页开着某个不认识的小网站。如果没有同源策略,那个小网站的脚本就能直接读取网银页面里的余额、账号,甚至替你发起转账。说白了,同源策略相当于办公楼里每家公司都锁自己的门——大家共用大楼(浏览器),但不许串门翻别人的抽屉。
子域名也算不同源这一条,实际影响很大。shop.example.com 和 pay.example.com 虽然是同一家公司的,在浏览器眼里也是两家。要让它们互通,得由服务器明确授权——这就是 CORS(Cross-Origin Resource Sharing,跨源资源共享)机制,服务器在响应头里点名说"我允许某某源来读我"。相当于一家公司主动给另一家发一张门禁卡,写清楚"此卡只能进会议室,不能进财务室"。
这里要澄清一个很常见的误解:同源策略限制的是"读取响应",不是"发出请求"。一个跨源的请求往往真的发出去了、服务器也真的处理了,只是浏览器不把结果交给发起它的脚本。换成大白话:信寄出去了、对方也回信了,但门口的保安把回信扣下不给你看。这个区别很要紧——它意味着"因为同源策略所以别人打不到我的接口"是错的想法,服务器端该有的校验一样都不能省。
整节小结:一次 URL 解析的完整时间线
把这一节讲的全部动作按真实顺序串起来,就是下面这张流水线。这也是本章的"第 0 和第 1 阶段"的全貌。
你按下回车(假设输入的是 shop.example.com/item/8848)
│
├─① 意图判断:含点、末段是 com → 判定为网址,不去搜索
│
├─② 补全 scheme:地址栏没写协议 → 补 https(现代浏览器倾向优先试 HTTPS)
│
├─③ 查 HSTS:这个域名在名单上吗?在 → 强制 https,明文请求不产生
│
├─④ 语法解析(按 RFC 3986 拆七段)
│ scheme = https
│ host = shop.example.com
│ port = 443(默认值,不是从字符串里读的)
│ path = /item/8848
│ query = (空)
│ fragment = (空)
│
├─⑤ 规范化:host 转小写、点段展开、编码大写化
│
├─⑥ 域名处理:含非 ASCII 吗?含 → Punycode 转成 xn-- 形式
│
├─⑦ 查 HTTP 缓存:有新鲜副本吗?
│ ├─ 有 → 直接进渲染(§ 4.6),整段网络流程全部跳过 ★
│ └─ 没有 → 继续
│
├─⑧ 查 Service Worker:有拦截脚本吗?有 → 交给它决定
│
├─⑨ 查连接池:跟 shop.example.com:443 还有活着的连接吗?
│ ├─ 有 → 跳过 § 4.3 和 § 4.4,直接发请求 ★
│ └─ 没有 → 继续
│
└─⑩ 交出主机名,进入 DNS 查询 → 这就是 § 4.2 的起点
注意那两个 ★ 号。它们标出的是真实世界里最常走的两条捷径——你刷一个已经打开过的网站时,绝大多数请求都从这两个岔口就结束了。本质上就是:完整走七个阶段这件事,在你一天的上网行为里可能只发生几十次;剩下成千上万次请求,都是靠这些捷径省下来的。
动手验证:三条你现在就能试的命令
光看不练记不住。下面三件事在你自己的电脑上就能做,花不到两分钟,做完你对这一节的理解会牢固得多。
第一件:看浏览器怎么拆一个 URL。在任何网页上按 F12 打开开发者工具,切到 Console(控制台),粘这一行按回车:
new URL('https://alice@shop.example.com:8443/a/b/../c?q=%E4%B8%AD&n=1#top')
浏览器会把它拆成一个对象,你会看到:
protocol : "https:"
username : "alice"
hostname : "shop.example.com"
port : "8443"
pathname : "/a/c" ← 注意 ".." 已经被解掉了
search : "?q=%E4%B8%AD&n=1"
hash : "#top"
这就是浏览器内部真实的解析结果——七段清清楚楚。
第二件:亲眼看 Punycode 转换。还在控制台,试这一行:
new URL('https://中国.cn/').host
输出会是 "xn--fiqs8s.cn" —— 你看到的中文,机器用的是这一串。
第三件:看缓存有没有命中。打开开发者工具的 Network(网络)面板,随便刷新一个网站,看 Size(大小)那一列:写着具体字节数的是真从网上拿的;写着 from memory cache 或 from disk cache 的,就是压根没出门的;写着 304 的,是出了门但只问了一句"变了吗"。这三种在同一页上往往同时存在——一次页面加载里,有些资源走了完整旅程,有些一步没动。
做完这三件事,你对"URL 解析"的印象就从"一个术语"变成了"一件看得见的事"。这也是本章的读法:每一节都尽量给你一个能自己验证的动作,因为流程类的知识光记顺序最容易忘,动手看过一次就忘不掉。
相对 URL:网页里九成的链接都不是写全的
到目前为止我们看的都是绝对 URL(写全了协议和主机的那种)。但你打开任何网页的源码,会发现里面绝大多数链接都是这个样子:/css/main.css、../images/logo.png、next.html。这些叫相对 URL——它们不写全,靠"以当前页面为基准"来推算。
浏览器要把相对 URL 补成绝对 URL 才能发请求,这个过程叫解析基准(Resolution against a base URL),RFC 3986 专门有一节讲它的算法。规则不难,看一张表就懂:
| 写法 | 叫什么 | 以 https://a.com/blog/2026/post.html 为基准,补成 |
|---|---|---|
https://b.com/x | 绝对 URL | 就是它自己,跟基准无关 |
//b.com/x | 协议相对 | https://b.com/x——只借协议,主机另说 |
/css/main.css | 根相对 | https://a.com/css/main.css——从网站根开始 |
img/logo.png | 路径相对 | https://a.com/blog/2026/img/logo.png——从当前目录开始 |
../img/logo.png | 上级相对 | https://a.com/blog/img/logo.png——先退一级 |
?page=2 | 只换查询串 | https://a.com/blog/2026/post.html?page=2 |
#top | 只换片段 | 同一页面,只滚动位置——不发请求 |
换成大白话,相对 URL 就是"省略了共同前缀的说话方式"。你在办公室里跟同事说"去三楼会议室",不用说"某市某区某路 88 号写字楼 A 座三楼会议室"——因为你俩都在这栋楼里,前缀是共识。但一旦这句话被传到楼外的人耳朵里,就完全没法用了——他不知道是哪栋楼的三楼。
这正好解释了两个常见 bug:
- 把网页里的链接复制到别处就打不开。因为复制走的是相对写法,脱离了原来的"基准"。这就是为什么复制链接要用右键"复制链接地址"(拿到的是浏览器已经补全的绝对 URL),而不是从源码里抠。
- 页面移动一层目录,所有相对链接一起坏掉。把
/blog/post.html挪到/blog/2026/post.html,里面写的img/a.png就从指向/blog/img/a.png变成了指向/blog/2026/img/a.png。相当于整栋楼的门牌没变,但你把办公室从二楼搬到三楼,所有"隔壁那间"的说法全部指错了地方。
这也是为什么老练的开发者偏好根相对写法(/css/main.css):只要网站根不变,页面挪到哪一层都不影响。说白了就是把参照点从"我现在站的地方"换成"大楼门口"——参照点越稳定,说法越不容易失效。
常见的五种 URL 出错场景与它们的症状
这一节的知识最直接的用处,是让你看到一个打不开的链接时能先判断问题出在哪一段,而不是笼统地说"这链接坏了"。下面五种是最常遇到的,症状各不相同。
| 症状 | 大概是哪一段出了问题 | 为什么会这样 | 怎么确认 |
|---|---|---|---|
| 浏览器直接去搜索了,没访问网站 | scheme / 意图判断 | 输入被当成了搜索词(常见于单词主机名、含空格) | 手动补上 http:// 再试 |
| 提示"无法找到该服务器"或 DNS 相关错误 | host 主机名 | 域名拼错、域名过期、或本机 DNS 有问题 | 用 nslookup 域名 看能不能解析出 IP(§ 4.2 详讲) |
| 页面一直转圈直到超时 | port 端口或防火墙 | 端口写错、或对方端口没开、或被中间设备拦了 | 换个网络试;确认端口号写对了 |
| 返回 404 找不到页面 | path 路径 | 路径拼错、大小写错、或资源已被删除移动 | 把路径逐段删掉往上试,看哪一层还在 |
| 页面打开了但内容不对/参数没生效 | query 查询串 | 参数名写错、值没做百分号编码、或 & 被误写成 & | 逐个删参数试,定位是哪个参数的问题 |
这张表里最值得学的是第四行那个"逐段删路径往上试"的手法。比如 https://a.com/docs/2026/guide/intro.html 报 404,你依次试 /docs/2026/guide/、/docs/2026/、/docs/——哪一层能打开,问题就在下一层。本质上就是二分排查:相当于找不到某间办公室时,先确认楼对不对、再确认层对不对、最后才确认门牌对不对,而不是站在门口反复敲同一扇门。
另外补一条特别容易忽略的:从聊天软件或文档里复制来的链接,末尾常常粘上了标点或空格。中文输入法打出的句号、右括号、书名号跟在链接后面,会被一起复制进去,于是路径莫名多了个字符导致 404。相当于抄地址时把句号一起抄进了门牌号里。碰到"这链接明明是对的却打不开",先检查一下末尾有没有多余字符——这一招解决过无数次问题。
这一节讲的是整段旅程里唯一一个字节都还没出门的阶段——但它决定了后面所有阶段的走向。
URL 七段(RFC 3986):scheme 协议 · userinfo 用户信息 · host 主机 · port 端口 · path 路径 · query 查询串 · fragment 片段。只有 scheme 和 host 是必须的;端口有默认值(http 80 / https 443);片段永远不出门。
浏览器在联网前做七件事:判断意图(网址还是搜索词)→ 补全与规范化 → 查 HSTS(可能直接改写成 https)→ 查 HTTP 缓存 → 查 Service Worker → 查 DNS 缓存 → 查连接池。其中四件事的目标都是"能不联网就不联网"——最快的请求永远是没发出去的那个。
三条最实用的细节:域名不分大小写、路径分大小写(这是 404 的常见来源);密码绝不能放查询串(它会进日志、历史、代理记录至少四个地方);域名里的中文走 Punycode(xn--),路径里的中文走百分号编码(%XX)——两套完全不同的机制,别混。
前三段(协议+主机+端口)合起来叫"源",它决定了同源策略——子域名也算不同源,而且这条策略限制的是"读响应"不是"发请求"。
下一步交出什么:这一节的最终产物就是一个主机名 shop.example.com。它马上要被交给下一位——DNS,去换回一个真正能拨号的 IP 地址。§ 4.2 就从这里接着走。