计算机组成原理

全局框架 - 全局框架 - 系统层次 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/18/20260218105815855.png,424,537) - 核心矛盾:速度差 - CPU 运算快、内存慢、外设更慢 - 系统用层次结构(寄存器/Cache/内存/磁盘)和并行性(流水线/乱序/多核/DMA)来隐藏或者摊薄延迟 - 指令在 CPU 里按“取值-译码-执行-访存-写回“的数据通道运行 - 为了加速,做流水线把这 5 步重叠 - 内存访问通过 Cache 利用局部性降低平均延迟 - 虚拟内存通过 MMU/TLB 把程序看到的地址映射到物理内存 - I/O 慢是由于设备与总线带宽/延迟远弱于 CPU,因此依赖中断、缓冲与 DMA 来减少 CPU 参与 指令系统与数据表示 - 指令系统与数据表示 - 整数与位运算的数据表示 - 位、字节、字 - bit:0/1 - Byte:8bit - Word(字):CPU 自然处理的宽度,与寄存器/ALU/地址宽度相关 - 为什么 byte 是最小编址单位:大多数现代机器按照字节编址,方便表示字符/结构体/网络数据等 - 原码/反码/补码 - 原码 - 表示方法:最高位符号位,剩下是绝对值 - 问题: - 有 +0 和 -0 两种零 - 加减法需要额外处理符号,硬件复杂 - 反码 - 正数同原码 - 负数:对正数位取反 - 问题 - 仍然有 +0 和 -0 - 加法需要回卷进位 - 补码 - 正数:与无符号相同 - 负数:按位取反 + 1 - 关键性质: - 只有一个 0(没有 -0) - 加减法统一为同一套加法器:a-b=a+(~b+1) - 符号扩展简单:高位补符号位即可 - 范围不对称 [-2^{n-1},2^{n-1}-1] - 快速转换 - 补码->十进制 - 最高位 0:按无符号直接算 - 最高位 1:取反 +1 得到绝对值,再加负号 - 十进制->补码 - 正数:直接写二进制,左侧补 0 - 负数:先写绝对值二进制 -> 取反+1 - 有符号/无符号加法与溢出判断 - 无符号溢出(Unsigned overflow) - 本质:结果超出 [0, 2^n-1] - 判定方法:看进位 Carry-out - 有符号溢出 - 本质:结果超出 [-2^{n-1},2^{n-1}-1] - 判断口径统一 - 同号相加,异号结果 => 溢出 - 异号相加不会溢出 - 等价位级判定 - 符号位的进位与最高位进位输出不同=>溢出 - 字节序:大端 vs 小端 - 对 32-bit 只 0x12 34 56 78 高位在左 - 大端 - 低地址存高字节 - 内存从低地址到高地址 12 34 56 78 - 直觉:与人类书写一致 - 小端 - 低地址存低字节 - 内存从低地址到高地址:78 56 34 12 - 直觉:低地址就是低位,做逐字节扩展/截断更自然 - x86 等主流架构长期使用小端 - 对齐与 Padding - 对齐 - 要求:某类型对象的地址必须是某个中的倍数(4/8) - 为什么要对齐 - 硬件访问效率:CPU/总线按照字/双字读写;未对齐可能需要两次内存读写再拼接 - Cache line 与内存访问粒度:对齐能减少跨行/跨 cache line 的概率 - Padding(填充) - 为了满足对齐,编译器会在结构体字段之间或末尾插入空字节 - 结构体大小通常是其最大对齐需求的倍数 - 浮点数(IEEE 754) 与误差本质 - IEEE 754 基本格式 - 浮点数表示 - S(符号位):0 正 1 负 - E(指数/阶码):带拍支 bias - F(尾数/小数部分):有效数 significand 的小数部分 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/18/20260218112914998.png,218,39) - 单精度 float(32-bit) - S: 1 位 - E: 8 位,bias = 127 - F: 23 位 - 双精度 double(64-bit) - S:1 位 - E: 11 位,bias=1023 - F: 52 位 - 规格化、非规格、0、INF、NaN - 规格化 - 指数 E 既不是全 0 也不是全 1 - 有隐含的 leading 1:1.F - 非规格化 - 指数 E 全 0,尾数 F 非 0 - 没有隐含 1:有效数是 0.F - 目的:渐进下溢,让非常接近 0 的数仍然能表示 - 0 - E 全 0,F 全 0 - 有 +0 和 -0 - Inf 无穷 - E 全 0,F 全 0 - 有 +0 和 -0 - NaN(非数) - E 全 1,F 非 0 - 来自非法操作:0/0 - NaN 会传播 - 0.1 + 0.2 != 0.3 - 十进制小数在二进制中往往是无线不循环小数 - 0.1 的二进制是 0.0001100111 - float/double 只能保存有限位尾数 - 存储时发生舍入,计算时再舍入,误差叠加 - 比较时用 ==,就可能不相等 CPU 执行与控制 - CPU 执行与控制 - 单周期 Datapath - 必备组件 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/18/20260218153017482.png,429,229) - PC 当前指令地址寄存器 - Instruction Memory/I-Cache:根据 PC 取指令 - Register File (寄存器堆):两读一写(rs1、rs2 读;rd 写) - Immediate Generator(立即数生成/符号扩展) - ALU:算数逻辑/比较/地址计算 - Data Memory/D-Cache:load/store 访问 - Adders: - PC + 4 - PC + imm (分支目标) - 关键 MUX(多路选择器) - ALUSrc:ALU 第二操作数是来自 rs2 还是 imm - MemToReg:写回 rd 的数据来自 ALU 还是内存 - PCSrc:下一 PC 来自 PC+4 还是分支目标 - Control Unit(控制器):根据 opcode/funct 产生控制信号 - datapath = 一堆功能模块 + 多根线 + MUX 选路 - 四条指令在 datapath 里怎么走 - R 型算数:add rd, rs1, rs2 - 目标:rd = rs1 + rs2 - 数据流 - PC -> 取指令 - 寄存器堆读 rs1、rs2 - ALU 计算 ALUOut = rs1 + rs2 - 写回:rd <- ALUOut - PC 更新:PC <- PC + 4 - 关键控制 - RegWrite = 1 写寄存器 - ALUSrc = 0 第二操作数来自 rs2 - MemRead = 0, MemWrite = 0 - MemToReg = 0(写回来自 ALU) - Branch = 0(不分支) - ALUOp = R-type(由 funct 决定是 add/sub/and/or) - 访存读:lw rd, imm(rs1) - 目标:rd = Mem[rs1+imm] - 数据流 - 取指 - 读寄存器 rs1 (base),rs2 通常也读但不用 - ALU 计算有效地址:ALUOut = rs1 + imm - 数据内存读:MemOut = Mem[ALUOut] - 写回:rd <- MemOut - PC <- PC + 4 - 访存写:sw rs2, imm(rs1) - 目标:Mem[rs1+imm] = rs2 - 数据流 - 取指 - 读 rs1(base) 和 rs2(要写入的数据) - ALU 算地址:ALUOut = rs1 + imm - 数据内存写:Mem[ALUOut] <- rs2 - PC <- PC + 4 - 条件分支:beq rs1, rs2, imm - 目标:如果 rs1 == rs2,则 PC <- PC + imm;否则 PC <- PC + 4 - 数据流 - 取指 - 读 rs1,rs2 - ALU 做比较:Zero = (rs1-rs2==0) - 同时计算目标分支:Target = PC + imm - PC 选择 - if Branch & Zero:PC <- Target - else: PC <- PC + 4 - 单周期 vs 多周期 - 单周期 - 每条指令都在一个周期完成 - 周期必须长到容纳最慢指令路径 - 结果:简单但浪费,端指令也被迫跑长周期 - 多周期 - 把一条指令拆分成多个步骤,每一步一个周期 - 不同指令用不同步骤数量 - 可以复用硬件 - 性能 - 性能公式 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/18/20260218153851413.png,354,137) - IC:执行的指令条数 - CPI:平均每条指令需要多少周期 - Cycle Time:每周期多长时间 (=1/主频) - Amdahl 定律(优化上限) - 如果系统中某部分占比位 f,该部分加速 S 倍,总加速比 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/18/20260218154107114.png,227,64) - IPC、吞吐、延迟 - 延迟:完成单个任务/单条指令/一次请求需要的时间 - 吞吐:单位时间完成多少任务 - IPC:每周期完成多少条指令 - 吞吐 = IPC x 频率 流水线与冒险 - 流水线与冒险 - 五级流水线核心内容 - 五段功能定义 - IF:取值;PC->I-Cache;计算 PC+4 - ID:译码;读寄存器;生成立即数 - EX:ALU 运算;地址计算;分支比较 - MEM:访问 D-Cache - WB:写回寄存器 rd - 流水线寄存器 - 为了让每段每周期独立推进,段与段之间插入寄存器 - IF/ID、ID/EX、EX/MEM、MEM/WB 它们保存上一段的输出给下一段用(包含数据与控制信号) - 理想吞吐与延迟 - 非流水:一条指令用 5 个周期,下一条等它结束 - 流水:启动后每周期完成 1 条 - 延迟:单条仍然=5 个阶段 - 吞吐:理想从 1/5 提升到 1 - 流水线的代价 - 需要通多寄存器与控制逻辑 - 分支错预测要清刷流水线 - 结构冲突要加资源或停顿 - 结构冒险 - 定义 - 同一周期内两个阶段需要同一个硬件资源 - 例子 - 单端口存储器:IF 要取值,MEM 要访存 - 同一周期冲突 -> 必须 stall 或改造硬件 - 解决方案 - 分离 I-Cache/D-Cache - 多端口存储器 - 数据冒险 - 三种依赖 - RAW(Read After Write)真依赖:后读依赖前写,最关键 - WAR(Write After Read)反依赖:后写不能早于前读 - WAW(Write After Write)输出依赖:两条写同一目的寄存器 - 为什么会 RAW:写回晚,读取早 - 读寄存器在 ID - 写回寄存器在 WB - Fowwarding (旁路/转发)机制 - 不等写回到寄存器堆,直接把产生的结果从后端绕回到前段使用 - 转发路径 - EX/MEM -> ID/EX(给下一条的 EX 用) - MEM/WB -> ID/EX (给下一条或下下条的 EX 用) - Stall(插泡)与 load-use hazard - load-use 典型序列 - lw r1, 0(r2) - add r3, r1, r4 - 原因 - lw 的数据到 MEM 末才可用 - add 需要在 EX 用到 r1 - 时间上赶不上,即使有 forwarding 也不够快 - 结论:需要插入一个 bubble - 实现 - 冻结 PC 和 IF/ID - 往 ID/EX 注入一个 NOP (控制信号清零)形成 bubble - 控制冒险 - 定义:PC 的下一值不确定:到底走 PC+4 还是分支目标 - 分支决定在哪里做决定 penalty - 如果分支在 EX 才能确定 - 在分支结果出来之前,流水线已经取了后续若干条 - 一旦发现分支 taken,就要把错路径指令 flush 掉 - 基础策略 - stall until branch resolved:等分支算出来再取后续 - predict not taken:默认不跳,先取顺序;若实际 taken -> flush - 动态分支预测:用分支历史表、2-bit 饱和计数器等降低错预测 - BTB:预测 taken 时快速给出目标地址 - 延迟槽 - penalty - 分支罚时:错预测或 taken 导致需要清空的阶段数 存储体系 - 存储体系 - 核心矛盾 - CPU 周期是纳秒级,DRAM 访问通常是几十到上百纳秒级,差一个数量级 - 解决思路:分层存储 + 局部性 - Cache 用更贵更快的 SRAM 缓存最近用过的数据块,依赖时间/空间局部性降低平均访存时间 - Cache 基础概念 - Cache line (缓存行) - Cache 以“行/块“位单位搬运数据 - CPU 读写某地址时,实际会把该地址所在的整条 cache line 搬到 Cache。 - 推论:顺序访问数组通常很友好;跨行访问容易 miss - Hit/Miss 与三大指标 - Hit time:命中时访问延迟 - Miss rate:未命中比例 - Miss penalty:未命中后去下一级/内存取回的代价 - 平均访存时间 AMAT - AMAT = Hit Time + Miss Rate * Miss Penalty - MISS 分类 - Compulsory miss(冷启动):第一次访问必 miss - Capacity miss(容量):工作集超过 Cache 容量 - Conflict miss(冲突):映射/组相联导致不同块争同一行 - 映射方式:直接映射/全相联/组相联 - 前置数据 - Cache 总容量 (不含tag/元数据) = C bytes - Cache line 大小 = B bytes - 总行数 (lines) = L = C/B - 相联度 (ways) = A - 组数 (sets) = S = L/A - 直接映射 - 每个内存块只能映射到唯一一个 cache 行 - 优点:硬件简单、命中快 - 缺点:冲突 miss 多 - 全相联 - 任意内存块可放任意行 - 优点:几乎无冲突 miss - 缺点:需要对所有行比较 tag(实现复杂、命中更慢、耗能更高) - 组相联 - 内存块映射到某个 set,但可以放在 set 内任一 way。 - 折中方案 - 地址拆分 - 对于字节编址系统,地址通常拆分为:tag+index+block offset - offset 块内偏移 - 表示在一条 cache line 內的字节位置 - 位数 offset bits = log2(B) - index 组索引 - 选择 set - index bits = log2(S) - tag 标记 - 用于判断该 set 中某一 way 是否是想要的内存块 - tagbits = Address bits - index bits - offset bits - 替换策略 - LRU:替换最久没用的行 - Pseudo-LRU(伪 LRU):用更少 bit 近似 LRU - FIFO:先进先出 - 写策略 - write-throught(写穿) - 写命中:同时写 Cache 和下一级 - 优点:下一级始终最新,实现简单 - 缺点:写流量大 - write-back(写回) - 写命中:只写 Cache,把该行标记为 dirty - 该行被替换时才写回下一级 - 优点:显著减少写带宽压力 - 缺点:一致性更复杂(多核) - 写不命中 write miss 的两种策略 - write-allocate(写分配):先把整条 line 读入 Cache,再在 Cache 上写,与 write-back 搭配 - no-write-allocate(不写分配):不把 line 加入 Cache,直接写下一级,常与 write-throught 搭配 - 多级 Cache 的 AMAT - L1/L2 两级 - AMAT=HitTime_L1​+MissRate_L1​×(HitTime_L2​+MissRate_L2​×MissPenalty_L2​) - 多核:一致性 vs 一致性协议 - Cache Coherence(一致性) - 目标:对于同一内存地址 X,各核的 cache 中 X 的副本要看起来像一个指在变化,不能读各自的旧值 - 解决办法:一致性协议(MESI)保证写入能让其他核的副本失效/更新 - Memory Consistency(一致性模型,针对不同地址的顺序) - 目标:规定不同核对不同地址的读写在时间顺序上允许怎么样被观察到 - 是内存模型讨论的问题,涉及重排序与屏障。 - Coherence 解决同一地址副本一致,Consistency 解决不同地址操作的可见顺序,前者靠协议,后者靠内存模型和 fence。 - MESI 协议 - False Sharing - 定义 - 不同核访问的是不同变量,但是这些变量恰好落在同一条 cache line 里 - 其中一个核写它自己的变量,会把整条 line 的其他核副本失效,导致另一核反复 miss - 性能问题 - 引发 invalidation(整条 line 粒度) - 两个核交替写同一条 line 上不同字节 - line 在核之间反复迁移,带来大量一致性流量 虚拟内存 - 虚拟内存 - 要解决的问题 - 抽象:每个进程看到连续的虚拟内存空间(VA) - 保护:权限控制(R/W/X,用户态/内核态) - 共享:共享库/共享内存/页共享 - 扩展:按需分页 + 换入换出 - 页 - 虚拟页:进程看到的虚拟地址空间被切分成的固定大小块 - 物理页框:正式物理内存被切成同样大小的块 - 二者的大小相同,比如常见 4KB - 虚拟内存用虚拟页编号,物理内存用页框编号,页表负责把虚拟页号 -> 物理页框号 - 地址和页对应 - 页大小是 4KB - 4KB = 4096 字节 = 2^12 - 一个地址的低 12 位代表页内偏移 - 高位表示虚拟页号(VPN) - 地址转换链路 - VA -> TLB -> page table -> PTE -> PA -> Cache/Memory - 术语解释 - VA:进程发出的地址 - PA:真实内存地址 - VPN:虚拟页号 - VPO:页内偏移 - PPN:物理页框号 - PTE:页表项,记录 VPN -> PPN 的映射与权限等元数据 - TLB:页表项的高速缓存 - 页大小与地址拆分 - 页大小是 PageSize = 2^k 字节 - offsetBits = k - VPNBits = AddrBits - k - 转换过程 - CPU 产生 VA - MMU 用 VA 的 VPN 去查 TLB - TLB hit:直接得到 PPN + 权限 - 物理地址 PA = (PPN << offsetBits) | Offset - TLB miss:用 VPN 去查页表 - 拿到 PTE - 有效 -> 填入 TLB -> 形成 PA -> 继续访问 - 无效 -> 触发异常 - 页表结构 - PTE 包含什么 - PPN:物理页框号 - Present/Valid:是否在内存 - R/W/X:读写执行权限 - U/S:用户/内核权限 - Accessed/Referenced(A):是否被访问过 - Dirty(D):是否被写过 - 为什么需要多级页表 - 单级页表 - VA 空间大,VPN 组合数巨大 - 每个进程都有一张覆盖整个虚拟空间的大表 - 多级页表做法 - 把 VPN 再拆分成多端:VPN1/VPN2/.../VPNn - 顶层页表只在需要时为某个范围分配下一级页表 - 没用到的地址区间不分配页表 -> 按需分配页表 - 多级页表的 Page Walk - 先用 VPN 的高段索引顶层页表,取到二级页表的地址 - 再用下一段索引二级页表页 - 最终找到叶子 PTE 得到 PPN - 代价:要读多次内存 - TLB - 没有 TLB - 每次 load/store 都要走 page walk,可能是 4/5 级 - 意味着每次访问内存前都要额外多次访存 - TLB 的本质 - 缓存最近用过的 VPN -> PPN + 权限 - 命中时地址翻译几乎是常数空间 - 缺页异常 - 典型原因 - Demand paging(按需分页):页还没加载到内存 - Swap out:页被换出到磁盘 - 权限违规:写只读页 - Copy-on-write:写共享只读页 - 缺页处理流程 - CPU 访问 VA,发现 PTE present = 0 或者权限不符合 - 触发异常,陷入内核(保存上下文,切换到内核栈) - 内核 fault handler 判断 fault 类型 - 是合法的按需页/非法地址 - 合法 - 找到磁盘位置 - 分配物理页框 - 发起 IO 把页读入内存 - 更新页表 PTE - 更新 TLB - 恢复进程执行 - 页面置换 - 当需要一个物理页框但是没有空闲页时,OS 必须选择一个牺牲页换出 - LRU - Clock/Second-Chance(近似 LRU) - 维护环形指针 - 每个页有 Accessed/Referenced 位 - 扫描时 - 若 A=0:淘汰 - 若 A=1:把 A 清 0,给第二次机会,指针继续走 - 共享与写时复制(COW) - 为什么需要 COW - fork 的语义:子进程得到父进程内存的副本 - 如果把所有页都复制一遍,成本极高 - COW - fork 后,父子进程的页表都指向同一批物理页框 - 把这些页的 PTE 权限改为只读 - 当父或子尝试写入某页 - 写入触发 page fault - 内核分配新物理页 - 复制旧页内容到新页 - 修改该进程页表让它指向新页,恢复写权限 - 另一个进程仍然指向旧页 IO 总线 - IO 总线 - IO 本质上慢 - 设备物理特性:SSD/网卡/磁盘的延迟远远高于 CPU - 路径长:系统调用、内核态切换、驱动、协议栈、队列管理 - 数据搬运:拷贝次数多、缓存一致性/同步开销高 - I/O 的基本硬件/软件组件 - 设备、控制器、驱动 - 设备:网络、磁盘、USB 设备等 - 控制器:设备侧的执行单元,负责 DMA、队列、寄存器接口等 - 驱动:内核中的软件,负责配置设备、提交请求、处理中断、管理队列 - 数据通路常见参与者 - CPU 核心 - Cache - 内存 - I/O 互联 (PCIe) - 设备控制器 - 设备介质 - 轮询 vs 中断 - 轮询 - 做法:CPU 不断读取设备状态寄存器 - 优点:实现简单、延迟可控 - 缺点:浪费 CPU、高并发/多设备会严重占用 CPU - 中断 - 设备完成后发中断信号,CPU 暂停当前执行,进入中断处理程序(ISR) - 优点: - CPU 不需要忙等->提升整体吞吐 - 缺点: - 有中断开销:恢复/保存上下文、切换栈、处理 ISR - 中断频繁造成抖动 - DMA - 不用 DMA 的程序搬运 - CPU 循环从设备寄存器读数据,再写入内存 - 缺点:CPU 被数据搬运绑死,带宽高、功耗高 - DMA 的本质 - DMA 让设备/控制器直接读写内存,CPU 只做设置与收尾 - CPU 配置 DMA 描述符:内存地址、长度、方向、队列位置 - DMA 引擎通过总线把数据直接写入/读出 DRAM - 完成后用中断通知 CPU(或写状态寄存器) - DMA 为什么更快 - 释放 CPU 周期 - DMA 更贴近总线/设备,能更高效做 burst 传输 - 配合 ring buffer/descriptor queue 能批量提交 - 内存映射 I/O 与端口 I/O - MMIO - 设备寄存器被映射到某段物理地址空间 - CPU 用普通的 load/store 指令读写这些地址,就等价于读写设备寄存器 - Port-mapped I/O - 使用专门的 IN/OUT 指令访问 I/O 端口空间 - 现代系统多以 MMIO 为主 - 零拷贝 - 传统读文件再发网络的拷贝路径 - read():磁盘->内核页缓存->拷贝到用户缓冲区 - write():用户缓冲区->再拷贝回内核 socket buffer -> 网卡 DMA 发走 - 至少需要两次 CPU 拷贝(内核<->用户,用户<->内核) - sendfile 思路 - 直接让内核把 page cache 中的文件页挂到 socket 发送队列 - 避免拷贝到用户态再拷回内核态 - 网卡通过 DMA 从内核缓冲直接发 - mmap 思路 - mmap 把文件映射进进程地址空间 - 应用像访问内存一样访问文件内容,缺页时 OS 按需把页载入 page cache - 适合随机访问,但是直接转发场景 sendfile 更适合

