Skip to content

第 4 讲|HTTP 和 Web

浏览器找到服务器之后,还需要一套双方都能理解的"通信格式"——这就是 HTTP(HyperText Transfer Protocol)

理解 HTTP 在整个通信栈中的位置:

作用
HTTP应用层协议,规定"请求什么、如何回应"
TLS在 TCP 之上加密(HTTPS 时存在)
TCP提供可靠的双向字节流
IP在网络中路由数据包

HTTP 是文本协议,人类直接能读。这一特性让 HTTP 比二进制协议更易调试——用 curl -v 或浏览器开发工具看到的内容,基本就是协议传输的原始文本。

本讲围绕 HTTP 的核心概念展开:请求与响应的结构、HTTP 方法、Header、状态码、登录状态的实现机制,以及静态资源与动态接口的分工。

一、请求与响应的结构

HTTP 请求和响应是一来一回的文本约定

HTTP 请求

一个最简单的 GET 请求的完整文本:

http
GET /docs/index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html

结构:

含义
第 1 行请求行:方法 + 路径 + 协议版本
第 2 行起Header:键值对,每行一个
空行标记 Header 结束
空行之后Body(POST 等才有)

HTTP 响应

服务端的响应有相同的结构:

http
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 1024
Cache-Control: max-age=300

<html>...</html>
含义
第 1 行状态行:协议版本 + 状态码 + 状态文本
第 2 行起Header
空行标记 Header 结束
空行之后Body(响应体)

用 curl 看完整 HTTP 文本

bash
curl -v https://www.example.com

-v 参数让 curl 输出请求和响应的完整文本(包括所有 Header)。这是排查 HTTP 问题最直接的工具——比浏览器 Network 面板更原始、信息更完整,且可在服务器内部直接使用。

更精细的用法:

bash
curl -I https://www.example.com                          # 只看响应头(HEAD 请求)
curl -X POST -H "Content-Type: application/json" \
     -d '{"name":"test"}' https://api.example.com/users  # 自定义方法和 Body
curl -H "Cookie: session=abc" https://api.example.com    # 带 Cookie
curl --resolve www.example.com:443:1.2.3.4 \
     https://www.example.com                              # 强制 DNS 解析到指定 IP

二、HTTP 方法

HTTP 方法表明请求的语义意图——读、写、改、删:

方法语义典型场景是否幂等
GET读取资源打开页面、查询列表、获取详情
POST创建资源 / 提交数据登录、提交表单、新建任务
PUT整体替换资源更新一个对象的全部字段
PATCH局部更新资源只改某一个字段
DELETE删除资源删除任务、删除文件
HEAD只取响应头,不取响应体检查资源是否存在
OPTIONS查询服务端支持的方法CORS 预检请求

幂等性的运维意义

幂等指同一个请求执行多次的效果与执行一次相同。这一性质对运维和应用设计都很关键:

  • 幂等方法(GET / PUT / DELETE)在网络异常时可以安全重试
  • 非幂等方法(POST)重试可能造成重复扣款、重复提交等副作用

实际项目中需要保证 POST 的幂等性时,通常引入幂等键(idempotency key)——客户端在请求 Header 中带一个唯一标识,服务端用它去重。

方法与路径的关系是约定而非强制

GET /api/users 通常表示"列出用户",POST /api/users 通常表示"创建用户"——这是 REST 风格的约定,不是协议强制规则。

服务端的路由系统决定每个"方法 + 路径"组合对应的代码处理逻辑。仅看一个 URL 无法确定它的行为,必须参考接口文档或代码。运维侧排查时遇到陌生接口,直接查后端代码的路由配置(如 Spring 的 @RequestMapping、Flask 的 @app.route、FastAPI 的 @app.post),比猜测可靠。

三、HTTP Header

Header 是请求和响应的元数据,信息密度高,排查时经常需要细看。

常用 Header

