qinghuan
← 计算机基础 / 全部笔记

单元 32 / 计算机基础

P0 · 必会主线L1 · 基础

DNS、TCP 与 TLS:一次 HTTPS 请求经过哪些阶段

把访问网站拆成查地址、建连接、验证证书和处理 HTTP 请求,逐步定位问题出在哪一层。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能画出 HTTPS 常见路径,并为每层列出证据能够和不能证明的内容。

学习理由、准备要求与知识点

为什么学它

把网络失败分层,才能区分域名、端口、加密握手和应用响应的问题。

先读这些内容

知识点清单

  • DNS 层次与递归解析
  • TTL 和缓存观察差异
  • TCP 有序字节流
  • 连接与业务确认
  • TLS 身份验证与加密
  • HTTP/3 路径差异
  • 分层超时与错误
  • 副作用请求重试

运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。

01先弄懂它是什么

网站打不开时,可以把一次请求拆开检查。URL 写明协议、主机、端口和路径;DNS 帮助把域名查成地址;本篇讨论的 TCP 连接负责可靠、有序地传送字节;TLS 负责加密和验证对端身份;HTTP 再规定请求与响应的含义。前一步成功只证明这一层可用,例如端口能连上,并不代表登录接口一定正常。

同一个域名可能查到多个地址,查询结果也可能暂存在客户端或 DNS 解析服务中。TTL 告诉缓存中的记录还能复用多久,不表示地址以后不会改变。命中缓存时不一定重新发起 DNS 查询,直接使用 IP 地址时也不需要按域名查地址。排查时记下输入的域名、查到的地址、实际连接地址和时间,才能比较两次访问的差异。

TCP 传的是连续字节,不会替应用标好“这是一条消息”。你分两次写,对端不一定恰好分两次读,应用仍要自己识别消息边界。TCP 会处理建连、丢包重传和流量控制,但它不知道这些字节代表一次扣款还是一次查询,因此不能保证业务只执行一次。TLS 还要另做证书、主机名和信任链检查;即使 HTTPS 连接正常,应用权限、请求内容和业务代码仍可能有问题。

文中这几个词,先认一认

递归解析器
替客户端继续向 DNS 系统查询的服务,也可能保存结果供下次使用。
TTL
一条缓存记录可以继续使用多久;具体单位和计时起点要看相应协议或实现。
字节流
连续排列的字节,本身不说明哪几字节属于同一条业务消息。
主机名验证
核对服务器提供的证书是否允许用于这次访问的域名。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

一次常见的 HTTPS 请求,可以先按四步理解:查域名对应的地址,建立 TCP 连接,完成 TLS 握手,再发送 HTTP 请求。但不是每次都从头走:DNS 缓存可能省掉查询,已有连接可能直接复用。HTTP/3 的传输方式也不同,所以本页流程图只解释 HTTPS 走 TCP 的常见情况。

页面打不开时,先找到卡在哪一步。域名查不到,就检查名称和 DNS 解析器;TCP 连不上,就检查目标地址、端口及中间网络;TLS 握手失败,再看证书、主机名、协议和设备时间;已经收到 HTTP 错误,才继续查状态码、请求内容和应用日志。ping 通只能说明对应探测成功,不能替代后面这些检查。

只判断“还能用多久”时,最好依据已经过去的时长,或不会随日历校时倒退的单调时间来计算预算,避免墙上时钟被调整后期限跟着变。过期后应重新获取;若刷新失败,能否暂时使用旧值,需要另外规定。下面的模拟只在期限内返回缓存,到期就当作缺失。它不实现递归 DNS、查无记录等结果的缓存、网络重试或证书校验。

正在加载流程图…

限定为 HTTPS over TCP 的常见新连接路径;缓存与连接复用可能省略部分步骤。
查看流程图源码(Mermaid)
flowchart LR
 A["解析 HTTPS URL"] --> B["DNS 或地址缓存"]
 B --> C["选定地址与端口"]
 C --> D["建立 TCP 字节流"]
 D --> E["TLS 协商与证书主机名验证"]
 E --> F["发送 HTTP 请求"]
 F --> G["应用处理与 HTTP 响应"]