计算机网络

网络分层 + 端到端路径 - 网络分层 + 端到端路径 - 分层模型 - 链路层 (LINK) - 负责:同一链路/局域网内把 IP 包送到下一跳 - 典型概念:以太网、WIFI、ARP、MAC、MTU - 网络层 (IP) - 负责:跨网络把包从源 IP 路由到目的 IP - 典型概念:IP 地址、路由表、NAT、ICMP、分片 - 传输层 (Transport) - 负责:端到端进程通信,提供端口与可靠性 - 应用层 (Application) - 负责:定义应用语义 - 端到端路径(一次 HTTPS URL 发生了什么) - URL 解析与策略决策(应用层前置) - 浏览器先拆 URL: - schema:https -> 需要 TLS - host:example.com -> 需要 DNS - port:默认 443 - path:/index.html - 是否可用缓存 - 是否复用已有连接 - DNS 解析:把域名变成 IP - 目标:得到服务器的 IP (A=IPv4/AAAA=IPv6) - 典型流程 - 浏览器/OS 先查本地缓存 - 没命中 -> 向递归解析器发起查询 - 递归解析器询问:根 -> TLD -> 权威 DNS - 返回 IP + TTL - TTL 决定缓存时长 - 同一域名可能返回多个 IP:简单的负载均衡,但是不可控 - TCP 连接(传输层) - 目标:建立一条到服务器IP:443的 TCP 连接 - 典型流程 - 客户端选一个临时端口作为源端口 - 形成 socket 四元组 - 三次握手完成后,连接进入 ESTABLISHED - 连接的唯一标识符是四元组 - NAT 环境下:srcIP 可能是私网 IP,但是公网会被 NAT 改写 - RTT 大时,建连成本高:三次握手至少消耗 1 个 RTT - NAT:内网 IP 也能访问公网(网络层/链路层交叉) - 大多数客户端在 NAT - 内网 192.168.x.x:ramdomPort - 出口 NAT 把它映射成:公网 IP:另一个 Port - NAT 设备维护一张映射表(内网四元组<->公网四元组) - 映射有超时:长时间无数据可能被回收 - 并发连接太多可能端口耗尽 - TLS 握手:在 TCP 上建立安全通道 - 目标 - 验证服务器身份 - 协商密钥 - 后续数据用对称加密 + 完整性保护 - TLS 发生在 TCP 建立之后、HTTP 之前 - 证书用于“证明对方是它声称的域名“ - 发送 HTTP 请求并接受响应 - HTTP 内容 - 请求行:GET /index.html - 头:Host、User-Agent、Accept、Cookie - 体:GET 通常无,POST 常见 - 服务器返回 - 状态码:200/301/404/500 - 头:Content-Type、Content-Length、Set-Cookie - 体:HTML/JSON/图片等 - 连接复用与关闭:不是每次都重新握手 - HTTP/1.1 默认倾向长连接(Keep-Alive) - 浏览器有连接池,会复用到同一个 host:port 的连接 - 关键概念锚点 - RTT:网络交互的时间单位 - RTT = 一个来回的延迟 - 建连/TLS/重传都依赖 RTT - 高 RTT 下,交互次数越多越慢 - 吞吐与 BDP(带宽-时延积) - MTU 月 MSS - MTU:链路层最大承载 - MSS:TCP 单段可承载的应用数据最大值 - 包太大可能被分片或者丢弃 - 合理 MSS 能减少分片风险,提升稳定性 TCP 精讲 - TCP 精讲 - TCP 本质:在不可靠 IP 上提供可靠字节流 - TCP 提供内容 - 面向连接:双方都维护连接状态(序号、窗口、定时器等) - 可靠、有序:丢包重传、乱序重排、去重 - 字节流:没有消息边界(粘包/拆包的根源) - 流量控制:避免把对方接收缓冲打爆(rwnd),端到端 - 拥塞控制:避免把网络打爆,端到网络 - 报文段关键字段 - Sequence Number:本段数据在字节流里的起点序号 - Acknowledgment Number:我期望收到的下一个收到的序号 - Flags:SYN/ACK/FIN/RST/PSH/URG - Window(rwnd):接收端通告窗口 - Options:MSS,Window Scale - 连接建立:三次握手 - 标准流程 - C->S: SYN(seq=x) - S->C: SYN+ACK (seq=y,ack=x+1) - C->S: ACK (ack=y+1) - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/17/20260217111458329.png,514,267) - 为什么必须三次 - 双方都要确认两件事 - 对方能收:对方收到了我的东西 - 对方能发:对方能把东西发回来让我收到 - 两次握手只能证明客户端 -> 服务端这条路通了;但是服务端无法证明服务端->客户端也通、且客户端确实收到了服务端的回应,第三次 ACK 就是让服务端拿到这个确认(服务端无法确认客户端能收) - 如果只有两次握手 - 服务端回 SYN + ACK 丢失了 - 服务端仍为连接建立,开始分配连接资源,等待数据 - 客户端实际上没有收到任何回应,会重试或放弃 - 三次握手还能防止历史重复 SYN 造成的“幽灵连接“ - 历史重复 SYN 指的是:某次旧连接尝试时发出的 SYN,由于网络拥塞、路由绕行、链路重传、设备缓存/异常等原因,在很久之后才到达服务器 - 如果服务器把旧 SYN 当新建连: - 服务器会分配资源 - 如果后续又出现旧 ACK 包,让服务器误以为连接已建立,交付错误的数据 - 幽灵连接:就是服务端自嗨式建立,客户端不认可 - 半连接队列与 SYN Flood - 服务端收到 SYN 后进入 SYN_RECV,会占用半连接资源(backlog) - SYN Flood:大量 SYN 不完成握手 —> 半连接队列被占满 -> 正常连接进不来 - 典型缓解:SYN cookies、增大 backlog、限速、丢弃策略、前置抗 DDoS - 连接关闭:四次挥手、半关闭 - 主动关闭方 A->B:FIN(不再发了) - B -> A:ACK (知道了) - B -> A:FIN (我也不再发了) - A -> B:ACK (知道了) - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/17/20260217113908534.png,440,230) - 为什么常见是四次 - TCP 是全双工:A 不发 != B 不发 - B 可能还有数据要发完,所以 ACK 和 FIN 分开 - 半关闭 - A 发 FIN 表示 A 的发送方向关闭,但仍然可以接受 B 的数据 - 工程上常见于“请求发送完但继续接受响应流“的场景 - RST:不是正常关闭,是异常打断 - RST(Reset)是 TCP 里面非常硬的控制信号:立刻终止连接/拒绝连接 - 不是正常的四次挥手关闭,告诉对方:“这条连接不存在/状态不一致,马上停“ - RST 常见原因 - 端口没监听 - 应用崩溃/强制关闭 - 中间设备丢弃状态后注入 RST - TIME_WAIT、CLOSE_WAIT - 主动关闭方在发送最后一个 ACK 后进入 TIME_WAIT,并等待 2MSL。 - 必须 TIME_WAIT - 保证对方收到最后 ACK:最后 ACK 丢了,对方重发 FIN;TIME_WAIT 还能再次 ACK - 让旧连接的延迟报文过期:避免旧数据混入新连接 - 为什么是 2MSL - MSL = 报文在网络中可能存活的最大时间 - 等待 2MSL 是为了覆盖对方 FIN 重传 + 我 ACK 再次发送等往返的不确定性。 - TIME_WAIT 多了怎么办 - 常见场景:短连接高 QPS - 治理思路 - 优先做连接复用:HTTP Keep-Alive/连接池 - 调整应用行为:减少主动 close、合并请求、复用长连接 - 系统参数:允许端口更快复用等 - 通常是我方主动关 + 短连接 - CLOSE_WAIT 多是什么 - CLOSE_WAIT 表示:对方已经 FIN 了,但是我方应用还没有 close - 常见原因:应用层忘记关闭连接、连接泄露、线程阻塞导致没有走到 close - 通常是我方没关 - 可靠传输机制:序号、ACK、重传、SACK - 序号与累计 ACK - TCP 把数据看成连续字节流:每个字节都有序号 - ACK 是累计的:ack = N 表示 0..N-1 都收到了,下一步要 N - 好处:ACK 开销小 - 坏处:遇到乱序/丢包,单靠累计 ACK 不够精细 - 重传触发机制 - 超时重传(RTO):等不到 ACK 就重发(RTO 动态估计,基于 RTT 抖动) - TCP 发送一个数据段后,会期待收到 ACK,如果在某个时间窗口没有等到 ACK,TCP 可能认为丢了,于是触发超时重传。 - 等多久的时间阈值就是 RTO(Retransmission Timeout)。 - 触发条件:发送后,计时器到期,仍未收到能确认该段的 ACK - 行为:重传未确认的数据段 - RTO 动态估计 - 网络 RTT 会变,而且会抖动,RTO 必须跟随着链路实际 RTT 变化 - SRTT + RTTVAR -> RTO - 名词解释 - RTT sample:一次测得的 RTT(从发出某段到收到它对应 ACK 的时间) - SRTT (Smoothed RTT):平滑后的 RTT 均值。 - RTTVAR:RTT 的波动。 - RTO = SRTT + 4 * RTTVAR - SRTT 给出正常情况下 ACK 来回要多久 - RTTVAR 给出不确定性有多大 - 乘以 4 是给一个安全幅度:抖动越大,RTO 增长越明显,避免误重传 - 指数退避:超时一次,RTO 翻倍 - RTO 重传 vs 快速重传 - RTO 重传 - 触发:计时器到期,没等到 ACK - 特点:更“保底“,但通常更慢;而且代表网络更糟糕 - 常伴随:指数退避,cwnd 通常会大幅下降 - Fast Retransmit (快速重传) - 触发:重复 ACK - 特点:更快,更轻判拥塞 - 一半比 RTO 更温和 - 快速重传(Fast Retransmit):收到多个重复 ACK (典型 3 个)推断某段丢失了,立即重传) - RTO 重传局限性 - 必须等待一段时间没收到 ACK 才重传 - 在 RTT 较大或 RTO 比较保守时,恢复会很慢 - 快速重传的思路 - 不等超时,利用接收端反馈的 ACK 形态更早推断丢包 - 重复 ACK - 如果接收端收到了更后面的段,但是缺了中间某段 - 不能把乱序段交付给应用 - 会继续发送同一个累计 ACK,告诉发送端还在等从 1000 开始的那段,后面来的我先缓存 - ACK 值没前进,但又反复出现的 ACK 就是重复 ACK - 3 个重复 ACK 会触发快速重传 - 收到 3 个重复 ACK 代表某段大概率丢了 - 立即重传缺口对应的最早未确认段 - 快恢复 - 原因 - 收到快重传代表完了给没有完全堵死(还能持续收到 ACK) - 不必像 RTO 那样极度保守把发送速率打到很低 - Reno 直觉 - 触发快速重传后,认为发生拥塞 -> 降低 cwnd - 但利用重复 ACK 的到来,允许发送端在恢复期间适度继续发,避免完全停摆 - 当收到新的 ACK 后,退出快恢复,回到拥塞避免阶段 - SACK(选择确认) - 痛点:累计 ACK 信息不够 - 当出现丢包 + 乱序到达时,累计 ACK 只能不断重复同一个 ACK=N,发送端只知道缺口在 N,不知道 - 后面哪些段已经到了 - 究竟丢失了几段,丢失了哪几段 - 重传时是否把已到达的也重传 - 目标是把接收端实际收到的乱序块显式告诉发送端 - SACK 是什么:在 ACK 里带已收到的区间块 - SACK 是 TCP 的一个 Option,启用后 - 接收端在 ACK 报文的 TCP Options 中携带若干个 SACK blocks - 每个 block 描述一个已经收到的连续字节区间 - SACK block 的语义 - SACK block = (Left Edge, Right Edge) - 表示接收端已经收到:[LeftEdge, RightEdge) 这个区间 - ACK 字段仍然存在,表示最左侧连续已收前缀的下一个期待序号 - 启用流程:SACK Permitted - SACK 需要在三次握手时协商 - 双方在 SYN/SYN+ACK 中携带 SACK Permitted 选项 - 只要双方都声明支持,后续才会在 ACK 中携带 SACK blocks - 乱序与重排 - 网络层可能乱序,TCP 负责在接收端按序交付给应用 - 接收端会缓存乱序段,等缺口补齐再交付 - 滑动窗口 - 痛点 - 如果每发一段就必须等 ACK 再发下一段,吞吐会被 RTT 严重限制:链路大部分时间在等回音。 - 滑动窗口允许发送端在未收到 ACK 前,先发出一批数据,让数据在路上飞,从而把链路利用起来。 - 窗口 = 允许在途未确认数据的上限;窗口越大,在高 RTT 链路上容易跑满带宽 - rwnd(receive window,接收窗口,流量控制) - 由接收端通告(在 TCP 头的 Window 字段里) - 表示接收端缓冲区还剩多少空间 - 目的:防止发送端把接收端内存撑爆 - cwnd(congestion window,拥塞窗口,拥塞控制) - 由发送端维护 - 表示发送端认为网络当前能够承受的在途数据量上限 - 目的:防止把网络打爆 - swnd(send window,实际发送窗口) - 发送端真正允许“在途未确认“的上限 - 近似于 min(rwnd,cwnd) - 发送端视角:窗口如何限制发送 - 发送端维护两个关键指针 - SND.UNA:最早未确认的序号(UnACKed) - SND.NXT:下一个准备发送的序号(Next to send) - 窗口约束可以理解为: - 允许发送的序号范围:[SND.UNA, SND.UNA + swnd) - 已发送未确认的范围:[SND.UNA, SND.NXT) - 还能继续发送的额度:swnd - (SND.NXT - SND.UNA) - ACK 到来时发生什么 - 收到 ACK,把 SND.UNA 往前推进 - 已确认数据从窗口左边滑出 - 窗口右边腾出了空间,允许继续发送新的数据 - 接收端视角:为什么会出现 dupACK/乱序缓存 - 接收端维护两个关键指针 - RCV.NXT:下一段按序期望的序号 - 接收缓冲:可以暂存乱序到达的段 - 出现乱序 - 接收端会缓存后到的段,但 ACK 仍然回 RCV.NXT(累计 ACK) - 发送端看到重复 ACK (dupACK),可能触发快速重传 - 窗口、吞吐、RTT - 吞吐上限 = 窗口大小/RTT - RTT=100ms,窗口=64KB,吞吐=64KB/0.1s=640KB/s - BDP(带宽-时延积),窗口需要=BDP 才能跑满带宽 - 表示一条网络路径在任意时刻在路上飞着的数据量大概有多少,也叫管道容量 - BDP = 带宽 * 往返时延 (RTT) - 想要把链路跑满,发送端需要让“在途未确认数据“接近 BDP - 在途未确认数据主要受窗口限制:in_flight <= min(cwnd, rwnd) - 要跑满带宽,需要窗口大小 >= BDP - 零窗口与窗口探测 - 如果接收端缓冲满了,会通告 rwnd=0 - 发送端此时必须停止发送新数据,但是要避免死锁 - TCP 有 persist timer:周期性发送 Zero Window Probe 探测包 - 一旦接收窗口恢复,发送端继续发 - 粘包/拆包 - 粘包:在发送端 send() 了两条业务消息,接收端一次 recv() 读到了两条合在一起的字节。 - 拆包:在发送端 send() 了一条业务消息,接收端需要 recv() 多次才能读完 - TCP 不保证 send 一次就对应端 recv 一次 - 原因 - TCP 的抽象是流 - 像读文件一样读一串字节 - send():把字节追加到发送缓冲区 - recv():从接收缓冲区取走当前可读的字节 - 分段由 TCP 决定,不由应用决定 - 接收端读取粒度由应用决定 - 解法 - 在应用层定义消息边界 - 长度前缀(Length-Prefixed,最通用) - 格式 | 4-byte length (big-endian) | payload bytes... | - length 表示 payload 长度 - 接收端先读满 4 字节得到 length,再继续读满 length 字节 - 分隔符(Delimiter-Based,适合文本协议) - 行协议:以 \r\n 结尾 - 固定长度(Fixed-Size) - 每条消息固定 N 字节 - 拥塞控制 - 拥塞不是对端瘦不下,那是流量控制 rwnd,拥塞是网络路径上的队列/链路承载不住 - 路由器/交换机队列堆积 -> RTT - 队列溢出 -> 丢包 - 丢包/排队抖动 -> 吞吐下降、延迟暴涨 - 拥塞控制控制的是发送端允许在网络中飞着的未确认数量,cwnd - Reno 的两个核心状态变量 - cwnd:拥塞窗口(发送端认为网络现在能承受的在途数据量) - ssthresh:慢启动阈值,决定从指数增长到线性增长的分界点 - 四个阶段 - 慢启动 - 目的:刚开始不知道网络能承受多少,先快速试探 - 规则: - 初始 cwnd 为一个较小值,历史是 1MSS - 每收到一个 ACK,cwnd 增加 1MSS - 因为一个 RTT 内会收到大约 cwnd/MSS 个 ACK,所以每个 RTT cwnd 近似翻倍 - 每 RTT 把在路上的包数翻倍,迅速逼近瓶颈带宽 - 太快可能把队列灌满,引发丢包 - 拥塞避免 - 当 cwnd 到达或超过 ssthresh,从慢启动切换到拥塞避免 - 每 RTT 让 cwnd 大约增加 1 MSS,每个 ACK 增加 MSS*MSS/cwnd - 大多数时间连接处于的阶段 - 快重传:用 dupACK 提前判断丢包,不等 RTO - 触发条件 - 收到 3 个重复 ACK dupACK - ACK 号不前进,连续出现多次 - 快恢复:丢包后减速但不中断,比 RTO 温和 - 发生快重传后的关键动作 - 设定新的值 - ssthresh = cwnd/2 - cwnd 不回到很小,而是进入快恢复逻辑 - 当收到能够推进 ACK 的新 ACK,退出快恢复,进入拥塞避免 - RTO vs dupACK - RTO:最严重,认为网络可能很糟 - ssthresh = cwnd / 2 - cwnd 下降到很小,经典是 1MSS - 回到慢启动(重新探测) - RTO 通常会指数退避,导致恢复更慢 HTTP 与 Web 体系 - HTTP 与 Web 体系 - HTTP 基础语义 - HTTP 是无状态应用层协议:每个请求都自包含信息,状态由 Cookie/Token/Session 等机制在应用层实现 - HTTP 定义的语义,具体传输方式由 HTTP/1.1、HTTP/2、HTTP/3 决定 - 方法:语义 + 幂等 + 安全性 - GET:获取资源,应当安全+幂等 - HEAD:只要响应头,不要响应体,用作探测/缓存探测 - POST:提交数据,创建子资源或触发动作,通常非幂等 - PUT:整体替换/创建指定资源,通常幂等 - PATCH:部分更新,是否幂等取决于语义设计 - DELETE:删除资源,通常幂等 - OPTIONS:探测服务器能力(常用于 CORS 预检) - 状态码 - 1xx:信息(少见) - 2xx:成功 - 200 OK - 201 Created 创建成功 - 204 No Content 成功但无响应体 - 3xx:重定向 - 301 永久 - 302 临时(历史语义混乱) - 303 让客户端用 GET 拿 - 307/308 保持方法不变 - 4xx:客户端错误 - 400 参数/格式问题 - 401 需要认证 - 403 拒绝 - 404 不存在 - 409 冲突 - 429 限流 - 5xx:服务器错误 - 500、502 上游坏/网关问题 - 503 不可用 - 504 上游超时 - Header - 通用与内容相关 - Content-Type:实体类型(json/html...) - Content-Length:长度(HTTP/1.1 重要) - Content-Encoding:gzip/br 等压缩 - Accept/Accept-Encoding/Accept-Language:内容协商 - Host:HTTP/1.1 必须(虚拟主机) - Connect:连接管理(HTTP/2 中不再使用) - 会话与安全 - Cookie/Set-Cookie - Authorization (Bearer/Basic 等) - Origin/Refer:CORS/CSRF 相关 - Strict-Transport-Security:强制 HTTPS - Content-Security-Policy(CSP):前端安全策略 - 缓存相关 - Cahce-Control - Expires - ETag/If-None-Match - Last-Modified/If-Modified-Since - Vary - Age (代理缓存的年龄) - Pragma:no-cache (历史兼容) - HTTP 缓存 - 两类缓存:强缓存vs协商缓存 - 强缓存 - 命中强缓存时浏览器直接用本地缓存,不走网络 - 典型控制 - Cache-Control: max-age= - Expires:老机制 - 协商缓存(发请求但可能不下载) - 浏览器发请求带校验文件,服务器判断资源是否变化 - ETag 流程 - 响应:ETag: "xxx" - 下次请求:If-None-Match: "xxx" - 若未变:返回 304 Not Modified - Last-Modified 流程 - 响应:Last-Modified: ... - 下次请求:If-Modified-Since:... - 未变:304 - Cache-Control 关键指令 - max-age=N:资源在 N 秒内视为新鲜(强缓存) - no-store:绝对不缓存 - no-cache:可以缓存,但每次使用前必须去服务器验证 - must-revalidate:过期后必须验证,不能用陈旧副本 - public:允许被共享缓存(CDN/代理)缓存 - private:只允许浏览器私有缓存,不允许共享缓存缓存 - s-maxage=N:给共享缓存的 max-age - Vary 存储维度的分桶键 - Vary 告诉缓存,同一个 URL 的响应会因为某些请求头不同而不同 - Vary:Accept-Encoding,gzip/br 不同版本要分开缓存 - Vary:Origin:CORS 常见 - Vary 过多会让缓存碎片化,命中率下降 - ETag 的强弱、代理与一致性 - 强 ETag:字节级一致才算命中 - 若 ETag (W/):语义上等价即可 - HTTP/1.1:连接、报文、队头阻塞 - HTTP/1.1 报文与传输 - 文本协议:请求行 + 头 + 空行 + body - 连接默认与持久化:Keep-Alive (减少握手成本) - Chunked Transfer-Encoding:服务端可流式传输,不提前知道 Content-Length - HTTP/1.1 的性能问题:应用层 HOL - 同一连接上请求/响应按顺序进行 - 一个慢请求会阻塞后续的请求(队头阻塞) - HTTP/2:二进制分帧与多路复用 - HTTP/2 为什么快 - 二进制分帧:把大消息拆成帧 - 多路复用:多个 stream 在同一 TCP 连接上并发交错传输 - 头部压缩:HPACK(减少重复头部开销) - HTTP/2 性能问题:TCP 层 HOL(队头阻塞) - TCP 一旦丢包,需要重传并按序交付 - 丢的那段之前的洞补不上,后续数据即使到了也不能交给上层 - 结果:一个丢包会让同连接上所有 stream 都受影响 - HTTP/3/QUIC - 核心变化 - HTTP/3 跑在 QUIC 上,QUIC 基于 UDP - QUIC 自己实现可靠传输与拥塞控制 - 按 stream 提供独立的有序交付,一个 stream 丢包不影响其他 stream 的交付 - TLS 与 HTTPS - 位置与成本 - HTTPS = TCP 建连后再 TLS 握手,再 HTTP - 首次访问慢常见原因:DNS + TCP + TLS 多次 RTT - 解决的问题 - 身份认证:验证连接的是域名的真实服务器 - 机密性:传输内容加密 - 完整性:防篡改 - TLS 握手 - TLS 1.2 - ClientHello:客户端支持的版本/套件,随机数,SNI 等 - ServerHello:选定版本/套件、随机数,让双方对本次会话参数单程一致 - Certificate:服务器发送证书链,客户端据此验证:链是否可信、域名是否匹配、是否过期,用途是否正确 - ServerKeyExchange:服务器提供 ECDHE 参数与公钥,客户端通过验证签名,确保密钥协商参数的安全性 - ServerHelloDone:告诉客户端,材料已经发完 - ClientKeyExchange:客户端协商参数,客户端发送自己的 ECDHE 公钥,双方据此计算共享 secret - ChangeCipherSpec + Finished:双方切换到加密通信并校验握手完整性 - Web 会话:Cookie/Session/Token - Web 会话解决的问题 - 认证:你是谁 - 授权:你能做什么 - 会话保持:登录态怎么持续、怎么过期、怎么注销 - 安全边界:防窃取、防伪造、防重放、防跨站 - Cookie:浏览器自动携带的会话容器 - Cookie 是浏览器保存的一组 key=value,由服务器通过响应头 Set-Cookie 下发。 - 后续请求中,浏览器会按照规则自动带上 Cookie 头,无需 JS 参与 - Cookie 关键属性 - Domain/Path:决定发送范围 - Domain=example.con:子域也可能携带 - 不设置 Domain:仅当前 host - Path=/api:只对当前路径生效 - Expires/Max-age:决定生命周期 - Max-Age=0:立即删除 - Session Cookie:不设过期时间,随浏览器进程结束而清理 - Secure:只在 HTTPS 发送 - HttpOnly:禁止 JS 读取 - 有 HttpOnly 的 cookie,JS 读取不到 - 不能防止 CSRF - SameSite:控制跨站请求是否携带 - Strict:几乎不跨站携带 - Lax:默认常用 - None:允许跨站携带,必须同时设置 Secure - Session:服务端保存状态,客户端只存门票 - Session 的典型模式 - 服务端维护 session_id -> session_data - 客户端用 Cookie 存 session_id - 每次请求:浏览器自动带 cookie -> 服务端查 session store -> 识别用户 - Token(尤其是 JWT):把身份证明带在请求里 - Token 的核心模式 - 服务端签发 token 给客户端 - 客户端后续请求带 token - 服务端验证 token 来识别用户 - JWT 结构 - JWT=header.payload.signature - header:算法等元信息 - payload:claims 声明 - signature:服务端用密钥签名 - Refresh Token 与 Access Token - Access Token:短有效期,每次用于请求 - Refresh Token:长有效期,用于换取新的 Access Token - 安全边界:XSS、CSRF、会话固定、重放 - XSS(跨站脚本) - 若 token/cookie 可以被 JS 读取,xss 可以直接窃取并盗用 - cookie 用 HttpOnly + Secure - CSP 减少脚本注入 - 输入输出编码 + 模板转义 - CSRF 跨站请求伪造 - CSRF 利用浏览器自动携带 cookie - 攻击者诱导用户在登录态下访问恶意页面,发起跨站请求到受害站点 - 防护手段 - SameSite Cookie (优先) - CSRF Token(表单/请求头携带,服务端校验) - 检查 Origin/Rerfer - 同源策略与 CORS - 痛点 - 浏览器安全模型默认假设:不同站点之间互不信任 - 浏览器用同源策略(Same-Origin Policy,SOP)限制一个网页脚本能读取另一个来源的资源内容 - 业务需要跨域调用 API,有了 CORS(CrossOrigin Resource Sharing):在 SOP 的基础上提供一个受控的放行机制。 - Orgin 源是什么:同源的三元组 - Origin = scheme + host + port - https://example.com:443 - 同源策略具体限制 - 跨源读被禁止 - JS 用 fetch 请求 https://xxx.com,请求可能能发出去,但是浏览器会阻止 JS 读取响应,除非响应满足 CORS 规则 - 跨源写/发送很多情况下是允许的 - <form action="xxx.com" method="POST"> 可以提交,因此 CSRF 能成立,请求可以发出去,但是页面读取不到 bank.com 的响应内容 - 部分资源加载天然跨源允许 - <img> <scirpt> <link> 浏览器允许加载这些资源 - CORS 本质:服务器声明允许,浏览器执行拦截 - 浏览器在请求带上 Origin - 服务器在响应返回 Access-Control-* 头 - 浏览器据此决定:是否把响应暴露给 JS - CORS 两种请求路径:简单请求 vs 预检请求 - 简单请求 - 方法是 GET/HEAD/POST - Content-Type 只能是三种之一 - application/x-www-form-urlencoded - multipart/form-data - text/plain - 请求头只能是 CORS-safelistedhead,不能随意加自定义头 - 预检请求 - 不满足简单请求条件,浏览器会先发送一个 OPTIONS 请求,询问服务器是否允许 - 预检请求携带关键头 - Origin: https://a.com - Access-Control-Request-Method:PUT - Access-Control-Request-Headers:x-token(将要携带的自定义头列表) - 服务器如果允许,需要在 OPTIONS 响应返回 - Access-Control-Allow-Origin - Access-Control-Allow-Methods - Access-Control-Allow-Headers - Access-Control-Max-Age(预检缓存多久) - 通过后,浏览器才会发真实请求 - CORS 响应头:每个头怎么用 - Access-Control-Allow-Origin - 指定允许的 Origin - 可以是精确 Origin 或者 * (允许任意源),如果要带 Cookie/凭证,不能用 * - Access-Control-Allow-Methods - 允许的方法列表:GET/POST - 主要用于预检响应 - Access-Control-Allow-Headers - 允许的自定义请求头列表 - 必须覆盖预检中 Access-Control-Request-Headers 请求到的那些头 - Access-Control-Allow-Credentials - true 表示允许携带凭证,例如 Cookie - 配合前端请求 credentials: 'include' - Access-Control-Expose-Headers - 默认情况下,JS 能够读取的响应头很有限,如果希望暴露,必须显式添加 - Vary: Origin - 按 Origin 分桶缓存 - DNS 解析 - 基础对象 - 记录类型 - A(IPv4) - AAAA(IPv6) - CNAME(别名) - NS(权威) - 解析角色 - Stub resolver(客户端/OS) - Recursive resolver(递归解析器:运营商/公司 DNS/公共 DNS) - Authoritative server (权威 DNS) - 正向代理、反向代理、透明代理与隧道 - 代理:本质上是位于客户端与服务端之间的一种中间节点,代表一侧发起连接、转发数据、并可能施加策略 - 承担职责 - 路由/转发:把请求送到正确上游 - 安全控制:鉴权、WAF、访问控制 - 性能优化:连接复用、压缩、缓存、限流 - 可观测性:日志、指标、追踪、采样 - 协议转换:HTTP <-> grpc 等 - 正向代理 - 正向代理站在客户端一侧,客户端明确知道自己在用代理,由代理代表客户端访问外部资源 - 反向代理 - 反向代理站在服务端一侧,对外它像真正的服务器入口,对内把请求转发到后端服务 - 反向代理常用原因 - 隐藏后端拓扑:外部只有一个入口 - 统一 TLS 终止:证书集中管理 - 统一流量治理:限流、熔断、灰度、路由、鉴权 - 连接复用:对外维持大量连接、对内复用少量连接 - 观测与审计:同一日志/指标/链路追踪 - 透明代理 - 隧道:Connect 到底干了什么 - 隧道:代理不解析上层协议,只做字节透传。 - HTTP Connect 隧道流程 - 客户端先对代理发 CONNECT - 客户端 -> 代理 - CONNECT bank.com:443 HTTP/1.1 - Host: back.com:443 - 代理建立到目标的 TCP 连接 - 代理 -> bank.com:443:发起 TCP 连接 - 代理回客户端 - HTTP/1.1 200 Connect Established - 隧道建立,字节透传 - 客户端与 bank.com 的 TLS 握手数据会被原封不动地通过代理转发,代理看不懂也不需要看懂 - TLS 会话在客户端<->目标服务器之间建立 - 负载均衡:L4 vs L7、算法、会话保持、健康检查 - 目标 - 扩展性:把流量分散到多台实例,提高吞吐 - 高可用:实例故障自动摘除,流量不中断或快速恢复 - 性能:就近、减少重连、减少排队、提升尾延迟 - 治理:灰度、限流、熔断、路由、可观测 - 边界 - LB 解决的是把请求推送到谁,不解决业务正确性 - LB 层任何自动重试/超时,都可能改变业务语义 - L4 与 L7 的区别 - L4 LB(传输层):基于 IP/端口/连接做转发 - 优点:性能高,对协议透明 - 缺点:无法基于 URL/Header 做路由 - 常见实现:内核转发、四层 VIP、DSR、NAT 模式等 - 适合长连接:WebSocket、MQ、数据库连接等 - L7 LB(应用层):理解 HTTP,按照 Host/Path/Header/Cookie 路由 - 常见常见:API Gateway、Ingress、CDN 回源入口 - 负载均衡算法 - Round Robin(轮询) - 依次分发到每个后端 - 适合:请求耗时相近、后端同质、短连接 - 问题 - 后端性能差异大时不均衡 - 长连接场景下连接一旦分配就会粘住,RR 也会失衡 - Weighted Round Robin - 按权重分配(机器规格不同,冷热分层) - 适合:异构机器,逐步扩容 - 问题:权重需要维护;负载时间随时间变化时仍然不准 - Least Connections(最少连接) - 连接数最少的后端连接 - 适合:长连接、连接数与负载相关的业务 - 问题: - 连接数不等于负载 - 需要 LB 维护连接计数状态 - Least Request/Least Load(最少请求/最小负载) - 按当前活跃请求数或综合指标选择 - 适合:请求耗时差异大,尾延迟敏感 - 问题:实现复杂、依赖观测准确性;指标延迟可能导致震荡 - Random/Power of Two Choices(随机/二选一) - 随机挑两个后端,选最优者 - 优点:简单且实际效果接近最优,分布更稳定,大规模下能显著降低热点概率 - 适合:大规模集群、需要避免中心化状态 - Consistent Hash(一致性 hash) - 按 key 映射到固定节点 - 适合: - 需要缓存命中(本地 cache) - 会话保持(不想引入共享 session) - 分片存储(按 key 分片) - 关键性质:节点增减时迁移量小 - 问题: - 热点 key 会压垮单节点(热点拆分/虚拟节点) - 节点故障时 key 重映射会引发抖动。 - 会话保持 - 为什么需要 sticky - 后端有状态 - 本地缓存必须命中 - WebSocket/长连接绑定到某实例 - 常见 sticky 手段 - Source IP hash - 按客户端 IP 做 hash 选节点 - 优点:简单 - 缺点: - NAT/代理下大量用户共享同一出口 IP -> 严重倾斜 - 用户换网 IP 变化导致漂移 - Cookie 粘性 - LB 注入 cookie ,后续按照 cookie 路由 - 优点:比 IP 稳定 - 缺点:跨域,隐私策略,cookie 清理,灰度/回源复杂 - 一致性哈希(按 userId/会话键) - 工程风险 - 降低容错:节点挂了,该节点的用户都受影响 - 产生热点:某些用户/租户流量大->单节点热 - 扩缩容抖动:重新分配导致缓存冷启动、延迟尖刺 - 推荐的替代方案 - 应用无状态 + 共享 session (Redis) - 把状态外置(DB/Cache),实例可以随时替换 - 长连接常见:连接层 sticky 不可避免,要做好限流与迁移策略 - 健康检查 - L4 健康检查 - 探测 TCP 能否 connect 成功 - 优点:便宜、通用 - 缺点:应用假活时仍然会通过(线程池满、依赖挂) - L7 健康检查 - 请求 /healthz 或 /readyz - 可检查 - 进程存活 - 是否可接新流量 - 关键依赖是否可用 - 设计要点 - liveness vs readliness 分离 - liveness:进程是否需要重启 - readliness:是否接流量 - 避免健康检查过重 - 如果每秒大量探测并且检查依赖,会把依赖也打挂 - 阈值与抖动控制 - 连续 N 次失败才摘除 - 连续 M 次成功才恢复 - 慢启动与预热 - 新实例启动后先不接流量,预热缓存/JIT,再标 ready - 网络排障 - L3/L4 基础联通与路径 - ping - 测试的是什么 - ping 用的是 ICMP Echo Request/Echo Reply - 发 Echo Request(带序号 seq、时间戳等) - 对端回 Echo Reply - 测量往返时延 RTT - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/18/20260218103650584.png,492,184) - 查看延迟 - 看 avg 和 min/max - 抖动 - RTT 的波动程度 - stddev(标准差) 大,说明网络队列/无线/拥塞波动明显 - 丢包 - packet loss > 0 说明有包没有收到回包 - tracerout/mtr 的原理 - traceroute:用 TTL 把路径探测出来 - IP 包里有 TTL,每过一跳路由器把 TTL - 1 - 当 TTL 变为 0,路由器丢包并回 ICMP Time Exceeded - traceroute 依次发 TTL=1,2,3 的探测包 - 可以看到每一跳的地址与该跳的 RTT - 每一跳显式的 RTT 是你->该跳->你的往返,不是单程,也不是该跳->下一跳的延迟 - mtr:traceroute + ping 的结合 - mtr 会对每一跳持续发探测包并统计 - 丢包率 - 最快/平均/最慢 RTT - 抖动 - mtr 更适合定位抖动/偶发丢包/拥塞点 - 用 traceroute 看路径与绕路 -