Header出现位置作用
Host请求访问的域名,Nginx 按它分流到不同后端
Content-Type请求 / 响应Body 的格式(application/jsontext/htmlmultipart/form-data)
Content-Length请求 / 响应Body 的字节数
Authorization请求认证信息,通常是 Bearer <token>
Cookie请求浏览器存的站点数据,登录态常依赖它
Set-Cookie响应服务端要求浏览器存储的 Cookie
Cache-Control请求 / 响应缓存策略,客户端和 CDN 都参考它
User-Agent请求客户端类型(浏览器、爬虫、SDK 版本等)
Referer请求当前请求是从哪个页面跳过来的(注意拼写,历史遗留)
Location响应重定向目标 URL
X-Forwarded-For请求经过代理后保留的原始客户端 IP
X-Real-IP请求同上,Nginx 常用的另一种命名

Host 头的关键作用

同一个 IP 后面可能挂多个站点,服务器靠 Host 头区分:

同一个 IP 通过 Host 头分到不同站点

nginx
# Nginx 配置示例
server {
    server_name www.example.com;
    proxy_pass http://web-backend;
}

server {
    server_name api.example.com;
    proxy_pass http://api-backend;
}

www.example.comapi.example.com 解析到同一个 IP,但 Nginx 根据请求的 Host 头分发到不同后端。

缺失或错误的 Host 头会导致 Nginx 走 default_server 配置,通常返回错误页或 404。

X-Forwarded-For:获取真实客户端 IP

请求经过 Nginx、负载均衡、API 网关等多层代理后,后端直接 connect 的对端 IP 不是真实用户 IP,而是上一层代理的 IP

获取真实 IP 的标准做法:每一层代理在请求 Header 中追加 X-Forwarded-For,后端读取这个 Header。

Nginx 配置示例:

nginx
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

后端读取这个 Header(以 Python Flask 为例):

python
real_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
# X-Forwarded-For 可能有多个,逗号分隔:client, proxy1, proxy2
# 第一个才是真实客户端
if real_ip and ',' in real_ip:
    real_ip = real_ip.split(',')[0].strip()

X-Forwarded-For 是可伪造的——客户端可以在请求中直接带这个 Header。生产环境需要把"信任的代理 IP 列表"作为校验依据,只接受来自这些代理设置的 X-Forwarded-For,而不是盲目使用 Header 中的第一个 IP。

CORS 相关 Header

跨域请求(前端域名 ≠ 后端域名)由 CORS(Cross-Origin Resource Sharing)规范控制:

Header出现位置作用
Origin请求请求的源(协议+域名+端口)
Access-Control-Allow-Origin响应服务端允许哪些源访问
Access-Control-Allow-Methods响应允许的 HTTP 方法
Access-Control-Allow-Headers响应允许的自定义请求 Header

跨域问题在 第 5 讲 前端和后端 展开。

四、状态码

状态码按数字范围分类,记住范围比记具体码更重要:

HTTP 状态码给排查方向分层

范围类别含义
1xxInformational请求已收到,继续处理
2xxSuccess请求成功
3xxRedirection资源已移到其他位置或使用缓存
4xxClient Error客户端请求有问题
5xxServer Error服务端处理出错

常用状态码与排查方向

状态码含义排查方向
200成功无需排查
301 / 302永久 / 临时重定向重定向配置,如 HTTP → HTTPS
304Not Modified,客户端用了本地缓存不是错误
400Bad Request,请求格式错误前端发送的字段、JSON 格式问题
401Unauthorized,未登录或认证失败Token 过期、Cookie 缺失
403Forbidden,已认证但无权限角色不足、IP 黑名单、WAF 拦截
404Not Found,资源不存在路径错、路由未注册、文件已删除
405Method Not Allowed,方法不被允许用 GET 访问只支持 POST 的接口
408Request Timeout客户端发送请求过慢被服务端关闭
413Payload Too Large上传文件超过限制
429Too Many Requests,被限流触发了应用或网关的限流规则
500Internal Server Error后端代码异常,看应用日志
502Bad Gateway入口层连不上后端,看 Nginx error log
503Service Unavailable服务不可用、过载、维护模式
504Gateway Timeout入口层等后端超时

同一状态码的多种根因

同一个状态码可能对应不同的根因,仅看状态码无法确定问题,需要结合日志和上下文:

404 的可能原因:

  • URL 路径拼写错误
  • Nginx location 配置未匹配
  • 后端路由未注册(如 Flask 缺少对应的 @app.route)
  • 静态资源文件已被删除
  • K8s Ingress 规则未匹配