按顺序看,每一步做了什么

  1. 确认原始 URL 的协议、主机和端口。
  2. 记录解析结果与缓存时效。
  3. 单独判断 TCP 建连是否完成。
  4. 单独判断 TLS 身份与协议验证。
  5. 最后检查 HTTP 与应用结果。

03对照实现代码

Java 名称查询入口与下层解析边界 (新标签页)↗

Microsoft OpenJDK 17.0.20.1;java.base/java/net/InetAddress.java · java.net.InetAddress#getAllByName(String)

以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。

核心机制代码 · java
import java.net.UnknownHostException;
public final class SourceModel {
    @FunctionalInterface
    public interface Resolver { String[] lookup(String host) throws UnknownHostException; }
    public static String[] resolveName(String host, Resolver resolver) throws UnknownHostException {
        if (host == null || host.isBlank()) throw new IllegalArgumentException("host");
        // 仅模拟名称查询委托;真实地址字面量、本地主机与缓存路径未实现。
        String[] addresses = resolver.lookup(host);
        if (addresses == null || addresses.length == 0)
            throw new UnknownHostException(host);
        return addresses.clone();
    }
}

真实 InetAddress.getAllByName(String) 委托到内部重载,后续路径区分地址字面量、名称解析、缓存与系统相关解析能力。模型只展示名称查询如何委托给可替换解析器,使用字符串数组简化结果;它不是 DNS 协议的 Java 重实现,也没有实际访问网络。

方法可能返回多个地址,不应把第一个 IP 永久认定为这个域名的唯一地址。后面选择哪个地址、连接失败是否换一个、结果缓存多久,还要由更大的网络处理过程决定。这个 Java 方法只返回地址,并没有顺便建立 TCP、完成 TLS 握手或发送 HTTP,请按它实际完成的步骤理解源码。

模型拒绝空主机,而真实 API 对 null 或空输入存在自己的本地主机相关约定;这一差异是为了让教学输入更明确,不能用模型替代文档。相同名字也可能受系统配置、缓存和网络环境影响得到不同结果,纯函数式测试通常应注入解析结果而非依赖实时 DNS。

返回数组前做一份复制,是为了让调用方修改这个数组时,不会连带改掉原数组;它不能保证里面的地址以后一直有效。结果缓存多久、查无记录的结果是否缓存、什么时候刷新,都需要另外设计,而且不同 JDK 和系统配置可能有不同表现。本文显式传入时间,只用来演示到期规则,没有假定某个默认 TTL 永远不变。

代码先调用谁,再调用谁

  1. getAllByName 进入内部重载
  2. 按输入区分字面量与名称路径
  3. 查询缓存或委托系统名称解析
  4. 返回地址集合或 UnknownHostException,建连仍由其它 API 完成

04动手写一个例子

显式时间下的严格 TTL 缓存模拟

用非递减的非负模拟时间管理 ASCII 主机名到字符串结果的缓存。名称忽略 ASCII 大小写;到期时刻立即失效,TTL 零表示不缓存。只演示正结果缓存,不执行 DNS 查询。

输入 1
时刻0存A,TTL5;时刻4读取
输出 1
命中A
输入 2
同一记录时刻5读取
输出 2
缺失
输入 3
同名时刻2写入新值TTL1;时刻3读
输出 3
缺失新值

每条记录保存值与绝对到期时刻,写入时由 now+ttl 计算。读取先找到记录,再检查 now 是否小于到期时刻;相等时已经失效,这个边界要在测试中明确。过期记录在本次读取时移除,未被访问的过期记录可能仍占内存,不能把懒删除当成容量管理。

模拟时间作为参数传入,保证测试完全可重复,并拒绝时间倒退。真实应用可以使用单调时钟计算期限,但其绝对值不用于日历展示。期限相加使用精确运算,超出 long 就失败;TTL 零直接删除旧项,避免刷新为零时还错误保留先前缓存。

名称规范化限定为 ASCII 主机标签,使用 Locale.ROOT 小写化,避免地区大小写规则影响键。这里只将主机名作为查询键,不验证域名实际存在,也不承担国际化域名和证书主机名校验。数据值是教学字符串,缓存命中只说明模拟记录有效,不代表远端服务可用。

