本篇就是计算机网络系列的第二篇,主要讲 URL 和 DNS。

其余几篇的目录:
第一篇:计算机网络 - IP 与端口

URL

URL,全称是 统一资源定位符 (Uniform Resource Locator),是我们访问互联网上任何资源的“地址”。它由几个部分组成,共同指明了“去哪里”以及“如何去”。

在接下来的内容中,我们将逐一解析构成 URL 的几个核心元素。

域名

上一篇我们讲到了 IP 和端口,然而绝大多数的时候我们在浏览器地址栏中输入的都不是这些数字,而是由英文字母和部分字符组成的地址,也就是 域名

对于一般人而言,一串 IP 地址很难记忆,人们更擅长以字词为单位,赋予地址含义再加以记忆。于是乎人们发明了 “域名” 这个概念,用来代替 IP 地址去记忆。

域名的结构

一个域名通常由几个部分组成,用点(.)分隔,从右到左看,层级越来越具体。以 www.google.com 为例,让我们来剖析一下域名的格式和结构:

  • .com:这是顶级域名 (Top-Level Domain, TLD)。它表示域名的类别。
  • google:这是二级域名 (Second-Level Domain, SLD)。这是域名的核心部分,通常是公司、品牌或个人的名称。这是用户注册和拥有的部分。
  • www:这是子域名标签 (Subdomain Label)。它是一个可选的、用于进一步细分主域名的“标签”或“前缀”。
    • 它允许你在同一个主域名下创建不同的分区或服务。
    • www 是最常见的子域名,通常指向网站的主页。
    • 其他子域名也很常见,比如 mail.google.com 指向邮件服务,drive.google.com 指向云盘服务。

还可以从整体的角度看一下:

  • google.com:这被称为主域名(Main Domain),准确来说是 “注册域”(Registered Domain)。
    • 当我们谈论 “域名” 时,通常指的就是这一部分。
    • 通常也是我们在域名注册商那边购买的那部分。
  • www.google.com:被称为完全限定域名 (Fully Qualified Domain Name, FQDN) 或 主机名 (Hostname)
    • 也可以被称为子域名 (Subdomain)

最后以本站为例说明一下域名的各个部分:

  • blog 是子域名标签
  • rakuyoo 是二级域名
  • .top 是顶级域名
  • rakuyoo.top 是主域名
  • blog.rakuyoo.top 是一个完全限定域名(或完整的子域名)

协议

除了域名之外,URL 还有一个重要的组成部分:协议(Protocol)。它是一套规则、标准或约定,规定了计算机之间如何进行通信和数据交换。协议定义了数据传输的格式、时序、错误处理等,它将告诉客户端和服务器“如何”进行通信。

常见的协议比如 httphttps 以及 ftp 等,因为都非常常见了,所以在本文这一小节就先不展开介绍了。未来涉及到 TCP 数据传输时可能还会再次见面。

协议对于 URL 而言是非常重要的,当我们访问 www.google.com 时,实际上我们是 “用 HTTPS 协议连接到 www.google.com”,而不能只说 “连接到 www.google.com”。

别忘了 “端口”

在没有域名时,我们访问一个服务是通过 “IP 地址 + 端口” 来实现的。那么有了域名之后呢?平时我们在浏览器中输入地址时也没有用到端口呀。

其实不是没有用到,而是浏览器自动帮我们处理了。比如 https 协议的默认端口是 443,所以当我们在浏览器中输入 https://www.google.com 时,其实我们访问的是 https://www.google.com:443。如果我们需要访问某个特殊的端口,那么也需要像上面说的那样显式指定端口号。

DNS:一本厚字典

域名帮助我们省去了记忆复杂 IP 的麻烦,那么,域名是如何找到其对应的 IP 地址的呢?答案就是通过 DNS(Domain Name System)。嗯... 从名字上来看它就很 “域名”。

DNS 的工作原理(简化版)

