进程和线程
进程是系统进行资源分配的基本单位,拥有独立的内存空间和系统资源
线程是进程的执行单元,属于进程的一部分,共享进程的内存空间和系统资源,但每个线程都有自己的栈和寄存器状态
区别
进程可以看成独立应用,线程不能
进程是资源分配的最小单位,线程是CPU调度的最小单位
线程间可以直接共享同一进程的资源,而进程通信需要借助进程间通信
进程切换比线程切换开销大。(某个进程中的线程切换到另一个进程的线程时,会引起进程切换)
进程系统开销大,创建和撤销进程时,系统要为之分配或回收资源;进程间切换时,涉及当前执行进程CPU环境、状态保存、新调度进程状态设置。线程开销小,线程切换只需要保存设置少量寄存器内容
联系
二者均可并发执行
线程是进程的执行单元,也是进程的可调度实体。一个程序至少有一个进程,一个进程至少有一个线程,一个线程只属于一个进程
资源分配给进程,同一进程的所有线程共享该进程的所有资源
进程的状态
基本状态包括就绪状态、运行状态和阻塞状态。在某些系统中,还可能有创建状态和终止状态。
- 就绪状态:进程已经准备好运行,正在等待CPU资源分配。此时进程被放入就绪队列中,等待调度程序选择。
- 运行状态:进程当前正占用CPU资源,其指令正在被执行。一个进程只能在一个时刻处于运行状态。
- 阻塞状态:也称为等待状态,进程由于等待某种事件(如I/O操作完成)而暂停执行。只有当等待的事件发生后,进程才会从阻塞状态转为就绪状态。
- 创建状态:新进程刚刚被创建,但尚未进入就绪队列。这个状态通常是短暂的。
- 终止状态:进程完成了任务或因错误被终止,不再参与调度。
- 状态转换:进程的状态会随着事件的发生而改变。例如,当操作系统调度一个就绪进程时,它会从就绪状态变为运行状态;如果运行中的进程发起I/O请求,则会从运行状态变为阻塞状态。
进程间通信
管道
管道是一种简单且常用的进程间通信方式,主要包括两种类型,匿名管道和命名管道。
(1)匿名管道:主要用于具有亲缘关系的进程间通信,即父子进程之间的通信。匿名管道由系统调用 pipe 创建,管道的两端分别用于读和写数据,只能单向流动。
(2)命名管道:即有名字的管道,可以在没有亲缘关系的进程间使用。命名管道由系统调用 mkfifo 创建,并在文件系统中以路径名表示。
特点:
Fifo:管道遵循先进先出的原则,保证数据的顺序性。
半双工:管道是半双工的通信方式,数据只能单向流动,如果需要双向通信,需要创建两个管道。
管道只能用于本地进程间的通信,无法跨网络使用。
信号
信号是一种相对复杂的进程间通信方式,主要用于通知进程某些事件的发生。
用户可以使用 kill 命令将信号发送给其他进程。该命令不仅仅可用于杀死进程,还可以用来发送各种信号,以通知进程执行特定操作。
特点:异步通信:信号的发送和接收是异步的,不需要接收方主动轮询。信号处理:接收进程可以定义信号处理函数,当信号到达时,操作系统会自动调用该函数。信号的实现和处理相对复杂,特别是在需要处理多个信号时,需要注意信号的优先级和处理顺序。
消息队列
消息队列允许进程以消息的形式发送和接收数据,每条消息都有一个类型标识,这使得消息队列比管道更灵活。
消息队列是为了克服信号传递信息量少,管道只能承载无格式字节流以及缓冲区大小受限等问题而设计的。
特点
有格式:消息队列可以存储具有特定格式的数据,每条消息可以包含一个类型标识和数据部分。
异步通信:消息发送方和接收方可以独立运行,不需要同时在线。
消息队列适用于需要在多个进程间传递结构化数据的场景。
共享内存
共享内存是最快最有用的进程间通信方式,它允许多个进程直接读写同一块内存空间。不同进程可以及时看到对方进程中共享内存中数据的更新。
特点
高速通信:由于数据直接存取,共享内存的速度非常快,适合需要大量数据交换的场景。
同步机制:由于多个进程可以同时访问同一块内存,需要依靠某种同步机制来保证数据的一致性和避免竞争条件。
共享内存非常高效,但需要小心处理同步和互斥问题,否则可能导致数据不一致或竞争条件。
信号量
信号量主要作为进程之间及同一进程的不同线程之间同步和互斥的手段。信号量通常用于控制对共享资源的访问。
特点
同步机制:信号量主要用于解决进程间的同步问题,例如控制对共享资源的访问,避免竞争条件。信号量可以有效避免多个进程同时访问共享资源而引发的竞争条件。
信号量是实现进程间同步和互斥的重要工具,适用于需要严格控制资源访问的场景。
套接字
套接字是一种广泛使用的进程间通信方式,尤其适用于跨网络的通信。套接字提供了一种标准化的通信机制,允许不同主机上的进程进行通信。套接字支持多种通信协议,如 TCP 和 UDP 。
特点
跨网络通信:套接字不仅可以用于本地进程间通信,还可以用于不同主机上的进程间通信。在分布式环境下相对安全
灵活性:套接字支持多种协议,适用于多种应用场景。
套接字是网络编程的基础,广泛应用于客户端-服务器模型的应用程序中。
进程调度
进程调度的定义:操作系统中的进程调度是指操作系统管理和分配 CPU 资源给不同进程的过程。它确保多个进程能够有效地共享 CPU 时间,从而提高系统的并发性。
常见的调度策略:
- 先来先服务(FCFS):按进程到达的顺序进行调度。优点是简单易实现,但缺点是可能导致长进程阻塞短进程(即“饥饿”现象)。
- 短作业优先(SJF):优先调度执行时间短的进程。可以减少平均周转时间,但难以实现,因为需要预测进程的执行时间。
- 轮转调度(RR):为每个进程分配固定的时间片,时间片用完后将进程放回队列,等待下一次调度。适合对响应时间有要求的系统,但频繁切换可能导致额外开销。
- 优先级调度:为每个进程分配优先级,优先级高的进程先执行。可能导致低优先级进程的饥饿问题。
进程的三个状态:就绪、运行、阻塞
进程调度就是从进程的就绪队列中按照一定的算法选择一个进程并将CPU分配给他运行,以实现进程的并发执行。
线程数据安全
线程的数据安全是指在多线程环境下,确保多个线程对共享数据的访问不会导致数据不一致或错误的结果。这通常通过使用同步机制如锁、信号量、原子操作等来实现。
首先识别出哪些数据是被多个线程共享的,然后确定这些数据在并发访问时可能出现的问题,例如竞态条件、死锁等。接着选择合适的同步机制来保护这些共享数据,确保在任何时候只有一个线程可以修改数据,或者确保读写操作是原子的。
在多线程环境中,多个线程可能同时访问和修改共享数据,如果没有适当的同步机制,就可能导致数据不一致。常见的同步机制包括互斥锁(Mutex)、读写锁(RWLock)、信号量(Semaphore)和原子操作(Atomic Operations)。互斥锁是最常用的同步工具,它确保在同一时间只有一个线程可以进入临界区,即访问共享资源的代码段。读写锁允许多个线程同时读取数据,但只允许一个线程写入数据。信号量用于控制同时进入某个资源的最大线程数。原子操作则是确保某些操作(如递增计数器)在硬件级别上不可中断。
并发和并行
并行是指同时执行多个任务,通常需要多核处理器的支持。每个核心可以独立地执行不同的任务。
并行的优点是可以显著提高程序的执行速度,特别是在处理大规模数据集时。然而,并行编程也带来了挑战,例如如何有效地分配任务、如何处理共享资源的竞争条件等。常见的并行编程模型包括MPI(Message Passing Interface)和OpenMP。
并发则是指在单个处理器上通过快速切换任务来实现多个任务的交替执行,从而让程序看起来像是同时运行。可以通过时间片轮转等调度策略实现多个任务的交替执行。
(操作系统会将CPU的时间划分为小的时间片,然后在不同的任务之间快速切换,使得每个任务都有机会获得CPU时间。这种切换非常快,以至于用户感觉像是多个任务在同时运行。)
并发的优点是提高了资源利用率,但也会带来一些问题,如死锁、竞态条件等。
并发则更适合于I/O密集型任务,例如Web服务器处理多个客户端请求。此外,并发还可以通过异步编程模型来实现,这种方式可以避免阻塞操作导致的性能下降。
并行强调的是真正的“同时”执行,而并发强调的是“交替”执行。
非抢占式进程调度算法
当进程正在运行,他会一直运行,直到该进程完成或发生某个时间被阻塞时,才会把CPU让给其他进程
先到先服务调度算法:按照进程到达的先后顺序进行调度
最短作业/进程优先调度算法:选择运行时间最短的
高响应比优先算法:选择响应比最高的进程
抢占式进程调度算法
当进程正在运行时,可以被打断,把CPU让给其他进程
最短剩余时间优先算法
轮转调度算法
最高优先级调度算法:不属于抢占式和非抢占式
浏览器缓存策略
浏览器缓存是一种性能优化机制,通过将已请求的资源(如HTML、CSS、JS、图片等)存储在本地,使得后续请求相同资源时可以直接从缓存中读取,而无需再次向服务器发起网络请求,从而减少延迟、降低带宽消耗并提升页面加载速度。
根据是否需向服务器发起HTTP请求,将缓存过程划分为两个部分: 强制缓存和协商缓存,强缓优先于协商缓存。
强缓存:浏览器根据响应头中的Cache-Control或Expires字段判断资源是否仍处于有效期内。若在有效期内,则直接使用缓存,不向服务器发送请求。
协商缓存:当强制缓存失效后,浏览器会向服务器发起请求,携带缓存标识(如If-Modified-Since或If-None-Match),服务器判断资源是否更新。若未更新,则返回304 Not Modified,告知浏览器继续使用本地缓存;否则返回200和新资源。
强制缓存通常应用于不经常变动的静态资源,而协商缓存适用于可能被频繁更新的资源
- JS、css、图片等文件通常是静态资源,构建时会加入内容哈希(content hash),一旦内容变化,文件名随之改变,新URL会触发新请求,因此可以安全地长期缓存,使用强缓存。
- HTML文件是入口文件,用户访问的是固定URL(如/index.html),即使内容更新,URL不变。若设置强缓存,浏览器可能长期不请求服务器,导致用户无法获取最新版本的页面结构或资源引用。因此应设置较短的max-age或直接禁用强缓存,启用协商缓存,让浏览器每次检查是否有更新。
HTTP缓存都是从第二次请求开始的:第一次请求资源时,服务器返回资源,并在response header中回传资源的缓存策略;第二次请求时,浏览器判断这些请求参数,击中强缓存就直接200,否则就把请求参数加到request header头中传给服务器,看是否击中协商缓存,击中则返回304,否则服务器会返回新的资源。
在普通刷新下,强缓存(直接从本地缓存读取,不发请求)基本会失效,但协商缓存(发请求问服务器)仍然有效。因此,缓存可能“发生变化”,因为服务器可能返回了新的内容(200),也可能确认使用旧缓存(304)。
在强制刷新下,强缓存和协商缓存都会被绕过。浏览器一定会从服务器获取所有资源的最新副本,因此你看到的绝对是“发生变化”后的最新页面。
强缓存
控制强制缓存的字段有:Cache-Control(http1.1)和Expires(http1.0)
Cache-control是相对时间,表示自上次请求正确的资源之后的多少秒的时间段内缓存有效。
max-age:即最大有效时间。
must-revalidate:缓存过期后必须向服务器验证,不能使用陈旧资源
no-cache:不使用强制缓存,但允许协商缓存(即每次请求需向服务器验证)。
no-store: 完全禁止缓存,每次请求都必须从服务器获取完整资源。
public:所有的内容都可以被缓存 (包括客户端和代理服务器, 如 CDN)
private:所有的内容只有客户端才可以缓存,代理服务器不能缓存。默认值。
业务场景:1. 静态资源(如图片、CSS、JavaScript 文件)、2. API 接口、3. 内容分发网络:使用 CDN 时,强缓存可以帮助快速响应用户请求,减少延迟。
Expires是绝对时间。表示在这个时间点之前发起请求可以直接从浏览器中读取数据,而无需发起请求。
- 由于是绝对时间,用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,重新请求该资源。此外,即使不考虑修改,时差或者误差等因素也可能造成客户端与服务端的时间不一致,致使缓存失效。
Cache-Control的优先级比Expires的优先级高。前者的出现是为了解决Expires在浏览器时间被手动更改导致缓存判断错误的问题。如果同时存在则使用Cache-control。
当多个缓存头共存时,浏览器按以下优先级处理: no-store > no-cache > max-age > Expires ---只要存在no-store,就不缓存;存在no-cache则跳过强制缓存进入协商缓存。
协商缓存
协商缓存的状态码由服务器返回200或者304
当强缓存失效的时候或者请求头中设置了不走强缓存,并且在请求头中设置了If-Modified-Since(1.0) 或者 If-None-Match (1.1)的时候,会到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified 或者 ETag 属性。
Last-Modified/If-Modified-since表示的是服务器的资源最后一次修改的时间;浏览器下次请求时通过If-Modified-Since带上该时间,服务器判断是否在此之后修改。
优势:不存在版本问题
劣势:1、只要资源时间修改都会将该资源返回客户端。2、无法识别一秒内进行多次修改的情况。 3、某些服务器不能精确的得到文件的最后修改时间。
Etag/If-None-match表示的是服务器为资源生成的唯一标识符(如哈希值),用于精确判断资源是否变化。浏览器在后续请求中通过If-None-Match头发送ETag,服务器对比后决定是否返回304。
优势1、更精确的判断资源是否被修改,可以识别一秒内多次修改的情况。2、不存在版本问题,每次请求都去服务器进行校验。
劣势:1、计算ETag值需要性能损耗。 2、存在浏览器从A服务器上获得页面内容后到B服务器上进行验证时出现ETag不匹配的情况。
Etag/If-None-match的优先级比Last-Modified/If-Modified-since高。
缓存存储位置:
- Memory Cache:内存中的缓存,读取速度快,但容量小,关闭标签页后清除。
- Disk Cache:磁盘上的缓存,容量大,持久化,适合大文件(如图片)。
- Service Worker Cache:通过JavaScript控制的缓存,可实现离线访问和复杂缓存策略(如Cache API)。
- Push Cache:HTTP/2 Server Push产生的缓存,生命周期较短,优先级最低。
基于什么知道是否有缓存
1.查看响应,200(from cache)还是200 ok;也可以查看响应头
2.页面加载表现:加载快、离线可访问、内容滞后表示有缓存
缓存实现
缓存的实现通常包括缓存存储结构的选择(如哈希表)、缓存淘汰策略的设计(如LRU、FIFO、LFU等),以及线程安全机制(在多线程环境下)。一个典型的缓存系统可以基于LRU(最近最少使用)策略结合哈希表和双向链表实现,支持O(1)时间复杂度的插入、查找和删除操作。
-
缓存存储结构:常用哈希表(HashMap)作为主存储,实现键值对的快速查找(平均O(1))。 为了支持淘汰策略,往往需要额外维护访问顺序信息。
-
淘汰策略 :
LRU(Least Recently Used):淘汰最久未使用的条目。适合具有时间局部性的场景,如页面缓存。
FIFO(First In First Out):按插入顺序淘汰,简单但可能误删热点数据。
LFU(Least Frequently Used):淘汰访问频率最低的条目,适合访问分布稳定的场景。
Random Replacement:随机淘汰,实现简单,性能可预测。
LRU缓存的底层实现原理:
使用哈希表 + 双向链表:
哈希表:key -> Node指针,实现O(1)查找。 双向链表:按访问时间排序,头结点为最新访问,尾结点为最久未访问。 - 每次get或put一个key时,将其对应的节点移到链表头部。 - 当缓存容量满时,删除链表尾部节点,并从哈希表中移除对应key。
为什么用双向链表而不是单向链表? 因为在删除中间某个节点时,需要快速获取其前驱节点,单向链表无法O(1)完成,而双向链表可以。
为什么不用数组或栈? 数组删除元素效率低(O(n)),栈只能按后进先出顺序淘汰,不符合LRU逻辑。
-
线程安全性:
- 在多线程环境中,多个线程可能同时读写缓存,需加锁(如互斥锁)或使用无锁数据结构。
- 可使用读写锁优化读多写少场景。
- 高并发下可考虑分段锁(类似ConcurrentHashMap)降低锁粒度。
输入一个URL
1.输入网址并解析
输入URL,浏览器会对输入的内容进行判断。检查是否是合法的URL,如果合法则判断输入的URL是否完整,如果不完整,浏览器会对地址进行猜测,补全地址;如果不合法将输入内容作为搜索条件进行搜索。大部分浏览器会先从历史记录、书签等查找内容,并给出提示。
2.DNS解析(Domain Name System,域名系统)
域名是用于标识互联网上某个计算机(如服务器)或服务的人类可读的地址。它可以映射到IP 地址,帮助我们在浏览器中轻松访问网站。
(由于浏览器不能从域名直接找到对应的IP地址,所以需要DNS解析。域名的作用是对 IP 地址进行抽象化,方便用户访问。)
浏览器将域名转换为服务器的IP地址,获得IP后将HTTP的传输交给协议栈
默认端口是53
DNS底层使用UDP协议实现,进行域名解析和数据传输,因为基于UDP实现DNS能够提供低延迟、简单快速、轻量级的特性,更适合DNS这种需要快速响应的域名解析服务。
DNS-prefetch
是一种DNS预解析技术。当浏览网页时,浏览器会在加载页面时对网页的域名进行解析缓存,这样在单击当前页面中的连接时就无需进行DNS的解析,减少用户等待时间,提高用户体验
步骤:
-
浏览器先查自己的缓存(短时间访问过的域名,可能有 TTL)。如果命中,直接用缓存的 IP,不会再发请求。
-
浏览器没命中时,调用系统的 DNS API(如 Windows 的
DNS Client Service、Linux 的glibc)。系统会查本地缓存和hosts文件(静态映射)。 -
若本地也没有,系统向配置好的 本地 DNS 服务器 发送查询(通常是网络服务商 ISP 的 DNS,也可以是 8.8.8.8 等公共 DNS)。 本地域名服务器负责全程帮你找结果,所以它是递归解析的起点(Recursive Resolver)。
-
如果本地域名服务器没有缓存,会先向 根域名服务器 查询
.com、.org等顶级域的地址线索。根服务器不返回最终 IP,而是返回负责顶级域的 顶级域名服务器(TLD Server)的地址。 -
顶级域名服务器接到
example.com查询时,返回该域名所属权威 DNS(Authoritative DNS)的地址。 -
权威 DNS 服务器是域名注册商或网站维护方设置的最终权威信息。返回具体的解析记录(如 A 记录 → IPv4,AAAA → IPv6,CNAME → 某个别名)。
-
本地点递归解析器拿到结果后,会:缓存记录(遵守 TTL 时长)。返回给请求方(浏览器 → 操作系统 → 应用)。
前端最常遇到的 DNS 问题
1. DNS 解析慢导致首次访问卡顿
表现: 页面首屏加载变慢;Performance 中看到 DNS Lookup 阶段耗时高
解决方式:域名预解析:<link rel="dns-prefetch" href="//xxx.com">;域名预链接:<link rel="preconnect" href="https://xxx.com">;尽量减少域名数量(同站资源放子域)
2. 使用了太多域名导致 DNS 时间被放大
解决方式:域名合并;静态资源尽量统一 CDN 域;关键资源使用 preconnect
3. 企业或某些地区会遭遇 DNS 劫持
表现: 打开公司某个域名跳到广告页面;CDN 域名访问失败;图片加载不出来;请求被 302 重定向到奇怪地址
解决办法: 所有接口/资源全用 HTTPS(能避免被劫持);使用自带 DNS 的服务(如阿里云);不让用户访问 HTTP 内容(严格模式)
DNS调试
dig - 专业 DNS 查询工具(推荐)
# 基本查询
dig www.example.com
# 简短输出
dig +short www.example.com
# 指定记录类型
dig example.com MX
dig example.com NS
dig example.com TXT
# 指定 DNS 服务器
dig @8.8.8.8 www.example.com
dig @1.1.1.1 example.com A
# 跟踪完整解析过程
dig +trace www.example.com
host - 简单查询工具
# 基本查询
host www.example.com
# 查询特定记录
host -t MX example.com
host -t NS example.com
# 指定 DNS 服务器
host www.example.com 8.8.8.8
浏览器 DNS 调试
清除浏览器 DNS 缓存
// Chrome 地址栏输入:
chrome://net-internals/#dns
// 然后点击 "Clear host cache"
// 查看 DNS 信息
chrome://net-internals/#events
浏览器开发者工具
# 1. 打开开发者工具 (F12)
# 2. 转到 Network 标签
# 3. 刷新页面
# 4. 查看每个请求的 DNS 查询时间
# 5. 点击请求查看详细信息,包括 DNS 解析时间
3.建立TCP连接
浏览器获取到服务器IP地址后,向服务器80端口发送TCP连接请求,通过TCP三次握手与服务器建立连接
4.发送HTTP/HTTPS请求
浏览器通过已建立的TCP连接,向服务器发送一个HTTP请求
5.服务器处理请求
服务器接收到请求后,会根据请求的资源路径和参数进行处理
6.服务器返回HTTP响应
服务器将生成的HTTP响应发送回浏览器
响应包括:状态码、响应头、响应体
7.浏览器解析和渲染
A记录和CNAME记录的区别
A 记录:把域名直接映射到 IPv4 地址(4 组数字)
CNAME 记录:把域名映射到 另一个域名(别名 → 正式名)。查询时需要再解析一次目标域名,直到得到最终 IP。
| 对比维度 | A 记录 | CNAME 记录 |
|---|---|---|
| 指向目标 | 直接指向 IP 地址 | 指向另一个域名 |
| 解析步骤 | 1 步(域名 → IP) | 至少 2 步(域名 → 域名 → IP) |
| 性能 | 更快(少一次查询) | 稍慢一点(多一次解析) |
| 灵活性 | 要改 IP 时需更新每条 A 记录 | 只改被指向域名的解析即可,所有别名自动生效 |
| 是否可与其他记录共存 | 同一主机记录,可与 AAAA 共存 | CNAME记录不能与其他记录(A, MX等)共存,除非是不同子域 |
| 常见用途 | 网站主要入口,如 www.example.com | CDN接入、负载均衡、将子域重定向到主域 |
前端可优化的部分
资源请求阶段优化
1 使用缓存,减少资源请求次数。
2 静态资源 CDN 化,图片 / JS / CSS 放到 CDN,加速节点分发。
3 减少资源体积:代码压缩、CSS 压缩、图片压缩
HTML 解析阶段优化
1 减少 DOM 数量
- DOM 过多 → 重排重绘多
- 使用虚拟列表渲染大量数据(IntersectionObserver)
2 关键内容优先加载
CSS 解析阶段优化
1 CSS 放在 head(避免阻塞渲染),浏览器会阻塞渲染直到 CSS 下载完。
JS 执行阶段优化(重点)
1 脚本异步加载
defer:下载并行,不阻塞 HTML,按顺序执行async:下载并行,不阻塞 HTML,执行时阻塞
2 分包 / 代码拆分(Webpack SplitChunks)
减少主包体积,加速首屏。
3 Tree Shaking
去掉未使用代码,减小 bundle。
4 懒加载(路由懒加载、组件懒加载、图片懒加载)
渲染阶段优化
1减少重排和重绘
2 GPU 加速,对动画使用 transform 和 opacity:
渲染
客户端渲染和服务端渲染
客户端渲染
客户端发送页面请求给服务器
服务器接收请求后,返回一个包含HTML结构和JS的响应
客户端接收到响应后,下载所需的JS文件和其他静态资源
客户端解析并执行下载的JS 文件,生成页面的DOM结构
客户端通过JS发起异步请求,从服务器获取所需的数据
客户端使用获取到的数据,通过JS操作DOM,将数据填充到页面的相应位置
客户端浏览器根据最终的DOM结构和样式,渲染并展示页面给用户
优点: 更快的页面切换速度;减轻服务器压力;适用于复杂的交互和动态内容;服务器压力小,只提供静态资源。
缺点: 首屏加载速度较慢,因为需等待 JS 下载、执行、请求数据、渲染;不利于搜索引擎优化(SEO);对浏览器兼容性要求高
服务端渲染
客户端发送页面请求给服务器
服务器收到请求,根据请求的路由和数据执行相应的处理逻辑
服务器从数据库获取所需的数据,进行必要的处理和转换
服务器使用获取到的数据和事先定义好的模板,将数据填充到模板,生成完整的HTML页面
服务端将渲染好的HTML页面作为响应返回给客户端浏览器
客户端浏览器收到HTML响应,进行解析和渲染,最终呈现给用户
优点: 更快的首屏加载速度;更好的搜索引擎优化;更好的可访问性
缺点: 服务器需为每个请求生成 HTML,服务器压力大;开发复杂度较高;复杂的交互和动态内容有延迟
执行环境: CSR:运行在浏览器(客户端)。SSR:运行在服务器(Node.js 环境或其他服务端语言)。
网络传输效率: CSR:初始 HTML 小,但后续需多次请求数据。SSR:初始 HTML 大,但减少后续数据请求次数。
可交互时间(TTI): CSR:内容出现晚,交互延迟高。SSR:内容早出现,但若 JS 包过大,hydration 仍可能导致延迟。
ssr node端请求后端数据接口报错,如何处理
SSR 下请求后端数据的特点
在 SSR 时,请求是在服务器(Node.js)发出的,而不是浏览器。 所以:
- 浏览器的同源策略、CORS 限制在 Node 中不存在
- 但 Node 环境下的 接口地址 必须是后端可访问的真实地址(不能用相对 / 前端代理路径)
- 不能依赖浏览器的 cookie / localStorage(除非你自己手动把 cookie 带上)
常见问题与处理方法
① 接口地址问题
原因:在浏览器中调用 API 用的是相对路径 /api/...,由前端代理(如 webpack devServer proxy)或 Nginx 转发。但在 SSR 的 Node 进程中,没有这个代理,所以会请求 Node 自身 localhost,导致找不到。
解决方法: 在服务端代码中使用完整的后端 API 根地址,例如:
`// isServer 判断是否在 Node 端
const baseURL = isServer ? 'http://backend-service:8000' : '/api';
axios.get(baseURL + '/xxx');`
或者使用环境变量:
API_SERVER=http://backend-service:8000
在 SSR 中引用 `process.env.API_SERVER`
③ Cookie / 鉴权丢失
原因:浏览器访问时自动带 cookie/token,Node SSR 请求时没有自动带,需要手动从请求头中取出来再带到后端 API 请求里。
解决方法:
// SSR 中拿到客户端请求的 cookie
export async function fetchData(ctx) {
const cookie = ctx.req ? ctx.req.headers.cookie : '';
const resp = await axios.get('http://backend/api', {
headers: { cookie }
});
return resp.data;
}
④ 异步错误未捕获
原因:SSR 渲染逻辑里,如果异步请求出错,没有 try/catch,会直接让 Node 报错,导致 500。
解决方法:
try {
const data = await axios.get(...);
return data;
} catch (err) {
console.error('[SSR API Error]', err.message);
// 返回兜底数据,不要让 SSR 整个崩掉
return { list: [] };
}
在 Nuxt、Next.js 中尤其要在 asyncData/getServerSideProps 里做错误处理。
⑤ 超时 & 重试
建议 SSR 请求后端接口设置合理的超时时间,避免因为接口阻塞导致整个 HTML 渲染卡死。
axios.get(url, { timeout: 5000 })
.catch(err => {
// 重试一次或返回默认数据
});
ssr后端失效,如何切换为前端渲染
如果 SSR 在前端失效,我通常会通过入口逻辑做降级处理。
首先,我会检查 SSR 渲染的根节点是否有内容,如果是空壳,就直接走 CSR 渲染,保证页面可访问。
其次,我会用 try/catch 包裹 hydration 过程,一旦抛错,就清空节点再用 CSR 重新挂载。
用户体验优化
SSR -> CSR 切换时会有短暂空白,可以:
- 在 HTML 中渲染一个 loading 占位 UI
- 输出从缓存中取到的旧数据作为过渡
- 如果关键 SEO 页面,SSR 必须保证至少返回结构,内容可懒加载
ssr造成服务端负载增大,如何缓解
一、减少渲染次数,不是每个请求都执行完整的 SSR
1.静态化(SSG / 静态缓存)
对不频繁变动的页面(如产品详情、博客文章、营销页),提前在构建阶段生成静态 HTML 文件,直接由服务器或 CDN 返回。
2.页面缓存(HTML Cache)
SSR 渲染完的 HTML 结果缓存到 Redis 或内存。下次同样参数的请求直接返回缓存,不重复渲染。
3. API 数据缓存
SSR 常要调用多个 API,可对 API 结果做短期缓存。避免每次 SSR 都去数据库/后端服务拉数据。
二、加速渲染过程,让单次 SSR 更快,减少 CPU 占用。
1. 避免阻塞 / 增加并行
不要在 SSR 阶段写长时间同步逻辑(例如读取大文件)。数据请求用 Promise.all 并行执行
三、分摊渲染成本,把 SSR 的计算压力分布到其他地方。
1. 边缘渲染
- 把 SSR 部署到 CDN 边缘节点(如 Vercel、Cloudflare Workers、Netlify Edge)
- 离用户更近,分担中心服务的压力
2. 后端预渲染
- 对热门页面提前定时跑一遍 SSR(定时任务),存成静态文件或缓存
- 用户访问时直接读缓存,而不是实时渲染
3. 混合渲染策略
- 首屏用 SSR 保证 SEO
- 次要页面用 CSR
- 部分内容懒加载(岛屿架构 / partial hydration)
浏览器渲染
HTML 解析 → DOM 树
浏览器逐行解析 HTML,生成 DOM树。遇到 <script> 会阻塞解析(除非标记 async/defer)。
CSS 解析 → CSSOM 树
解析 CSS 文件,生成CSSOM树。 CSS 是渲染阻塞资源(必须完全解析后才能渲染)。
合并 DOM + CSSOM → 渲染树(Render Tree)
1️⃣ 遍历 DOM 树,从根节点开始
2️⃣ 对每个可见节点(非 display:none)找到匹配的 CSS 样式规则
3️⃣ 计算节点的最终样式(继承 + 级联 + 优先级计算)
4️⃣ 生成对应的 渲染树节点(Render Tree Node)
排除不可见元素(如 display: none),生成用于布局的 Render Tree。(link 元素不会阻塞DOM Tree的构建过程,但是会阻塞 Render Tree的构建过程)
为什么分成 DOM / CSSOM / Render Tree
分离结构和样式:HTML 专注结构,CSS 专注样式,分别解析更高效 合成时做可见性优化(减少渲染开销) 便于浏览器做样式级联计算和继承逻辑 让渲染过程更容易增量更新(DOM/CSSOM变更 → 局部渲染树更新)
布局(Layout)
计算每个节点的 大小、位置(如 width、margin)。
绘制(Paint)
将布局结果转换为屏幕上的 像素点(填充颜色、文字等)。
能分层绘制(如 GPU 加速的 transform 属性)。
合成(Composite)
将各层合并为最终图像,显示到屏幕上。默认情况下,标准流中的内容时绘制在同一图层中,一些特殊属性会创建一个新的合成层,并且新的图层可以利用GPU来加速绘制,因为每个合成层都是单独渲染的
元素何时会被提升为合成层?
根元素(html)、使用了 transform: translate3d(), translateZ(0) 等3D变换、opacity 动画(且未与 transform 外的动画共存)、will-change: transform / opacity、video、canvas、iframe 等原生插件、使用了 filter 属性
重排:当渲染树中的一部分或全部因为元素的规模尺寸、布局、隐藏等改变而需要重新构建,就叫做回流(重排)。每个界面至少需要一次回流(重排)。
触发重排:添加或删除可见的DOM元素、修改元素的几何属性(如宽高、边距、显示方式等)、内容变化(如文本改变或图片加载完成)、页面初始化渲染、窗口尺寸调整(resize)、计算元素的几何属性(如offsetTop、clientWidth等)。
避免回流:修改样式时尽量一次性修改;避免频繁操作DOM;对某些元素使用position的absolute或fixed,让其脱离文档流;使用visibility(重绘)而不是display(重排重绘);使用Will-change
重绘:一个元素外观改变触发浏览器的行为,浏览器会根据元素的新属性重新回值,使元素呈现新的外观;
transform(translate、scale、rotate)、opacity、filter不会引起重排。因为他们的变化被视为是合成层的变化,跳过布局和绘制流程,直接交给合成线程处理。浏览器只需要将元素移到一个新的合成层,使用GPU来处理。这种方式为硬件加速。
字体大小改变的回流规则
- 通常情况:会触发回流(改变了文本所占的空间大小。文本行高、段落高度可能变化。相邻元素位置可能被挤压或重新排列。)
- 特殊情况:如果元素在视口外、display: none、或使用transform,可能避免回流
defer和async
浏览器在解析HTML的过程中,遇到script元素会停止构建DOM树,先下载执行JS代码,执行完后再继续解析HTML,构建DOM树。 ---因为JS的作用之一就是操作DOM,修改DOM如果等到DOM元素构建完成并且渲染再执行JS,会造成严重的回流和重绘,影响页面性能
defer:告诉浏览器不要等待JS下载,而是继续解析HTML,构建DOM Tree。
原理:浏览器会按文档中的出现顺序依次执行所有 defer 脚本。下载完执行时浏览器会把 defer script 按 文档出现顺序排进一个队列
JS脚本会由浏览器下载,但是不会阻塞DOM Tree的构建,如果JS脚本提前下载完,他会等DOM Tree构建完成。
另外多个带defer的脚本可以保持正确顺序执行
defer仅适用于外部脚本,对于script默认内容会被忽略。
async:也能让脚本不阻塞页面,但是不能保证顺序。他通常用于独立的脚本。
DOM树和布局树关系
- DOM树:代表HTML文档的结构,包含所有节点(元素、文本、注释等)。
- 布局树(或渲染树) :代表页面可见内容的视觉结构和几何信息(位置、尺寸),用于实际绘制到屏幕上。
以下情况的DOM节点不会被包含在布局树中:
使用 display: none 的元素
visibility: hidden的元素会在布局树上,因为它仍然占据空间,只是不可见。
<head> 标签及其内部的元素
<head> 内的 <meta>, <title>, <link>, <script>(未设置display属性的)等元信息标签,本身不是可见内容,因此不会进入布局树。
某些不参与布局的HTML元素
例如:<script>, <comment>,以及被CSS隐藏的元素等。
未加载的替代内容
- 例如,一张未加载的图片 (
<img>) 的alt文本,在图片加载失败前,DOM中存在图片节点和文本节点,但布局树中可能只有文本节点。加载成功后,布局树会更新为图片。
哪些元素在布局树上有,而DOM树上没有?
伪元素:像 ::before, ::after, ::first-line, ::first-letter 这样的伪元素,它们不在HTML DOM中存在,但CSS为它们生成了内容并应用了样式。为了正确布局和绘制这些内容,浏览器会为它们在布局树中创建相应的节点。
(只有3D变换拥有属于自己的图层,2D变换没有。)
CSS 生成的内容(计数器、列表标记等):- 使用 content、counter() 等生成的文本或符号只存在于渲染树(用于绘制),不在 DOM。
浏览器内置控件与装饰:表单控件的子部件(如 input[type=range] 的滑块、视频播放器的控件)、滚动条、插入符号(光标)、选中高亮等 UI 元素会参与绘制/合成,但不在 DOM。
图像替换与占位:图片加载失败时显示的 alt 文本会以渲染对象形式出现,文本本身并非独立 DOM 节点。
浏览器怎么监听回流和重绘的触发
1.使用 Chrome 开发者工具
- 开启“布局变更(Reflow)”调试(Performance 面板)
回流对应的条目是:Layout / Recalculate Style
重绘对应的条目是:Paint / Composite Layers
- 开启 Paint Flashing(闪烁提示重绘)
DevTools → Rendering 面板 → 勾选
Paint flashing
每当浏览器重绘页面时,被重绘的区域会闪烁绿色。
2.使用 MutationObserver(监控 DOM 结构变化)
不直接告诉你有没有回流,但可以发现哪些操作可能导致回流(如节点插入、删除)
HTTP请求类型
get、put、post、delete
patch:对资源进行部分修改
head:获取报文头部
options:询问支持的方法,用来跨域请求、预检请求
connect:在于代理服务器通信时建立管道,使用管道进行TCP通信
trace:让服务器原样返回客户端请求的信息
GET和POST的请求的区别
Post 和 Get 是 HTTP 请求的两种方法,其区别如下:
- 应用场景: (GET 请求是一个幂等的请求)一般 Get 请求用于对服务器资源不会产生影响的场景,比如说请求一个网页的资源。(而 Post 不是一个幂等的请求) 一般用于对服务器资源会产生影响的情景,比如注册用户这一类的操作。(幂等是指一个请求方法执行多次和仅执行一次的效果完全相同)
- 是否缓存: 因为两者应用场景不同,浏览器一般会对 Get 请求缓存,但很少对 Post 请求缓存。
- 传参方式不同: Get 通过查询字符串传参,Post 通过请求体传参。
- 安全性: Get 请求可以将请求的参数放入 url 中向服务器发送,这样的做法相对于 Post 请求来说是不太安全的,因为请求的 url 会被保留在历史记录中。
- 请求长度: 浏览器由于对 url 长度的限制,所以会影响 get 请求发送数据时的长度。这个限制是浏览器规定的,并不是 RFC 规定的。
- 参数类型: get参数只允许ASCII字符,post 的参数传递支持更多的数据类型(如文件、图片)。
get请求修改数据技术上可以,但是强烈不建议,因为他违反了get是幂等的,且参数暴露在url中,不安全
POST和PUT请求的区别
PUT请求是向服务器端发送数据,从而修改数据的内容,但是不会增加数据的种类等,也就是说无论进行多少次PUT操作,其结果并没有不同。(可以理解为时更新数据)
POST请求是向服务器端发送数据,该请求会改变数据的种类等资源,它会创建新的内容。(可以理解为是创建数据)
post请求
一个典型的HTTP POST请求由以下部分组成:
- 请求行:包含请求方法(POST)、请求URI和HTTP版本。
- 请求头:包含关于请求的附加信息,如Host、User-Agent、Content-Type等。
- 空行:请求头部和请求体之间用空行分隔。
- 请求主体:可选,包含请求的数据,通常用于POST请求等需要传输数据的情况。
请求头字段包括但不限于以下内容:
- Host:指定服务器的域名或IP地址。
- User-Agent:标识发起请求的客户端软件(如浏览器类型、操作系统等)。
- Accept:告诉服务器可以接受的MIME类型(如text/html, application/json等)。
- Accept-Language:表示客户端可接受的语言偏好。
- Accept-Encoding:表示客户端支持的内容编码方式(如gzip, deflate)。
- Connection:控制网络连接的行为(如keep-alive, close)。
- Authorization:用于提供访问资源所需的认证信息(如Basic Auth, Bearer Token)。
- Content-Type:如果请求包含主体数据,则此字段说明主体数据的媒体类型。
- Content-Length:表示请求主体的长度(以字节为单位)。
- Referer:表明当前请求是从哪个页面跳转过来的。
- Cookie:发送存储在客户端的与站点相关的Cookie信息。
响应报文
- 状态行:包含HTTP协议版本、状态码和状态信息。
- 响应头部:包含关于响应的附加信息,如Content-Type、Content-Length等
- 空行:响应头部和响应体之间用空行分隔。
- 响应体:包含响应的数据,通常是服务器返回的HTML、JSON等内容
常见的HTTP POST请求头:
- Content-Type:指定请求主体的MIME类型,例如
application/json或application/x-www-form-urlencoded。 - Content-Length:请求主体的字节长度。
- Authorization:用于身份验证的凭据,例如Bearer令牌。
application/x-www-form-urlencoded
这应该是最常见的 POST 提交数据的方式了。浏览器的原生 form表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。
multipart/form-data
常见的post请求方式,一般用来上传文件
application/json
告诉服务端消息主体是序列化的JSON字符串
HTTP/1、2、3
HTTP1.0和HTTP1.1有哪些区别
1.连接方面,HTTP1.0使用非持久连接;HTTP1.1使用持久连接,使多个HTTP请求复用一个TCP连接。
2.资源请求,HTTP1.0存在浪费带宽的现象,例如客户端只需要某个对象的一部分,而服务器却将整个对象送过来了,且它不支持断点续传;HTTP1.1允许只请求资源的某个部分。
3.HTTP1.1新增了host字段,用来指定服务器的域名,可以将请求发到同一台服务器的不同网站。
4.HTTP1.1增加了请求方法,如PUT, HEAD, OPTIONS。
HTTP/1.1 的主要特点与问题
基于文本协议,请求响应模式为串行或并发短连接。
存在队头阻塞(Head-of-line blocking):即使多个请求可以并行发起(通过多个 TCP 连接),但在同一个连接上仍需按顺序处理响应。、
每个资源都需要一次完整的请求-响应周期,导致延迟高。
使用 Keep-Alive 实现连接复用,但无法彻底解决问题。
HTTP keep-alive(持久连接)
层次:应用层(HTTP 协议)
作用:一个 TCP 连接可以传输多个 HTTP 请求和响应,而不是“请求一次就关连接”。客户端可以在这条连接上复用发送后续的请求,从而减少重复的 TCP/TLS 握手和慢启动开销,显著降低延迟、提升吞吐。
关键词:连接复用、减少握手延迟、节省资源
控制方式:HTTP/1.1 默认启用(除非 Connection: close)。服务器可通过响应头 Keep-Alive: timeout=<秒>; max=<请求数> 控制。
优点:大幅减少 TCP 握手次数、降低网络延迟、提升页面加载性能、减少服务器资源消耗
限制:仍然存在队头阻塞问题、浏览器有连接数限制(通常6个)、需要合理的超时和连接管理
应用场景:浏览器加载网页时同时请求多张图片、CSS、JS,可以复用已有 TCP/TLS 会话。
TCP keepalive
层次:传输层(TCP 协议)
作用:探测一个长期空闲的 TCP 连接是否还活着(对方是否在线/网络可达)。
过程: 服务器与客户端长时间没有数据交互。到了系统设定的空闲时间(如 2 小时),TCP 协议会发一个 ACK 探测包。如果对方回复 ACK → 连接仍有效;否则重试若干次。重试失败 → 内核关闭连接,应用收到错误
关键词:存活检测、防止半开连接
控制方式:内核参数或套接字选项(如 setsockopt(SO_KEEPALIVE))
- 三个核心参数:
- tcp_keepalive_time:空闲多久开始探测
- tcp_keepalive_intvl:探测包间隔
- tcp_keepalive_probes:失败多少次断开连接
应用场景:
- 长连接服务器(数据库连接、聊天服务)需要检测断网或异常退出,避免无限占用资源。
- NAT、负载均衡可能会清理空闲连接,通过 keepalive 探测可维持连接。
HTTP/2优化
1.头部压缩,如果同时发送多个请求,他们的头部是一样或相似的,协议就会帮你消除重复的头部
2.采用二进制格式,增加数据传输效率。在HTTP/1.1中,报文的头信息是文本(ASCII编码),数据体可以是文本,也可以是二进制。
3.并发传输/多路复用:多个请求和响应可以在同一个 TCP 连接上并行传输,避免队头阻塞(在应用层)
4.数据流,HTTP/2使用数据流,因为HTTP/2的数据包是不按顺序发送,同一个连接里连续的数据包可能属于不用的请求。因此需要对数据包做标记,指出它属于哪个请求。HTTP/2将每个请求或回应的所有数据包称为一个数据流。
5.基于二进制分帧层:将 HTTP 消息分解为更小的帧(如 HEADERS 帧、DATA 帧),并允许交错传输。
6.服务器推送,允许服务器未经请求,主动向客户端发送消息,不再是被动响应。在这里HTTP/2下的服务器主动推送的是静态资源。
允许服务器在客户端请求某个资源时,
主动推送它认为客户端接下来会需要的资源
(如 HTML 页面中引用的 JS/CSS 文件)。
推送发生在 HTTP/2 的流(stream)中,
使用 PUSH_PROMISE 帧预先通知客户端即将推送的内容。
客户端可以选择接受或拒绝(通过 RST_STREAM 关闭推送流)。
注意:这不是持久化的消息推送,
而是针对当前页面加载的性能优化手段,
且现代浏览器已逐渐弃用此功能(如 Chrome 91+ 已移除支持),因为容易造成缓存冲突和带宽浪费。
1. HTTP/2 服务器推送 ≠ WebSocket 或 SSE
服务器推送是短暂的、上下文相关的、一次性资源预加载行为,
不是持续的消息通道。
而 WebSocket 和 SSE 是为了实现长时间运行的
服务器到客户端的数据推送能力。
2. WebSocket
是一种独立于 HTTP 的协议(但初始握手使用 HTTP Upgrade 机制)。
特点:
全双工通信:客户端和服务器都可以随时发送数据。
长连接:建立后保持打开状态,适合高频交互(如聊天室、游戏、协同编辑)。
使用 ws:// 或 wss:// 协议。
工作流程:
1. 客户端发送带有 `Upgrade: websocket` 的 HTTP 请求
2. 服务器响应 101 Switching Protocols
3. 底层 TCP 连接升级为 WebSocket 协议,后续通信不再走 HTTP
数据以“消息”为单位传输,支持文本和二进制帧。
3. Server-Sent Events(SSE)
基于 HTTP 的单向服务器推送技术。
只能由服务器向客户端发送事件流,客户端不能反向发送数据。
使用 `text/event-stream` MIME 类型。
自动重连机制:客户端断开后会尝试重新连接。
支持事件标识(event ID)、自定义事件类型等。
适用于新闻更新、股票行情、日志流等场景。
缺点:
1.如果有丢包请求会等待重传,阻塞后面的数据。
2.多路复用导致服务器压力上升,多路复用没有限制同时请求数。
3.大批量的请求同时发送,由于HTTP2连接内存在多个并行的流,资源会被稀释,可能导致超时。、
HTTP/2为什么能实现并发传输?
实现并发传输依靠的是stream这个设计,多个stream复用一条TCP连接,达到并发效果。
一个stream里包含一个或多个message(http/1中的请求或响应),一个message里包含一个或多个Fream(帧:http/2的最小单位,以二进制压缩格式存放http/1的内容)
不同stream的帧可以乱序发送,因此可以并发不同的stream。每个帧头部都有stream ID 信息,接收端可以通过streamID 将其组装成HTTP消息,但是同一Stream内部的帧必须严格有序
同一连接的streamID 不能复用,只能顺序递增
http/2通过stream实现的并发比http/1.1通过TCP连接实现的并发更好。因为http/2只需要建立一次TCP连接,而1.1需要建立多个TCP连接,每个连接都需要经历TCP握手、慢启动以及TLS握手。
HTTP/2使用的前置条件
服务器和客户端必须支持HTTP/2
必须使用 HTTPS(基于 TLS 的 ALPN)
原因
浏览器强制要求:所有主流浏览器只在加密连接(HTTPS)上实现 HTTP/2。几乎找不到一个支持明文 HTTP/2 的浏览器。虽然 HTTP/2 在理论上可以支持不加密的连接(例如通过 TCP 连接),但大多数浏览器只在 HTTPS 上支持 HTTP/2。此举是为了提高安全性。
协商机制:HTTP/2 使用 TLS 的一个扩展ALPN来进行协议协商。
- 过程:在 TLS 握手期间,客户端通过 ALPN 告诉服务器它支持
h2(HTTP/2 over TLS),服务器也通过 ALPN 选择h2作为应用层协议。这样,在加密连接建立的同时,双方也确认了使用 HTTP/2 进行通信。
HTTP/3优化
HTTP/3将HTTP底层的TCP协议改成了UDP(,无关发送顺序,也不管丢包,不会出现队头阻塞问题,UDP是不可靠传输,但是基于UDP的QUIC协议可以实现类似的可靠性传输)
TCP+TLS (HTTP2) 需要 3 次握手 + TLS 握手(可能需要两轮往返(RTT)),http3 内置 TLS 1.3 加密,握手和加密一次性完成(同时首次连接只需要 1RTT,甚至 0RTT)。所有数据包都是加密的。无法像 TCP 那样被中间商简单劫持或篡改。
TCP 连接依赖于四元组(源 IP、源端口、目的 IP、目的端口),移动网络环境中 IP 变化会导致连接断开。 QUIC 有连接 ID(CID),即使 IP/端口变了,只要保持 CID 一致可继续通信。
基于此,实现了:
1.无队头阻塞,当某个流发生丢包,只会阻塞这个流,其他流不会受到影响,因此也不存在队头阻塞。
2.更快的连接建立
3.连接迁移,当IP地址变化,可以实现复用原连接
但是,需要服务端、客户端和中间网络环境都支持 UDP/QUIC,有些企业防火墙会限制 UDP。
队头阻塞
队头阻塞是由HTTP的基本的请求-响应模型导致。HTTP规定报文必须是一发一收,这就形成了一个先进先出的队列,没有优先级,只有入队的顺序。如果对头的响应处理时间过长,那么队列后面的响应都需要等待,就形成了队头阻塞。
解决办法:
1.异步处理,通过非阻塞IO模型,让每个请求独立处理而不影响其他请求
2.多路复用,利用IO多路复用技术,同时监听多个请求
3.并发连接,对一个域名分配多个长连接,相当于增加任务队列(HTTP/1.1)
4.域名分片,将域名分出很多二级域名,指向同一台服务器,使得并发的长连接数变多(HTTP/1.1)
HTTP和HTTPS协议区别
- HTTPS协议需要CA证书,费用较高;而HTTP协议不需要;
- HTTP协议是超文本传输协议,信息是明文传输的,HTTPS则是具有安全性的SSL加密传输协议;
- 使用不同的连接方式,端口也不同,HTTP协议端口是80,HTTPS协议端口是443;
- HTTP协议连接很简单,是无状态的;HTTPS协议是有SSL和HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP更加安全。
HTTP工作原理
HTTP 是一种单向通信协议,即客户端发起请求,服务器响应请求。它本身不支持服务器主动推送数据给客户端的双向通信。
HTTP 连接过程:
- DNS 解析获取服务器 IP
- 通过 TCP 三次握手建立连接
- 在 TCP 连接上发送 HTTP 请求报文
- 服务器返回 HTTP 响应报文
- 根据协议版本决定是否保持连接或关闭连接
http流式响应
HTTP的流式响应(Streaming Response)是指服务器在处理请求时,不是一次性生成完整响应数据后发送给客户端,而是边生成数据边通过HTTP连接逐步发送,客户端可以逐步接收和处理这些数据块。这种机制适用于传输大文件、实时日志、视频流或需要低延迟返回部分结果的场景。
底层协议支持:HTTP/1.1 引入了“分块传输编码”,允许服务器在不知道内容总长度的情况下发送数据。每个数据块包含一个十六进制长度前缀,后跟数据和CRLF,最后以长度为0的块表示结束。
连接管理:流式响应通常依赖持久连接(Keep-Alive)。在HTTP/1.1中,默认启用持久连接,避免每次请求都重新建立TCP连接。
服务端实现原理:在Web框架中,流式响应通常通过返回一个“可读流”(Readable Stream)对象来实现。
流式响应是广义概念,SSE(Server-Sent Events)是其一种标准化形式,专用于服务器向客户端推送文本事件,格式为text/event-stream。 WebSocket是双向通信,而流式响应仍是单向(服务器→客户端),但比SSE更灵活,可自定义数据格式。
HTTP/2 的影响:HTTP/2 虽然不再强制使用chunked编码(因其内部使用二进制帧),但仍支持流式传输,且多路复用提升了并发流的效率。
实际应用中,流式响应常见于:大文件下载(避免将整个文件加载到内存);AI大模型的Token逐步输出(如ChatGPT的逐字生成);视频直播的HLS或DASH流(虽然更高层封装,但底层仍基于HTTP流)
HTTPS加密传输
超文本传输安全协议是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,利用SSL/TLS来加密数据包。 HTTPS的主要目的是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。
HTTP协议采用明文传输信息,存在信息窃听、信息篡改和信息劫持的风险,而协议TLS/SSL具有身份验证、信息加密和完整性校验的功能,可以避免此类问题发生。
安全层的主要职责就是对发起的HTTP请求的数据进行加密操作 和 对接收到的HTTP的内容进行解密操作。
对称加密
对称加密的特点是文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥
优点: 计算量小、加密速度快、加密效率高。
缺点: 在数据传送前,发送方和接收方必须商定好秘钥,然后双方保存好秘钥。如果一方的秘钥被泄露,那么加密信息也就不安全了。最不安全的地方, 就在于第一开始, 互相约定密钥的时候!!! 传递密钥!
使用场景:本地数据加密、https 通信、网络传输等
加密算法:
| 算法 | 简介 | 应用 |
|---|---|---|
| AES(Advanced Encryption Standard) | 目前最常用的对称加密标准,支持 128/192/256 位密钥。 | HTTPS、加密存储、VPN |
| DES(Data Encryption Standard) | 老旧的标准,密钥太短(56位),现在基本不安全。 | 历史遗留系统 |
| 3DES(Triple DES) | DES的升级版,执行3次加密,但效率低。 | 银行系统老项目 |
| Blowfish | 快速且灵活,适合加密小块数据。 | VPN、加密压缩 |
| Twofish | Blowfish的继任者,适合硬件加速。 | 安全存储 |
| RC4 | 流加密算法,已被认为有安全问题,不推荐新项目使用。 | 早期HTTPS、WEP |
| ChaCha20 | 现代高效的流加密算法,抗侧信道攻击。 | TLS1.3、移动设备加密 |
非对称加密
通信的双方使用不同的秘钥进行加密解密,即秘钥对(私钥 + 公钥)。
特征: 私钥可以解密公钥加密的内容, 公钥可以解密私钥加密的内容
- 优点:非对称加密与对称加密相比其安全性更好
- 缺点:加密和解密花费时间长、速度慢,资源消耗大,只适合对少量数据进行加密。
使用场景:https 会话前期、CA 数字证书、信息加密、登录认证等
加密算法
| 算法 | 简介 | 应用 |
|---|---|---|
| RSA | 最经典的公钥加密算法,基于大数因子分解难题。 | HTTPS、数字签名、身份验证 |
| DSA(Digital Signature Algorithm) | 专用于数字签名,不适合加密数据。 | 签名系统(如政府认证) |
| ECC(Elliptic Curve Cryptography) | 椭圆曲线加密,密钥短但安全性高,比RSA更轻量。 | 移动设备、区块链、TLS1.3 |
| ElGamal | 用于加密和签名,数学基础是离散对数问题。 | 某些加密投票系统 |
| EdDSA(Edwards-curve Digital Signature Algorithm) | ECC的一个变种,更快更安全。 | 新一代加密通信 |
证书校验流程
数字证书是HTTPS中用来验证网站身份的工具,验证服务器身份的合法性,防止中间人攻击。包含的信息主要有:
- 证书持有者的公钥:用于加密数据。
- 证书有效期:说明证书的使用时间范围。
- 证书颁发机构的数字签名:确保证书的真实性和完整性。
- 证书持有者的身份信息:通常包含组织名称、域名等信息,便于用户确认网站的合法性。
-
检验基本信息:首先浏览器读取证书中的证书所有者、有效期等信息进行一一校验
-
校验CA机构:浏览器开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发;如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。
-
解密证书:如果找到,那么浏览器就会从操作系统中取出 颁发者CA 的公钥,然后对服务器发来的证书里面的签名进行解密
-
比对hash值:浏览器使用相同的hash算法计算出服务器发来的证书的hash值,将这个计算的hash值与证书中签名做对比
-
对比结果一致,则证明服务器发来的证书合法,没有被冒充
-
此时浏览器就可以读取证书中的公钥,用于后续加密了
为什么 HTTPS 难拦,但还是能被挡?
虽然 HTTPS 加密了数据内容,但SNI(Server Name Indication)明文存在于 TLS 握手阶段,运营商 DPI 设备可以通过 SNI 字段识别目标域名。
如果在 hosts 里手动指定了 google.com 的 IP,运营商怎么拦?
因为 hosts 文件优先于 DNS
你访问 google.com → 系统先查本地 hosts → 找到 IP,直接发起 TCP 连接,不走 DNS
即使你不走 DNS,请求的 TCP 数据包还要经过运营商网关、核心路由、DPI 设备
IP ACL 黑名单: 核心路由/防火墙直接禁止访问特定 IP
DPI检查SNI: 即便IP通了,HTTPS握手阶段TLS的SNI字段明文,DPI可以发现,直接TCP RST强制断链
TCP和UDP
TCP是面向连接的可靠的传输协议,提供数据流的有序、无重复传输,保证数据包的完整性
UDP是面向无连接的不可靠的传输协议,不保证数据的顺序和完整性,但具有较低的开销和延迟
区别:
1.连接:TCP面向连接的传输协议,传输数据前需要建立连接;UDP不需要连接,直接传输数据
2.服务对象:TCP是一对一的两点服务;UDP支持一对一,一对多,多对多的交换
3.可靠性:TCP是可靠交付,数据可以有序、无差错、不重复、不丢失到达;UDP是尽最大努力交付,不保证可靠交付,但是可以基于UDP实现可靠传输协议---QUIC
4.首部开销:TCP头部至少20字节(含源端口、目的端口、序列号、确认号、标志位、窗口大小等)。UDP头部仅8字节(源端口、目的端口、长度、校验和),开销更小。
5.流量控制、拥塞控制:TCP有拥塞控制、流量控制,保证数据传输的安全性;UDP没有,网络拥堵不影响UDP发送效率
6.传输方式:TCP是无边界的流式传输,保证数据传输的顺序和可靠;UDP是一个包一个包传输,有边界,但是会丢包和乱序
应用场景:
TCP:FTP文件传输、HTTP/HTTPS
UDP:包总量较少的通信,音频、视频等多媒体传输,广播通信、直播会议
为什么直播会议用UDP?
- 实时性优先于完整性: 音视频通话中,用户更关心“当前画面是否流畅”,而不是“每一帧都必须收到”。如果某一帧丢失,后续帧仍可继续播放,人眼和耳朵有一定容错能力(称为“感知冗余”)。而TCP一旦丢包就会触发重传,导致后续数据被阻塞(队头阻塞问题),造成明显卡顿。
- 减少延迟抖动(Jitter): TCP的重传机制会导致数据到达时间不确定(例如某个包重传花了100ms),破坏音视频同步。UDP即使丢包也保持时间线性推进,接收端可通过插值、静音补偿等方式处理。
- UDP支持单播、广播、多播(组播),特别适合一对多的直播场景。
TCP三次握手四次挥手
一开始,客户端和服务端都处于close状态,先是服务端主动监听某个端口,处于listen状态
客户端随机初始化序号,将此序号至于TCP首部的序号字段,同时把SYN标志为1,表示SYN报文。接着把第一个SYN报文发送给服务端,向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT状态
服务端收到客户端的SYN报文后,把服务端也随机初始化序号,将此序号填入TCP首部的序号字段,其次TCP首部的确认应答号字段填入客户端序号+1,接着报SYN,ACK标为1,最后把报文发给客户端,该报文也不包括应用层数据,之后服务端处于SYN-RECEIVED状态
客户端收到服务端报文后,还要向服务端回应一个应答报文,首先改应答报文TCP首部ACK标志为1,其次确认应答号字段填入服务端序号+1,最后把报文发给服务端,这次报文可以携带客户端到服务端的数据,之后客户端处于ESTABLISHED状态
服务端收到客户端应答报文后,也进入ESTABLISHED状态
为什么是三次握手
三次握手才可以阻止重复历史连接的初始化
两次握手情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史建立,造成资源浪费。
在两次握手的情况下,服务端在收到SYN报文后进入ESTABLISHED状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入ESTABLTSHED状态,假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回RST报文来断开连接,而服务端在第一次握手的时候就进入RST状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到RST报文后,才会断开连接。浪费了服务端的资源。
三次握手才可以同步双方的初始序列号
三次握手能同步序列号,是因为它确保双方都“确认对方已收到自己发的 ISN”。两次握手只能单向确认,可能导致服务器发数据给一个不存在的客户端,资源被浪费,所以不可靠、不安全。
为什么要同步初始序列号?
同步初始序列号是为了让双方明确从哪开始收发数据,保证数据可靠有序,同时防止历史连接干扰和攻击。
四次握手也能可靠的同步双方的初始化序号,但是第二步和第三步可以优化成一步
三次握手才可以避免资源浪费
如果只有两次握手,服务端不清楚客户端是否收到了自己回复的ACK报文,所以服务端每收到一个SYN就只能主动建立一个连接,会建立多个冗余的无效链接,造成不必要的资源浪费
为什么每次建立TCP连接,初始化的序列号要不一样?
如果每次建立连接,客户端和服务端的初始化序列号都一样,很容易出现历史报文被下一个相同的四元组的连接接收。
握手丢失
第一次握手丢失,会触发超时重传,且重传的SYN报文的序列号是一样的
第二次握手丢失,客户端会认为是自己的SYN报文(第一次握手)丢失,客户端会触发超时重传,重传SYN报文;同时服务端收不到第三次握手,服务端会触发超时重传,重传SYN-ACK报文
第三次握手断掉后,服务器端处于SYN_RECV状态,客户端如果没有收到第三次握手的确认,会重传SYN包;如果服务器没有收到第三次ACK,会重传SYN-ACK,最终超时后连接无法建立,服务器端将连接从半连接队列中清除,进入CLOSED状态。
注意,ACK报文不会重传,当ACK丢失,会由对方重传对应的报文
四次挥手
客户端打算关闭连接,此时会发送一个TCP首部FIN标志被置为1的报文,之后客户端会进入FIN_WAIT_1状态
服务端收到该报文后,就会向客户端发送ACK应答报文,服务器进入CLOSE_WAIT状态
客户端收到ACK应答报文后,进入FIN_WAIT_2状态
等待服务器处理完数据后,会向客户端发送FIN报文,服务器进入LAST_ACK状态
客户端收到FIN报文后,发送一个ACK应答报文,进入TIME_WAIT状态
服务端收到ACK应答报文后门进入CLOSE状态,服务端连接关闭
客户端在经过2MSL后,自动进入CLOSE状态,客户端连接关闭
主动关闭连接的,才有TIME_WAIT状态
为什么要挥手四次
服务端通常要等待完成数据的发送和处理,所以服务端ACK和FIN会分开发送,故需要四次挥手
挥手丢失
第一次挥手丢失,客户端收不到服务端的ACK,会触发超时重传
第二次挥手丢失,客户端收不到服务端的ACK,会触发超时重传
第三次挥手丢失,服务端收不到服务端的ACK,会触发超时重传
第四次挥手丢失,服务端收不到服务端的ACK,会触发超时重传
TCP保证数据传输可靠性的方式:
校验和 连接管理
- 序列号:TCP为每个字节分配一个序列号,确保接收方能够按照正确的顺序重组数据。
- 确认应答:接收方在成功接收到数据后,会向发送方发送确认应答(ACK)。如果发送方在规定时间内没有收到确认应答,它将重新发送数据。
- 重传机制:TCP使用超时重传机制,如果发送的数据在一定时间内未被确认,发送方会重新发送这些数据。
- 流量控制:TCP使用滑动窗口机制来控制数据流量,防止接收方被过多数据淹没。
- 拥塞控制:TCP通过算法(如慢启动、拥塞避免、快速重传和快速恢复)来控制网络拥塞情况,确保网络的稳定性和可靠性。
TCP滑动窗口
为什么需要滑动窗口
在网络传输中,发送方以一定速率发送数据,而接收方可能处理能力有限。
为了防止发送方发太快、接收方来不及接收,TCP 必须有一种动态调节机制,让发送方根据接收方的“接收能力”控制发送速度。
这就是 “窗口(window)” 的作用 —— 表示此时允许发送方还能发送多少数据(尚未被确认的最大字节数)
滑动窗口核心目标是:
- 提高效率:允许发送方在收到确认之前,连续发送多个数据包,从而充分利用网络带宽。
- 流量控制:确保发送方不会发送得过快,导致接收方(因为处理能力或缓冲区不足)来不及接收和处理。滑动窗口是 TCP 流量控制的核心手段。
- 保证顺序:尽管数据包是批量发送的,但接收方能够按照正确的顺序将数据交付给应用层。
这个窗口由四个指针分为三部分:
| 区域 | 描述 |
|---|---|
| 已发送并已确认 | 发送成功,且接收方已确认。这部分数据已经从窗口中移出。 |
| 已发送但未确认 | 已经发出,但还在等待接收方的确认。 |
| 可发送但未发送 | 在窗口范围内,可以立即发送的数据。 |
| 不可发送 | 窗口之外的数据,不允许发送,因为接收方还没有准备好。 |
滑动过程:
- 初始状态:窗口包含一系列可发送的数据包序号(比如 1-10)。
- 发送数据:发送方将窗口内(1-10)的数据包连续发送出去。
- 收到确认:当接收方成功收到数据包(比如 1-4)后,会返回一个 ACK 确认包。
- 窗口滑动:发送方收到 ACK 后,就将窗口的左边界向右移动,将已确认的数据(1-4)“移出”窗口。同时,窗口的右边界也可以向右移动(取决于接收方的通告窗口大小),让新的数据(比如 11-14)进入窗口。
- 重复过程:发送方继续发送窗口内新的可用数据(比如 5-14)。
两个关键窗口:发送窗口与接收窗口
a) 接收窗口
定义:这是由接收方决定的。它告诉发送方:“我这边还有多少空闲的缓冲区可以接收数据”。
传递方式:接收方在每次发送 ACK 确认包时,都会通过 TCP 首部中的 window 字段,将自己的当前接收窗口大小通告给发送方。
作用:这是流量控制的根本。发送方必须遵守接收方的窗口大小。
b) 发送窗口
定义:这是发送方维护的。它是发送方根据接收方的通告窗口以及网络拥塞情况(拥塞控制,是另一个话题)最终决定的可发送数据范围。
计算公式(简化) :
发送窗口 = min(接收方通告窗口, 拥塞窗口)
- 接收方通告窗口:接收方的处理能力。
- 拥塞窗口:发送方根据网络状况估算的,避免造成网络拥堵的窗口大小。
发送窗口是实际指导发送行为的窗口。
OSI的七层模型
应用层:直接为用户应用提供服务
表示层:数据格式转换和加密
会话层:建立、管理、终止会话
传输层:提供进程到进程的可靠传输
网络层:实现端到端的逻辑寻址和路由
数据链路层:提供节点到节点的可靠传输
物理层:传输原始比特流
OSI七层网络模型 | TCP/IP四层概念模型 | 对应网络协议 |
|---|---|---|
| 7:应用层 | 应用层 | HTTP、RTSP TFTP(简单文本传输协议)、FTP、 NFS(数域筛法,数据加密)、WAIS`(广域信息查询系统) 。服务器、客户端设备 |
| 6:表示层 | 应用层 | Telnet(internet远程登陆服务的标准协议)、Rlogin、SNMP(网络管理协议)、Gopher 。无特定硬件设备 |
| 5:会话层 | 应用层 | SMTP(简单邮件传输协议)、DNS(域名系统)。无特定硬件设备 |
| 4:传输层 | 传输层 | TCP(传输控制协议)、UDP(用户数据报协议)) 。无特定硬件设备 |
| 3:网络层 | 网络层 | ARP(地域解析协议)、RARP、AKP、UUCP(Unix to Unix copy)。路由器、三层交换机 |
| 2:数据链路层 | 网络接口层 | FDDI(光纤分布式数据接口)、Ethernet、Arpanet、PDN(公用数据网)、SLIP(串行线路网际协议)PPP(点对点协议,通过拨号或专线方建立点对点连接发送数据)。交换机 |
| 1:物理层 | 网络接口层 | SMTP(简单邮件传输协议)、DNS(域名系统)。中继器 |
对网络的理解
网络是由多个节点(如计算机、服务器、路由器等)通过通信介质(如光纤、无线电波等)相互连接而成的系统,旨在实现数据的传输、共享和交流。网络可以分为局域网(LAN)、广域网(WAN)和互联网等不同类型,每种网络都有其特定的应用场景和技术实现。
AJAX、Fetch、XHR、AXIOS
XHR是原生JS对象,是浏览器内置的一种对HTTP请求和响应的处理方式,是fetch和ajax的基础。可以在不刷新页面的情况下请求特定的URL,可以获取任何类型的数据,而不仅仅是XML。XHR可同步可异步,但是不支持promise API
AJAX是一种实现无刷新的情况下使用XHR对象向服务器发起请求获取数据的方法。可以异步工作。本质是通过JS进行异步HTTP请求。核心是XHR
Fetch是基于promise的API,它会返回一个promise,支持async/await,发请求时可以设置请求头、请求参数、以及处理响应数据。
axios是基于promise的网络请求库,用于在浏览器和node进行HTTP请求。在服务端它使用原生node http模块,而在客户端使用XHR
CDN
CDN---内容分发网络,利用分布式节点技术,在全球部署服务器,即时将静态资源或动态资源内容分发到用户所在的最近节点,提高用户访问速度和稳定性,降低网络拥塞和延迟,减轻源站压力
基本原理:将源站的内容分发到离用户最近的节点上进行缓存,通过智能路由,负载均衡等技术保证用户快速稳定访问到所需资源。CDN将源站和用户之间的网络传输距离缩短,通过多节点并行传输,显著降低网络延迟和带宽消耗。
CDN的访问过程依赖DNS的重定向技术,将用户定向至地理位置上距离其最近的边缘CDN节点服务器
CDN加速通过负载均衡、缓存机制、数据传输优化、动态加速和安全保障等方面实现网络加速。
CDN回源是指当CDN边缘节点没有命中用户请求的内容时,CDN节点会向源站服务器发起请求以获取所需内容的过程。这个过程包括检查缓存、向源站发起请求、获取内容并存储到缓存中供后续请求使用。
- 缓存策略:CDN节点需要根据不同的资源类型设置合适的缓存时间(TTL),以平衡缓存命中率和内容更新频率。常见的策略包括基于文件扩展名、URL路径等设定不同的TTL值。
- 回源方式:回源可以是HTTP/HTTPS协议,也可以通过其他自定义协议进行。此外,还有多种回源方法如GET、POST等,具体取决于应用需求和安全性考虑。
- 负载均衡:在大规模CDN网络中,回源请求可能会被分发到多个源站服务器上,这就需要使用负载均衡技术来优化流量分配,提高系统的可靠性和性能。
- 安全机制:为了防止恶意攻击者利用CDN回源机制对源站造成压力,通常会在回源过程中加入各种安全措施,例如IP白名单、签名验证等。
有 CDN 和没有 CDN 的流量路径比较
没有 CDN 的情况下,用户的请求会直接到达原始服务器,路径如下:用户 -> 互联网 -> 源服务器(通常是位于某个固定数据中心的服务器)
这种路径下的关键问题是:
- 地理距离:如果用户和原始服务器之间的地理距离较远,请求和响应的延迟就会增加。
- 带宽压力:所有的请求都直接到达原始服务器,可能会导致服务器的带宽资源被大量占用,尤其是流量高峰时,原始服务器可能会承受过重的负载。
- 网络拥堵:如果原始服务器所处的网络存在瓶颈,用户访问速度就会变慢。
使用 CDN 后,流量路径会发生改变,通常会通过位于用户附近的 CDN 边缘服务器中转:用户 -> 最近的 CDN 边缘服务器 -> 原始服务器(如果边缘服务器没有缓存数据)
优点:
- 减少延迟:CDN 边缘服务器通常距离用户更近,减少了访问时的地理距离和响应时间。
- 减轻源服务器压力:大量的请求会被边缘服务器缓存,源服务器只需处理缓存未命中的请求,减轻了源服务器的负载。
- 更快的网络速度:CDN 可以选择最佳的路径传输数据,绕过一些可能的网络瓶颈,提升速度。
- 冗余和可靠性:如果某个 CDN 边缘服务器出现故障,其他边缘服务器仍然可以提供服务,提高网站的可靠性。
有了 CDN 一定比没有 CDN 更快吗?
虽然 CDN 对大部分用户访问速度有很大提升,但并不是所有情况下都有明显的速度优势。下面是一些可能导致 CDN 效果不显著的情况:
如果用户的地理位置离 CDN 边缘服务器较远,或者 CDN 的覆盖网络没有到达用户所在的区域,那么使用 CDN 可能不会显著加速用户的访问,甚至可能比直接访问原始服务器的延迟更高。
如果你的网站流量较小,且大部分请求都能直接由原始服务器处理,那么使用 CDN 的优势就不明显。CDN 的维护和配置可能带来一定的复杂性和成本,如果请求量不高,CDN 的性能提升可能不值得。
CDN 的缓存主要针对静态资源(如图片、JavaScript、CSS 文件等)。对于频繁变化的动态内容(如用户登录信息、实时数据等),CDN 并不能提供缓存,因此,动态内容的加载速度可能不会有显著的提升。
如果源服务器本身性能不足,或者网络带宽不够,CDN 即使提供缓存加速,也可能无法解决源服务器本身的问题。此时,最有效的解决方案是优化源服务器。
应用场景 网站静态资源加速-- HTML/CSS/JS/图片/音视频等托管在 CDN 海量并发防抖--电商大促、直播、秒杀等高并发访问
白屏
白屏是指用户进入页面在加载过程中长时间无法正常显示内容
主要表现:页面空白没有内容、页面仅展示骨架屏加载态、页面仅展示背景、页面只有侧边导航菜单
导致白屏的原因:
js异常:js代码存在 语法错误或逻辑问题导致页面渲染被中断导致无法正常显示内容
css问题:css错误导致页面布局混乱或者无法正常显示内容
网络问题;兼容性问题;第三方服务问题
白屏检测
可见元素检测:检查页面是否存在真正可见的元素。该检测方法着重定位于页面第一个可见元素或者是依据特定需求寻找的元素
页面内容得分检测:通过广度优先遍历白屏检测区DOM树的节点来收集数据,遇到可视元素则相应计分,得分越低越可能白屏。
页面采样识别检测:在页面垂直/交叉位置确定多个采样点,判断采样点冒泡元素集合的第一个元素是否是容器元素、加载态元素或指定元素(结合可见元素识别检测),当存在一定比例的采样点元素为容器元素、加载态元素、指定元素则判定为白屏。
其他检测方案:检测根节点是否渲染内容、页面截屏对比检测、页面渲染状态检测
利用JavaScript监听DOM加载事件和资源加载事件,检查页面的关键元素是否存在或是否渲染完成。如果关键元素缺失或者页面加载超时,则可以判定为白屏。随后通过弹窗、提示框或者其他形式通知用户。
思路:1. 监听DOMContentLoaded事件,确保HTML文档已经加载并解析完成。 2. 检查页面中关键的DOM元素(如秒杀按钮、商品图片等)是否存在。如果这些元素不存在,则可能出现了白屏。 3. 设置一个全局的定时器,在一定时间(例如5秒)内监测页面是否完全加载。如果超时且页面未完全加载,则认为是白屏。 4. 如果检测到白屏,可以向用户展示友好的提示信息,比如“页面加载失败,请刷新重试”或者自动刷新页面。
检测时机策略
轮询检测、基于MutationObserver API 监听页面DOM变化,变化时检测、页面报错全局捕获、自定义检测时机
埋点
埋点(tracking) 是指在应用程序中插入代码或工具来记录某些事件的行为和属性,这些数据可以被用来分析用户行为、优化产品功能、改进用户体验等。通过在关键位置插入埋点代码,开发人员可以捕获和跟踪用户与应用程序的交互行为。
埋点主要用于收集和分析用户行为数据,以便进行数据驱动的决策。通过对收集到的数据进行分析,开发人员和产品团队可以了解用户行为模式、优化产品功能、改善用户体验、评估转化率、针对不同用户群体制定营销策略等。
埋点实现
1.确定需要收集的数据;2.选择合适的埋点工具;3.在代码中插入埋点;4.进行数据收集和分析:在应用程序运行时,埋点工具会自动收集数据,并将数据上传到服务器,然后进行数据分析和处理。
死锁
为了防止多线程竞争共享资源而导致数据错乱,会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。
当两个线程为了保护两个不同的共享资源而使用了两个互斥锁,那么这两个互斥锁应用不当的时候,可能会造成两个线程都在等待对方释放锁,在没有外力的作用下,这些线程会一直相互等待,就没办法继续运行,这种情况就是发生了死锁。
死锁只有同时满足以下四个条件才会发生:
分别为互斥条件(资源只能被一个进程独占使用)
请求和保持条件(一个已经持有资源的进程可以请求新的资源,而不会释放已有的资源。)
不剥夺条件(已分配的资源不能被强制剥夺,只能由进程主动释放)
循环等待条件(存在一组进程形成环路,每个进程都在等待下一个进程所占有的资源)
避免死锁问题只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法
把进程从死锁状态中解脱出来,常采用的方法有:
剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
银行家算法是一种典型的避免死锁的算法,它模拟了银行贷款的过程,在每次资源分配前先做一次试探性分配,预测分配后是否会导致系统进入不安全状态。如果不安全则暂时不进行实际分配。
web component
Web Components 是一种用于构建可重用的自定义元素的技术。每个 Web Component 都具有自己的 DOM 和样式隔离,避免了全局 CSS 和 JavaScript 的冲突问题。它还支持自定义事件和属性,可以与其他组件进行通信和交互。
它由三部分组成:Custom element(自定义元素):允许开发者自定义新的 HTML 元素、Shadow DOM(影子 DOM):它可以将一个隐藏的、独立的 DOM 附加到一个元素上、HTML template(HTML 模板):<template> 元素允许开发者在 HTML 中定义一个模板,其中可以包含任意的 HTML 结构、文本和变量占位符。此元素及其内容不会在 DOM 中呈现,但仍可使用 JavaScript 去引用它,使用 <slot> 则能进一步展示不同的自定义内容。
使用场景:创建可在多种框架中使用的通用 UI 组件。构建统一的、可在不同项目间共享的组件库。
JSON
JSON是一种轻量级的数据交换格式,主要目的是以简单易读的方式传递数据。然而,它并没有为某些复杂的数据类型(如日期和正则表达式)提供原生支持。
- 数据冗余:JSON是一种基于文本的格式,使用大量的标点符号(如大括号、方括号、引号等)来描述数据结构。这会导致数据传输时占用更多的带宽,尤其是在数据量较大或需要频繁传输的情况下。例如,一个简单的数字列表
[1,2,3]在JSON中会被完整地序列化为字符串,增加了额外的开销。 - 类型支持有限:JSON只支持基本的数据类型,如字符串、数字、布尔值、数组和对象。它无法直接表示复杂的数据类型,例如日期时间、二进制数据、函数或循环引用。如果需要表示这些类型,通常需要通过字符串或其他间接方式实现,这会增加解析和转换的复杂性。
- 性能问题:由于JSON是基于文本的格式,在解析和序列化时需要进行大量的字符串操作,这可能会导致性能瓶颈。特别是在处理大规模数据或实时性要求较高的应用中,JSON的解析速度可能成为瓶颈。
- 安全性隐患:JSON数据容易受到注入攻击(如JavaScript代码注入)。如果直接将未经验证的JSON数据用于前端渲染,可能会引发安全问题。此外,JSON的宽松语法也可能导致意外的错误,例如多余的逗号或未转义的字符。
在使用JSON格式时,日期和正则表达式可能会出现问题。JSON本身只支持基本的数据类型,并不直接支持日期或正则表达式这种特定的类型。因此,如果需要在JSON中表示日期或正则表达式,通常会将它们转换为字符串形式,但这可能带来一些问题,例如解析时需要额外处理,或者可能出现格式不一致的情况。
解决方案:
- 对于日期,可以采用标准化的格式(如ISO 8601),并在反序列化时显式地将其转换为日期对象。
- 对于正则表达式,可以在字符串前添加标识符(如"regex/^[a-z]+$/i"),以便解析器能够区分普通字符串和正则表达式。
- 还可以使用自定义的JSON扩展格式(如BSON),这些格式支持更多复杂的数据类型。
JavaScript中的JSON.parse可以接受一个reviver函数,用于在解析过程中对特定字段进行转换。类似地,JSON.stringify也可以接受一个replacer函数,用于在序列化时对特定字段进行处理。
八种排序
1. 冒泡排序 (Bubble Sort)
原理:重复遍历列表,比较相邻元素并交换位置,每次遍历将最大元素"冒泡"到末尾。
平均和最坏情况下的时间复杂度为O(n^2),最好情况下为O(n)。空间复杂度O(1).
2. 选择排序 (Selection Sort)
原理:每次从未排序部分选择最小(或最大)元素放到已排序部分的末尾。 不稳定
平均和最坏情况下的时间复杂度为O(n^2),最好情况下也是O(n^2)。空间复杂度O(1).
3. 插入排序 (Insertion Sort)
原理:构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置插入。
平均和最坏情况下的时间复杂度为O(n^2),最好情况下为O(n)。空间复杂度O(1).
4. 希尔排序 (Shell Sort)
原理:改进的插入排序,通过将数组分成多个子序列进行插入排序,逐步缩小子序列间隔。 不稳定
平均情况下的时间复杂度均为O(n log n),最坏情况下的时间复杂度为O(n^2)。空间复杂度O(1).
5. 归并排序 (Merge Sort)
原理:分治法,将数组分成两半分别排序,然后合并两个有序数组。 有可能不稳定
平均、最好和最坏情况下的时间复杂度均为O(n log n)。空间复杂度O(n).
6. 快速排序 (Quick Sort)
原理:分治法,选择一个基准元素,将数组分为小于基准和大于基准的两部分,递归排序。 不稳定
平均情况下的时间复杂度为O(n log n),最坏情况下为O(n^2)。空间复杂度O(log n).
7. 堆排序 (Heap Sort)
原理:利用堆这种数据结构,将数组构建成大顶堆,然后逐个取出堆顶元素。 不稳定
平均、最好和最坏情况下的时间复杂度均为O(n log n)。空间复杂度O(1).
8. 计数排序 (Counting Sort)
原理:非比较排序,统计每个元素出现的次数,然后按顺序输出。
时间复杂度为O(n+k),其中k是输入数据的范围。空间复杂度O(k).
数据结构
链表和数组的本质区别在于内存分配方式和访问模式。数组是一种连续的内存块,可以通过索引直接访问元素,时间复杂度为O(1)。链表是由节点组成的数据结构,每个节点包含数据和指向下一个节点的指针,访问某个节点需要从头节点开始遍历,时间复杂度为O(n)。
队列是一种先进先出(FIFO)的数据结构,而循环链表是一种特殊的链表形式,最后一个节点的指针指向第一个节点,形成一个环。两者都可以用于缓冲区管理、任务调度等场景,但循环链表在处理环形缓冲区时更为高效。
树和图是更复杂的数据结构,广泛应用于表示层次关系和网络结构。树是一种特殊的图,具有无环和连通的特点,常用于文件系统、决策树等场景。图可以表示复杂的网络关系,如社交网络、地图路径规划等。
堆和栈
栈是一个后进先出的数据结构。栈的操作都是 O(1) ,非常高效。
堆是一种特殊的完全二叉树。所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点。由于堆的特殊结构,我们可以用数组表示堆。堆的插入/删除操作是 O(log n) ,堆顶读取是 O(1)。
- 分配方式:
- 堆:需要程序员显式地申请和释放内存。
- 栈:由编译器自动管理。当函数被调用时,编译器为函数的参数、局部变量等分配栈空间;当函数返回时,栈空间自动释放。
- 访问速度:
- 堆:由于堆内存的分配和释放涉及复杂的操作(如查找空闲内存块),因此访问速度较慢。
- 栈:栈的操作非常简单,遵循后进先出(LIFO)原则,因此访问速度较快。
- 大小限制:
- 堆:通常没有严格的大小限制,取决于操作系统的内存管理和可用物理内存。
- 栈:每个线程的栈空间大小通常是固定的,且较小(例如几MB)。如果栈空间耗尽,可能会导致栈溢出错误。
- 生命周期:
- 堆:堆内存的生命周期由程序员控制,直到显式释放才会被回收。如果忘记释放可能导致内存泄漏。
- 栈:栈内存的生命周期与函数调用相关,函数返回后栈空间自动释放。
- 用途:
- 堆:适合存储生命周期较长的对象或大块数据结构,例如动态数组、链表等。
- 栈:适合存储生命周期较短的临时变量,例如函数参数、局部变量等。
最小堆实现
-
插入元素:
- 将新元素添加到堆的末尾
- 向上调整堆(Heapify Up),直到满足堆性质
-
提取最小值:
- 最小值总是位于堆顶(数组第一个元素)
- 将最后一个元素移到堆顶
- 向下调整堆(Heapify Down),直到满足堆性质
-
获取最小值:直接返回堆顶元素
对于插入操作,新元素被添加到数组末尾,然后通过上滤操作将其移动到合适的位置以保持堆的性质。对于删除操作,我们将堆顶元素与最后一个元素交换,然后移除最后一个元素并通过下滤操作调整堆。构建堆则是将一个无序数组调整为满足最小堆性质的结构。
栈和队列
队列:是限定只能在表的一端进行插入和在另一端进行删除操作的线性表。先进先出(FIFO)。新来的成员总是加入队尾,每次离开的成员总是队列头上。
栈:是限定只能在表的一端进行插入和删除操作的线性表。后进先出(LIFO)
队列是基于地址指针进行遍历,而且可以从头部或者尾部进行遍历,但不能同时遍历,无需开辟空间,因为在遍历的过程中不影响数据结构,所以遍历速度要快。
栈是只能从顶部取数据,也就是说最先进入栈底的,需要遍历整个栈才能取出来,而且在遍历数据的同时需要为数据开辟临时空间,保持数据在遍历前的一致性。
使用场景方面,栈常用于函数调用、撤销操作等场景,而队列则常用于任务调度、数据缓冲等场景。
数组和链表
| 操作类型 | 数组(Array) | 链表(Linked List) | 说明 |
|---|---|---|---|
| 随机访问(查找第 k 个) | 🟢 O(1) | 🔴 O(n) | 数组可以通过下标直接定位;链表需要从头或尾遍历 |
| 按值查找 | 🔴 O(n) | 🔴 O(n) | 都得遍历,没优势 |
| 头部插入/删除 | 🔴 O(n)(要搬移元素) | 🟢 O(1) | 链表只改指针,数组得整体移动 |
| 中间插入/删除 | 🔴 O(n)(搬移元素) | 🟡 O(n)(先找到位置再改指针) | 数组要整体移动,链表要先遍历 |
| 尾部插入/删除 | 🟢 O(1)(如已知 length) | 🟢 O(1)(若有 tail 指针) | 两者都快(如果链表有尾指针) |
BFS和DFS
BFS(广度优先搜索)
基本思想:从起点开始,一层一层地向外扩展,先访问邻居节点,再访问邻居的邻居。
类似于“波纹扩散”或“排队”。
使用的数据结构:队列(Queue) 👉 先进先出(FIFO)
算法步骤 1.将起始节点加入队列 2.队列不为空时:取出队头节点,访问它,将它所有未访问的邻居节点加入队列
特点
- 层层推进,最先访问离起点“近”的节点
- 可以用来找到最短路径(无权图)
- 使用队列,空间开销可能较大
DFS(深度优先搜索)
📌 基本思想:从起点开始,一条路走到底,直到走不下去,再回溯到上一个分叉点继续。 类似于“迷宫走法”或“递归探索”。
🧠 使用的数据结构: 栈(Stack) 👉 后进先出(LIFO)
👉 也可以用递归实现(系统栈)
📜 算法步骤:1.从起始节点开始 2. 标记该节点为访问过 3. 遍历它的邻居,对未访问过的邻居递归调用 DFS
特点
- 一条路走到底,再回溯
- 适合遍历整个图、搜索所有可能路径
- 使用栈,空间相对灵活
- 不保证最短路径
实际应用场景
-
BFS
- 迷宫/棋盘中找最短路径
- 层序遍历树
- 无权图的最短路径搜索
-
DFS
- 拓扑排序
- 图的连通性检测
- 全排列 / 组合 / 回溯问题
- 树的深度遍历
create
curring 柯里化
柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
函数柯里化的好处:
(1)参数复用:需要输入多个参数,最终只需输入一个,其余通过 arguments 来获取
(2)提前确认:避免重复去判断某一条件是否符合,不符合则 return 不再继续执行下面的操作
(3)延迟运行:避免重复的去执行程序,等真正需要结果的时候再执行
函数柯里化就像我们往卡里存钱,存够了,才能执行买房操作,存不够,接着存。
function curry(fn) {
return function curried(...args) {
// 如果传入的参数数量 >= 原函数需要的参数数量,直接执行
if (args.length >= fn.length) {
return fn.apply(this, args);
}
// 否则返回一个新函数,继续接收剩余参数
else {
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
}
}; }
节流防抖
节流:在规定的单位时间内,只有一次触发事件的回调能被执行,如果在单位时间内触发多次,只有一次能生效
核心思想是控制执行频率。当事件第一次触发时立即执行函数,并设置一个定时器,在定时器结束之前忽略所有后续的事件触发。当定时器结束后,允许再次执行函数并重新设置定时器。
适用于需要限制事件触发频率的场景,比如滚动事件、点击按钮等。其实现可以通过时间戳或者定时器来控制。使用时间戳的方式是记录上一次触发的时间,当前触发时间与上次触发时间的差值大于设定的时间间隔时才执行函数;使用定时器的方式是在设定的时间间隔内只允许执行一次函数。
防抖: 在函数触发的n秒后执行回调,如果在这n秒内再次被触发,则重新计时
核心思想是延迟执行。当事件触发时,设置一个定时器,在定时器结束时执行函数。如果在定时器结束前再次触发事件,则清除之前的定时器并重新设置一个新的定时器。这样可以确保只有最后一次触发事件后,经过设定的时间间隔,函数才会被执行。
适用于需要等待用户操作完全停止后再执行的场景,比如输入框搜索建议、窗口大小调整等。其实现依赖于JavaScript中的setTimeout和clearTimeout方法。每次触发事件时,都会先清除之前的定时器,再设置一个新的定时器。这样可以保证在连续触发事件的过程中,只有最后一次触发会生效。
flatten扁平化
getAllNodes
quickSort快排
setInterval
setTimeout
const startTime = Date.now();
let timerId = { isCancelled: false }; // 用于标识定时器是否被取消
function check() {
// 如果定时器已被取消,直接返回
if (timerId.isCancelled) return;
const currentTime = Date.now();
if (currentTime - startTime >= delay) {
callback(); // 时间到了,执行回调
} else {
// 继续检查(使用微任务避免阻塞)
Promise.resolve().then(check);
// 或者用:setTimeout(check, 0);
// 或者用:requestAnimationFrame(check);
}
}
check(); // 开始检查
// 返回取消函数
return function clearMyTimeout() {
timerId.isCancelled = true;
};
}
// 使用示例
const clearTimer = mySetTimeout(() => {
console.log('这个不会执行,因为被清除了');
}, 3000);
// 2秒后清除定时器
setTimeout(clearTimer, 2000);
sleep
数组去重
发布订阅模式
//首先构造一个 `EventBus` 类,初始化一个空对象用于存放所有的事件
class EventCenter {
constructor() {
// 1. 事件容器,存储事件类型和对应的处理器数组
this.handlers = {};
}
// 2. 添加事件监听
addEventListener(type, handler) {
if (typeof handler !== 'function') {
throw new Error('Handler must be a function');
}
if (!this.handlers[type]) {
this.handlers[type] = [];
}
// 避免重复添加同一 handler
if (!this.handlers[type].includes(handler)) {
this.handlers[type].push(handler);
}
}
// 3. 触发事件
dispatchEvent(type, ...params) {
const handlers = this.handlers[type];
if (!handlers || handlers.length === 0) {
console.warn(`No handlers for event "${type}"`);
return false;
}
handlers.forEach(handler => {
try {
handler(...params);
} catch (err) {
console.error(`Error executing handler for event "${type}":`, err);
}
});
return true;
}
// 4. 移除事件监听
removeEventListener(type, handler) {
if (!this.handlers[type]) {
console.warn(`Event "${type}" does not exist`);
return false;
}
if (!handler) {
// 移除整个事件类型
delete this.handlers[type];
return true;
}
const index = this.handlers[type].indexOf(handler);
if (index === -1) {
console.warn(`Handler not found for event "${type}"`);
return false;
}
this.handlers[type].splice(index, 1);
// 如果该事件类型没有处理器了,清除空数组
if (this.handlers[type].length === 0) {
delete this.handlers[type];
}
return true;
}
// 5. 一次性事件监听(可选增强)
once(type, handler) {
const onceWrapper = (...args) => {
handler(...args);
this.removeEventListener(type, onceWrapper);
};
this.addEventListener(type, onceWrapper);
}
}
eventbus
class EventBus {
constructor() {
this.events = new Map();
}
on(eventName, callback, options = {}) {
if (typeof callback !== 'function') {
throw new Error('Callback must be a function');
}
if (!this.events.has(eventName)) {
this.events.set(eventName, new Set());
}
const eventCallbacks = this.events.get(eventName);
eventCallbacks.add({
fn: callback,
once: !!options.once
});
}
once(eventName, callback) {
this.on(eventName, callback, { once: true });
}
emit(eventName, ...args) {
if (!this.events.has(eventName)) return;
const eventCallbacks = this.events.get(eventName);
// 创建副本以防在回调中修改原集合
const callbacksToRun = new Set(eventCallbacks);
callbacksToRun.forEach(callbackObj => {
callbackObj.fn(...args);
if (callbackObj.once) {
eventCallbacks.delete(callbackObj);
}
});
// 如果该事件没有回调了,删除事件
if (eventCallbacks.size === 0) {
this.events.delete(eventName);
}
}
off(eventName, callback) {
if (!this.events.has(eventName)) return;
if (typeof callback === 'undefined') {
// 移除该事件的所有监听
this.events.delete(eventName);
return;
}
const eventCallbacks = this.events.get(eventName);
// 查找并删除匹配的回调
for (const cbObj of eventCallbacks) {
if (cbObj.fn === callback) {
eventCallbacks.delete(cbObj);
break; // 假设同一个函数不会多次添加
}
}
// 如果该事件没有回调了,删除事件
if (eventCallbacks.size === 0) {
this.events.delete(eventName);
}
}
clear() {
this.events.clear();
}
}
红绿灯
return new Promise(resolve => {
console.log(light);
setTimeout(resolve, duration);
});
}
function startTrafficLight() {
// 定义链式执行
delay(3000, '🔴 Red light')
.then(() => delay(2000, '🟢 Green light'))
.then(() => delay(1000, '🟡 Yellow light'))
.then(() => startTrafficLight()); // 递归调用,形成循环
}
startTrafficLight();
return new Promise(resolve => setTimeout(resolve, duration));
}
async function redLight() {
console.log('🔴 Red light');
await delay(3000); // 红灯亮3秒
}
async function yellowLight() {
console.log('🟡 Yellow light');
await delay(1000); // 黄灯亮1秒
}
async function greenLight() {
console.log('🟢 Green light');
await delay(2000); // 绿灯亮2秒
}
async function startTrafficLight() {
while (true) { // 无限循环
await redLight();
await greenLight();
await yellowLight();
}
}
startTrafficLight();