HTTP 深度解析

一、为什么还需要一篇 HTTP 文章?

前面三篇文章我们已经构建了完整的网络知识体系:从物理层到应用层的分层模型,从 TCP 可靠传输到 IP 路由寻址,再到 HTTPS 的加密握手。但对于 HTTP 协议本身,还有一大块"日常开发高频使用、面试中高频出现"的内容没有展开。

比如:你每天都在用 URL,但你真的理解它每一部分的含义吗?你知道为什么 URL 中有些字符必须转义吗?你知道 GET 和 POST 的本质区别不是"参数放哪"而是"幂等性"吗?你知道 Cookie 到底是怎么让无状态的 HTTP 变成"有状态"的吗?

这些就是本文要讲的内容。


二、URL:定位互联网资源的六把钥匙

2.1 一个 URL 到底由什么组成?

我们先看一个完整的 URL:

复制代码
https://user:pass@v.bitedu.vip:8080/personInf/student?userId=10000&classId=100#section

把它拆成六个部分:

复制代码
协议方案 | 用户信息 | 主机名          | 端口 | 路径                  | 查询字符串                        | 片段标识
https:// | user:pass | v.bitedu.vip | :8080 | /personInf/student | ?userId=10000&classId=100 | #section

协议方案 告诉客户端"用什么协议通信"。最常见的当然是 httphttps,但 URL 的协议方案不局限于此------ftp:// 是文件传输,mailto: 是发邮件,jdbc:mysql:// 是数据库连接字符串。协议方案后面必须跟双斜杠 //,这是早期 URL 规范定义的,现在已经成了标准。

用户信息 的格式是 用户名:密码@。这个字段在早期互联网中用于认证,但现在已经几乎没人用了。原因很直接:密码写在 URL 里太危险了。URL 会被记录在浏览器历史、服务器日志、代理服务器日志里,明文密码随时可能泄露。现代网站都把认证信息放到 Header(如 Authorization)或 Cookie 里,URL 里的用户信息字段基本废了。