502 的可能原因(看 Nginx error log 通常能直接定位):

connect() failed (111: Connection refused) — 后端端口无服务监听
no live upstreams while connecting to upstream — 所有后端实例健康检查失败
upstream prematurely closed connection — 后端进程崩了
upstream timed out — 应该返回 504 但有些版本返回 502

500 与 502 的本质区别:

  • 500:后端收到了请求,处理时出错(代码 bug、SQL 错、外部调用失败)。排查方向:应用日志
  • 502:入口层根本没连上后端。排查方向:入口层日志 + 后端进程状态

混淆这两个状态码会让排查方向完全错位。

Cookie Session Token 给无状态 HTTP 补上身份

HTTP 是无状态协议

HTTP 协议本身不维护客户端状态——服务端处理完一个请求后,与下一个请求没有任何关联。但实际应用中,用户登录后希望刷新页面、关闭浏览器再打开都保持登录状态。这种持续的"状态"是通过 Cookie、Session、Token 等机制叠加在 HTTP 之上实现的。

特点:

  • 用户标识(session_id)存在浏览器 Cookie 中
  • 实际的会话数据存在服务端(内存、Redis、数据库)
  • 浏览器每次请求自动带 Cookie

痛点:

  • 服务端要存会话数据,多实例必须共享 Session 存储(否则用户请求落到不同实例时会掉登录)
  • 跨域、跨服务难以共享会话

方案 2:Token / JWT(前后端分离主流)

特点:

  • Token(通常是 JWT)由服务端签发,前端自己存(localStorage、内存、SessionStorage)
  • 每次请求在 Authorization: Bearer <token> Header 中携带
  • 服务端通过签名校验 token,不需要服务端存会话
  • 天然支持多实例、跨域、移动端

缺点:

  • Token 一旦签发,在过期前无法主动撤销(除非维护黑名单)
  • Token 内容可被解码看到(只是签名防篡改,不是加密)——敏感信息不能放 Token 里

JWT 的结构:base64(header).base64(payload).base64(signature),可以在 jwt.io 在线解码查看。

方案 3:Session + Token 混合

实际生产中很多系统采用混合方案:

  • 短生命周期 Access Token(15 分钟过期)
  • 长生命周期 Refresh Token(7 天,服务端有记录可撤销)
  • Access Token 过期时,前端用 Refresh Token 换新 Access Token

兼顾性能(每次请求不查服务端)与安全性(可主动撤销)。

六、静态资源与动态接口

一个网站对外提供的内容大致分两类:

类型内容部署位置
静态资源HTML、CSS、JS、图片、字体、视频Nginx、对象存储、CDN
动态接口用户数据、订单、任务、统计数字后端应用 + 数据库 + 缓存

传统 vs 前后端分离

  • 传统模式(PHP、JSP 时代):服务端直接渲染完整 HTML 返回。前后端代码混在一起,通常由后端框架(如 Django、Rails)处理
  • 前后端分离(现代主流):浏览器先拉取静态资源(HTML/CSS/JS),JS 执行后调用后端 API 获取 JSON 数据,前端动态渲染

前后端分离的优势:

  • 前端可以独立部署、独立构建,迭代速度快
  • 静态资源放 CDN,加载快、源站压力小
  • 后端只关注数据接口,前端可以是 Web、移动端、桌面应用共用一套 API

白屏问题的排查流程

页面白屏时按以下顺序排查:

bash
# 1. 打开浏览器 Network 面板,看 HTML 是否 200
#    - 不是 200 → 入口层(Nginx/CDN)问题
#    - 是 200 → 继续

# 2. 看 JS / CSS 文件是否 200
#    - 404 → 构建路径错误、发布路径错误、版本号不一致
#    - 是 200 → 继续

# 3. 看 API 请求的状态码
#    - 401 → 登录态丢失,看 Token 是否有效
#    - 403 → 权限问题
#    - 500 → 后端代码异常,看后端日志
#    - CORS 报错 → 跨域配置问题(Access-Control-Allow-Origin)
#    - 200 但页面还白屏 → 前端 JS 报错,看 Console 面板

按这个流程通常能在几分钟内定位到具体哪一层出问题——比起没头绪地猜效率高得多。