为什么这样写是对的每个缓存项记录最近一次成功写入的值与到期时刻;get 只在当前时间严格早于该时刻时返回它。

时间 良好哈希分布下每次期望 O(h),h 为主机名长度;表操作期望常数。额外空间 O(kh) 上界,k 为保留项数量,另加值字符串存储。

每次名称校验与规范化扫描字符,随后固定次数映射操作;懒过期不遍历全表,也不自动回收所有旧项。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 58 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
public class Main {
    record Entry(String value, long expires) {}
    static final class Cache {
        private final Map<String,Entry> data = new HashMap<>();
        private long last = -1;
        private void time(long now) {
            if (now < 0 || now < last) throw new IllegalArgumentException("time");
            last = now;
        }
        private String key(String host) {
            if (host == null || !host.matches("[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*"))
                throw new IllegalArgumentException("ASCII teaching host");
            return host.toLowerCase(Locale.ROOT);
        }
        void put(String host, String value, long ttl, long now) {
            String key = key(host);
            if (value == null || ttl < 0) throw new IllegalArgumentException("entry");
            long expires = Math.addExact(now,ttl);
            time(now);
            if (ttl == 0) data.remove(key); // 零 TTL 不保留旧值。
            else data.put(key,new Entry(value,expires));
        }
        Optional<String> get(String host, long now) {
            String key = key(host); time(now);
            Entry entry = data.get(key);
            if (entry == null) return Optional.empty();
            if (now >= entry.expires()) { // 到期边界立即失效。
                data.remove(key); return Optional.empty();
            }
            return Optional.of(entry.value());
        }
    }
    static void require(boolean condition) {
        if (!condition) throw new AssertionError();
    }
    public static void main(String[] args) {
        Cache c = new Cache();
        c.put("Example.test","A",5,0);
        // 命中、大小写、恰好过期和刷新后的新期限。
        require(c.get("example.test",4).equals(Optional.of("A")));
        require(c.get("EXAMPLE.TEST",5).isEmpty());
        c.put("example.test","B",10,5);
        c.put("example.test","C",1,6);
        require(c.get("example.test",7).isEmpty());
        c.put("example.test","D",10,7);
        c.put("example.test","ignored",0,8);
        require(c.get("example.test",8).isEmpty());
        require(c.get("missing.test",8).isEmpty());
        boolean rejected = false;
        try { c.get("example.test",7); } catch (IllegalArgumentException e) { rejected = true; }
        require(rejected);
        System.out.println("PASS 6");
    }
}

这些特殊情况也要试一试

  • 恰好到期时返回缺失。
  • 同名更新替换旧值与旧期限。
  • TTL 零删除已有项。
  • 时间倒退被拒绝。
进阶选读 · 可选深入:缓存刷新风暴与负缓存

场景大量请求在同一时刻过期,解析或下游服务承受集中请求。

做法按键合并正在进行的刷新,限制并发和容量,分别设计成功结果、查无结果与暂时错误的缓存策略;若允许旧值服务,明确最大陈旧期。

复杂度查询通常常数期望成本,刷新协调与淘汰结构增加额外状态。

好处与代价减少下游压力与保持新鲜度存在取舍,错误不能无期限缓存成不存在。

05项目里怎么用

排查时,把证据按步骤记下来:请求哪个域名、用了哪个地址、连接花了多久、TLS 协商结果是什么、HTTP 返回什么状态,以及请求编号是什么。查到地址不代表接口已经正常。遇到证书错误,应查清原因,不要靠日常关闭证书检查解决。复现问题时使用不带凭证的公开测试地址或自己可控的环境,并记录失败发生在哪一步。

缓存与连接池会改变观察结果。修改 DNS 后旧连接可能继续使用,清除某一层缓存也不代表其它层已刷新;同一机器不同程序可能使用不同解析路径。排查应核对实际进程和连接,而不是根据浏览器一次刷新就推断整个系统已切换地址。