当我们访问一个域名时,DNS 会通过一系列查询操作,找到这个域名所对应的 IP 地址:

  1. 在浏览器中输入域名: 在浏览器中输入 www.google.com 并按下回车。

  2. 浏览器查询本地 DNS 缓存: 浏览器会首先检查自己有没有缓存过 www.google.com 对应的 IP 地址。如果有,就直接使用,这样速度最快。

  3. 操作系统查询本地 DNS 缓存和 hosts 文件: 如果浏览器没有,它会把请求交给操作系统。操作系统也会检查自己的 DNS 缓存,以及一个叫做 hosts 的本地文件。

  4. 请求发送给 DNS 解析器 (DNS Resolver): 如果本地都没有,操作系统会将请求发送给你的网络配置中指定的 DNS 解析器(通常是 ISP 提供的 DNS 服务器,或者手动配置的公共 DNS,如 Google 的 8.8.8.8)。

  5. DNS 解析器开始递归查询

    • 查询根域名服务器 (Root Name Servers): 解析器会首先问全球的 13 组根域名服务器:“www.google.com 的 IP 地址是什么?” 根服务器不会直接告诉你 IP,它会告诉你:“我不知道,但你可以去问负责 .com 域名的服务器。”
    • 查询顶级域名服务器 (TLD Name Servers): 解析器接着会去问负责 .com 域名的 顶级域名服务器:“www.google.com 的 IP 地址是什么?” .com 服务器会告诉你:“我不知道 www.google.com 的具体 IP,但你可以去问负责 google.com 的权威域名服务器。”
    • 查询权威域名服务器 (Authoritative Name Servers): 解析器最后会去问 google.com 的权威域名服务器(这是由 Google 自己或其域名注册商维护的服务器):“www.google.com 的 IP 地址是什么?” 这台服务器知道 www.google.com 对应的确切 IP 地址,并会把这个 IP 地址告诉 DNS 解析器。
  6. IP 地址返回给浏览器: DNS 解析器收到 IP 地址后,会将其返回给你的操作系统,再由操作系统返回给浏览器。同时,这个 IP 地址会被缓存起来,以便下次更快地访问。

  7. 浏览器连接服务器: 浏览器拿到 IP 地址后,就可以直接通过这个 IP 地址连接到 Google 的服务器,并请求网页内容。

iOS 系统的 DNS 解析和 PC 上的 DNS 解析还有一点点区别,那就是 iOS 不存在 “浏览器 DNS 缓存” 这一步,所有的 DNS 缓存都在 iOS 系统内部进行统一的管理。各个 App 如果不做特殊的实现,也不存在 “App 级别的 DNS 缓存”。

可能这个流程还是有一些抽象。我们还可以从 “绑定 IP 和域名” 的流程中来解释 DNS 的解析过程:

  1. 获取 IP:首先我们从云服务器商处购买了一台云服务器,这个时候我们会得到这台服务器的 IP 地址。
  2. 购买域名:之后我们从域名商那里挑选了一个域名。国内的几大云服务器厂商基本都提供域名购买服务。
  3. 配置 DNS 解析:有很多地方可以让我们把 IP 和域名绑定在一起,比如有的域名注册商就提供功能,云服务商也提供,甚至还有专门的 DNS 服务商。在这些平台上,我们需要将域名和 IP 绑定到一起。通常是设置一条记录,key 和 value 填写你的域名和 IP。
  4. 上传 DNS 记录:当我们配置好了 DNS 后,平台就会将我们的配置上传到真正的 DNS 服务器上,此时域名与 IP 的绑定关系才算建立完成,域名和 IP 被真正的绑定在一起。
  5. 后续访问:现在当我们再访问域名时,你可以理解为是一个巨大的查表过程,通过域名就可以查询到对应的 IP 了。

DNS 缓存

在上一节中,我们看到了一个域名需要经过一连串的查询才能最终找到它的 IP 地址。如果每一次访问网站都需要重复这个完整的过程,那效率未免也太低了。

为了解决这个问题,DNS 系统设计了一套至关重要的机制——缓存。简单来说,当一个 DNS 查询的最终结果被找到后,查询路径上的各个参与者(比如你的电脑、DNS 解析器)都会把这个 “域名-IP” 的对应关系暂存一段时间。如果在缓存有效期内再次请求同一个域名,就可以直接返回暂存的结果,从而跳过复杂的递归查询过程,极大地提高了响应速度。

我们在上一节中提到的 “浏览器缓存” 和 “操作系统缓存” 就是这套机制的体现。而控制这一切的核心,是一个叫做 TTL(Time-To-Live,生存时间)的值。

TTL (Time-To-Live)

TTL 是由域名管理员在配置 DNS 记录时设置的一个数值,单位是秒。它像一个“保质期”,告诉各级 DNS 服务器和解析器,这个查询结果可以被缓存多久。

例如,本站的 blog.rakuyoo.top 的 TTL 被设置为 600 秒(10 分钟),那么当 DNS 解析器第一次获取到它的 IP 地址后,就会将这个结果缓存 10 分钟。在这 10 分钟内,任何通过这个解析器查询 blog.rakuyoo.top 的请求,都会直接得到缓存中的 IP 地址,而无需再去麻烦权威域名服务器。

DNS 传播

TTL 机制在带来高效的同时,也引入了一个重要的现象:DNS 传播(DNS Propagation)。

当你修改了一条 DNS 记录(比如将网站服务器迁移到了一个新的 IP 地址),这个变更并不会立即在全球范围内生效。因为各地的 DNS 解析器仍然缓存着旧的记录,它们需要等到各自缓存中的 TTL 过期后,才会重新向权威服务器发起查询以获取最新的记录。

这个新旧记录交替、变更信息逐渐扩散到全球的过程,就叫做 DNS 传播。传播所需的时间,就取决于你之前设置的 TTL 值。如果 TTL 设置为 1 小时,那么最坏情况下,一些用户可能需要等待 1 小时才能访问到你的新服务器。