主机名 就是域名,通过 DNS 解析成 IP 地址。你可以用 ping 命令验证:ping v.bitedu.vip 会返回真实 IP(比如 118.24.113.28)。URL 中也可以直接写 IP 地址(比如 http://118.24.113.28/),只是不好记而已。

端口号 指定 TCP 端口。如果省略,浏览器会根据协议自动选择默认值:http 用 80,https 用 443,ftp 用 21。这就是为什么你在浏览器输入 http://example.com 时不需要写 :80

路径 表示资源在服务器上的层次化位置。但要注意:路径不一定对应实际的物理文件。在 Spring MVC 中:

java 复制代码
@GetMapping("/personInf/student")
public Student getStudent(@RequestParam Long userId) { ... }

这个路径只是路由规则的一个键,背后可能对应数据库查询、业务逻辑计算等等。它和文件系统没有直接关系。

查询字符串 是问号后面的键值对,用 & 分隔多个键值对,用 = 分隔键和值。你可以把它理解为函数调用的参数列表:

javascript 复制代码
// 函数调用
getUserById(userId=10000, classId=100)

// 等价的 URL query string
?userId=10000&classId=100

键和值的命名完全由后端程序员自定义,没有标准约束。

片段标识# 后面的部分,主要用于页面内跳转。比如 https://cn.vuejs.org/v2/guide/#起步 会让浏览器滚动到 id 为 起步 的元素。

这里有一个非常重要的细节:片段标识不会发送给服务器。当你访问上面的 URL 时,服务器收到的请求行是:

复制代码
GET /v2/guide/ HTTP/1.1

服务器完全不知道你是因为 #起步 才跳转过来的。这是前端用来做单页应用(SPA)路由的基础------Vue Router 的 hash 模式、React Router 的 hash 模式,都是利用这个特性实现的。

2.2 URL 中哪些部分可以省略?

部分 可省略吗 省略后的含义
协议名 可以 默认为 http://
用户信息 可以 几乎总是省略
端口号 可以 http→80, https→443
路径 可以 省略后相当于 /
查询字符串 可以 没有任何参数
片段标识 可以 页面内无跳转

HTML 标签中还有一套特殊的省略规则。当 srchref 属性只写路径时,会自动使用当前页面的域名和协议:

html 复制代码
<!-- 假设当前页面是 https://example.com/a/page.html -->
<img src="image.png">        <!-- 实际访问 https://example.com/a/image.png -->
<a href="/other/index.html"> <!-- 实际访问 https://example.com/other/index.html -->

前者是相对路径(相对于当前目录),后者是绝对路径(从域名根目录开始)。

2.3 URL Encode:为什么特殊字符必须转义?

URL 中有许多字符被赋予了特殊含义:/ 是路径分隔符,? 是查询字符串开始,& 是键值对分隔符,= 是键值分隔符,# 是片段标识开始,+ 在某些场景下表示空格,% 是转义符本身。

如果一个参数值中包含这些字符怎么办?比如你想搜索 a+b=c,直接拼接到 URL 中会变成 ?q=a+b=c。服务器收到后无法判断这是一个参数 q="a+b=c" 还是两个参数 q="a" + b="c"

所以需要对特殊字符进行转义编码。规则是:将字符转为十六进制,前面加 %,格式为 %XY

原始字符 编码后
+ %2B
= %3D
空格 %20+
中文"测试"(UTF-8) %E6%B5%8B%E8%AF%95

浏览器会自动处理这个问题。如果你在地址栏输入 ?q=a+b=c,浏览器实际发送的是 ?q=a%2Bb%3Dc。服务器收到后会自动做 URL Decode 还原成原始字符。

常见误区:"GET 只能传文本,POST 能传二进制"

这个说法不准确。GET 的 query string 虽然不能直接放二进制数据,但可以先 Base64 编码再 URL Encode,就能传任意数据了。只是不方便阅读、长度受限罢了。


三、HTTP Method:不只是 GET 和 POST

3.1 GET:获取资源的默认操作

GET 是最常用的 HTTP 方法,几乎所有浏览器地址栏的请求都是 GET。它的特点是:

  • 首行第一部分是 GET
  • query string 可以为空或不为空
  • header 部分有若干键值对
  • body 部分为空

触发 GET 请求的场景包括:浏览器地址栏输入 URL、HTML 中的 link/img/script/a 标签加载资源、JavaScript 的 fetch()/axios.get() 等。

关于 GET 的 URL 长度问题

网上有些资料说"GET 请求最多 1024KB",这是错误的。HTTP/1.1 标准(RFC 2616)原文明确指出:

"Hypertext Transfer Protocol -- HTTP/1.1" does not specify any requirement for URL length.

标准中没有规定 URL 的长度限制!实际限制来自浏览器和服务器的实现:Chrome 约 2MB,Firefox 约 65KB~2MB,Nginx 默认约 8KB(可配置),Apache 默认约 8KB。

这不是协议的限制,而是软件实现的限制。就像 TCP 本身不限制数据包大小,但以太网 MTU 限制为 1500 字节一样。

3.2 POST:提交数据的标准方式

POST 的特点是:

  • 首行第一部分是 POST
  • query string 一般为空(也可以不为空)
  • header 部分有若干键值对
  • body 部分一般不为空,数据格式由 Content-Type 指定

POST 多用于提交用户输入的数据给服务器,比如登录页面、下单页面、文件上传等。

3.3 GET 和 POST 的本质区别

这是面试中被问到烂的问题,但绝大多数人只答了一半。完整的回答应该从三个层面展开:

语义层面:GET 的语义是"获取资源",它是幂等且安全的------多次执行效果相同,不修改服务器状态。POST 的语义是"提交数据",它是非幂等的------每次执行可能产生不同的副作用。

实现层面

特性 GET POST
参数位置 URL 的 query string 请求体(Body)
长度限制 受浏览器/服务器实现限制 理论上无限制
浏览器缓存 可以被缓存 不会被缓存
浏览器书签 可以收藏 不可以收藏
浏览器回退 不会重新提交 会提示"确认重新提交"

常见误解纠正

有些资料说"POST 比 GET 安全",这是不科学的。是否安全取决于前端在传输密码等敏感信息时是否进行加密,和 GET/POST 无关。HTTPS 下 GET 参数同样是加密传输的。

有些资料说"GET 传输数据量小,POST 传输数据量大",这也是不科学的。标准没有规定 GET 的 URL 长度,也没有规定 POST 的 body 长度。传输数据量完全取决于浏览器和服务器的实现。

有些资料说"GET 只能传文本,POST 能传二进制",同样不准确。GET 的 query string 虽然无法直接传输二进制,但可以对二进制数据进行 URL Encode 后传输。

3.4 其他 Method 的详细语义

Method 语义 幂等性 典型用途
PUT 整体替换资源 更新整个用户资料
DELETE 删除指定资源 删除评论
HEAD 同 GET 但不返回 body 检查资源是否存在
OPTIONS 返回服务器支持的方法 CORS 预检请求

幂等性的定义:多次执行相同请求,结果与一次执行相同。

为什么 PUT 和 DELETE 是幂等的?PUT /users/123 {name:"张三"} 第一次把用户改成"张三",第二次还是同样的结果。DELETE /users/123 第一次删除了用户,第二次再删会返回 404,但不会"多删"一次。

为什么 POST 通常不是幂等的?POST /orders 创建订单,每次执行都会产生一个新的订单记录。两次执行就是两条记录。

RESTful API 的设计规范 就是基于这个原则:GET /users/123 获取,PUT /users/123 整体替换,PATCH /users/123 部分更新,DELETE /users/123 删除。Method 的语义决定了 API 的行为。


四、Header:键值对构成的元数据仓库

4.1 Host:服务器的地址和端口

即使你把 IP 写在 URL 里,HTTP 请求的 header 中仍然会有 Host 字段:

复制代码
GET /index.html HTTP/1.1
Host: www.example.com:80

为什么需要 Host?因为一个 IP 地址可能对应多个网站(虚拟主机)。比如腾讯云的一台服务器可能同时托管着 site-a.comsite-b.comsite-c.com。如果没有 Host header,服务器收到请求后不知道要把数据返回给哪个站点。HTTP/1.1 标准强制要求必须有 Host header

4.2 Content-Length:Body 的确切长度

复制代码
Content-Length: 105

表示 Body 部分的字节数是 105。为什么需要它?因为 TCP 是面向字节流的,不像 UDP 有固定的报文边界。如果没有 Content-Length,接收方可能永远不知道 Body 何时结束。

这和前面讲过的 TCP 粘包问题是同一个根源:TCP 不保留应用层的消息边界,所以必须在应用层自己定义"消息结束"的标记。Content-Length 就是 HTTP 定义的标记方式之一。

4.3 Content-Type:Body 的数据格式

这是最关键的 header 之一,决定了服务器如何解析 Body。HTTP 协议定义了三种主流的 Body 格式:

application/x-www-form-urlencoded

传统的表单提交格式:

复制代码
title=test&content=hello&tags=java,backend

就像 URL 的 query string 一样,是 key=value&key2=value2 的形式。适合简单的表单提交,但无法上传文件。

multipart/form-data

用于文件上传,格式复杂但功能强大:

复制代码
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3Trw

------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"

title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png

PNG 二进制数据...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--

boundary 定义了各个部分的边界,每部分有自己的 metadata(文件名、类型等)。这种格式可以传任意类型的数据(二进制、文本混合),适合文件上传。

application/json

现代 API 最常用的格式:

json 复制代码
{"username":"zhangsan","password":"123456"}

服务器通过 Content-Type: application/json 知道要用 JSON 解析器来解析 Body。JSON 结构清晰、嵌套方便、支持复杂数据类型(数组、对象、布尔值等),而且 JavaScript 原生支持(JSON.parse()JSON.stringify()),相比 XML 更简洁、占用更少带宽。

如何选择? 简单表单提交用 application/x-www-form-urlencoded,文件上传用 multipart/form-data,现代 API 通信用 application/json

4.4 User-Agent:浏览器的自我陈述

复制代码
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36

这个字符串包含了操作系统(Windows 10)、浏览器引擎(WebKit/Chrome)、浏览器版本(Chrome 91.0.4472.77)等信息。

为什么这么长?这是历史遗留问题。早期 Netscape 浏览器引入 UA 时只是简单声明"Mozilla N.0",后来为了兼容性各大厂商都延续了这种方式,把自己的信息塞进去。

User-Agent 的实际用途包括:服务器根据 UA 返回不同版本的页面(移动版/桌面版)、爬虫检测(很多网站会拦截非正常 UA 的请求)、统计分析(各浏览器市场份额)。很多爬虫会用最新的 Chrome UA 伪装身份,但实际上它们的代码根本没有实现渲染能力。

4.5 Referer:来源追踪

复制代码
Referer: https://gitee.com/login

表示"我是从哪个页面跳转到当前页面的"。注意:它是 Referer 而不是 Referrer,这是个拼写错误但沿用至今(HTTP/1.0 规范 RFC 1945 中的拼写错误,HTTP/1.1 决定保留以确保兼容性)。

如果直接从地址栏输入 URL、从书签或邮件中的链接点击,就没有 Referer。Referer 的用途包括:防盗链(检查请求是否来自合法站点)、流量分析(看用户从哪来)、安全(防止 CSRF 攻击的一部分手段)。

4.6 Cookie:会话身份的载体

复制代码
Cookie: username=zhangsan; gitee-session-n=M1Rhbk1QUUxQdWk1VEZVQ1BvZXYybG13ZUJFNGR1V0pSYTZyTllE

Cookie 是"键值对的集合",不同的键值对之间用 ; 分隔。每个域名下的 Cookie 是隔离的:gitee.com 的 Cookie 不会发给 baidu.comlogin.gitee.com 的 Cookie 也不会发给 www.gitee.com

Cookie 可以通过 ExpiresMax-Age 设置过期时间,也可以在响应中设为 session cookie(浏览器关闭就删除)。


五、登录流程:Cookie/Token 如何实现"有状态的会话管理"

5.1 从抓包看登录全过程

以 Gitee 为例,完整观察一遍登录:

第 1 步:清除 Cookie

打开开发者工具 → Network → Cookies,手动删除所有 gitee.com 相关的 Cookie。

第 2 步:发起登录请求

复制代码
POST https://gitee.com/login HTTP/1.1
Host: gitee.com
Content-Type: application/x-www-form-urlencoded

encrypt_key=password&utf8=%E2%9C%93&authenticity_token=36ZqO9tglSN6EB6pF6f2Gt%2B...

注意这里传的是密码,但没有看到任何 session token------此时我们确实还没有身份标识。

第 3 步:服务器返回 302 重定向 + Set-Cookie

复制代码
HTTP/1.1 302 Found
Location: https://gitee.com/HGtz2222
Set-Cookie: oschina_new_user=false; path=/
Set-Cookie: gitee_user=true; path=/
Set-Cookie: gitee-session-n=M1Rhbk1QUUxQdWk1VEZVQ1BvZXYybG13ZUJFNGR1V0pSYTZyTllE; path=/

这里有三个 Set-Cookie,最关键的是第三个 gitee-session-n。它的值是一串加密的长字符串,这就是会话令牌(Session Token)

第 4 步:后续请求自动携带 Cookie

现在访问个人主页:

复制代码
GET https://gitee.com/HGtz2222 HTTP/1.1
Cookie: oschina_new_user=false; user_locale=zh-CN; yp_riddler_id=1ce4a551-a160-4; gitee-session-n=M1Rhbk1QUUxQdWk1VEZVQ1BvZXYybG13ZUJFNGR1V0pSYTZyTllE

看到了吗?浏览器自动把上一步服务器设置的 Cookie 带上去了。这就是"会话保持"的秘密。

第 5 步:服务器验证 Cookie

服务器收到请求后,从 Cookie 中提取 gitee-session-n,用它去 Session 存储(Redis、内存等)查找对应的用户信息。找到就放行,找不到就返回 401/302 跳到登录页。

医院看病的比喻

  1. 挂号:提供身份证 → 拿到就诊卡(Session Token)
  2. 看病:每次出示就诊卡 → 医生识别身份 → 开药
  3. 结束:注销就诊卡 → Session 失效
  4. 再来看病:办一张新的就诊卡 → 得到新的 Token

前面讲的是 Cookie-based Session 模式。还有一种流行方式是 Token-based,以 JWT 为例:

复制代码
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

两者对比:

特性 Cookie-based Session Token (JWT)
状态存储 服务器端(Session Store) 客户端(JWT Self-contained)
携带方式 浏览器自动携带 前端手动放入 Header
跨域 受同源策略限制 不受限
CSRF 防护 需要额外机制 天然免疫
XSS 风险 HttpOnly 可防 localStorage 易被窃取
扩展性 需共享 Session(Redis) 无状态,天然水平扩展
主动失效 服务端删除即失效 过期前无法强制失效

Token 的工作方式:服务器生成一张带签名的卡片(JWT)给你,每次出示这张卡片时服务器验证签名即可,无需查数据库。卡片过期自然失效。

实际项目中的混合用法:很多系统会结合两种方式------用 Cookie 存一个 refresh token(HttpOnly 防 XSS),用 Token 存 access token(短有效期),Token 过期时用 Refresh Token 换取新 Token。这样既安全又灵活。


六、深入思考题

Q1:如果 GET 不允许有 Body,为什么实际中很多人会往 GET 请求的 Body 里放数据?

RFC 规范说 GET 不应该有 Body,但并没有禁止。实际中 axios、Postman 都允许 GET 有 Body。但这会导致问题:不符合规范可能被某些服务器拒绝,缓存行为不一致(有些缓存系统忽略 Body),语义混乱(GET 应该只是获取,不应该有 payload)。

最佳实践:严格遵守规范,GET 不放 Body,参数全部走 query string。

Q2:302 重定向和 301 有什么区别?307/308 呢?

状态码 名称 浏览器行为
301 Moved Permanently 永久重定向,浏览器会缓存,下次直接访问新地址
302 Found 临时重定向,浏览器不会缓存
307 Temporary Redirect 同 302,但严格要求保持原方法和 Body
308 Permanent Redirect 同 301,但严格要求保持原方法和 Body

为什么需要 307/308?因为历史上很多浏览器对 301/302 的处理不规范:当你用 POST 请求一个返回 302 的 URL 时,浏览器可能会把后续请求变成 GET(丢失 Body)。307/308 明确要求浏览器必须保持原方法和 Body。

Q3:为什么 referer 拼写成"referer"而不是"referrer"?

这是 HTTP/1.0 规范(RFC 1945)中的拼写错误。到了 HTTP/1.1(RFC 2068),标准决定保留这个错误以确保兼容性。所以正确的拼写应该是"referrer"(源指涉者),但 Header 字段名永远是 Referer


七、经典面试题补充

1. URL 中哪些部分是可选的?省略后会默认什么值?

协议名(默认为 http://)、用户信息(省略)、端口号(http→80, https→443)、路径(→/)、查询字符串(→无参数)、片段标识(→无跳转)。HTML 标签中还可以省略域名,自动使用当前页面域名。

2. application/x-www-form-urlencoded 和 multipart/form-data 的区别?

前者是简单的 key=value&key2=value2 格式,只能传文本;后者用 boundary 分隔,支持任意类型数据(如文件上传),但格式复杂。

3. Cookie 和 Token 的核心区别?

Cookie 存储在服务器端(Session),浏览器仅持有 ID;Token(如 JWT)包含完整用户信息,存在客户端。Cookie 有 CSRF 风险但可通过 HttpOnly 缓解,Token 无状态易于扩展但难以主动失效。

4. PUT 和 POST 的区别?

PUT 是幂等的,用于整体替换资源;POST 是非幂等的,用于创建新资源。PUT 要求客户端知道资源标识,POST 则由服务器分配 ID。

5. Content-Type 有哪些常见值?分别对应什么格式?

application/x-www-form-urlencoded(传统表单)、multipart/form-data(文件上传)、application/json(现代 API)、text/html(HTML 页面)、image/png(图片)等。


八、总结

本文深入讲解了 HTTP 协议的工程细节:URL 的六个组成部分、可省略规则、URL Encode 的必要性;HTTP Method 的语义与幂等性、GET/POST 的本质区别;Header 的实际用途、三种 Body 格式的适用场景;以及 Cookie/Session 如何实现"有状态的会话管理"。

这些内容补充了前三篇文章没有展开的应用层细节,构成了完整的 HTTP 知识体系。

相关推荐
tiantianuser1 小时前
NVME-oF IP 设计16 : 适于高速网络存储系统的IP设计2
网络协议·rdma·高速传输·roce v2·nvme of
数据知道1 小时前
网络安全实战:渗透测试全流程 SOP——从授权到报告交付
服务器·网络·安全·web安全·网络安全
吴声子夜歌1 小时前
网络安全——网络地址转换及其应用
网络·web安全·nat
猫咪宝妖1 小时前
【信息安全工程师】第一天考前准备
网络·安全·web安全
吴声子夜歌1 小时前
网络安全——网络安全防护技术(二)
网络·web安全
深圳恒讯2 小时前
乌克兰服务器的网络延迟、机房现状与稳定性情况
运维·服务器·网络
dogstarhuang2 小时前
实战:用 API 网关统一接入 GPT-5.6,多模型路由怎么省下 80% 成本
网络·gpt·大模型·api网关·ai推理·模型选型·按量计费
星夜夏空992 小时前
网络编程(5)—— Reactor实现(v1)
服务器·javascript·网络
筵陌2 小时前
Linux网络NAT、代理服务、内网穿透
linux·网络