超时既要分步骤设置,也要限制整个请求总共等多久。DNS、连接、TLS 和读取响应都可能耗时,给其中一步设五秒,不表示整次请求五秒一定结束。重试还会增加等待,并可能重复改变业务数据,需要配合重复请求处理和结果查询。模拟 TTL 只解释到期规则,实际 DNS 与网络能否工作还要分别验证。

容易踩的坑

  • 把 ping 成功当作 HTTPS 接口正常。
  • 把 TCP 两次写入当作对端两条完整消息。
  • 域名解析成功就宣称证书或业务已正常。
  • 懒过期缓存没有容量策略却长期无限增长。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
每次重新查询信息更及时且状态少延迟和下游压力较大需要强新鲜度且调用量可控
严格 TTL 缓存减少重复查询且期限明确到期集中刷新与陈旧窗口一般可容忍有限缓存时间
受控陈旧值回退刷新失败时提高可用性可能继续访问旧目标明确允许短期旧数据的场景

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01DNS、TCP、TLS、HTTP 分别回答什么?

展开参考答案与推导

DNS提供名称相关地址信息,TCP提供可靠有序字节流,TLS保护传输并验证对端身份,HTTP描述应用请求响应。每层成功只证明对应职责完成,不能相互替代。

02为什么 TCP 不保证业务恰好执行一次?

展开参考答案与推导

TCP 管理字节传输,不知道扣款或下单的业务事务。客户端超时后服务端可能已执行,重试需要幂等标识与结果确认,不能依靠传输可靠性自动去重。

03TTL 到期是否代表域名已经变化?

展开参考答案与推导

不代表。到期表示旧缓存不再按该期限直接复用,需要重新获取信息;新结果可能与旧值相同。TTL 管理信息时效,不是预测服务一定何时迁移。

换个情况,还会处理吗

01DNS 返回地址但 HTTPS 失败,下一步查哪里?

展开参考答案与推导

先确认 TCP 能否连接对应地址和端口,再查看 TLS 证书、主机名和协议诊断,最后检查 HTTP 层。不要继续反复更换 DNS 而忽略已完成解析后的失败阶段。

02修改域名后部分请求仍到旧地址,可能为什么?

展开参考答案与推导

可能存在 DNS 缓存、连接复用、不同解析器或进程配置差异。记录实际连接地址与时间,区分旧连接继续使用和新解析仍返回旧值,再定位需要更新的层次。

03过期缓存项仍占内存是不是实现错误?

展开参考答案与推导

在懒删除设计中,未被读取的旧项可能仍留在表里,这是策略边界。长期运行需要容量上限或主动清理,不能把读取时不返回过期值等同于内存已自动回收。

写代码试一试

01验证过期前一刻与恰好过期

输入 / 条件
时刻10写TTL2,时刻11和12读
输出 / 预期
命中后缺失

提示 比较采用 now>=expires。

展开参考答案与推导

使用新 Cache 避免时间倒退,分别断言两个时间点。若误用严格大于,到期时刻会多保留一单位;这类边界必须通过精确样本固定。

02增加容量限制

输入 / 条件
容量2,插入第三个不同主机
输出 / 预期
按明确策略拒绝或淘汰

提示 先选择淘汰含义。

展开参考答案与推导

可以先实现超容量拒绝,保持简单可测试;若选最近最少使用,需要维护访问顺序并说明过期项处理。不能随意删除 HashMap 遍历遇到的第一项后称为 LRU。

03给缓存增加真实解析回源

输入 / 条件
缓存缺失
输出 / 预期
调用解析器并记录期限

提示 使用可替换接口。

展开参考答案与推导

在缓存外层注入 Resolver,缺失时查询并按明确 TTL 写回,测试用固定返回值与调用计数。解析失败与正常无记录应区分,不能无条件写成永久空结果。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能画出 HTTPS 常见路径,并为每层列出证据能够和不能证明的内容。

本篇重点加练