因此,在计划进行服务器迁移等重大变更前,有经验的管理员通常会提前几天将相关域名的 TTL 值修改为一个非常小的值(比如 60 秒),以确保变更发生时,全球的 DNS 缓存能被快速刷新,从而实现平滑过渡。

灵活的绑定关系

一个主机名其实可以解析到多个 IP,这有利于实现负载均衡。只需要在配置 DNS 解析时,为同一个主机名添加多个 IP 地址,即可将一个主机名解析到多个 IP 上。

当用户访问这个主机名时,DNS 服务器会同时返回所有的 IP 地址,客户端(比如浏览器)会尝试连接其中一个,可能是随机选择某一个,也有可能是按顺序尝试。如果连接失败,则会尝试列表中的下一个 IP。

另外我们还可以以子域名标签为单位,或者准确的说,以 “完全限定域名” 为单位设置 DNS 解析。比如 mail.google.comdrive.google.com 虽然都在 google.com 这个域名下,但是他们可以部署在不同的服务器上,进而拥有不同的 IP 地址。

资源记录

在你添加 DNS 解析时,会接触到 “资源记录”(Resource Record)这个概念,在腾讯云服务器上它又叫做 “记录类型”。

DNS 十分强大,强大到它其实可以解析很多东西。所以系统设计了一个 “资源记录” 的概念,用来区分 DNS 所解析的内容的类型。常见的资源记录有以下几种:

  • A 记录 (Address Record)
    将一个域名或子域名指向一个 IPv4 地址。它是最基本的记录类型。

  • AAAA 记录 (Quad-A Record)
    与 A 记录类似,但它将一个域名或子域名指向一个 IPv6 地址。

  • CNAME 记录 (Canonical Name Record)
    将一个域名或子域名指向另一个域名(而不是直接指向 IP 地址),相当于创建了一个别名。它有以下几种使用场景:

    • 当你的服务提供商(如 CDN、博客平台)给你一个域名(而不是 IP 地址)让你指向时。
    • 当你希望多个子域名都指向同一个主域名,而主域名的 IP 地址可能会变动时。这样你只需要更新主域名的 A 记录,所有 CNAME 记录都会自动生效。

    另外需要注意: CNAME 记录不能指向 IP 地址,只能指向另一个域名。而且,通常情况下,根域名(如 yourdomain.com)不能设置为 CNAME 记录,因为它会与其他记录(如 MX 记录)冲突。

根据 DNS 规范(RFC 1034),如果一个域名存在 CNAME 记录,那么它就不能再有任何其他类型的记录(除了用于 DNSSEC 的 RRSIG 和 NSEC 记录)。而一个域的顶点(即 yourdomain.com 本身)必须有 SOA (Start of Authority) 和 NS (Name Server) 记录来标识这个域的权威信息。这两者是冲突的,所以根域名不能设置 CNAME。
本文并未涉及到 SOA 记录和 NS 记录等,此处仅作为拓展阅读。

Swift URL

相信阅读到此文章的大多数都是 iOSer,那么我们可以再从 Swift URL 的角度来看一下域名的各个组成部分,直接上代码:

let urlString = "https://www.google.com:443"

// 1. 创建 URL 对象
guard let url = URL(string: urlString) else {
    print("Invalid URL string")
    exit(1)
}

// 2. 访问 URL 的各个属性
print("Scheme (协议): \(url.scheme ?? "N/A")")
print("Host (主机名/域名): \(url.host ?? "N/A")")
print("Port (端口): \(url.port?.description ?? "N/A")")

// 3. 进一步拆解 host
// Swift 的 URL 类型没有直接提供“二级域名”或“顶级域名”的属性
// 你需要自己编写逻辑来从 host 中提取这些信息
let host = url.host ?? ""
let components = host.split(separator: ".").map(String.init)

if components.count >= 2 {
    let tld = components.last ?? "N/A" // .com
    let sld = components[components.count - 2] // google
    let mainDomain = "\(sld).\(tld)" // google.com
    print("推断的主域名 (Main Domain): \(mainDomain)")

    if components.count > 2 {
        let subdomainLabel = components.first ?? "N/A" // www
        print("推断的子域名标签 (Subdomain Label): \(subdomainLabel)")
    }
}

输出结果如下:

Scheme (协议): https
Host (主机名): www.google.com
Port (端口): 443
推断的主域名 (Main Domain): google.com
推断的子域名标签 (Subdomain Label): www

说一个小细节:还记得上文中我说过 “协议对于 URL 来说很重要” 吗?如果将示例中 urlStringhttps 部分去掉,改为 www.google.com:443,那么 url.scheme 的结果将会是 www.google.comurl.host 会是 nil

这是因为 Swift 的 URL 在解析 URL 字符串时,会严格按照 URL 的标准格式来识别各个组成部分,而一个标准的 URL 必须以协议开头,后面跟着 ://

对于 www.google.com:443 而言,URL 初始化器会将 : 前的内容都识别为协议。假如换成 www.google.com,即字符串中没有 :,那么 url.schemeurl.host 都将是 nil