人人都能看懂的HTTP学习笔记
HTTP的基本概念
整体架构
flowchart TD |
HTTP请求-响应流程图
sequenceDiagram |
生活案例
把HTTP通信想象成你去一家图书馆借书。
- 你(客户端) 想借一本书(资源)。
- 你知道书的准确位置(URL),比如“三楼社科区A-18架第3排”。
- 你填写一张借书单(HTTP请求)并交给图书管理员(服务器)。
- 图书管理员根据单子找到书,连同借阅凭证一起给你(HTTP响应)。
- 如果书不在或者你没借书证,他会告诉你“没找到”(404)或“请出示证件”(401)。
真实案例
在一个电商网站中,当你点击“我的订单”时:
- 你的浏览器(前端应用,如Vue/React)会发起一个HTTP GET 请求,URL可能是 https://api.ecommerce.com/orders?user_id=123。
- 这个请求通过互联网发送到电商的后端API服务器。
- 服务器验证你的身份,然后去数据库查询用户123的所有订单信息。
- 服务器将订单信息打包成一个JSON格式的字符串,放入HTTP响应体中,并返回给你的浏览器。
- 浏览器接收到JSON数据后,动态地将订单列表渲染到页面上。
在这个案例中,HTTP扮演了前后端数据通信的“信使”角色。
经典问题
问题: 当你在浏览器地址栏里输入一个URL,然后按下回车,直到你看到页面,这中间发生了什么?
1. 应用层 - URL解析与HTTP请求构建:
- 浏览器首先会解析我输入的URL,识别出协议(HTTP/HTTPS)、域名、端口、路径等信息。
- 它会构建一个HTTP请求报文,最核心的是请求行,比如 GET /index.html HTTP/1.1,以及包含Host在内的各种请求头。
2. 应用层 - DNS解析:
- 浏览器需要将URL中的域名(比如 www.example.com)解析成服务器的IP地址。
- 它会依次查询:浏览器缓存 -> 操作系统缓存 -> 本地Hosts文件 -> 本地DNS服务器。如果都找不到,本地DNS服务器会向根DNS服务器发起递归查询,最终找到目标IP地址。
3. 传输层 - TCP连接(三次握手):
- 知道了服务器IP和端口(HTTP默认80,HTTPS默认443)后,浏览器会通过TCP协议与服务器建立连接。
- 这个过程就是著名的“三次握手”:客户端发送SYN包,服务器回复SYN+ACK包,客户端再回复ACK包,连接建立。如果是HTTPS,这里还会进行TLS握手。
4. 网络层 - IP寻址与路由:
- TCP将HTTP请求报文分割成TCP报文段,并打包成IP数据包。通过IP寻址和路由器一跳一跳的转发,最终将数据包发送到目标服务器。
5. 服务器处理请求:
- 服务器接收到请求后,Web服务器(如Nginx)会进行处理,可能会将请求转发给后端的业务逻辑(如Node.js, Tomcat)。
- 后端应用处理请求,可能涉及数据库查询等操作,然后生成一个HTTP响应报文。
6. 返回响应与浏览器渲染:
- 服务器将HTTP响应报文(包含状态码200 OK和HTML页面内容)通过TCP连接发回给浏览器。
- 浏览器接收到响应后,开始解析HTML,构建DOM树。同时,如果HTML中包含CSS、JavaScript、图片等外部资源,浏览器会重复上述过程为每个资源发起新的HTTP请求。
- 最终,浏览器将DOM树、CSSOM树结合,进行布局(Layout)和绘制(Paint),将完整的页面呈现给我。
7. 连接关闭(四次挥手):
- 在HTTP/1.1的持久连接下,TCP连接可能不会立即关闭,以便复用。但最终会通过“四次挥手”来断开连接。
补充知识
CDN
- 流程中的位置:DNS解析阶段
- 核心概念: CDN(Content Delivery Network,内容分发网络)本质上是一个分布式的缓存系统。它将网站的静态资源(如图片、CSS、JavaScript文件,甚至部分动态内容)缓存到全球各地、靠近用户的“边缘节点”(Edge Node)上。
- 解决的问题: 物理延迟。如果你的服务器在纽约,上海的用户访问它,数据来回需要跨越太平洋,光速的限制是无法逾越的。CDN通过让用户访问离他最近的节点来获取资源,极大地缩短了物理距离,从而降低延迟。
sequenceDiagram |
负载均衡
- 流程中的位置:TCP连接建立阶段
- 核心概念: 负载均衡器是服务器集群的“交通警察”。当大量请求涌入时,它会将这些请求根据预设的策略(如轮询、最少连接数、IP哈希)分发到后方的多台Web服务器上。
- 解决的问题: 单点瓶颈和可扩展性。单个服务器的处理能力是有限的,无法应对高并发场景。负载均衡能将压力均分,使得系统可以通过简单地增加服务器数量(水平扩展)来提升整体处理能力。
flowchart TD |
HTTP/2多路复用
- 流程中的位置:浏览器获取页面资源阶段
- 核心概念: HTTP/1.1存在“队头阻塞”(Head-of-Line Blocking)问题。虽然浏览器可以开多个TCP连接(通常6-8个),但每个连接同一时间只能处理一个请求-响应。而HTTP/2的多路复用(Multiplexing)允许在单个TCP连接上同时发送和接收多个请求和响应,它们被分解成更小的帧,可以并行、交错地传输。
- 解决的问题: 网络传输效率。它消除了队头阻塞,减少了TCP连接建立的开销,使得页面资源加载速度更快。
flowchart TD |
HTTP协议
“无状态”是HTTP协议最根本的设计哲学之一。理解它,是理解为何需要Cookie、Session、Token等会话管理技术的起点。这是架构设计和面试中的高频话题。
在现代的分布式、微服务架构和云原生环境下,服务的“无状态性”变得前所未有的重要。因为一个无状态的服务可以被轻易地水平扩展(简单地增加服务器实例),也可以被负载均衡器自由地调度到任何一台机器上,而无需担心会话数据丢失。将“无状态”与“可伸缩性(Scalability)”强关联,是现代架构师的必备思维。
- Cookie-Session 时代: Cookie只在客户端存储一个无意义的session_id,所有用户的具体状态数据(如购物车、登录信息)都保存在服务器端的内存或数据库中。
- Token-Based (JWT) 时代: 为了让后端服务也“无状态”,我们不再在服务器端存储会话数据。取而代之的是,服务器在用户认证成功后,生成一个加密的、包含用户信息的Token (如JWT),并将其发送给客户端。客户端在后续请求中携带这个Token,服务器只需验证Token的合法性即可,无需查询自己的“会话存储”。这使得后端服务可以无限水平扩展。
生活案例
- 无状态协议就像是与一个记忆力很差的自动售货机打交道。你每次投币买东西,它都会给你对应的商品,但它完全不记得你之前买过什么。如果你想连续买两瓶可乐,你必须完整地做两次“投币-按按钮”的操作。
- 有状态协议则像是你和一个熟悉的酒吧老板打交道。你第一次去说:“我是张三,以后我的酒都记在账上。” 老板记住了。之后你每次去,只需要说:“老样子,来一杯。” 他就知道该给你什么,并记在你的账上。
真实案例
考虑一个大型电商的“购物车”功能。
- 早期设计(有状态): 每个用户的购物车内容都存在Web服务器的内存(Session)里。当用户量巨大时,服务器内存会成为瓶颈。如果该服务器宕机,用户的购物车数据就全部丢失了。而且,负载均衡器必须使用“粘性会话”(Sticky Session),把同一个用户的请求始终转发到同一台服务器,这降低了负载均衡的灵活性。
- 现代设计(无状态): 用户在未登录时,购物车信息被加密存储在客户端的Cookie或LocalStorage里。用户登录后,购物车信息被同步到服务端的分布式缓存(如Redis)或数据库中。Web服务器本身不存储任何购物车状态,每次请求过来,它都根据请求中的用户信息(可能是session_id或JWT)去后端存储中查询购物车数据。这样的服务器可以无限水平扩展。
经典问题
面试题: 既然HTTP是无状态的,那我们是如何实现用户登录状态的保持的?请比较一下基于Session和基于JWT的两种主要方案。
是的,HTTP的无状态性意味着服务器本身不记录客户端的状态,这就需要我们引入额外的机制来管理会话,其中最主流的就是基于Session和基于Token(特别是JWT)的方案。
1. 基于Cookie-Session的方案(传统方案):
* 流程: 客户端首次登录成功后,服务器会创建一个Session对象,里面存储着用户的状态信息(如用户ID、角色等),并为这个Session生成一个唯一的session_id。然后,服务器通过Set-Cookie响应头将这个session_id返回给客户端。客户端浏览器会自动保存这个Cookie。在后续的每次请求中,浏览器都会自动带上这个session_id。服务器收到请求后,通过session_id找到对应的Session,从而识别出用户身份。
* 优点: 状态数据存储在服务端,相对安全;客户端Cookie中只存储无意义的ID,数据量小。
* 缺点: 服务器有状态,不易扩展。在分布式环境下,需要解决Session共享问题,比如使用粘性会话、Session复制或集中的Session存储(如Redis),这增加了架构复杂性。
2. 基于JWT(JSON Web Token)的方案(现代主流):
* 流程: 客户端登录成功后,服务器不再创建Session。而是将用户的核心信息(如用户ID、角色、过期时间)编码成一个JWT字符串。这个JWT本身包含了签名,可以防止被篡改。服务器将这个JWT直接返回给客户端。客户端通常将其存储在LocalStorage或HttpOnly Cookie中。在后续请求中,客户端通过HTTP的Authorization头(通常是Bearer
* 优点: 服务器完全无状态。服务器无需存储任何会话信息,只需验证JWT签名的有效性即可。这使得后端服务可以非常容易地进行水平扩展。天然地避免了CSRF攻击(如果存储在LocalStorage中)。
* 缺点: Token本身可能较大;一旦签发,在过期前难以强制吊销;由于信息存在客户端,不适合存放敏感数据。
总结对比: Session方案将状态的包袱留给了服务端,而JWT方案则将状态(以加密Token的形式)“甩”给了客户端。在当今追求高可伸缩性、跨域通信和微服务的架构下,JWT这种服务端无状态的方案已成为事实上的主流选择。
补充知识
请求与线程的关系
一个HTTP请求通常会由一个后台线程来处理,但这只是最终呈现的结果。前端请求与后台线程的真正关系,取决于Web服务器的I/O模型和并发模型。
这个关系取决于服务器的并发模型。
- 在传统的阻塞I/O模型下,可以近似地认为一个请求独占一个线程。这种模式简单直观,但由于线程开销巨大,无法支持高并发。
- 在现代高性能服务器普遍采用的非阻塞I/O(或异步I/O)模型下,这种关系被解耦了。前端的大量请求首先由少数几个I/O线程来接收和分发,这些I/O线程利用事件循环机制,可以高效地管理成千上万的并发连接。而请求中真正的业务逻辑处理,则被封装成任务,交由一个数量固定的后台工作线程池来执行。
因此,前端的请求和后台的多线程是多对多的关系,但中间通过一个高效的I/O事件分发层进行了调度。我们Java后台开发中讨论的‘线程共享’,通常指的是工作线程池中的线程共享数据库连接池、缓存等公共资源。而我们说‘这个请求很耗时’,通常是指它在工作线程中执行业务逻辑(比如一个复杂的数据库查询)花费了很长时间,但这并不会阻塞I/O线程接收其他新的请求。
Cookie
Cookie是一种允许服务器在客户端(浏览器)上存储少量文本信息的机制。其核心流程是:
- 服务器通过在HTTP响应报文中添加Set-Cookie头部,将信息“种”到客户端。
- 客户端收到后,会将这些信息存储起来。
- 当客户端再次向同一个服务器发起请求时,会自动在HTTP请求报文中添加Cookie头部,将之前存储的信息回传给服务器。
HttpOnly属性:
- 作用: 这是最重要的安全属性。设置了HttpOnly的Cookie,将无法通过JavaScript的document.cookie API进行读写。
- 解决的问题: 有效地防御了绝大部分的跨站脚本攻击(XSS)。黑客即使在你的页面注入了恶意脚本,也无法窃取到这个Cookie,从而无法轻易地劫持你的会话。
- 业界标准: 所有承载敏感信息(如session_id, token)的Cookie,必须设置为HttpOnly。
Secure属性:
- 作用: 设置了Secure的Cookie,只有在HTTPS连接中才会被发送。在HTTP连接中,浏览器会忽略这个Cookie,不会发送它。
- 解决的问题: 防止Cookie在不安全的HTTP连接中被中间人嗅探和窃取。
- 业界标准: 所有承载敏感信息的Cookie,必须设置为Secure。
SameSite属性:
- 作用: 这是防御**跨站请求伪造(CSRF)**攻击的利器。它定义了浏览器在**跨站请求**时是否应发送Cookie。
- 它有三个值:
- Strict: 最严格。任何跨站请求(比如从A网站点击链接到B网站),都不会携带B网站的Cookie。
- Lax: (目前多数浏览器的默认值) 在一些安全的顶层导航(如点击链接、GET表单)时允许发送Cookie,但在POST请求、img、iframe等加载中则会禁止。
- None: 任何跨站请求都会发送Cookie。但必须同时设置Secure属性,否则无效。常用于需要跨域认证的API场景。
- 解决的问题: CSRF攻击的核心是利用了用户在A网站的操作,会自动携带B网站的Cookie去请求B网站。SameSite属性正是打破了这个“自动携带”的链条。
%%{init: { "themeVariables": { "sequenceNumberColor": "#546E7A", "actorBorder": "#78909C", "actorBkg": "#E3F2FD", "actorTextColor": "#1E88E5", "noteBkgColor": "#fffde7", "noteBorderColor": "#FFD600" }, "theme": "neo" } }%% |
生活案例
Cookie就像是游乐场的腕带。
- 你买票入场时(登录),工作人员(服务器)给你戴上一个腕带(Set-Cookie),上面有个独一无二的条形码(session_id)。
- HttpOnly属性就像这个腕带是一次性锁死的,你自己用手摘不下来(JS无法读取),只能由专门的机器(浏览器协议)来识别。
- Secure属性意味着只有在游乐场官方通道(HTTPS)才能扫描这个腕带,你在外面的小卖部(HTTP)用不了。
- SameSite=Strict属性就像是规定,这个腕带只能在游乐场内部使用。如果你跑到隔壁的商场,想用这个腕带打折,商场的扫描仪(其他网站)会拒绝识别。
真实案例
场景: 设计一个银行网站的“记住我”功能。
- 错误实践: 将用户的明文用户名和密码保存在普通Cookie中。这是极度危险的,一旦被XSS攻击,用户的凭证将立刻泄露。
- 正确实践:
- 用户登录时,如果勾选了“记住我”,服务器会生成一个长期的、高熵的、随机的令牌(Token)。
- 服务器在数据库中存储这个令牌,并关联到该用户,同时设置一个较长的过期时间(比如30天)。
- 服务器通过Set-Cookie将这个令牌返回给客户端,并必须设置以下属性:token=abc…xyz; Expires=…; HttpOnly; Secure; SameSite=Lax。
- 用户下次访问时,浏览器会自动携带这个令牌Cookie。服务器端的“自动登录”过滤器会检查这个令牌,在数据库中验证其有效性。如果有效,则为用户自动创建一次性的登录会话,实现“记住我”功能。
经典问题
HttpOnly和SameSite这两个Cookie属性分别是为了解决什么安全问题的?请具体说明。
这两个属性是现代Web安全中用于加固Cookie、防御两大核心攻击的关键手段。
- HttpOnly 主要防御的是跨站脚本攻击(XSS)。 XSS攻击的核心是攻击者在网页中注入了恶意的JavaScript脚本。在没有HttpOnly的情况下,这个脚本可以通过document.cookie窃取到用户的会话Cookie(比如session_id),然后发送到攻击者的服务器,攻击者就可以利用这个Cookie伪装成用户进行操作,这就是“会话劫持”。而设置了HttpOnly属性后,JavaScript就无法再访问这个Cookie,从根本上切断了XSS攻击窃取会话Cookie的路径,极大地提升了安全性。
- SameSite 主要防御的是跨站请求伪造(CSRF)。 CSRF攻击的核心是利用了浏览器在发送跨站请求时会自动携带目标站点Cookie的特性。攻击者会诱导已登录的用户(比如银行网站的用户)去点击一个恶意链接,这个链接会向银行服务器发起一个转账请求。由于浏览器会自动带上银行的Cookie,银行服务器会误以为这是用户的真实操作,从而导致资金被盗。SameSite属性通过限制跨站请求发送Cookie来防御这种攻击。SameSite=Strict最为严格,几乎禁止所有跨站Cookie发送;而Lax模式则是一种平衡,它允许在用户主动导航(如点击链接)这种风险较低的场景下发送Cookie,但在高风险的场景(如POST请求或通过
、
总结来说,HttpOnly保护Cookie不被“内鬼”(页面内的JS)偷走,而SameSite保护Cookie不被“外人”(其他网站)利用。 在实际开发中,对敏感Cookie同时设置这两个属性,是构建安全防线的标准操作。
持久连接
sequenceDiagram |
生活案例
- 短连接就像是每次去超市买一件商品,你都要完整地经历一次“开车去 -> 停车 -> 进店 -> 结账 -> 开车回家”的流程。买三件商品,就要跑三趟。
- 持久连接就像是你开车到超市后,把车停在停车场,然后进店里把购物车装满,最后统一结账再开车回家。你只跑了一趟,节省了大量的“开车”和“停车”时间(TCP握手和慢启动)。
- 管线化则像是,你把购物清单一口气全扔给一个超市导购员,但他必须严格按照清单的顺序一件一件找齐了,再统一给你。如果第一件商品(比如进口奶酪)特别难找,你就要一直等到他找到为止,即使他已经路过了后面清单上的所有商品。
真实案例
- 几乎所有现代Web网站的性能都受益于持久连接。以加载一个新闻门户网站为例,主页面index.html加载完成后,浏览器需要继续加载数十个CSS文件、JS文件、logo图片、广告图片、文章缩略图等。
- 如果没有持久连接,每个资源的加载都需要一次独立的TCP连接建立和关闭,页面加载时间可能会从2-3秒延长到10秒以上,用户体验将是灾难性的。
- HTTP/1.1的持久连接机制,配合浏览器的并行连接(通常对一个域名开6-8个持久连接),是支撑现代复杂网页快速加载的基础。
经典问题
HTTP/1.1的持久连接相比于HTTP/1.0的短连接,主要解决了什么问题?它自身又有什么局限性,而HTTP/2又是如何解决这些局限性的?
HTTP/1.1的持久连接主要解决了HTTP/1.0短连接模型的两大性能瓶颈:
- 高昂的连接建立开销: 短连接模型下,每个HTTP请求都需要经过一次TCP三次握手,这至少会产生一个RTT的延迟。对于包含大量小资源的页面,这些握手延迟累加起来非常可观。持久连接通过复用同一个TCP连接,完全避免了后续请求的握手开销。
- TCP慢启动的性能影响: 每个新的TCP连接都会经历一个“慢启动”过程,即连接的传输速度会从一个较低的值开始,慢慢提升。对于小文件频繁的Web请求,连接很可能在速度还没达到峰值时就被关闭了,导致TCP的性能优势无法发挥。持久连接由于长时间保持,可以使得连接“预热”,始终工作在较高的传输速率下。
然而,HTTP/1.1的持久连接自身也存在一个核心局限性,那就是队头阻塞(Head-of-Line Blocking):
- 在一个TCP连接上,虽然可以发送多个请求,但响应必须按请求的顺序串行返回。如果第一个请求的服务器处理时间很长,它就会阻塞后面所有请求的响应,即使后面的响应早已准备就绪。虽然管线化(Pipelining)技术试图解决这个问题,但由于其自身的复杂性和兼容性问题,并未被广泛采用。
HTTP/2则通过引入“多路复用”(Multiplexing)机制,完美地解决了队头阻塞问题:
- HTTP/2允许在单个TCP连接上,将多个请求/响应分割成更小的、独立的帧(Frame),并将它们交错地发送和接收。每个帧都带有自己的流标识符(Stream ID),所以接收端可以根据ID将它们重新组装成完整的请求或响应。这样,一个慢请求的响应就不会再阻塞其他快速的响应,真正实现了并行传输。
HTTP报文结构
graph TD |
生活案例
- HTTP/1.1的报文就像一封传统的信件。有信封上的地址和收件人(起始行),有信头写的日期和主题(首部),还有信纸上的正文内容(主体)。它们是作为一个整体寄送的。
- HTTP/2的报文则像是把这封信的内容拆分成了很多张标准大小的卡片(帧),并进行了编码。HEADERS帧就像是包含了所有收发件人信息的“地址卡”,DATA帧就像是一张张写着正文内容的“内容卡”。这些卡片可以和其他信件的卡片混在一起(多路复用)寄送,到了目的地再根据信件编号重新组装起来。
真实案例
当你在Chrome开发者工具(F12)的Network面板中查看一个请求时,你看到的“Headers”和“Payload”/“Response”标签页,就是对HTTP报文逻辑结构的完美呈现。
- General 和 Request/Response Headers 部分对应起始行和首部。
- Payload(对于POST)或 Response(对于GET)标签页的内容对应主体。
经典问题
HTTP/1.1和HTTP/2在报文结构上最核心的区别是什么?这个区别带来了什么好处?
最核心的区别在于,HTTP/1.1是一个文本协议,其报文结构是基于文本的,而HTTP/2是一个二进制协议,它引入了全新的帧(Frame)层。
具体来说:
- HTTP/1.1的报文是由可读的ASCII文本构成的,包括起始行、首部和主体,它们之间通过换行符(CRLF)分隔。这种格式对人类友好,但对机器解析效率不高,且存在安全注入的风险。
- HTTP/2则将一个逻辑上的请求或响应,在传输时分解为多个二进制编码的帧。比如,所有首部信息被打包到一个HEADERS帧中,而响应主体则被放入一个或多个DATA帧中。
这个从“文本”到“二进制帧”的根本转变,带来了两大革命性的好处:
- 实现了多路复用(Multiplexing): 来自不同请求的帧可以在同一个TCP连接上交错传输,每个帧都带有流ID,从而解决了HTTP/1.1的队头阻塞问题,极大地提高了并发传输效率。
- 实现了头部压缩(HPACK): 由于首部被独立成HEADERS帧,HTTP/2可以对大量重复的头部字段(如User-Agent, Accept等)使用HPACK算法进行高效压缩,显著减少了请求的体积,这在移动网络环境下尤其重要。
内容编码与传输编码
graph TD |
生活案例
想象你要寄送一个大型的乐高模型(原始资源)。
- 内容编码 (gzip/Brotli):你发现直接寄送盒子太大、运费太贵。于是你把乐高模型拆散成一个个零件,并用真空压缩袋把它们打包起来。这样体积就大大减小了。这个“真空压缩”的过程,就是内容编码,它改变了“内容”本身(从组装好的模型变成了零件袋),目的是让它变得更小。
- 传输编码 (chunked):现在你有一包压缩好的零件,但你没有一个足够大的箱子一次装下。于是你决定分批寄送。你找了几个小盒子,在第一个盒子上写“第1箱,共3箱”,在第二个盒子上写“第2箱,共3箱”… 这就是分块传输编码。它没有改变盒子里的内容(零件还是那些零件),而是改变了“寄送的方式”(从一个大包裹变成几个小包裹)。
真实案例
- 内容编码 (gzip/Brotli): 你打开任何一个现代网站,查看其HTML、CSS、JS文件的网络请求,你都会在响应头中看到Content-Encoding: gzip或Content-Encoding: br。这表明服务器对这些文本资源进行了压缩,你的浏览器在接收到后会自动解压。这是前端性能优化的标准操作。
- 传输编码 (chunked): 当你访问一个需要大量后台计算的报表页面时,比如“生成过去一年的销售报告”。服务器可能不会等所有数据都计算完毕(可能需要几十秒),而是采用流式处理:每计算出一部分数据,就立刻通过分块传输将这部分数据发送给浏览器。这样,用户很快就能看到页面的头部和第一部分数据,而不是面对一个长时间的白屏等待。你在开发者工具中会看到响应头里有Transfer-Encoding: chunked,并且看不到Content-Length头。
经典问题
Content-Encoding和Transfer-Encoding这两个HTTP头部有什么区别?
Content-Encoding和Transfer-Encoding虽然都涉及“编码”,但它们是HTTP中两个完全不同层面、用于解决不同问题的机制。
- 目的不同:
- Content-Encoding(内容编码)的目的是压缩实体主体,减少传输数据的大小,从而节省带宽、加快传输速度。常见的编码有gzip, deflate, br。它作用的对象是资源本身。
- Transfer-Encoding(传输编码)的目的是改变报文的传输方式,以利于网络传输。目前唯一的标准值是chunked(分块传输)。它解决的是服务器无法在传输前确定整个响应体大小的问题(比如动态生成的内容),使得服务器可以流式地发送数据。它作用的对象是整个报文的结构。
- 作用位置不同:
- 内容编码是在服务器生成响应后,发送给客户端前对实体主体进行的“端到端”编码。客户端接收后需要先解码,才能得到原始的实体内容。
- 传输编码是逐跳的,它只在相邻的两个节点间有效。一个代理接收到chunked编码的报文后,必须先解码(将分块数据合并),然后再决定是否以chunked或其他方式转发给下一个节点。
- 与Content-Length的关系:
- 当使用Content-Encoding时,Content-Length头部的值是**编码后(压缩后)**的实体主体大小。
- 当使用Transfer-Encoding: chunked时,必须不能出现Content-Length头部,因为内容的长度是未知的,由最后一个大小为0的块来表示结束。
总结来说,Content-Encoding是给“货物”做真空包装,而Transfer-Encoding是改变“送货”的方式。
范围请求
graph TD |
- 多线程下载器的工作原理: 现代的下载工具(如迅雷、IDM)和一些浏览器插件,正是利用了范围请求的**多部分请求(Multipart Ranges)**能力来加速下载。它们会同时对一个资源发起多个范围请求,每个请求下载文件的一部分,最后在本地将这些部分拼接起来。
- 视频流媒体的“拖动”播放: 当你在B站或YouTube上观看视频并拖动进度条时,你实际上触发了一个新的范围请求。播放器会计算出你想看的时间点对应于整个视频文件(比如一个mp4文件)的哪个字节范围,然后向服务器发起一个Range请求,只获取那一小段数据,从而实现快速的“空降”播放,而无需下载整个文件。
- 大文件上传的断点续传: 虽然范围请求主要用于下载,但其理念也被应用于上传。客户端可以先向服务器查询已上传了多少字节,然后通过类似Content-Range的机制(通常是自定义头部)从断点处继续上传剩余部分。
生活案例
想象你在读一本非常厚的电子书(资源)。
- 没有范围请求: 你每次打开书,都必须从第一页开始下载整本书,即使你只想看第500页。
- 有范围请求: 你可以直接告诉服务器:“我只想看第500页到第510页的内容”(Range: pages=500-510,如果是字节就是bytes=…)。服务器就只把这10页的内容发给你,非常高效。这就是断点续传和跳转阅读的原理。
真实案例
除了上面提到的视频播放和下载工具,云存储服务(如阿里云OSS、AWS S3)的API也大量使用了范围请求。当你需要从云端下载一个几GB大的备份文件时,它们的SDK(软件开发工具包)在底层就是利用范围请求,将文件分块下载,并支持在网络中断后从失败的块开始续传,大大提升了大文件操作的稳定性和效率。
经典问题
HTTP的206状态码是什么含义?它通常与哪两个头部字段一起工作来实现什么功能?
206 Partial Content是HTTP中一个表示部分内容的成功状态码。它表明服务器成功处理了客户端的范围请求(Range Request),并且响应体中只包含了资源的一部分,而不是全部。
它通常与以下两个核心的头部字段协同工作:
- 请求头:Range
- 由客户端发送,用于告诉服务器它想要获取资源的哪个或哪些部分。其值的单位最常见的是bytes,例如Range: bytes=0-499表示请求资源的前500个字节。
- 响应头:Content-Range
- 由服务器在返回206响应时发送,用于告诉客户端这次返回的部分内容在整个资源中的位置。它的格式通常是bytes
- / ,例如Content-Range: bytes 0-499/1234,表示这次发送的是第0到499字节,而整个资源的总长度是1234字节。
- 由服务器在返回206响应时发送,用于告诉客户端这次返回的部分内容在整个资源中的位置。它的格式通常是bytes
这套机制协同工作的核心功能是实现资源的部分获取,其最重要的两大应用场景是:
- 断点续传: 当下载大文件时,如果网络中断,客户端可以记录下已下载的字节数,然后通过Range头请求剩余的部分,服务器返回206响应,从而实现从断点继续下载。
- 流媒体播放: 在线视频播放器通过发送带有不同字节范围的Range请求,来获取用户当前想观看或者预加载的视频片段,实现了“即点即播”和进度条拖动的功能。
状态码
flowchart TD |
3xx重定向状态码
sequenceDiagram |
- 301 Moved Permanently (永久重定向): 表示请求的资源已被永久地移动到了新的URL。浏览器和搜索引擎在收到301后,会更新自己的书签和索引,未来会直接访问新的URL。
- 302 Found (临时重定向): 表示请求的资源临时被移动到了新的URL。浏览器和搜索引擎不会更新记录,下次还是会访问原始的URL。
- 304 Not Modified (未修改): 这是服务器对一个附带条件的请求(Conditional Request,通常是带有If-Modified-Since或If-None-Match头部)的回应。它告诉客户端,你本地缓存的资源还是最新的,直接用缓存吧,我这次就不给你发内容了。
生活案例
- 301 永久重定向:你搬家了,你去邮局办理了地址永久迁移。从此以后,所有寄给你的信件,邮局都会直接改投到你的新家,并且会通知你的朋友们:“他搬家了,以后请直接寄到新地址。”
- 302/307 临时重定向:你只是去朋友家暂住几天。你告诉邮局,这几天的信件临时转寄到朋友家。邮局不会更改你的档案,你的朋友们也不知道你出门了,信件还是会先寄到你家,再由邮局转送。
真实案例
- 301 永久重定向的典型应用:
- 网站换域名: 当公司将域名从 old-brand.com 更换为 new-brand.com 时,必须在旧域名的服务器上配置对所有请求返回301,指向新域名对应的页面。这对**SEO(搜索引擎优化)**至关重要,它能告诉搜索引擎将旧域名的权重和排名**全部转移**到新域名上。
- URL规范化: 确保 http://example.com、http://www.example.com 等多个入口都301重定向到唯一的、规范的URL(比如 https://www.example.com),避免搜索引擎认为这是重复内容。
- 302/307 临时重定向的典型应用:
- 未登录用户访问受限页面: 用户访问 my.example.com/dashboard,服务器发现用户未登录,会返回一个302/307重定向到登录页 my.example.com/login,并在URL中附带一个参数告诉登录页登录成功后应该跳回dashboard。
- 服务维护: 网站进行短暂维护时,可以临时将所有请求302重定向到一个静态的“维护中”页面。
经典问题
301和302在实际应用中,特别是对SEO,有什么本质区别?
301和302最本质的区别在于它们向客户端和搜索引擎传递的语义完全不同,这直接影响了缓存行为和SEO权重。
- 301 (永久重定向) 告诉浏览器和搜索引擎:“这个地址永远废弃了,请以后都去访问新的地址”。因此:
- 浏览器行为: 浏览器会强烈地缓存这个重定向关系。在缓存有效期内,下次访问旧地址时,它可能不再向旧地址发请求,而是直接从本地缓存读取新地址并发起请求。
- SEO影响: 这是对SEO最友好的方式。搜索引擎会理解为网站权重和排名的永久性转移,它会把旧URL积累的“链接权重”(link juice)几乎全部传递给新的URL。
- 302 (临时重定向) 告诉浏览器和搜索引擎:“这个地址只是临时不在,请这次先去新地址访问,但下次还应该来访问我这个旧地址”。因此:
- 浏览器行为: 浏览器不会缓存这个重定向关系,每次访问旧地址时,它还是会先请求旧地址,然后再根据服务器的302响应进行跳转。
- SEO影响: 搜索引擎会认为旧URL依然是主体,不会将权重传递给新URL。如果长期错误地使用302进行永久性的域名或URL更换,将导致新页面的权重始终无法提升,对SEO是灾难性的。
总结来说,网站改版、换域名等永久性变更必须使用301;而像登录跳转、服务临时维护等临时性跳转,则应该使用302(或者更精确的303/307)。
4xx 客户端错误 & 5xx 服务器错误
sequenceDiagram |
- 4xx (Client Error): 表明错误是由客户端引起的。比如,客户端请求了一个不存在的URL,或者没有提供有效的认证信息。
- 401 (Unauthorized): “未认证”。表示当前请求需要用户认证,但客户端没有提供认证信息,或者认证信息无效(比如Token错误或过期)。它暗示客户端应该去认证(或重新认证)。
- 403 (Forbidden): “未授权”。表示服务器已经成功识别了客户端的身份,但该用户没有权限访问这个资源。它暗示认证是没用的,换个有权限的账号来吧。例如,普通用户尝试访问管理员才能访问的后台接口。
- 5xx (Server Error): 表明错误发生在服务器端。服务器清楚地知道自己出了问题,无法完成一个看起来有效的请求。
- 502 (Bad Gateway): 通常是你的网关(如Nginx) 尝试去请求后端的上游服务(如一个Node.js应用),但上游服务出了问题,返回了一个无效的响应,或者上游服务直接挂了,连接被拒绝。网关无法理解这个响应,于是返回502。核心是:连接已建立,但上游服务没正常工作。
- 504 (Gateway Timeout): 网关向上游服务发起了请求,但上游服务在网关设定的超时时间内,一直没有返回任何响应。网关等不及了,就返回504。核心是:上游服务活着,但处理太慢了。
生活案例
- 4xx 客户端错误 就像是你去银行办业务:
- 400 Bad Request: 你填的表单格式不对,业务员看不懂。
- 401 Unauthorized: 你没带身份证,业务员要求你先证明你是你。
- 403 Forbidden: 你带了身份证,但你想查别人的账户,业务员告诉你“你没这个权限”。
- 404 Not Found: 你要办的这个业务,银行根本就没有。
- 5xx 服务器错误 则是银行内部的问题:
- 500 Internal Server Error: 你的请求完全合规,但银行的电脑系统突然死机了。
- 502 Bad Gateway: 你找的大堂经理(网关)去后台找柜员(上游服务)办业务,结果那个柜员给了他一张错误的单据,经理没法处理。
- 503 Service Unavailable: 银行正在年终决算,所有窗口都暂停服务。
- 504 Gateway Timeout: 大堂经理把你的申请交到后台了,但那个业务流程太复杂,后台一直没办完,经理等得不耐烦了,只能告诉你“超时了”。
真实案例
在一次线上问题排查中,监控系统报警大量出现502 Bad Gateway错误。
- 第一反应: 这不是客户端的问题,也不是网关本身的问题,而是网关后面的上游服务出了问题。
- 排查路径: 登录到API网关(Nginx)服务器,查看错误日志(error.log)。
- 发现日志: 日志中大量出现类似 (111: Connection refused) while connecting to upstream, client: … 的错误。
- 定位问题: Connection refused(连接被拒绝)是一个非常明确的信号,它表明网关尝试去连接上游服务的IP和端口时,上游服务进程已经不存在或者端口没有被监听。
- 解决问题: 登录到上游服务所在的服务器,发现对应的应用进程确实已经崩溃。重启服务后,问题解决。
经典问题
用户反馈网站打不开,你作为后端开发,会如何排查?如果抓包发现返回了502,这通常意味着什么?
网站打不开的原因有很多,我会从客户端到服务器逐层排查。但如果明确返回了502状态码,那么排查的焦点就可以非常集中。
502 Bad Gateway是一个服务器端的错误码,但它特指作为网关或代理的服务器,在尝试访问上游服务器时,收到了一个无效的响应。在现代微服务架构中,这通常是API网关(比如Nginx)返回的。
收到502,我基本可以断定:
- 客户端到网关的网络是通的。
- 网关本身是正常工作的。
- 问题出在网关与某个上游后端服务之间的通信上。
我会立刻采取以下排查步骤:
- 定位上游服务: 首先,我会根据用户访问的URL路径,去Nginx的配置文件中查找这个请求被proxy_pass到了哪个具体的上游服务(upstream)。
- 查看网关日志: 登录Nginx服务器,查看error.log。502错误通常伴随着具体的错误原因,最常见的有:
- connect() failed (111: Connection refused):这说明上游服务进程挂了,或者防火墙阻止了连接。我会立刻去检查上游服务的健康状态。
- upstream prematurely closed connection:这说明连接建立了,但上游服务在返回完整响应前就提前关闭了连接,很可能是上游服务代码内部发生了崩溃。
- upstream sent invalid header:上游服务返回的HTTP响应头不规范,Nginx无法解析。
- 检查上游服务: 根据网关日志的线索,直接去排查对应的上游服务。查看它的应用日志、系统资源(CPU、内存)使用情况,确定它为什么没有正常响应网关的请求。
HTTP首部
缓存控制首部
flowchart TD |
HTTP缓存分为两大类:**强制缓存(也叫强缓存)*和*协商缓存。
强制缓存 (Strong Caching): 浏览器在发起请求前,先检查本地缓存的副本是否在有效期内。如果在,就不向服务器发送任何请求,直接使用本地副本,HTTP状态码是 200 OK (from memory cache / from disk cache)。
协商缓存 (Negotiation Caching): 当强制缓存失效(过期)后,浏览器会向服务器发起一个验证请求。服务器根据请求中的验证信息判断资源是否有更新。如果没有更新,服务器返回一个极小的304 Not Modified响应,不包含响应体,告诉浏览器继续使用本地缓存。如果有更新,才返回200 OK和新的完整资源。
Cache-Control的绝对统治地位: 《图解HTTP》中提到的Pragma: no-cache是HTTP/1.0的产物,用于兼容性。Expires也是HTTP/1.0的,它使用绝对时间,如果客户端和服务器时间不同步,就会出问题。在HTTP/1.1中,Cache-Control使用相对时间(如 max-age=3600 秒),并且指令更丰富,已完全取代了Expires。当Cache-Control和Expires同时存在时,Cache-Control的优先级更高。
ETag优于Last-Modified:
- Last-Modified(最后修改时间)是协商缓存的一种验证方式,但它存在两个问题:1) 时间戳的精度只能到秒,一秒内多次修改无法识别;2) 某些服务器上,只是打开文件但未修改,也可能导致修改时间变化。
- ETag(实体标签)是服务器为资源生成的唯一标识符(类似文件指纹)。只要资源内容有任何变动,ETag就会改变。它比Last-Modified更精确、更可靠。
- 业界标准: 当ETag和Last-Modified同时存在时,服务器必须优先使用ETag 来进行验证。
生活案例
- 强制缓存就像是你冰箱里的牛奶。你每次想喝牛奶时,都会先看一眼保质期(Expires / Cache-Control: max-age)。只要还在保质期内,你就直接拿出来喝,根本不用出门去超市问。
- 协商缓存就像是牛奶过期了,但你觉得可能还能喝。你不会直接扔掉,而是会打开闻一闻(发起协商缓存请求)。如果闻起来没问题(304 Not Modified),你就继续喝了。如果闻起来坏了(200 OK + 新牛奶),你才会去超市买一瓶新的回来。ETag就像是牛奶包装上的一个独特的批次码,比单看生产日期更可靠。
真实案例
前端性能优化中对不同资源设置缓存策略。
- 不常变动的资源(如库文件 antd.js, vue.js, logo图片):
- 策略: 设置一个非常长的强制缓存时间。比如 Cache-Control: public, max-age=31536000 (一年)。
- 配合: 文件名中通常会带上内容的哈希值,如antd.a3b4c5.js。一旦文件内容有变动,哈希值会变,文件名也变了,这会触发一个新的URL请求。这种策略叫**“内容哈希命名”(Content-addressable storage)**。
- 效果: 用户第一次访问后,这些资源会被永久缓存,后续访问速度极快。
- 经常变动的资源(如业务逻辑index.js, 主index.html):
- 策略: 不设置或设置很短的强制缓存,但必须开启协商缓存。比如 Cache-Control: no-cache。no-cache并不是“不缓存”,而是“每次都必须去服务器验证一下”。
- 效果: 保证了用户总能获取到最新的业务逻辑,同时在逻辑未变时,又能通过304响应来利用本地缓存,节省带宽。
经典问题
请详细描述一下浏览器的缓存机制,特别是强制缓存和协商缓存的区别和联系。
浏览器缓存是HTTP性能优化的核心机制,它主要分为强制缓存和协商缓存两大类,两者是协同工作的。
1. 强制缓存(Strong Caching):
* 触发机制: 浏览器在请求资源前,会先检查本地缓存的副本是否在有效期内。
* 相关头部: 主要由服务器响应中的Cache-Control头(HTTP/1.1,优先级高)和Expires头(HTTP/1.0)控制。Cache-Control: max-age=3600表示资源在3600秒内有效。
* 表现: 如果缓存有效,浏览器不会向服务器发送任何请求,而是直接从本地内存或磁盘中读取资源,网络请求的状态码会是200 OK (from memory cache/disk cache),速度极快。
2. 协商缓存(Negotiation Caching):
* 触发机制: 当强制缓存过期后(或者收到了Cache-Control: no-cache指令),浏览器必须向服务器发起一次验证请求。
* 相关头部: 这个验证请求会携带一些“凭证”:
* If-None-Match: 携带上一次响应中的ETag值。
* If-Modified-Since: 携带上一次响应中的Last-Modified值。
* 服务器行为: 服务器收到请求后,会用这些凭证与当前资源进行比对。
* 如果资源未改变,服务器返回一个304 Not Modified状态码,响应体为空,告诉浏览器“你本地的版本还能用”。
* 如果资源已改变,服务器返回一个200 OK状态码,并在响应体中附上全新的资源内容和新的ETag/Last-Modified。
区别与联系:
- 联系: 两者是前后衔接的流程。浏览器总是先检查强制缓存,强制缓存失效后,再发起协商缓存。
- 区别: 强制缓存的核心是**“不问”,在有效期内完全不与服务器通信;而协商缓存的核心是“要问”**,每次都会与服务器通信,但服务器可能只返回一个极简的304响应,而不是完整的资源。
在性能上,强制缓存 > 协商缓存(命中304) > 无缓存(完整200)。
内容协商相关首部
sequenceDiagram |
- Accept头部的“退化”与“重生”:
- 传统Web页面: 在过去,浏览器发送的Accept头部非常复杂,比如Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8。它试图告诉服务器它能处理的各种内容类型。但实际上,大多数Web服务器都忽略了这个复杂的头部,因为它们通常只返回text/html。
- 现代API(RESTful): Accept头部在API交互中迎来了重生。一个设计良好的RESTful API,其同一个端点(URL)可以根据客户端Accept头部的不同,返回不同格式的数据。例如:
- GET /api/users/1 + Accept: application/json -> 返回JSON格式的用户数据。
- GET /api/users/1 + Accept: application/xml -> 返回XML格式的用户数据。
这使得API具有更好的灵活性和向后兼容性。
- 权重值q: q因子(quality value)是内容协商的量化工具,范围从0到1。它告诉服务器不同选项的优先级。例如 Accept-Language: fr-CH, fr;q=0.9, en;q=0.8 表示客户端最想要瑞士法语,其次是普通法语,再次是英语。
- Accept-Encoding的重要性: 这是内容协商中最常用、最有效的首部。浏览器通过发送 Accept-Encoding: gzip, deflate, br 告诉服务器它支持这三种解压算法,服务器则可以从中选择一种来压缩响应体,显著减少传输体积。
生活案例
内容协商就像你去一家国际餐厅点菜。
- 你对服务员(服务器)说:“我想点一份牛排(URL),我能看懂中文和英文菜单(Accept-Language),我喜欢吃七分熟的(Accept的一个变体,表示偏好),而且我不吃辣(Accept-Encoding的一个变体,表示能力限制)。”
- 服务员(服务器)听了你的要求,去厨房看了一下,发现今天有中文菜单,并且可以做七分熟不辣的牛排。
- 最后,他给你端上了七分熟的牛排,并附上了一张中文账单(Content-Language: zh)。
真实案例
设计一个图片服务API,需要为不同设备提供最优化的图片格式。
- 背景: 现代浏览器(如Chrome)支持一种名为 WebP 的新图片格式,它在同等画质下比JPEG和PNG的体积小很多。但老浏览器不支持。
- 实现:
- 服务器上为同一张图片存储多个版本,如 image.jpg, image.png, image.webp。
- 现代浏览器发起的图片请求,其Accept头会包含image/webp,例如:Accept: image/avif,image/webp,image/apng,image/,/*;q=0.8。
- 服务器(或CDN边缘节点)接收到请求后,检查Accept头部。如果发现其中包含image/webp,就优先返回image.webp文件。
- 如果Accept头中没有image/webp(说明是老浏览器),则根据情况返回image.jpg或image.png。
- 效果: 通过内容协商,为现代浏览器用户提供了加载速度更快的WebP图片,同时保持了对老浏览器的兼容性,实现了渐进式增强的性能优化。
经典问题
Accept-Encoding和Content-Encoding是如何配合工作的?这个机制有什么好处?
Accept-Encoding和Content-Encoding是HTTP内容协商中用于传输压缩的一对核心请求/响应头,它们共同实现了一种高效的数据传输机制。
工作流程如下:
- 客户端声明能力: 浏览器在发起请求时,通过Accept-Encoding请求头告诉服务器自己支持哪些解压算法。例如:Accept-Encoding: gzip, deflate, br,这表示浏览器能处理gzip、deflate和brotli这三种格式的压缩数据。
- 服务器选择并执行压缩: 服务器收到请求后,会检查这个头部。如果服务器也支持这些算法,并且认为对响应内容进行压缩是有益的(通常对文本类资源有效),它就会选择其中一种算法(通常是压缩率最高的Brotli或最通用的gzip)来压缩响应体。
- 服务器告知压缩方式: 服务器在返回的HTTP响应中,通过Content-Encoding响应头明确告知客户端,它实际使用了哪种算法进行压缩。例如:Content-Encoding: br。
- 客户端解码: 浏览器接收到响应后,根据Content-Encoding头的值,调用相应的解压算法对响应体进行解压,最终得到原始的、可渲染的内容。
这个机制最大的好处是:
- 显著减少了网络传输的数据量,通常能将HTML/CSS/JS等文本资源压缩到原始大小的20%-30%,从而大大加快了页面加载速度,降低了带宽成本,并改善了用户体验,尤其是在移动网络环境下效果非常明显。这是一种对客户端和服务器都非常有益的双赢机制。
连接管理首部
- Connection 首部: 这是一个非常特殊的“元”首部,它有两个核心作用:
- 控制连接的关闭/保持: 它的值可以是close或keep-alive。在HTTP/1.1中,默认是持久连接,所以通常只有在想明确关闭连接时,才会发送 Connection: close。
- 标识“逐跳”首部: 这是它更复杂也更重要的作用。Connection首部可以列出一些其他的首部字段名,这些被列出的首部字段被认为是“逐跳”的,绝不能被代理服务器转发到下一跳。
- Keep-Alive 首部: 这是一个已过时的、非标准的头部,最初在HTTP/1.0的持久连接中被引入。它通常包含一些参数,比如 timeout(连接空闲超时时间)和 max(此连接上可处理的最大请求数)。
sequenceDiagram |
经典问题
Connection头部字段有什么作用?
Connection头部在HTTP/1.1中是一个非常重要的“逐跳”首部(hop-by-hop header),它主要有两个核心作用:
- 控制连接的状态: 它的值可以是keep-alive或close。在HTTP/1.1中,连接默认是持久的,所以我们通常只在需要明确指示在响应后关闭连接时,才会发送Connection: close。这可以用来优雅地通知对端,这是我最后一个请求/响应了。
- 管理逐跳首部: 这是它更底层、更关键的作用。Connection头部的值可以是一个逗号分隔的其他首部字段名列表。所有被列在这里的首部字段,都被认为是“逐跳”的,即它们只对当前的单次TCP连接有效,绝不能被代理服务器转发给下一个节点。在转发报文前,代理必须删除Connection首部以及它里面列出的所有首部。最典型的例子就是Upgrade头部(用于协议升级,如WebSocket),它总是与Connection: Upgrade一起出现,以确保只有直接相连的服务器才会处理这个升级请求。
TCP/IP基础
OSI七层 & TCP/IP四层模型及协议对应关系
graph TD |
数据封装
graph LR |
生活案例
TCP/IP的数据传输就像一次国际快递。
- 应用层 (HTTP): 你要寄送的“商品”(比如一部手机)。
- 传输层 (TCP): 你把手机装进一个带有联系方式的快递盒。盒子上写着你和收件人的电话(端口号),并选择了“顺丰保价”(TCP,确保可靠送达)。
- 网络层 (IP): 快递公司在你的盒子上贴上国际运单,上面有收件人的详细家庭地址(IP地址)。
- 链路层 (MAC): 本地快递员来取件,他只关心下一站是哪个集散中心(下一个路由器的MAC地址),把你的包裹装上他的车。
- 整个过程,你的“商品”被一层层地包装起来,这就是封装(Encapsulation)。
真实案例
思考一个在线直播应用。它同时需要传输两种数据:
- 控制信令: 你发送的弹幕、点赞、送礼物。这些信息绝对不能丢失,必须准确送达。因此,承载这些信令的协议(比如一个自定义协议或HTTP)必须运行在TCP之上。
- 音视频流: 直播的画面和声音。这些数据量巨大,更强调实时性。偶尔丢掉一两帧画面(表现为轻微卡顿或花屏)是可以接受的,但不能因为重传一个旧数据包而导致整个画面延迟。因此,音视频流数据通常运行在UDP之上(使用RTP/RTCP等协议)。
经典问题
简单说说TCP和UDP的区别以及它们各自的应用场景。
TCP和UDP都位于传输层,但它们的设计哲学和应用场景完全不同。
核心区别有三点:
- 连接性与可靠性: TCP是面向连接的,通信前必须“三次握手”建立连接,它提供可靠的、有序的数据传输,保证不丢包、不乱序。而UDP是无连接的,它只管发,不保证对方是否收到,是一种“尽力而为”的不可靠传输。
- 开销与速度: 因为TCP要维护连接状态、进行确认、重传和拥塞控制,所以它的头部开销大(至少20字节),速度相对较慢。UDP没有这些复杂的机制,头部开销极小(固定8字节),速度非常快。
- 数据形态: TCP提供的是字节流服务,数据像水流一样没有边界。UDP提供的是数据报服务,发送方发送一个个独立的数据包,接收方也必须按包接收。
应用场景的选择完全基于这些区别:
- 选择TCP的场景: 凡是要求数据绝对完整、准确的应用,都必须使用TCP。例如:HTTP/HTTPS浏览网页、FTP文件传输、SMTP/POP3电子邮件。
- 选择UDP的场景: 凡是追求实时性、能容忍一定丢包率的应用,都适合UDP。例如:DNS解析(追求快速响应)、音视频通话/直播、在线游戏的数据同步。
补充知识
sequenceDiagram |
DNS
DNS是互联网的基石服务。不理解DNS,你甚至不知道请求该发往何处。它是所有网络请求的第一步。
- 缓存是关键: 为了性能,DNS查询结果会被层层缓存,顺序是:浏览器缓存 -> 操作系统缓存 -> 路由器缓存 -> 本地DNS服务器缓存。这极大地减少了对根DNS服务器的请求压力。
- 记录类型多样化: 除了将域名指向IPv4地址的A记录,还有指向IPv6的AAAA记录,定义别名的CNAME记录,以及邮件服务器的MX记录等。
- 智能DNS与负载均衡: 现代DNS服务(如CDN服务商提供的GeoDNS)能识别用户的地理位置,返回离他最近的服务器IP地址,这是实现CDN内容就近分发和全局负载均衡的核心技术。
flowchart TD |
面试题: 除了A记录,你还知道哪些常见的DNS记录类型,它们分别有什么作用?
除了最常见的、将域名映射到IPv4地址的A记录外,我还了解以下几种关键的DNS记录:
- AAAA记录: 功能与A记录类似,但用于将域名映射到IPv6地址,是IPv6时代的基础。
- CNAME记录(别名记录): 允许将一个域名指向另一个域名。比如,我可以把blog.example.com的CNAME记录指向user.github.io,这样访问我的博客就会被解析到GitHub Pages的服务。它常用于CDN加速,将用户域名指向CDN服务商的域名。
- MX记录(邮件交换记录): 指向负责处理该域电子邮件的服务器。比如example.com的MX记录会指向mail.example.com,这样发往xxx@example.com的邮件才知道该送往哪里。
- NS记录(域名服务器记录): 指定了哪个DNS服务器是该域的权威服务器,负责该域的解析。
TCP
sequenceDiagram |
面试题: 为什么TCP建立连接需要三次握手,而不是两次或者四次?
这个问题核心在于理解TCP作为全双工、可靠通信协议的本质。
- 不能是两次的原因: 无法防止历史的、已失效的连接请求报文突然又传送到了服务器,从而引发错误。如果只有两次握手,服务器收到一个旧的SYN包就会立即建立连接并等待客户端数据,这将浪费服务器资源。而三次握手时,客户端不会对旧连接的SYN+ACK进行确认,服务器收不到ACK就不会建立连接。更关键的是,两次握手无法让双方都确认对方的接收和发送能力都正常。
- 不需要四次的原因: 三次握手已经足够验证双方的收发能力并同步初始序列号了。服务器的确认(ACK)和同步请求(SYN)可以在第二步中合并为一个报文(SYN+ACK)发送,没有必要拆成两次。因此,四次握手虽然也能工作,但效率不如三次。
总结来说,三次握手是保证双方建立可靠连接所需的最少步骤。
IP
- IPv4地址耗尽: 32位的IPv4地址空间(约43亿个)已经完全耗尽,这是向IPv6迁移的根本动力。
- IPv6的优势: 128位的IPv6提供了近乎无限的地址空间,彻底解决了地址短缺问题。同时,它简化了报文头部,提高了路由效率,并且原生支持IPSec,安全性更好。
- NAT的角色: 了解IPv4时代广泛使用的NAT(网络地址转换)技术,它虽然缓解了地址不足,但也破坏了IP的端到端通信模型,给一些P2P应用带来了麻烦。
QUIC
在应用层重新实现了一个更智能、更适合现代网络的“TCP”,它之所以能做到这一点,主要依靠三大核心武器:流(Stream)、0-RTT/1-RTT握手、以及连接迁移。
基于“流”的多路复用,彻底消灭队头阻塞
TCP的问题(传输层队头阻塞):
想象一下TCP连接是一条单车道的高速公路。所有的数据包(HTTP请求1、请求2、图片数据等)都必须在这条单车道上排队行驶。如果其中一辆车(一个数据包)在路上抛锚了(网络丢包),后面的所有车都得堵着,等待这辆车被拖走(TCP重传机制)。这就是传输层的队头阻塞(Head-of-Line Blocking)。即使HTTP/2在应用层实现了多路复用,但它仍然跑在TCP上,这条高速公路还是单车道,堵车问题依旧存在。QUIC的解决方案(在连接内部开辟多条车道):
QUIC依然是建立一个连接,但它在这条连接内部,开辟了多条逻辑上独立的“流”(Stream)。每个HTTP请求-响应对(比如一个HTML请求,一个CSS请求)都跑在自己专属的流里。现在,如果承载CSS的那个流里的一个数据包丢失了,它只会阻塞CSS流自己。承载HTML和图片的流完全不受影响,它们的数据包会绕过这个“事故点”,继续被处理和交付
flowchart TD |
0-RTT/1-RTT快速握手,抢占先机
TCP+TLS的问题:握手延迟高
- 一个普通的HTTPS连接建立需要:
- TCP三次握手(耗时1个RTT)
- TLS握手(通常是1-2个RTT)
- 加起来总共需要2-3个RTT才能开始发送第一个HTTP请求数据,这在移动网络环境下延迟非常明显。
- 一个普通的HTTPS连接建立需要:
QUIC的解决方案:集成的加密和传输握手
QUIC把“我是谁”(加密认证)和“怎么通信”(传输控制)这两件事合并在一起做了。首次连接 (1-RTT): 客户端和服务器在第一个RTT内就能完成所有必要的加密和传输参数交换,之后立刻可以开始发送应用数据。
恢复连接 (0-RTT - 这是真正的黑魔法): 如果客户端之前访问过这个服务器,服务器会给它一个“凭证”。下次再访问时,客户端可以在第一个发送的数据包里就带上这个凭证和加密后的HTTP请求。服务器验证凭证后,无需任何等待,直接处理请求并返回数据。实现了真正的“零延迟”连接建立。
sequenceDiagram |
- 连接迁移,无缝切换网络
- TCP的问题:连接与IP地址强绑定
TCP连接由一个四元组定义:(源IP, 源端口, 目标IP, 目标端口)。当你的手机从Wi-Fi切换到4G网络时,你的源IP地址变了,这个四元组就失效了,TCP连接会立刻中断。所有正在进行中的下载、上传、网页加载都必须重新开始。 - QUIC的解决方案:基于连接ID(Connection ID)
QUIC连接不依赖于IP地址,而是由一个64位的连接ID来唯一标识。只要这个ID不变,无论你的手机IP地址和端口怎么变,QUIC连接都能继续保持。它会无缝地将数据包从旧地址迁移到新地址上,应用层完全无感。 - 生活中的比喻:
- TCP连接就像你家的座机电话,它绑定在你家的物理地址上,你一出门,电话就断了。
- QUIC连接就像你的手机,它绑定在你的手机号(连接ID)上,无论你是在家、在公司还是在地铁上,电话都能一直保持通话。
- TCP的问题:连接与IP地址强绑定
Http的安全问题
《图解HTTP》和《权威指南》都明确指出了HTTP协议在设计之初,并未过多考虑安全问题,其核心缺陷主要有三点:
- 通信使用明文,内容可能被窃听: HTTP报文在网络中以纯文本的形式传输,任何在传输路径上的节点(如路由器、网络运营商、黑客)都可以轻易地截获并查看通信内容,包括用户名、密码、信用卡号等敏感信息。
- 不验证通信方身份,可能遭遇伪装: HTTP协议本身无法验证客户端和服务器的真实身份。你以为你在访问银行网站,但实际上可能连接到了一个精心伪装的“钓鱼”网站。反之,服务器也无法确定请求是否真的来自一个合法的用户。
- 无法证明报文的完整性,可能遭篡改: HTTP协议没有提供校验机制来确保报文在传输过程中未被修改。攻击者可以在中途拦截报文,篡改内容(比如在网页中植入恶意广告或脚本),而接收方对此一无所知。这种攻击被称为中间人攻击(Man-in-the-Middle, MITM)。
sequenceDiagram |
生活案例
使用HTTP通信就像是在公共场合用明信片**写信和收信。
- 明文传输/窃听: 任何人(邮递员、邻居)在传递过程中都可以看到明信片上写了什么。
- 不验证身份/伪装: 你收到一张署名“你妈妈”的明信片,但你没法确定这真是你妈妈写的,可能是骗子冒充的。
- 不保完整/篡改: 有人在你的明信片上用涂改液改了几个字,你收到后也无法发现。
真实案例
- 最典型的案例就是公共Wi-Fi陷阱。当你在咖啡馆连接一个不安全的免费Wi-Fi时,这个Wi-Fi的提供者(可能是个黑客)就处在了你的通信链路上,成为了一个“中间人”。
- 如果你此时访问一个HTTP的网站并输入密码,黑客可以直接看到你的密码。
- 黑客还可以将你访问的HTTP网站重定向到一个他制作的假冒网站,骗取你的信任和信息。
- 他甚至可以在你正常浏览的HTTP网页中,动态注入恶意JavaScript脚本,来窃取你的其他信息。
- 而如果网站使用了HTTPS,上述所有攻击都将因为无法解密通信内容和伪造证书而失效。
经典问题
为什么说HTTP是“不安全”的?请从协议层面具体说明其存在的主要风险。
HTTP协议在设计之初主要目标是高效地传输超文本,因此并未内置安全机制,这使其在今天的互联网环境中存在三大核心风险,通常被称为“不安全”:
- 明文传输导致窃听风险: HTTP报文在网络上是以纯文本形式传输的,没有进行任何加密。这意味着在请求从客户端到服务器的整个传输路径上,任何一个中间节点(如路由器、恶意网关、网络嗅探工具)都能轻易地截获并直接读取通信的全部内容,包括像密码、支付信息这样的敏感数据。
- 缺少身份验证导致伪装风险: HTTP协议本身无法验证通信双方的身份。客户端无法确认它正在对话的服务器就是它声称的那个服务器,这为“钓鱼网站”等中间人攻击提供了可能。同样,服务器也无法确认请求方的真实身份,这使得它容易受到恶意请求的攻击。
- 缺乏完整性校验导致篡改风险: HTTP没有机制来保证报文在传输过程中不被修改。攻击者可以在中间环节拦截报文,任意篡改其内容(比如植入广告、病毒或恶意代码),然后再转发给接收方。由于没有校验机制,接收方无法察觉到报文已经被篡改,从而可能造成严重后果。
这三个根本性的缺陷——窃听、伪装、篡改——使得HTTP在处理任何涉及隐私、交易或需要信任的场景时都是完全不可靠的。而HTTPS正是为了解决这三大问题而诞生的。
HTTPS
HTTPS不是一个新协议。 它的全称是“HTTP over SSL/TLS”。它并不是在HTTP基础上修改,而是在HTTP和TCP之间增加了一个安全层(SSL/TLS)。
这个安全层提供了三大核心能力,正好对应了HTTP的三大缺陷:
- 内容加密: 通过混合加密算法,将HTTP报文加密成密文传输,解决了窃听问题。
- 身份认证: 通过数字证书(CA体系),验证服务器的身份,解决了伪装问题。
- 数据完整性: 通过报文摘要(MAC),确保数据在传输过程中未被篡改,解决了篡改问题。
SSL已死,TLS当立: 严格来说,SSL(Secure Sockets Layer)的所有版本都已因安全漏洞被废弃。现在我们使用的协议是其继任者TLS(Transport Layer Security)。目前业界主流是 TLS 1.2,而 TLS 1.3 因为其更高的性能和更强的安全性,正在迅速普及。面试时,能清晰说出TLS 1.2和1.3握手过程的区别,是巨大的加分项。
握手的核心目标: 不要陷入繁杂的步骤细节。TLS握手的核心目标只有两个:
- 安全地协商出会话密钥: 客户端和服务器需要商定一个对称加密的密钥,用于后续的HTTP报文加密。这个协商过程本身必须是安全的,不能被中间人窃听和篡改。
- 验证服务器身份: 客户端必须确认它正在与之通信的服务器是它声称的那个,而不是一个冒牌货。
混合加密: 为什么不直接用非对称加密传数据?因为非对称加密非常慢,只适合加密少量数据。而对称加密快得多,适合加密大量数据。所以TLS采用了混合加密的策略:用非对称加密来安全地交换对称加密的密钥,然后用这个对称密钥来加密真正的HTTP报文。
sequenceDiagram |
生活案例
HTTPS握手就像一次高度机密的线下交易。
- ClientHello: 你(客户端)对卖家(服务器)说:“我们来交易吧!我懂得用A、B、C三种暗号(加密套件)。”
- ServerHello & Certificate: 卖家说:“好的,我们就用A暗号吧。这是我的身份证和营业执照(数字证书),由工商局(CA)颁发,你可以去验证真伪。”
- 客户端验证证书 & ClientKeyExchange: 你打电话给工商局验证了执照是真的。然后你把交易的**接头暗语(预主密钥)写在一张纸条上,放进一个只有卖家用他私钥才能打开的保险箱(证书公钥加密)**里,交给了卖家。
- 双方生成会话密钥 & Finished: 卖家用私钥打开保险箱,拿到接头暗语。现在,你们双方都用这个暗语推算出了今天交易用的最终密码本(会话密钥)。为了确认无误,你们各自把之前的对话内容用这个密码本加密算了个摘要,发给对方验证。
- 加密通信: 验证通过,交易正式开始。你们所有的对话(HTTP报文)都用这个最终密码本加密。
经典问题
请简述一下HTTPS的握手过程。TLS 1.3相比于1.2做了哪些核心优化?
HTTPS的核心是其底层的TLS握手过程。我以目前最主流的TLS 1.2为例,其握手过程可以概括为以下几个核心步骤,主要目的是安全地协商会话密钥并验证服务器身份。
TLS 1.2 握手流程 (简化版):
- 客户端问候 (ClientHello): 客户端向服务器发送它支持的TLS版本、一组加密套件(包含密钥交换算法、对称加密算法、MAC算法)和一个随机数。
- 服务器响应 (ServerHello & Certificate): 服务器从客户端的加密套件中选择一个双方都支持的,并返回给客户端。同时,服务器会将其数字证书和另一个随机数一并发送给客户端。
- 客户端验证与密钥交换:
- 客户端首先验证服务器证书的有效性,包括检查签发机构是否受信任、是否过期、域名是否匹配等。
- 验证通过后,客户端生成一个**“预主密钥”(Pre-Master Secret),然后用证书中的公钥**对其进行加密,并通过ClientKeyExchange消息发送给服务器。
- 生成会话密钥与结束:
- 服务器用自己的私钥解密ClientKeyExchange消息,获取到预主密钥。
- 现在,客户端和服务器都拥有了三个相同的输入:客户端随机数、服务器随机数、预主密钥。它们各自使用相同的算法,将这三个输入混合生成最终的会话密钥(对称密钥)。
- 最后,双方互发一个Finished消息,将之前所有握手报文的摘要用这个刚生成的会话密钥加密后发送给对方,以验证握手过程没有被篡改。
TLS 1.3 的核心优化:
TLS 1.3 对这个过程进行了大刀阔斧的简化,主要目标就是提升性能和安全性:
- 减少RTT: TLS 1.3的握手过程从2个RTT(往返时延)缩减到了1个RTT。它在ClientHello时就会猜测服务器可能支持的加密参数并发过去,服务器在ServerHello中一次性返回所有需要的信息,大大缩短了连接建立时间。
- 更安全的加密套件: 废除了所有不安全的、过时的加密算法(如RC4, MD5)和密钥交换方式(如静态RSA),只保留了少数几个安全性极高的算法。
- 0-RTT模式: 对于恢复连接,TLS 1.3引入了0-RTT模式,允许客户端在第一个包中就带上加密的应用数据,实现了零延迟的连接恢复,这对于API调用和移动端应用是巨大的性能提升。
Web攻击
flowchart TD |
跨站脚本攻击(XSS)
核心思想: XSS的本质是“恶意代码注入”。攻击者设法将恶意的HTML或JavaScript代码,注入到一个受信任的网站中。当其他用户访问这个网站时,这些恶意代码就会在用户的浏览器中被执行,从而达到窃取信息、破坏页面的目的。
攻击对象: XSS攻击的目标是用户(的浏览器),而不是服务器本身。它利用的是网站对用户的信任。
分类:
- 存储型XSS (Stored XSS): 最危险的一种。恶意代码被存储在服务器的数据库中(比如一篇包含