重点巩固业务确认与分层错误证据,防止把网络现象直接当作业务执行结果。

  • 连接与业务确认
  • 分层超时与错误

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    画出浏览器访问学习站点时可能参与的本地缓存、递归解析器与权威 DNS,说明 TTL、缓存年龄和观察时间为何要一同记录。

    提交内容
    DNS 路径图和观测字段表。
    怎样算完成
    • 不同层缓存分别标明
    • 缓存记录不被当作永久映射

    练到的知识点DNS 层次与递归解析 · TTL 和缓存观察差异

  2. 作业 02代码推演

    客户端发送两段订单字节,连接随后断开但未收到业务响应。列出传输层和业务层各自可能完成的阶段,以及每个阶段仍缺少的确认。

    提交内容
    通信时间线和已知/未知表。
    怎样算完成
    • 字节传送与业务提交分开
    • 不以断线直接断言未执行

    练到的知识点TCP 有序字节流 · 连接与业务确认

综合应用

  1. 作业 03原理问答

    为 HTTPS over HTTP/1.1 与 HTTP/3 画出协议层关系,标出身份验证、加密和业务消息所在层;说明诊断时为什么必须先知道实际使用的协议。

    提交内容
    两张协议栈图与诊断前提。
    怎样算完成
    • 不把所有 HTTP 都套用同一传输链
    • 证书身份与网络可达性分开

    练到的知识点TLS 身份验证与加密 · HTTP/3 路径差异

  2. 作业 04面试场景

    请求日志显示名称解析 5ms、连接 20ms,随后在握手或读取阶段超时,订单是否创建未知。设计下一步诊断与业务恢复流程。

    提交内容
    阶段证据表和恢复政策。
    怎样算完成
    • 错误阶段由具体证据支持
    • 副作用重试有独立业务合同

    练到的知识点分层超时与错误 · 副作用请求重试

  3. 作业 05编程实践

    实现注入时钟的 DNS 结果缓存模型,条目含主机、地址列表和 TTL,明确 TTL 为零、重复更新和到期边界;模型只使用虚拟数据,不访问真实 DNS。

    提交内容
    Java 实现、自写时间/空结果/更新测试与运行证据。
    怎样算完成
    • 时钟和缓存状态可控
    • 返回结果不受外部修改污染
    • 模型能力边界明确

    练到的知识点TTL 和缓存观察差异 · 分层超时与错误

  4. 作业 06编程实践

    编写纯 Java 请求阶段模拟器,事件包括解析完成、连接完成、发送完成、业务提交与响应到达;测试在不同事件后注入超时,返回可确认事实集合。

    提交内容
    Java 状态模型、自写阶段/非法顺序测试与运行证据。
    怎样算完成
    • 事实与推测采用不同表示
    • 终止时未知部分仍明确保留

    练到的知识点连接与业务确认 · 分层超时与错误

  5. 作业 07面试场景

    服务端日志记录开始处理但客户端收到连接重置,数据库查询暂时不可用。制定向用户反馈与后续核验政策,避免在没有证据时承诺已失败或已成功。

    提交内容
    事件时间线和恢复沟通合同。
    怎样算完成
    • 用户反馈对应事实边界
    • 最终业务确认有可执行路径

    练到的知识点连接与业务确认

  6. 作业 08设计评审

    日志仅有“请求耗时 5s,超时”,无法判断解析、握手或读取阶段。设计最少分层时间点和关联字段,不记录完整敏感请求。

    提交内容
    诊断事件格式和示例时序。
    怎样算完成
    • 每项记录解决具体未知
    • 时钟与耗时计量前提明确

    练到的知识点分层超时与错误

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    完成可观测性基础后:多地域部分用户访问失败,设计最小诊断数据集,覆盖解析地址、协议、证书、时序和请求关联,同时避免记录凭据。

    提交内容
    诊断模板与分层排查树。
    怎样算完成
    • 能比较不同用户的路径差异
    • 结论不超出采样证据

    练到的知识点DNS 层次与递归解析 · TTL 和缓存观察差异 · TLS 身份验证与加密 · 分层超时与错误 · 副作用请求重试

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    调用链新增一层会自动重试的代理,客户端代码不变。分析原超时与业务次数证据会遗漏什么,设计跨层关联与重复请求验收。

    提交内容
    代理前后时间线和证据补充。
    怎样算完成
    • 客户端尝试与下游尝试分别计数
    • 业务效果仍需单独核验

    练到的知识点连接与业务确认 · 分层超时与错误

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