Linux网络学习【深入理解应用层HTTP协议、Cookie与Session机制及HTTPS协议原理】


🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统与网络知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》 《MySQL数据库学习》 《Python知识内容》 《cpolar知识学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

打开网页、登录账号、提交表单,这些日常操作背后,涉及三个紧密相关的问题:浏览器与服务器如何交换数据?为什么登录一次后,后续请求仍能识别用户?账号密码等敏感信息在传输过程中如何得到保护? 从Socket通信走向完整的网络应用,就需要进一步理解HTTP协议、Cookie与Session机制,以及HTTPS的安全原理.HTTP定义了请求与响应的交互规则,但协议本身是无状态的,不会自动记住前一次请求中的用户信息.在典型的会话管理方案中,浏览器通过Cookie携带会话标识,服务器根据该标识查找Session,从而关联多次请求、保持登录状态.不过,保持会话并不等于保障传输安全 .HTTPS则借助TLS,通过证书验证、密钥协商和加密通信,验证服务器身份,并保护传输数据的机密性与完整性.本文将以 "请求与响应 → 会话保持 → 安全传输" 为主线,首先梳理HTTP报文结构、请求方法与状态码,再结合登录场景分析Cookie的保存与发送、Session的创建与管理,最后深入HTTPS的证书验证、握手与加密原理.通过串联一次浏览器访问背后的关键环节,建立从应用层交互、用户状态管理到通信安全的连贯认识.

目录

一、HTTP协议

HTTP(HyperText Transfer Protocol,超文本传输协议)是一种应用层网络协议,用于客户端(如浏览器)与服务器之间传输数据.

简单来说:HTTP定义了客户端如何向服务器发送请求,以及服务器如何返回响应.

1.HTTP 的工作原理

HTTP 采用 请求---响应(Request--Response)模型.

例如,在浏览器输入 https://example.com:

  1. 浏览器与服务器建立连接.
  2. 浏览器发送 HTTP 请求.
  3. 服务器接收并处理请求.
  4. 服务器返回 HTTP 响应.
  5. 浏览器解析数据并显示网页.

2.HTTP 请求报文

一个 HTTP/1.1 请求通常包含请求行、请求头、空行和可选的请求体.

复制代码
POST /api/login HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "username": "admin",
  "password": "123456"
}
组成部分 作用
请求行 指定请求方法、路径和协议版本
请求头(Header) 提供请求的附加信息
空行 分隔请求头与请求体
请求体(Body) 携带需要发送的数据

这里的密码只是演示数据.实际登录应使用 HTTPS,并避免记录明文密码.

3.HTTP响应报文

服务器收到请求后返回响应:

复制代码
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 200,
  "message": "success"
}

响应主要包含:

  • 状态行:HTTP 版本、状态码、状态描述.
  • 响应头:数据类型、缓存策略等信息.
  • 响应体:HTML、JSON、图片等实际内容.

4.常见HTTP请求方法

方法 作用 常见场景
GET 获取资源 查询用户信息
POST 提交数据 登录、创建订单
PUT 创建或完整替换资源 更新用户资料
PATCH 部分修改资源 修改用户昵称
DELETE 删除资源 删除用户记录
HEAD 获取响应头 检查资源信息
OPTIONS 查询支持的方法或通信选项 CORS 预检请求

5.HTTP状态码

HTTP 状态码分为五类:

类型 含义 常见状态码
1xx 信息响应 100 Continue
2xx 请求成功 200 OK、201 Created
3xx 重定向 301、302、304
4xx 客户端错误 400、401、403、404
5xx 服务器错误 500、502、503

常见需要掌握的状态码:

  • 200:请求成功
  • 301:永久重定向
  • 400:请求格式或参数存在问题
  • 401:需要身份认证
  • 403:服务器拒绝访问
  • 404:资源不存在
  • 500:服务器内部错误
  • 502:网关收到无效响应
  • 503:服务暂时不可用

6.HTTP 的核心特点

  1. 无状态

HTTP 协议本身不会自动记住之前的请求.实际应用可以通过 Cookie、Session、Token 等机制维护登录状态.

  1. 客户端---服务器模式

客户端主动发起请求,服务器返回响应.

  1. 支持多种数据格式

HTTP 不仅可以传输 HTML,还支持 JSON、XML、图片、视频、文件等.

  1. 支持持久连接

HTTP/1.1 默认支持持久连接,可以通过一个 TCP 连接处理多个请求,减少重复建立连接的开销.

7.HTTP 与 HTTPS 的区别

对比 HTTP HTTPS
安全性 默认不加密 使用 TLS 加密
默认端口 80 443
数据保密性 无协议层加密保护 提供加密保护
身份认证 默认不提供 通过 TLS 证书等机制
典型用途 内部测试、特殊场景 网站、API、登录、支付

推荐:生产环境优先使用 HTTPS.

8.HTTP/1.1、HTTP/2、HTTP/3

版本 关键特点
HTTP/1.1 持久连接、文本报文格式
HTTP/2 二进制分帧、多路复用、头部压缩
HTTP/3 基于 QUIC(使用 UDP),改善连接建立与丢包场景下的表现

需要注意,三个版本在 HTTP 语义上保持高度一致,主要区别在于底层的数据传输机制和性能优化.


二、认识URL

URL(Uniform Resource Locator),中文叫作统一资源定位符,也就是我们常说的「网址」.

URL 的作用是:告诉浏览器要访问哪个资源,以及如何访问这个资源.

例如:

复制代码
https://www.example.com:443/products?id=100&page=2#details

1.URL 的基本结构

一个完整的 URL 通常由以下部分组成:

组成部分 示例 作用
协议(Scheme) https 指定访问方式
域名(Host) www.example.com 指定访问的主机
端口(Port) 443 指定服务器端口
路径(Path) /products 定位资源
查询参数(Query) ?id=100&page=2 传递附加参数
片段(Fragment) #details 定位资源内部的位置

2.逐个理解URL的组成

协议(Protocol)

例如:

复制代码
https://www.example.com

常见协议包括:

  • http://:使用 HTTP 协议
  • https://:通过 TLS 安全传输 HTTP
  • ftp://:用于文件传输

其中,https 是现代网站最常用的方式.

域名(Domain)

复制代码
www.example.com

域名用于标识服务器主机.

浏览器通常需要通过 DNS(域名系统)将域名解析为IP地址,才能建立网络连接.

端口(Port)

复制代码
https://www.example.com:443

端口用于区分服务器上的不同网络服务.

协议 默认端口
HTTP 80
HTTPS 443

使用默认端口时,一般可以省略.

路径(Path)

复制代码
https://www.example.com/user/profile

其中:

/user/profile 是资源路径.

它可以表示某个页面、文件,也可以对应后端 API 接口.

查询参数(Query)

例如:

复制代码
https://www.example.com/search?keyword=python&page=2

查询参数从 ? 开始,多个参数之间用 & 分隔.

  • keyword=python:搜索关键词为 python
  • page=2:请求第 2 页

查询参数常用于搜索、筛选、排序、分页等场景.

片段标识符(Fragment)

复制代码
https://www.example.com/docs#installation

#installation 表示资源内部的某个位置,例如网页中的「安装说明」章节.

需要注意:浏览器发起 HTTP 请求时,不会将 URL 的片段标识符发送给服务器.

3.URL与HTTP的关系

两者分工不同:

  • URL:告诉浏览器访问哪里.
  • HTTP:规定浏览器与服务器如何通信.

例如用户访问:

复制代码
https://www.example.com/users?id=10

浏览器可能发送以下 HTTP/1.1 请求:

复制代码
GET /users?id=10 HTTP/1.1
Host: www.example.com

服务器接收到请求后,处理 /users 路径和 id=10 参数,再返回响应.

4.动手练习:拆解 URL

5.URL、URI、URN 的区别

这几个概念经常出现在 HTTP 学习和面试中.

名称 含义
URI 统一资源标识符,用于标识资源
URL 统一资源定位符,描述资源的位置或访问方式
URN 统一资源名称,通过名称标识资源

URL 和 URN 都属于 URI 的不同形式.

总结:记住 URL 的六个主要部分------协议、域名、端口、路径、查询参数、片段.理解 URL 之后,再学习HTTP请求方法、请求头和API接口,就会更容易理解浏览器和服务器之间的数据交互.


三、urlencode和urldecode(了解)

1.什么是 URL Encode?

URL Encode(URL编码)是将URL中的特殊字符转换成符合URL规范的表示形式的过程.

因为URL中有些字符具有特殊含义,例如:

  • ?:表示查询参数开始
  • &:用于分隔多个查询参数
  • =:用于分隔参数名和参数值
  • #:表示片段标识符开始
  • 空格、中文等字符需要进行适当编码

URL Encode通常使用百分号编码(Percent-Encoding),格式为 % 加两个十六进制数字.

示例

原始内容:

复制代码
你好 世界

URL 编码后:

复制代码
%E4%BD%A0%E5%A5%BD%20%E4%B8%96%E7%95%8C

其中:

  • 你 → %E4%BD%A0
  • 好 → %E5%A5%BD
  • 空格 → %20
  • 世 → %E4%B8%96
  • 界 → %E7%95%8C

中文通常先转换为UTF-8字节,再对相应字节进行百分号编码.

2.什么是 URL Decode?

URL Decode(URL解码)是URL Encode的逆过程.

将编码后的字符恢复为原始内容.

例如:

复制代码
编码前:你好 世界

URL Encode:
%E4%BD%A0%E5%A5%BD%20%E4%B8%96%E7%95%8C

URL Decode:
你好 世界

3.常见字符编码对照

原始字符 URL Encode
空格 %20
中文「你」 %E4%BD%A0
& %26
= %3D
? %3F
# %23
/ %2F
% %25

注意:这些是字符被百分号编码后的形式,并不意味着 URL 中出现这些字符时都必须编码.例如,作为路径分隔符的 / 通常保留原样.

4.在线体验URL编码与解码

5.实际应用场景

例如,在浏览器中搜索:

复制代码
HTTP 协议

请求的URL可能是:

复制代码
https://example.com/search?keyword=HTTP%20%E5%8D%8F%E8%AE%AE

其中:

HTTP%20%E5%8D%8F%E8%AE%AE

解码后就是:

HTTP 协议

另外,在 HTML 表单常用的 application/x-www-form-urlencoded 编码格式中,空格通常会转换成 +,而不一定是 %20.

6.总结

对比 URL Encode URL Decode
中文名称 URL 编码 URL 解码
作用 将字符转换为 URL 安全的编码形式 将编码恢复为原始字符
例子 空格 → %20 %20 → 空格
使用场景 构造 URL、传递参数 解析 URL 参数

一句话记忆:

  • URL Encode:原始字符 → 编码后的字符
  • URL Decode:编码后的字符 → 原始字符

注意:URL Encode不是加密,任何人都可以解码,不能用它保护密码等敏感信息.

urlencode工具


四、HTTP协议请求与响应格式

HTTP请求

• ⾸⾏: ⽅法 + url + 版本

• Header:请求的属性,冒号分割的键值对;每组属性之间使⽤\r\n分隔;遇到空⾏表示Header部分结束.

• Body: 空⾏后⾯的内容都是Body. Body允许为空字符串.如果Body存在,则在Header中会有⼀个Content-Length属性来标识Body的⻓度.

编写HTTP请求的代码---验证http请求

bash 复制代码
需要现场基于历史代码,先架构处⼀个基本的HTTP服务器,然后⽤浏览器进⾏验证
需要验证前端内容,直接AI即可

HTTP响应

⾸⾏: 版本号 + 状态码 + 状态码解释

• Header: 请求的属性,冒号分割的键值对;每组属性之间使⽤\r\n分隔;遇到空⾏表⽰Header部分结束.

• Body: 空⾏后⾯的内容都是Body.Body允许为空字符串.如果Body存在,则在Header中会有⼀个Content-Length属性来标识Body的⻓度;如果服务器返回了⼀个html⻚⾯,那么html⻚⾯内容就是在body中.

基本的应答格式


五、HTTP常⻅⽅法


5.1GET⽅法

GET 方法是 HTTP 协议中最常用的请求方法之一,主要用于从服务器获取资源或查询数据.

简单理解:GET = 向服务器要数据,而不是向服务器提交数据进行修改.

例如:浏览网页、搜索商品、查看用户信息、查询天气等,通常都可以使用 GET 请求.

1.GET 方法的工作原理

例如,用户在浏览器中查询商品:

  1. 浏览器向服务器发送 GET 请求.
  2. 服务器接收到请求,查询商品信息.
  3. 服务器返回查询结果.
  4. 浏览器展示商品信息.

2.GET 请求的格式

假设访问:

复制代码
https://www.example.com/search?keyword=phone&page=1

浏览器可以发送以下 HTTP 请求:

复制代码
GET /search?keyword=phone&page=1 HTTP/1.1
Host: www.example.com
Accept: application/json

逐个分析:

内容 含义
GET 请求方法,获取资源
/search 请求路径
? 查询参数开始
keyword=phone 搜索关键词
& 分隔多个参数
page=1 查询第一页
HTTP/1.1 HTTP 协议版本
Host 目标服务器

这里的 keyword=phone&page=1 就是GET请求中常见的URL查询参数(Query Parameters).

3.服务器如何响应 GET 请求?

假设服务器查询成功,可能返回:

复制代码
HTTP/1.1 200 OK
Content-Type: application/json

{
  "code": 200,
  "data": {
    "name": "iPhone",
    "price": 5999
  }
}

其中:

  • 200 OK:请求成功.
  • Content-Type: application/json:返回的是 JSON 数据.
  • data:服务器返回的业务数据.

浏览器或者前端程序可以根据这些数据展示商品信息.

4.GET 方法的主要特点

  1. 主要用于获取数据

例如查询用户、查看文章、获取商品列表等.

  1. 请求参数通常放在 URL 中

    /search?name=zhangsan&age=20

注意:GET 并不是只能通过 URL 传递信息,它也可以携带请求头.GET请求体没有通用定义的语义,实际开发中通常不使用.

  1. 具有安全性(Safe)

HTTP 规范要求GET的语义是读取资源,不应被设计为执行删除、修改等操作.

这里的"安全"指的是不要求改变服务器资源状态,并不是说GET具有数据加密能力.

  1. 具有幂等性(Idempotent)

对同一资源重复执行相同的 GET 请求,不应产生额外的预期资源修改效果.

例如,连续查看同一篇文章,不应该因此删除或修改这篇文章.

  1. 支持缓存

在满足缓存条件时,浏览器或中间缓存可以缓存 GET 响应,减少重复请求.

5.动手练习:认识GET请求

分析下面这个 GET 请求:

复制代码
GET /users?id=100&page=2 HTTP/1.1
Host: api.example.com

想一想:请求方法、请求路径、查询参数分别是什么?


5.2POST⽅法

POST 是 HTTP 协议中最常用的请求方法之一,主要用于向服务器提交数据,让服务器对数据进行处理.

简单理解:

  • GET:向服务器获取数据(查)
  • POST:向服务器提交数据(提交、创建、处理)

例如:用户登录、注册账号、提交表单、创建订单、上传文件等,通常会使用POST请求.

1.POST 方法的工作原理

假设用户在网页登录:

整个过程:

  1. 用户在登录页面填写用户名、密码.
  2. 浏览器使用 POST 请求提交数据.
  3. 服务器接收并验证用户名和密码.
  4. 服务器返回登录结果.

2.POST请求报文格式

POST 请求同样由四个部分组成.

完整示例:

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

username=zhangsan&password=123456

逐个解释:

内容 含义
POST 请求方法
/login 登录接口路径
HTTP/1.1 HTTP 协议版本
Host 服务器主机名
Content-Type 请求体的数据格式
空行 分隔请求头与请求体
username 用户名参数
password 密码参数

3.POST 提交数据的三种常见格式

POST 的一个重要知识点是 Content-Type,它用来说明请求体的数据类型.

1.application/x-www-form-urlencoded

这是传统 HTML 表单常用的数据格式.

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

username=admin&password=123456

特点:

  • = 连接参数名和值.
  • & 分隔多个参数.
  • 特殊字符会根据表单编码规则进行编码.

这与之前学习的 URL Encode 有直接关系.

2.application/json

现代前后端分离项目中非常常见.

复制代码
POST /api/login HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "username": "admin",
  "password": "123456"
}

请求体使用 JSON 格式,结构清晰,也适合复杂数据.

3.multipart/form-data

主要用于提交文件和表单数据.

例如:

上传头像、提交图片、上传 PDF 文件等.

这种格式可以在一个请求中携带多个部分,常见于文件上传.

4.POST 方法的主要特点

  1. 通常使用请求体传递数据

与 GET 常用 URL 查询参数不同,POST通常通过Body提交业务数据.

但是POST也可以在URL中携带查询参数.

  1. 不保证幂等性

幂等性指多次执行相同请求,其预期的服务器资源效果与执行一次相同.

例如:

向 /orders 发送一次 POST 请求创建订单,可能产生一个订单.

如果重复发送同一个 POST 请求,可能创建多个订单.

因此,支付、创建订单等业务通常需要额外设计防重复提交机制.

  1. 可以传输多种格式的数据

POST 可以提交表单数据、JSON、文件、二进制数据等.

  1. 不代表数据一定安全

POST 请求体中的数据不会直接显示在 URL 查询参数中,但仍可能通过开发者工具查看,也可能被网络设备或服务器日志记录.

涉及密码等敏感数据时,应使用 HTTPS.

5.结合实际登录请求理解 POST

前面提供过这样一段 HTTP 报文:

复制代码
POST /companyLogin.do HTTP/1.1
Host: job.xjtu.edu.cn
Content-Type: application/x-www-form-urlencoded

username=hgtz2222&password=********

这就是典型的 POST 登录请求.

请求行:

POST /companyLogin.do HTTP/1.1

告诉服务器:我要向登录接口提交数据.

请求头:

Content-Type: application/x-www-form-urlencoded

告诉服务器:我提交的是表单编码数据.

请求体:

username=...&password=...

告诉服务器:这是用户填写的登录信息.

服务器解析请求后,验证登录信息并返回相应结果.


5.3PUT⽅法

PUT是HTTP协议中的一种请求方法,主要用于创建资源或更新服务器上已经存在的资源,尤其适合完整替换资源.

简单理解:

  • GET:查询数据
  • POST:提交数据、创建资源
  • PUT:创建或完整更新资源
  • DELETE:删除资源

例如:修改用户资料、更新商品信息、替换文章内容等,可以使用PUT请求.

1.PUT方法的工作原理

假设服务器中已经存在一个用户:

复制代码
{
  "id": 1001,
  "name": "张三",
  "age": 20
}

现在用户想修改自己的信息:

整个过程:

  1. 客户端准备修改后的用户数据.
  2. 浏览器或程序向服务器发送 PUT 请求.
  3. 服务器找到 ID 为 1001 的用户资源.
  4. 服务器按照接口规则更新资源.
  5. 服务器返回更新结果.

2.PUT 请求报文格式

PUT 请求与 POST 一样,也可以通过请求体提交数据.

例如:

复制代码
PUT /api/users/1001 HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "name": "李四",
  "age": 21
}

逐个解释:

内容 含义
PUT 请求方法
/api/users/1001 要更新的目标资源
HTTP/1.1 HTTP 协议版本
Host 服务器主机名
Content-Type 请求体数据格式
空行 分隔请求头与请求体
JSON 数据 更新后的用户信息

这里最重要的是:

PUT 请求通常会携带资源更新后的完整表示,让服务器使用它替换目标资源的当前状态.

3.PUT方法的主要特点

1.主要用于更新资源

例如:

复制代码
PUT /api/products/100 HTTP/1.1

表示针对 ID 为 100 的商品资源执行PUT操作.

2.具有幂等性(重点)

幂等性:对同一个资源执行一次 PUT 请求和重复执行相同的 PUT 请求,预期的服务器资源状态相同.

例如将用户年龄设置为 20:

复制代码
{
  "age": 20
}

如果接口允许这样更新:

第一次 PUT:年龄设置为 20.

第二次 PUT:年龄仍然是 20.

第三次 PUT:年龄仍然是 20.

无论执行多少次,最终资源状态相同.

注意:幂等不代表每次返回的状态码都必须相同,也不意味着服务器不会记录日志.

3.可以创建资源

PUT 不仅能更新资源,也可以创建资源.

例如:

复制代码
PUT /api/users/1001 HTTP/1.1

如果服务器允许客户端指定资源地址,而用户1001不存在,服务器可以创建该资源.

通常:

  • 创建成功:201 Created
  • 更新成功:200 OK 或 204 No Content

具体行为由服务器接口设计决定.


5.4HEAD⽅法

HEAD是HTTP协议中的一种请求方法,主要用于获取服务器返回的响应头信息,而不获取响应体.

简单理解:

  • GET:获取响应头 + 响应体
  • HEAD:只获取响应头,不返回响应体

例如:想知道服务器上的一张图片是否存在、文件有多大、文件是什么类型,就可以使用 HEAD 请求,而不必下载整个文件.

1.HEAD 方法的工作原理

假设服务器上有一张图片 logo.png.

整个过程:

  1. 客户端向服务器发送 HEAD 请求.
  2. 服务器找到指定资源.
  3. 服务器返回资源的响应头信息.
  4. 客户端根据响应头判断资源是否存在、大小和类型等.

重点:HEAD 请求不会让服务器返回响应体.

2.HEAD请求报文格式

例如,客户端想查看一张图片的信息:

复制代码
HEAD /images/logo.png HTTP/1.1
Host: example.com

服务器可能返回:

复制代码
HTTP/1.1 200 OK
Content-Type: image/png
Content-Length: 204800
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT

解释如下:

内容 含义
HEAD 请求方法
/images/logo.png 请求资源路径
200 OK 资源请求成功
Content-Type: image/png 资源类型为 PNG 图片
Content-Length: 204800 对应 GET 响应内容长度为 204800 字节
Last-Modified 资源最后修改时间
空行之后 没有响应体

注意:虽然响应头中有 Content-Length: 204800,但HEAD响应不会实际返回这204800字节的图片数据.这里的长度描述的是对应 GET 响应的内容大小.

3.HEAD 方法的主要特点

1.只获取响应头

HEAD 和 GET 的主要区别在于:

  • GET 返回响应头和响应体.
  • HEAD 返回响应头,不返回响应体.

2.减少数据传输

假设一个文件有 500 MB.

使用 GET 可能下载整个文件,而使用 HEAD 可以只获取文件相关信息,无需传输文件本身.

3.具有安全性和幂等性

HEAD 和 GET 一样,都是HTTP语义上的安全方法和幂等方法.

重复发送相同 HEAD 请求,不应产生修改目标资源的预期效果.

4.可以检查资源信息

HEAD 常用于:

  • 检查资源是否存在.
  • 查询资源大小.
  • 查看文件类型.
  • 检查资源最后修改时间.
  • 配合缓存验证资源是否更新.

需要注意,服务器不一定返回所有这些响应头.

4.实际应用案例

假设你想知道某个文件是否存在:

复制代码
https://example.com/files/test.pdf

可以使用 HEAD 请求:

复制代码
HEAD /files/test.pdf HTTP/1.1
Host: example.com

可能出现以下结果:

响应状态码 说明
200 OK 资源可访问
301 Moved Permanently 资源已永久重定向
403 Forbidden 服务器拒绝访问
404 Not Found 资源不存在或服务器不愿透露其存在
405 Method Not Allowed 服务器不允许对该资源使用 HEAD

5.使用 curl 测试 HEAD

在终端中输入:

复制代码
curl -I https://example.com

-I 表示发送 HEAD 请求并显示响应头.

如果想查看详细请求和响应过程,可以使用:

复制代码
curl -I -v https://example.com

5.5DELETE⽅法

DELETE 是HTTP协议中的一种请求方法,主要用于请求服务器删除指定的资源.

简单理解:

  • GET:查询数据
  • POST:提交数据、创建资源
  • PUT:创建或更新资源
  • HEAD:只获取响应头
  • DELETE:删除资源

例如:删除用户、删除商品、删除文章、取消某条记录等,都可能使用DELETE请求.

1.DELETE方法的工作原理

整个过程:

  1. 客户端发送 DELETE 请求.
  2. 服务器接收到请求.
  3. 服务器根据 URL 找到指定资源.
  4. 服务器根据权限及业务规则执行删除.
  5. 服务器返回处理结果.

2.DELETE请求报文格式

假设需要删除 ID 为 1002 的用户.

客户端发送:

复制代码
DELETE /api/users/1002 HTTP/1.1
Host: example.com
Authorization: Bearer <token>

其中:

内容 含义
DELETE 请求方法,表示删除资源
/api/users/1002 要删除的目标资源
HTTP/1.1 HTTP 协议版本
Host 目标服务器
Authorization 身份认证信息(示例)

重点:DELETE 请求通常通过 URL 指定要删除哪个资源.

服务器删除成功后,可能返回:

复制代码
HTTP/1.1 204 No Content

这里:

  • 204:请求处理成功.
  • No Content:没有响应体.

3.DELETE方法的主要特点

1.主要用于删除资源

例如:

复制代码
DELETE /api/products/100 HTTP/1.1

表示请求服务器删除 ID 为 100 的商品资源.

注意:DELETE 表示请求删除资源,并不一定意味着数据库中的记录会被物理删除.服务器也可能采用软删除,即只将记录标记为已删除.

2.具有幂等性(重点)

幂等性:对同一个资源执行一次 DELETE 和重复执行相同的 DELETE,请求所期望的资源最终状态相同.

例如删除用户 1002:

即使第二次 DELETE 返回 404 Not Found,它仍然是幂等的,因为目标资源已经不存在,最终状态没有改变.

3.DELETE 不属于安全方法

HTTP 中的安全方法是指不要求改变资源状态的方法.

DELETE 会请求服务器改变资源状态,因此它不是安全方法.

不要把这里的"安全"与 HTTPS 的加密安全混淆.

4.DELETE 请求通常不需要请求体

例如:

复制代码
DELETE /api/users/1002 HTTP/1.1
Host: example.com

通常通过 URL 就可以确定目标资源.

DELETE 请求在 HTTP 规范中没有通用定义的请求体语义.部分 API 会自行定义请求体的用途,但客户端和服务器需要事先约定.

4.DELETE 常见响应状态码

状态码 含义
200 OK 删除请求处理成功,可以返回结果
202 Accepted 已接受删除请求,尚未完成
204 No Content 删除处理成功,没有响应体
401 Unauthorized 需要身份认证
403 Forbidden 没有删除权限
404 Not Found 目标资源不存在或不可见
500 Internal Server Error 服务器内部错误

最常见的成功响应是 200 和 204.

5.DELETE与其他请求方法的区别

请求方法 主要作用 是否幂等
GET 获取资源 是
POST 提交数据、执行处理 不保证
PUT 创建或完整替换资源 是
PATCH 部分修改资源 不保证
HEAD 获取响应头 是
DELETE 删除资源 是

例如,一个用户管理系统可能包含这些接口:

复制代码
GET    /api/users           获取用户列表
GET    /api/users/1001      获取用户信息
POST   /api/users           创建用户
PUT    /api/users/1001      更新用户
PATCH  /api/users/1001      部分修改用户
DELETE /api/users/1001      删除用户

这种接口设计属于常见的 RESTful API 风格.

6.使用curl发送DELETE请求

在终端中可以使用:

复制代码
curl -X DELETE https://example.com/api/users/1002

其中:

  • curl:发送 HTTP 请求的命令行工具.
  • -X DELETE:指定 DELETE 请求方法.
  • URL:指定需要删除的目标资源.

如果需要查看响应头:

复制代码
curl -i -X DELETE https://example.com/api/users/1002

注意,DELETE可能触发真实删除操作,练习时应使用测试接口.


5.6OPTIONS⽅法

OPTIONS 是 HTTP 协议中的一种请求方法,主要用于查询服务器或指定资源支持哪些通信选项,也常用于浏览器的 CORS 跨域预检请求.

简单理解:

  • GET:获取数据
  • POST:提交数据
  • PUT:更新数据
  • DELETE:删除数据
  • HEAD:只获取响应头
  • OPTIONS:询问服务器允许怎样请求

例如:客户端想知道某个接口是否允许使用 GET、POST、DELETE 等方法,就可以发送 OPTIONS 请求.

1.OPTIONS方法的工作原理

假设客户端想知道 /api/users 接口支持哪些请求方法.

整个过程:

  1. 客户端发送 OPTIONS 请求.
  2. 服务器接收请求.
  3. 服务器根据配置确定允许的通信选项.
  4. 服务器通过响应头返回相关信息.

注意:OPTIONS 只是询问,不会实际执行 GET、POST、DELETE 等操作.

2.OPTIONS 请求报文格式

假设查询用户接口支持哪些方法.

客户端发送:

复制代码
OPTIONS /api/users HTTP/1.1
Host: example.com

服务器可能返回:

复制代码
HTTP/1.1 204 No Content
Allow: GET, POST, OPTIONS

逐个解释:

内容 含义
OPTIONS 请求方法
/api/users 查询的目标资源
HTTP/1.1 HTTP 协议版本
204 No Content 请求成功,没有响应体
Allow 列出该资源支持的 HTTP 方法

其中:

复制代码
Allow: GET, POST, OPTIONS

表示目标资源支持 GET、POST、OPTIONS 方法.

注意,实际支持的方法由服务器决定,OPTIONS响应也不一定都会包含 Allow 响应头.

3.OPTIONS 最重要的应用:CORS 跨域预检

这是实际开发中 OPTIONS 最常见的应用之一,也是需要重点理解的内容.

1.什么是跨域?

假设有两个网站:

前端页面:

复制代码
https://www.example.com

后端接口:

复制代码
https://api.example.com

由于域名不同,它们属于不同的源(Origin).

当浏览器中的 JavaScript 访问另一个源的接口时,就涉及跨域请求.

浏览器会按照同源策略与 CORS 规则限制跨域访问.

2.什么是预检请求?

对于某些跨域请求,浏览器会先自动发送 OPTIONS 请求,询问服务器是否允许后续的实际请求.

例如,前端想跨域发送 DELETE 请求.

3.完整的跨域预检请求示例

浏览器自动发送:

复制代码
OPTIONS /api/users/1001 HTTP/1.1
Host: api.example.com
Origin: https://www.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization

重要请求头:

请求头 含义
Origin 请求来自哪个源
Access-Control-Request-Method 接下来准备使用什么方法
Access-Control-Request-Headers 实际请求准备携带哪些非简单请求头

服务器可能返回:

复制代码
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
Access-Control-Allow-Headers: authorization

解释:

  • Access-Control-Allow-Origin:允许指定来源跨域访问.
  • Access-Control-Allow-Methods:允许该来源在跨域请求中使用哪些方法.
  • Access-Control-Allow-Headers:允许实际跨域请求携带哪些请求头.

浏览器确认预检通过之后,才会发送实际 DELETE 请求.

注意:不是每个跨域请求都会触发 OPTIONS 预检.例如符合 CORS 简单请求条件的 GET 请求,通常不需要预检.

另外,CORS预检通过也不等于服务器已经授权删除资源.实际请求仍然需要经过身份认证和权限检查.

4.OPTIONS 方法的主要特点

  1. 查询通信选项

用于了解服务器或目标资源支持的请求方式和其他通信选项.

  1. 常用于跨域预检

浏览器可能自动发送 OPTIONS,判断跨域请求是否被允许.

  1. 属于安全方法

OPTIONS 请求不应该要求服务器修改目标资源.

  1. 具有幂等性

重复发送相同的 OPTIONS 请求,不应该造成资源状态变化.

  1. 通常不需要请求体

实际开发中,OPTIONS 通常只通过请求行和请求头传递信息.

5.OPTIONS与其他HTTP方法对比

请求方法 主要用途 是否幂等
GET 获取资源 是
POST 提交数据、执行处理 不保证
PUT 创建或完整替换资源 是
HEAD 只获取响应头 是
DELETE 删除资源 是
OPTIONS 查询通信选项、跨域预检 是

6.使用curl测试OPTIONS

可以在终端输入:

复制代码
curl -i -X OPTIONS https://example.com

其中:

  • curl:HTTP 请求工具.
  • -X OPTIONS:指定 OPTIONS 方法.
  • -i:显示响应头和响应体.

如果想模拟浏览器的CORS预检:

复制代码
curl -i -X OPTIONS https://api.example.com/users \
  -H "Origin: https://www.example.com" \
  -H "Access-Control-Request-Method: DELETE"

这里使用的是示例地址,实际测试应替换成支持对应接口的服务器.


六、HTTP的状态码

HTTP 状态码是服务器在响应客户端请求时返回的三位数字,用来表示请求的处理结果.

简单理解:客户端向服务器发送请求,服务器通过HTTP状态码告诉客户端:请求成功了、需要跳转、请求有问题,还是服务器出错了.

例如:

复制代码
HTTP/1.1 200 OK

其中:

  • HTTP/1.1:HTTP 协议版本.
  • 200:HTTP 状态码,表示请求成功.
  • OK:状态码对应的描述.

1.HTTP 状态码的五大分类

HTTP 状态码按照第一位数字分为五大类.

最常见、最需要掌握的是 2xx、3xx、4xx、5xx.

  1. 1xx:信息响应

1xx 表示服务器已收到请求,当前通信或处理仍在继续.

状态码 含义
100 Continue 可以继续发送请求内容
101 Switching Protocols 服务器同意切换协议
103 Early Hints 提前提供资源加载提示

例如:

复制代码
HTTP/1.1 100 Continue

表示服务器允许客户端继续发送请求体.

记忆:1xx = 继续处理.

3.2xx:请求成功

2xx 表示服务器成功接收、理解或处理了请求.

状态码 含义 常见场景
200 OK 请求成功 获取网页、查询用户
201 Created 创建资源成功 新增用户、创建订单
202 Accepted 请求已接受,尚未完成处理 异步任务
204 No Content 处理成功,但无响应体 删除资源、更新数据
206 Partial Content 返回部分资源内容 断点续传、范围下载

例如:200 OK

客户端请求:

复制代码
GET /api/users/1001 HTTP/1.1
Host: example.com

服务器响应:

复制代码
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 1001,
  "name": "张三"
}

表示服务器成功返回用户信息.

例如:201 Created

客户端请求创建用户:

复制代码
POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "name": "李四"
}

服务器可能返回:

复制代码
HTTP/1.1 201 Created
Location: /api/users/1002

表示新用户资源已经创建成功.

记忆:2xx=请求成功.

4.3xx:重定向

3xx 表示客户端需要采取进一步操作才能完成请求,常见于网页跳转和缓存验证.

状态码 含义
301 Moved Permanently 永久重定向
302 Found 临时重定向
304 Not Modified 资源未修改,可以复用缓存
307 Temporary Redirect 临时重定向,保持原请求方法
308 Permanent Redirect 永久重定向,保持原请求方法

例如:302 临时重定向

用户访问:

复制代码
http://example.com/login

服务器返回:

复制代码
HTTP/1.1 302 Found
Location: /new-login

浏览器接收到 302 后,会根据 Location 指定的地址发起新的请求.

另外,304并不是错误,它通常意味着浏览器可以继续使用已经缓存的资源.

记忆:3xx = 跳转或缓存相关操作.

5.4xx:客户端错误(重点)

4xx 表示服务器认为请求存在问题,或者无法满足当前请求.

这是前后端开发中经常遇到的一类状态码.

状态码 含义 常见原因
400 Bad Request 请求有问题 参数格式错误
401 Unauthorized 未通过身份认证 未登录、Token 无效
403 Forbidden 禁止访问 没有访问权限
404 Not Found 资源不存在 请求路径或资源不存在
405 Method Not Allowed 请求方法不被允许 接口不支持该方法
408 Request Timeout 请求超时 服务器等待请求超时
409 Conflict 资源状态冲突 数据版本冲突
415 Unsupported Media Type 不支持提交的数据格式 Content-Type 不被支持
422 Unprocessable Content 内容格式可理解,但无法处理 业务字段校验失败
429 Too Many Requests 请求过于频繁 触发接口限流

重点:401、403、404 的区别

这三个状态码容易混淆.

记忆:4xx = 请求存在问题或无法满足.

6.5xx:服务器错误(重点)

5xx 表示服务器在处理请求时发生错误,或者暂时无法完成请求.

状态码 含义 常见原因
500 Internal Server Error 服务器内部错误 后端程序异常
501 Not Implemented 不支持所请求的功能 不支持某种请求能力
502 Bad Gateway 网关错误 上游服务器响应无效
503 Service Unavailable 服务不可用 服务器维护、过载
504 Gateway Timeout 网关超时 上游服务器响应超时

重点:500、502、503、504 的区别

假设网站的架构是:

记忆:5xx = 服务器侧发生错误.

7.HTTP 状态码与请求方法结合理解

假设开发一个用户管理系统:

操作 HTTP 请求 可能返回的状态码
查询用户 GET /users/1001 200、404
创建用户 POST /users 201、400
更新用户 PUT /users/1001 200、204、404
删除用户 DELETE /users/1001 204、404
查看响应头 HEAD /users/1001 200、404
查询支持的方法 OPTIONS /users 200、204

这些状态码只是常见示例,实际返回什么状态码取决于服务器处理结果与接口设计.

8.重点总结(建议记忆)

最常见的 12 个 HTTP 状态码:

状态码 记忆方式
200 请求成功
201 创建成功
204 成功,但没有响应体
301 永久重定向
302 临时重定向
304 缓存资源未修改
400 请求有问题
401 没有通过身份认证
403 没有权限
404 找不到资源
500 服务器内部错误
502 网关错误

最后记住这五句话:

1xx:继续处理

2xx:请求成功

3xx:需要跳转或使用缓存

4xx:客户端请求无法被满足

5xx:服务器侧发生错误

另外,HTTP状态码与JSON响应体中的业务code是两个不同的概念.例如HTTP返回 200 OK,但JSON中的 code 可能表示登录失败.实际排查问题时,需要同时查看HTTP状态码和业务响应内容.


七、HTTP常⻅Header

HTTP Header(HTTP 头部)是HTTP请求和响应中的重要组成部分,用来传递附加信息.

简单理解:

  • 请求头(Request Headers):客户端告诉服务器"我是谁、我要什么、我发送的是什么数据".
  • 响应头(Response Headers):服务器告诉客户端"我返回的是什么数据、如何处理这些数据".

例如,浏览器请求一个网页,除了发送URL,还会告诉服务器自己支持什么语言、能接收什么数据格式等.

1.HTTP Header 在报文中的位置

HTTP Header 的基本格式:

复制代码
Header-Name: Header-Value

例如:

复制代码
Content-Type: application/json

其中:

  • Content-Type:头字段名称.
  • application/json:头字段的值.
  • ::分隔名称和值.

HTTP 头字段名称不区分大小写.

2.常见的HTTP请求头(Request Headers)

请求头由客户端发送给服务器.

请求头 作用
Host 指定目标服务器主机
User-Agent 表示客户端软件信息
Accept 客户端希望接收的数据类型
Accept-Language 客户端希望接收的语言
Accept-Encoding 客户端支持的内容压缩编码
Content-Type 请求体的数据类型
Content-Length 请求体的字节长度
Authorization 携带身份认证凭据
Cookie 携带客户端保存的 Cookie
Referer 表示请求的来源页面
Origin 表示请求来源的源
Cache-Control 指定缓存相关要求

下面解释最常用的几个.

1.Host:指定服务器

复制代码
Host: www.example.com

告诉服务器客户端要访问哪个主机.

同一个 IP 地址可能对应多个网站,服务器可以通过 Host 判断应该处理哪个网站的请求.

2.User-Agent:客户端信息

复制代码
User-Agent: Mozilla/5.0

用于描述客户端软件.

实际的 User-Agent 可能包含浏览器、操作系统等信息.

注意:User-Agent 可以被伪造,不能作为可靠的身份验证依据.

3.Accept:希望接收什么数据

复制代码
Accept: application/json

意思是:客户端希望服务器返回 JSON 格式的数据.

例如:

复制代码
Accept: text/html

表示客户端希望接收 HTML.

4.Content-Type:发送的是什么数据

复制代码
Content-Type: application/json

表示客户端发送的请求体使用 JSON 格式.

常见类型:

Content-Type 数据格式
application/json JSON
application/x-www-form-urlencoded 表单编码数据
multipart/form-data 多部分表单,常用于文件上传
text/plain 纯文本
text/html HTML

重点区分:

  • Accept:我希望接收什么格式.
  • Content-Type:我发送的请求体是什么格式.

服务器也会使用 Content-Type 响应头,说明返回的响应体格式.

5.Authorization:身份认证

复制代码
Authorization: Bearer <token>

常用于客户端向服务器证明身份.

例如用户登录后获得Token,后续请求可能通过Authorization携带它.

Token 应当妥善保管,避免泄露.

6.Cookie:携带会话信息

复制代码
Cookie: sessionId=abc123

Cookie 可以帮助服务器识别客户端的会话状态.

例如:

用户登录成功后,浏览器保存会话 Cookie,后续请求可能自动携带该 Cookie.

7.Referer 与 Origin:请求来源

复制代码
Referer: https://example.com/login
Origin: https://example.com

两者的主要区别:

  • Referer:可以提供来源页面的完整 URL.
  • Origin:通常只包含协议、主机和端口.

Origin 常用于 CORS 跨域访问控制.

3.常见的 HTTP 响应头(Response Headers)

响应头由服务器返回给客户端.

响应头 作用
Content-Type 服务器返回的数据类型
Content-Length 响应体字节长度
Content-Encoding 响应内容的压缩编码方式
Server 服务器软件标识
Date 响应生成时间
Set-Cookie 要求浏览器设置 Cookie
Location 指定重定向目标地址
Cache-Control 控制响应缓存
ETag 资源版本标识,用于缓存验证
Last-Modified 资源最后修改时间
Access-Control-Allow-Origin 指定允许跨域访问的来源
Transfer-Encoding 消息的传输编码方式(HTTP/1.1)

1.Content-Type:响应数据类型

复制代码
Content-Type: text/html; charset=UTF-8

表示服务器返回的是 HTML,使用UTF-8字符编码.

如果是:

复制代码
Content-Type: application/json

则表示服务器返回 JSON 数据.

2.Content-Length:响应体长度

复制代码
Content-Length: 1024

表示响应体长度为1024字节.

注意,它表示字节数,不是字符个数.

3.Content-Encoding:内容压缩方式

复制代码
Content-Encoding: gzip

表示响应内容使用 gzip 编码压缩.

浏览器通常会自动解压,再按照 Content-Type 指定的格式处理内容.

例如:

4.Set-Cookie:设置 Cookie

复制代码
Set-Cookie: sessionId=abc123; HttpOnly; Secure

服务器通过 Set-Cookie 通知浏览器保存 Cookie.

其中:

  • HttpOnly:限制 JavaScript 直接读取该 Cookie.
  • Secure:要求 Cookie 仅通过安全连接发送(存在少量特殊例外).

5.Location:重定向地址

复制代码
HTTP/1.1 302 Found
Location: https://example.com/home

表示服务器要求客户端访问新的地址.

Location 经常与 301、302、307、308 等重定向状态码配合使用.

6.Cache-Control:缓存控制

复制代码
Cache-Control: max-age=3600

表示相关缓存的新鲜度期限为 3600 秒.

其他常见指令:

  • no-cache:允许存储,但使用前需要验证.
  • no-store:不允许缓存存储该响应.
  • private:只允许私有缓存存储,例如浏览器缓存.
  • public:允许共享缓存存储,仍需遵守其他缓存条件.

7.Access-Control-Allow-Origin:跨域许可

复制代码
Access-Control-Allow-Origin: https://app.example.com

表示服务器允许指定来源的网页,按CORS规则读取响应.

该字段主要由浏览器执行跨域访问控制时使用.

4.完整 HTTP 请求与响应示例

假设客户端查询用户信息.

客户端发送请求

复制代码
GET /api/users/1001 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer <token>
Accept-Language: zh-CN

服务器返回响应

复制代码
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "id": 1001,
  "name": "张三",
  "age": 20
}

这一过程可以理解为:

客户端说: 我要查询 ID 为 1001 的用户,我希望接收 JSON 格式的数据,这是我的身份认证信息.

服务器说: 请求成功(200),返回JSON数据,并要求不要缓存该响应.

5.几个容易混淆的 Header(重点)

请求头 响应头 区别
Accept Content-Type 希望接收的类型 vs 实际返回的类型
Accept-Encoding Content-Encoding 支持的内容编码 vs 实际使用的内容编码
Cookie Set-Cookie 浏览器携带 Cookie vs 服务器设置 Cookie
Origin Access-Control-Allow-Origin 请求来源 vs 允许的跨域来源

还有一个容易混淆的地方:

  • Content-Encoding: gzip:表示响应内容使用 gzip 压缩.
  • Transfer-Encoding: chunked:表示 HTTP/1.1 消息使用分块传输编码.

前者是对内容进行编码,后者是消息的传输分帧方式,两者不是一回事.


八、最简单的HTTP服务器

实现⼀个最简单的HTTP服务器,只在⽹⻚上输出 "hello world"; 只要我们按照HTTP协议的要求构造数据,就很容易能做到:

cpp 复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void Usage() {
    printf("usage: ./server [ip] [port]\n");
}

int main(int argc, char* argv[]) {
    if (argc != 3) {
        Usage();
        return 1;
    }

    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        perror("socket");
        return 1;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(argv[1]);
    addr.sin_port = htons(atoi(argv[2]));

    int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
    if (ret < 0) {
        perror("bind");
        return 1;
    }

    ret = listen(fd, 10);
    if (ret < 0) {
        perror("listen");
        return 1;
    }

    for (;;) {
        struct sockaddr_in client_addr;
        socklen_t len;
        int client_fd = accept(fd, (struct sockaddr*)&client_addr, &len);
        if (client_fd < 0) {
            perror("accept");
            continue;
        }

        char input_buf[1024 * 10] = {0}; // 用一个足够大的缓冲区直接把数据读完.
        ssize_t read_size = read(client_fd, input_buf, sizeof(input_buf) - 1);
        if (read_size < 0) {
            return 1;
        }

        printf("[Request] %s", input_buf);

        char buf[1024] = {0};
        const char* hello = "<h1>hello world</h1>";
        sprintf(buf, "HTTP/1.0 200 OK\nContent-Length:%lu\n\n%s", strlen(hello), hello);
        write(client_fd, buf, strlen(buf));
    }

    return 0;
}

编译启动服务.在浏览器中输⼊http://ip:port,就能看到显⽰的结果"Hello World".


九、HTTP历史及版本核⼼技术与时代背景

HTTP(HyperText Transfer Protocol,超文本传输协议)是万维网(WWW)的基础通信协议.

HTTP从诞生到现在,经历了多个重要版本.每一次升级,基本都是为了解决上一个版本在实际使用中暴露的问题.

HTTP 的发展主线可以概括为:从简单传输网页 → 支持复杂网站 → 减少连接开销 → 提高并发效率 → 降低网络延迟.

1.HTTP的发展历史时间线

1.1990---1991 年:HTTP/0.9

万维网诞生初期,只需要简单地获取HTML文档.

关键词:简单、GET

2.1996年:HTTP/1.0

网站开始普及,需要传输图片等不同类型的资源.

关键词:Header、状态码

3.1997年:HTTP/1.1

网页资源越来越多,需要减少频繁建立TCP连接的开销.

关键词:持久连接、缓存

4.2015年:HTTP/2

现代网页包含大量 CSS、JavaScript、图片等资源,需要提高并发传输效率.

关键词:二进制分帧、多路复用

5.2022 年:HTTP/3

移动网络、弱网环境越来越普遍,需要进一步减少延迟和连接内的丢包阻塞.

关键词:QUIC、UDP、TLS 1.3

这些时间对应各版本的早期实现或重要标准发布时间.HTTP/1.1 在 1999 年进一步修订,HTTP/2 也在 2022 年发布了更新的规范.

2.为什么会诞生 HTTP?

1989 年,万维网的构想诞生.

英国计算机科学家 Tim Berners-Lee 当时在欧洲核子研究中心(CERN)工作.

他希望不同计算机上的科研人员能够方便地查找、共享和访问文档.

为实现这个目标,逐渐形成了三项核心技术:

技术 主要作用
HTML 描述网页内容和结构
URL 标识资源的位置
HTTP 规定客户端与服务器如何传输资源

例如:

用户在浏览器输入 URL → 浏览器通过 HTTP 请求网页 → 服务器返回 HTML → 浏览器将 HTML 显示为网页.

1990 年底,第一台Web服务器和第一款浏览器已经在CERN运行.1991年,万维网软件开始向更广泛的用户传播.

3.HTTP 各版本分别解决了什么问题?

1.HTTP/0.9:能传网页就够了(1990---1991)

时代背景: 互联网还处于早期,网页以简单的超文本文档为主,不需要复杂的交互功能.

核心技术:单行请求协议.

客户端请求:

复制代码
GET /index.html

服务器直接返回:

复制代码
<html>
  <h1>Hello World</h1>
</html>

它的特点如下:

特点 HTTP/0.9
请求方法 只有 GET
请求头 Header 不支持
HTTP 状态码 不支持
响应体 主要是 HTML
协议版本号 请求报文中没有

主要问题: 无法灵活传输图片等其他类型的数据,也没有状态码告诉客户端请求是否成功.

HTTP/0.9 最初没有正式版本号,0.9 是后来的追溯命名.

2.HTTP/1.0:让网页通信规范化(1996)

时代背景: 20世纪90年代,图形化浏览器开始普及.网页除了HTML,还需要图片、样式及更多类型的资源.

核心技术:引入HTTP版本、请求头、响应头和状态码.

例如:

复制代码
GET /index.html HTTP/1.0
Host: example.com

服务器响应:

复制代码
HTTP/1.0 200 OK
Content-Type: text/html

<html>
  <h1>Hello World</h1>
</html>

与HTTP/0.9相比,HTTP/1.0可以通过Content-Type描述不同类型的数据,并且使用状态码表达处理结果.

但是,它存在一个重要问题:默认采用非持久连接.

假设一个网页需要加载三张图片:

这在资源很少的网页中问题不大,但随着网站越来越复杂,反复创建TCP连接就会影响加载效率.

HTTP/1.0 的代表性文档是 1996 年 5 月发布的 RFC 1945.

3.HTTP/1.1:减少连接开销(1997)

时代背景: 网站快速发展,网页包含更多图片、文件和动态内容,频繁建立连接的问题越来越明显.

核心技术:持久连接(Keep-Alive)。

HTTP/1.1 默认使用持久连接,一条TCP连接可以处理多个 HTTP 请求.

HTTP/1.1 还带来了多项改进:

技术 解决的问题
Keep-Alive 减少重复建立 TCP 连接
Host 请求头 支持一台服务器承载多个域名
Chunked 分块传输 可以不预先确定整个响应体的长度
Cache-Control 等缓存机制 更精细地控制缓存
Pipelining 管线化 允许在收到前一个响应前发送后续请求

但 HTTP/1.1 仍有明显局限。

主要问题:队头阻塞(Head-of-Line Blocking,HOL).

管线化虽然允许连续发送请求,但服务器需要按照请求顺序返回对应响应,慢响应可能阻碍后续响应.由于兼容性等原因,浏览器也没有广泛使用HTTP/1.1管线化.

4.HTTP/2:提高并发传输效率(2015)

时代背景: 进入智能手机和现代 Web 应用时代后,网页中可能包含大量 JavaScript、CSS、图片、字体和接口请求.

仅依靠 HTTP/1.1 的持久连接已经不够高效。

Google 曾探索 SPDY 协议,其思路对 HTTP/2 的设计产生了重要影响。

HTTP/2 最重要的三项技术:

核心技术 作用
二进制分帧 将消息拆分成结构化的二进制帧
多路复用 同一连接上并发传输多个请求和响应
HPACK 头部压缩 减少重复 HTTP 头字段的传输开销

重点理解:什么是多路复用?

但它没有彻底解决队头阻塞.

因为HTTP/2仍然基于TCP.当TCP数据包丢失时,TCP需要按序恢复数据,这可能影响同一连接中的多个HTTP/2流.

HTTP/2于2015年正式标准化,当前主要规范为2022年发布的RFC 9113.

5.HTTP/3:改用 QUIC,优化弱网体验(2022)

时代背景: 移动互联网普及,用户会在 Wi-Fi、4G、5G 等网络之间切换,同时还可能遇到丢包、延迟和网络波动.

HTTP/2 虽然支持多路复用,但底层TCP仍然可能出现连接级队头阻塞.

HTTP/3 的核心变化:使用QUIC替代TCP作为传输基础.

HTTP/3 的关键技术:

核心技术 作用
QUIC 在 UDP 之上实现可靠、低延迟传输
独立数据流 避免 TCP 式的跨流队头阻塞
TLS 1.3 为 QUIC 连接提供加密和身份验证
QPACK 针对 HTTP/3 优化的头字段压缩
Connection ID 支持连接迁移,例如网络切换

HTTP/3 在连接建立和有丢包的情况下可能比 HTTP/2 表现更好,但并不意味着所有网络环境下都一定更快.

2022年6月发布的RFC 9114正式定义了HTTP/3,RFC 9204则定义了QPACK.

4.HTTP 各版本核心技术对比(重点)

对比项 HTTP/1.0 HTTP/1.1 HTTP/2 HTTP/3
传输基础 TCP TCP TCP QUIC / UDP
连接复用 默认无 支持 支持 支持
报文传输形式 文本 文本 二进制帧 二进制帧
多路复用 不支持 不支持真正的多路复用 支持 支持
头部压缩 无内建机制 无内建机制 HPACK QPACK
连接级队头阻塞 存在 存在 TCP 层仍存在 避免 TCP 式跨流阻塞
加密 可使用 TLS 可使用 TLS 浏览器通常配合 TLS QUIC 内建 TLS 1.3

HTTP/0.9 是更早的简化版本,只有单行GET请求,因此未列入这张现代协议特性对比表.

5.HTTP/1.1、HTTP/2、HTTP/3 的报文是否相同?

这里需要结合之前学习的 HTTP 请求与响应格式.

HTTP/1.1

之前看到的这种格式:

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

就是 HTTP/1.1 的文本请求格式.

HTTP/2 和 HTTP/3

HTTP/2、HTTP/3 在网络上传输的是二进制帧,不再直接使用 HTTP/1.1 的文本请求行与状态行.

不过,它们保留了大部分HTTP语义,例如:

GET、POST、PUT、DELETE、200、404、500 等.

HTTP/2、HTTP/3 使用伪头字段表示部分关键信息.下面是逻辑层面的示意,而不是实际网络字节:

复制代码
:method: GET
:scheme: https
:authority: example.com
:path: /index.html

响应中的状态码可以表示为:

复制代码
:status: 200

所以,HTTP版本升级,主要改变的是传输与消息表示机制,而不是重新发明 GET、POST 等请求语义.

6.为什么 HTTP/3 出来了,还要学习 HTTP/1.1?

原因很简单:HTTP/1.1、HTTP/2 和 HTTP/3 并不是完全替代关系,它们仍然可以共存.

例如,之前使用C语言Socket编写的服务器,返回:

复制代码
HTTP/1.0 200 OK
Content-Length: 20

<h1>hello world</h1>

这就是一个简单的 HTTP/1.0 响应.

最重要的五句话:

HTTP/0.9: 能请求网页就行.

HTTP/1.0: 让请求和响应更加规范.

HTTP/1.1: 一个 TCP 连接可以重复使用.

HTTP/2: 一个 TCP 连接可以并发承载多个数据流.

HTTP/3: 基于QUIC,进一步减少延迟和不同数据流之间的阻塞.


HTTP Cookie 是服务器发送给浏览器、由浏览器保存的一小段数据.浏览器在后续符合条件的 HTTP 请求中,会自动携带相应Cookie,帮助服务器识别会话或保存用户状态.

10.1定义

HTTP Cookie(也称为Web Cookie、浏览器Cookie或简称Cookie)是服务器发送到⽤户浏览器并保存在浏览器上的⼀⼩块数据,它会在浏览器之后向同⼀服务器再次发起请求时被携带并发送到服务器上.通常,它⽤于告知服务端两个请求是否来⾃同⼀浏览器,如保持⽤户的登录状态、记录⽤户偏好等.

Cookie 是以 名称和值(Key=Value) 形式保存的小型数据.

例如:

复制代码
Cookie: username=zhangsan; sessionId=abc123

常用于记录登录会话、用户偏好等信息.

HTTP 本身是无状态协议,Cookie 可以配合服务器端的 Session 实现状态管理.


10.2工作原理

  1. 浏览器访问网站,向服务器发送请求.
  2. 服务器通过 Set-Cookie 响应头设置Cookie.
  3. 浏览器保存Cookie.
  4. 后续满足条件的请求中,浏览器通过 Cookie 请求头携带它.
  5. 服务器读取Cookie,识别会话或处理用户状态.
  6. 当⽤⼾第⼀次访问⽹站时,服务器会在响应的HTTP头中设置Set-Cookie字段,⽤于发送Cookie到⽤户的浏览器.
  7. 浏览器在接收到Cookie后,会将其保存在本地(通常是按照域名进⾏存储).
  8. 在之后的请求中,浏览器会⾃动在HTTP请求头中携带Cookie字段,将之前保存的Cookie信息发送给服务器.

10.3分类

分类方式 类型 说明
按生命周期 会话 Cookie 通常在浏览器会话结束时失效
按生命周期 持久 Cookie 根据 Expires 或 Max-Age 设置有效期
按使用场景 第一方 Cookie 与当前访问网站相关的 Cookie
按使用场景 第三方 Cookie 与第三方站点相关的 Cookie,受到浏览器隐私策略限制

10.4安全性

Cookie如果配置不当,可能导致会话被盗用、信息泄露等问题.

常见安全属性:

属性 作用
HttpOnly 阻止 JavaScript 直接读取 Cookie
Secure 限制 Cookie 通过 HTTPS 等安全连接发送
SameSite 限制跨站请求携带 Cookie,帮助防范 CSRF
Max-Age 设置 Cookie 的有效期

例如:

复制代码
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax

注意:Cookie不适合直接保存明文密码等敏感信息.登录系统通常通过Cookie保存会话标识,而不是密码.


10.5用途

常见应用包括:

  • 登录状态保持:避免用户每次访问页面都重新登录.
  • 用户偏好设置:保存网站语言、主题等偏好.
  • 购物车状态:关联用户购物车信息.
  • 数据统计:辅助记录访问行为和分析网站使用情况.

十一、认识Cookie

Cookie是服务器发送给浏览器、由浏览器保存的一小段数据,主要用于保存用户状态、识别用户会话等.


11.1基本格式

Cookie 通常采用 键值对(Key=Value)的形式.

例如:

复制代码
username=zhangsan

其中:

  • username:Cookie 名称(Key).
  • zhangsan:Cookie 的值(Value).

多个 Cookie 可以通过分号分隔:

复制代码
Cookie: username=zhangsan; theme=dark

11.2完整的Set-Cookie示例

服务器可以通过 HTTP 响应头 Set-Cookie 通知浏览器保存 Cookie.

复制代码
HTTP/1.1 200 OK
Set-Cookie: sessionId=abc123; Max-Age=3600; Path=/; HttpOnly; Secure; SameSite=Lax
Content-Type: text/html

各字段的作用:

属性 说明
sessionId=abc123 Cookie 的名称和值
Max-Age=3600 有效期为 3600 秒
Path=/ Cookie 适用的路径
HttpOnly 禁止 JavaScript 直接读取
Secure 限制通过安全连接发送
SameSite=Lax 限制部分跨站请求携带 Cookie

浏览器后续发送请求时,可能携带:

复制代码
Cookie: sessionId=abc123

注意:Set-Cookie 用于服务器设置 Cookie,Cookie 用于浏览器向服务器发送Cookie.

时间格式必须遵守RFC1123标准,具体格式样例:Tue, 01 Jan 2030 12:34:56GMT或者UTC(推荐).

📌 GMT(格林威治标准时间)和UTC(协调世界时)是两个不同的时间标准,但它们在⼤多数情况下⾮常接近,常常被混淆.以下是两者的简单解释和区别:

  1. GMT(格林威治标准时间):
    ◦ GMT是格林威治标准时间的缩写,它是以英国伦敦的格林威治区为基准的世界时间标准.
    ◦ GMT不受夏令时或其他因素的影响,通常⽤于航海、航空、科学、天⽂等领域.
    ◦ GMT的计算⽅式是基于地球的⾃转和公转.
  2. UTC(协调世界时):
    ◦ UTC全称为"协调世界时",是国际电信联盟(ITU)制定和维护的标准时间.
    ◦ UTC的计算⽅式是基于原⼦钟,⽽不是地球的⾃转,因此它⽐GMT更准确.据称,世界上最精确的原⼦钟50亿年才会误差1秒.
    ◦ UTC是现在⽤的时间标准,多数全球性的⽹络和软件系统将其作为标准时间.

📌 GMT和UTC的英⽂全称以及相关信息如下:

  1. GMT(格林尼治标准时间)
    ◦ 英⽂全称:Greenwich Mean Time.GMT是指位于英国伦敦郊区的皇家格林尼治天⽂台的标准时间,因为本初⼦午线被定义为通过那⾥的经线.理论上来说,格林尼治标准时间的正午是指当太阳横穿格林尼治⼦午线时的时间.
    ◦ 但值得注意的是,地球的⾃转是有些不规则的,且正在缓慢减速.因此,格林尼治时间已经不再被作为标准时间使⽤.
  2. UTC(协调世界时)
    ◦ 英⽂全称:Coordinated Universal Time.
    ◦ UTC是最主要的世界时间标准,其以原⼦时秒⻓为基础,在时刻上尽量接近于格林尼治标准时间.
    ◦ UTC被⼴泛使⽤在计算机⽹络、航空航天等领域,因为它提供了⾮常准确和可靠的时间参考.总结来说,GMT和UTC都曾是或现在是国际上重要的时间标准,但由于地球⾃转的不规则性和原⼦钟的精确性,UTC已经成为了全球性的标准时间,⽽GMT则更多被⽤作历史和地理上的参考.

11.3Cookie的注意事项

使用Cookie时,需要注意:

  1. 大小限制:单个Cookie通常只有约4KB的存储空间,具体限制取决于浏览器.
  2. 自动携带:符合 Domain、Path、Secure 等条件的 Cookie,浏览器通常会自动发送.
  3. 存储位置:Cookie主要由浏览器管理,也可以通过浏览器开发者工具查看.
  4. 不适合存储敏感明文:不要直接保存用户密码等敏感信息.
  5. Cookie可以被修改:服务器不能直接信任客户端传来的 Cookie 值.

11.4Cookie的生命周期

Cookie根据有效期,主要分为两类.

如果需要删除Cookie,可以让服务器返回:

复制代码
Set-Cookie: sessionId=; Max-Age=0; Path=/

浏览器会删除与相应名称、域名和路径匹配的 Cookie.


11.5Cookie的安全性(了解即可)

Cookie如果泄露,可能造成用户会话被盗用.

主要掌握三个安全属性:

属性 作用
HttpOnly 防止 JavaScript 直接读取 Cookie
Secure 限制 Cookie 通过 HTTPS 等安全连接发送
SameSite 限制跨站请求携带 Cookie,帮助防范 CSRF

例如:

复制代码
Set-Cookie: token=abc123; HttpOnly; Secure; SameSite=Strict

这几个属性可以降低安全风险,但不能完全消除 XSS、CSRF 或会话劫持等攻击风险.


十二、实验测试Cookie

代码⽂件结构

cpp 复制代码
//Comm.hpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

enum{
    Usage_Err = 1,
    Socket_Err,
    Bind_Err,
    Listen_Err
};

#define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)
cpp 复制代码
//HttpProtocol.hpp
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <ctime>
#include "TcpServer.hpp"
#include "Log.hpp"

const std::string HttpSep = "\r\n";
// 可以配置的
const std::string homepage = "index.html";
const std::string wwwroot = "./wwwroot";


class HttpRequest
{
public:
    HttpRequest() : _req_blank(HttpSep), _path(wwwroot)
    {
    }
    bool GetLine(std::string &str, std::string *line)
    {
        auto pos = str.find(HttpSep);
        if (pos == std::string::npos)
            return false;
        *line = str.substr(0, pos); // \r\n
        str.erase(0, pos + HttpSep.size());
        return true;
    }
    bool Deserialize(std::string &request)
    {
        std::string line;
        bool ok = GetLine(request, &line);
        if (!ok)
            return false;
        _req_line = line;

        while (true)
        {
            bool ok = GetLine(request, &line);
            if (ok && line.empty())
            {
                _req_content = request;
                break;
            }
            else if (ok && !line.empty())
            {
                _req_header.push_back(line);
            }
            else
            {
                break;
            }
        }

        return true;
    }
    void DebugHttp()
    {
        std::cout << "_req_line: " << _req_line << std::endl;
        for (auto &line : _req_header)
        {
            std::cout << "---> " << line << std::endl;
        }
    }
    ~HttpRequest()
    {
    }

private:
    // http报文自动
    std::string _req_line; // method url http_version
    std::vector<std::string> _req_header;
    std::string _req_blank;
    std::string _req_content;

    // 解析之后的内容
    std::string _method;
    std::string _url; // /   /dira/dirb/x.html     /dira/dirb/XX?usrname=100&&password=1234 /dira/dirb
    std::string _http_version;
    std::string _path;   // "./wwwroot"
    std::string _suffix; // 请求资源的后缀
};

const std::string BlankSep = " ";
const std::string LineSep = "\r\n";

class HttpResponse
{
public:
    HttpResponse() : _resp_blank(LineSep), _http_version("HTTP/1.0"), _status_code(200), _status_code_desc("OK")
    {
    }
    void SetCode(int code)
    {
        _status_code = code;
    }
    void SetDesc(const std::string &desc)
    {
        _status_code_desc = desc;
    }
    void MakeStatusLine()
    {
        _status_line = _http_version + BlankSep + std::to_string(_status_code) + BlankSep + _status_code_desc + LineSep;
    }
    void AddHeader(const std::string &header)
    {
        _resp_header.push_back(header+LineSep);
    }
    void AddContent(const std::string &content)
    {
        _resp_content = content;
    }
    std::string Serialize()
    {
        MakeStatusLine();
        std::string response_str = _status_line;
        for (auto &header : _resp_header)
        {
            response_str += header;
        }
        response_str += _resp_blank;
        response_str += _resp_content;

        return response_str;
    }
    ~HttpResponse() {}

private:
    std::string _status_line;
    std::vector<std::string> _resp_header;
    std::string _resp_blank;
    std::string _resp_content; // body

    // httpversion StatusCode StatusCodeDesc
    std::string _http_version;
    int _status_code;
    std::string _status_code_desc;
};

class Http
{
private:
    std::string GetMonthName(int month)
    {
        std::vector<std::string> months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
        return months[month];
    }
    std::string GetWeekDayName(int day)
    {
        std::vector<std::string> weekdays = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
        return weekdays[day];
    }
    std::string ExpireTimeUseRfc1123(int t) // 秒级别的未来UTC时间
    {
        time_t timeout = time(nullptr) + t;
        struct tm *tm = gmtime(&timeout); // 这里不能用localtime,因为localtime是默认带了时区的. gmtime获取的就是UTC统一时间
        char timebuffer[1024];
        //时间格式如: expires=Thu, 18 Dec 2024 12:00:00 UTC
        snprintf(timebuffer, sizeof(timebuffer), "%s, %02d %s %d %02d:%02d:%02d UTC", 
            GetWeekDayName(tm->tm_wday).c_str(),
            tm->tm_mday,
            GetMonthName(tm->tm_mon).c_str(),
            tm->tm_year+1900,
            tm->tm_hour,
            tm->tm_min,
            tm->tm_sec
        );
        return timebuffer;
    }
public:
    Http(uint16_t port)
    {
        _tsvr = std::make_unique<TcpServer>(port, std::bind(&Http::HandlerHttp, this, std::placeholders::_1));
        _tsvr->Init();
    }
    std::string ProveCookieWrite() // 证明cookie能被写入浏览器
    {
        return "Set-Cookie: username=zhangsan;";
    }
    std::string ProveCookieTimeOut()
    {
        return "Set-Cookie: username=zhangsan; expires=" + ExpireTimeUseRfc1123(60) + ";"; // 让cookie 1min后过期
    }
    std::string ProvePath()
    {
        return "Set-Cookie: username=zhangsan; path=/a/b;";
    }
    std::string ProveOtherCookie()
    {
        return "Set-Cookie: passwd=1234567890; path=/a/b;";
    }
    std::string HandlerHttp(std::string request)
    {
        HttpRequest req;
        req.Deserialize(request);
        req.DebugHttp();
        lg.LogMessage(Debug, "%s\n", ExpireTimeUseRfc1123(60).c_str());
        HttpResponse resp;
        resp.SetCode(200);
        resp.SetDesc("OK");
        resp.AddHeader("Content-Type: text/html");

        // resp.AddHeader(ProveCookieWrite()); //测试cookie被写入与自动提交
        // resp.AddHeader(ProveCookieTimeOut()); //测试过期时间的写入
        // resp.AddHeader(ProvePath()); // 测试路径
        resp.AddHeader(ProvePath());
        resp.AddHeader(ProveOtherCookie());

        resp.AddContent("<html><h1>helloworld</h1></html>");
        return resp.Serialize();
    }
    void Run()
    {
        _tsvr->Start();
    }
    ~Http()
    {}
private:
    std::unique_ptr<TcpServer> _tsvr;
};
cpp 复制代码
//InetAddr.hpp
#pragma once
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }
    InetAddr(const std::string &ip, uint16_t port)
    {
        _ip = ip;
        _port = port;
    }
    std::string Ip() { return _ip; }
    uint16_t Port() { return _port; };
    std::string PrintDebug()
    {
        std::string info = _ip;
        info += ":";
        info += std::to_string(_port); // "127.0.0.1:4444"
        return info;
    }
    const struct sockaddr_in &GetAddr()
    {
        return _addr;
    }
    bool operator==(const InetAddr &addr)
    {
        // other code
        return this->_ip == addr._ip && this->_port == addr._port;
    }
    ~InetAddr() {}

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};
cpp 复制代码
//LockGuard.hpp
#pragma once

#include <pthread.h>

// 不定义锁,默认认为外部会给我们传入锁对象
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock):_lock(lock)
    {}
    void Lock()
    {
        pthread_mutex_lock(_lock);
    }
    void Unlock()
    {
        pthread_mutex_unlock(_lock);
    }
    ~Mutex()
    {}

private:
    pthread_mutex_t *_lock;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock): _mutex(lock)
    {
        _mutex.Lock();
    }
    ~LockGuard()
    {
        _mutex.Unlock();
    }
private:
    Mutex _mutex;
};
cpp 复制代码
//Log.hpp
#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

enum
{
    Debug = 0,
    Info,
    Warning,
    Error,
    Fatal
};

enum
{
    Screen = 10,
    OneFile,
    ClassFile
};

inline std::string LevelToString(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    default:
        return "Unknown";
    }
}

const int defaultstyle = Screen;
const std::string default_filename = "log.";
const std::string logdir = "log";

class Log
{
public:
    Log() : style(defaultstyle), filename(default_filename)
    {
        mkdir(logdir.c_str(), 0775);
    }
    void Enable(int sty) //
    {
        style = sty;
    }
    std::string TimeStampExLocalTime()
    {
        time_t currtime = time(nullptr);
        struct tm *curr = localtime(&currtime);
        char time_buffer[128];
        snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
                 curr->tm_year + 1900, curr->tm_mon + 1, curr->tm_mday,
                 curr->tm_hour, curr->tm_min, curr->tm_sec);
        return time_buffer;
    }
    void WriteLogToOneFile(const std::string &logname, const std::string &message)
    {
        umask(0);
        int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
        if(fd < 0) return;
        write(fd, message.c_str(), message.size());
        close(fd);
        // std::ofstream out(logname);
        // if (!out.is_open())
        //     return;
        // out.write(message.c_str(), message.size());
        // out.close();
    }
    void WriteLogToClassFile(const std::string &levelstr, const std::string &message)
    {
        std::string logname = logdir;
        logname += "/";
        logname += filename;
        logname += levelstr;
        WriteLogToOneFile(logname, message);
    }

    void WriteLog(const std::string &levelstr, const std::string &message)
    {
        switch (style)
        {
        case Screen:
            std::cout << message;
            break;
        case OneFile:
            WriteLogToClassFile("all", message);
            break;
        case ClassFile:
            WriteLogToClassFile(levelstr, message);
            break;
        default:
            break;
        }
    }
    void LogMessage(int level, const char *format, ...) // 类C的一个日志接口
    {
        char leftbuffer[1024];
        std::string levelstr = LevelToString(level);
        std::string currtime = TimeStampExLocalTime();
        std::string idstr = std::to_string(getpid());

        char rightbuffer[1024];
        va_list args; // char *, void *
        va_start(args, format);
        // args 指向了可变参数部分
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);
        va_end(args); // args = nullptr;
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ",
                 levelstr.c_str(), currtime.c_str(), idstr.c_str());

        std::string loginfo = leftbuffer;
        loginfo += rightbuffer;
        WriteLog(levelstr, loginfo);
    }
    // void operator()(int level, const char *format, ...)
    // {
    //     LogMessage(int level, const char *format, ...)
    // }
    ~Log() {}

private:
    int style;
    std::string filename;
};

extern Log lg;

class Conf
{
public:
    Conf()
    {
        lg.Enable(Screen);
    }
    ~Conf()
    {}
};

extern Conf conf;
cpp 复制代码
//Main.cc
#include "HttpProtocol.hpp"
#include "Log.hpp"
#include "Comm.hpp"
#include <memory>
#include <iostream>

using namespace std;

Log lg;
Conf conf;

void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n"
              << std::endl;
}
// ./tcp_server 8888
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }
    uint16_t port = stoi(argv[1]);
    std::unique_ptr<Http> http = make_unique<Http>(port);
    http->Run();
    return 0;
}
cpp 复制代码
//Makefile
http_server:Main.cc
	g++ -o $@ $^ -std=c++14 -lpthread

.PHONY:clean
clean:
	rm -f http_server
cpp 复制代码
//Socket.hpp
#pragma once

#include <cstdlib>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <memory>
#include <vector>

#define Convert(addrptr) ((struct sockaddr *)addrptr)

namespace Net_Work
{
    const static int defaultsockfd = -1;
    const int backlog = 5;

    enum
    {
        SocketError = 1,
        BindError,
        ListenError,
    };

    // 封装一个基类,Socket接口类
    // 设计模式:模版方法类
    class Socket
    {
    public:
        virtual ~Socket() {}
        virtual void CreateSocketOrDie() = 0;
        virtual void BindSocketOrDie(uint16_t port) = 0;
        virtual void ListenSocketOrDie(int backlog) = 0;
        virtual std::shared_ptr<Socket> AcceptConnection(std::string *peerip, uint16_t *peerport) = 0;
        virtual bool ConnectServer(std::string &serverip, uint16_t serverport) = 0;
        virtual int GetSockFd() = 0;
        virtual void SetSockFd(int sockfd) = 0;
        virtual void CloseSocket() = 0;
        virtual bool Recv(std::string *buffer, int size) = 0;
        virtual void Send(std::string &send_str) = 0;
        // TODO
    public:
        void BuildListenSocketMethod(uint16_t port, int backlog)
        {
            CreateSocketOrDie();
            BindSocketOrDie(port);
            ListenSocketOrDie(backlog);
        }
        bool BuildConnectSocketMethod(std::string &serverip, uint16_t serverport)
        {
            CreateSocketOrDie();
            return ConnectServer(serverip, serverport);
        }
        void BuildNormalSocketMethod(int sockfd)
        {
            SetSockFd(sockfd);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket(int sockfd = defaultsockfd) : _sockfd(sockfd)
        {
        }
        ~TcpSocket()
        {
        }
        void CreateSocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
                exit(SocketError);
            int opt = 1;
            setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 地址复用
        }
        void BindSocketOrDie(uint16_t port) override
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));
            local.sin_family = AF_INET;
            local.sin_addr.s_addr = INADDR_ANY;
            local.sin_port = htons(port);

            int n = ::bind(_sockfd, Convert(&local), sizeof(local));
            if (n < 0)
                exit(BindError);
        }
        void ListenSocketOrDie(int backlog) override
        {
            int n = ::listen(_sockfd, backlog);
            if (n < 0)
                exit(ListenError);
        }
        std::shared_ptr<Socket> AcceptConnection(std::string *peerip, uint16_t *peerport) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int newsockfd = ::accept(_sockfd, Convert(&peer), &len);
            if (newsockfd < 0)
                return nullptr;
            if (peerip != nullptr)
            {
                char ip[INET_ADDRSTRLEN];
                if (inet_ntop(AF_INET, &peer.sin_addr, ip, sizeof(ip)) == nullptr)
                {
                    ::close(newsockfd);
                    return nullptr;
                }
                *peerip = ip;
            }
            if (peerport != nullptr)
                *peerport = ntohs(peer.sin_port);
            std::shared_ptr<Socket> sock = std::make_shared<TcpSocket>(newsockfd);
            return sock;
        }
        bool ConnectServer(std::string &serverip, uint16_t serverport) override
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(serverip.c_str());
            server.sin_port = htons(serverport);

            int n = ::connect(_sockfd, Convert(&server), sizeof(server));
            if (n == 0)
                return true;
            else
                return false;
        }
        int GetSockFd() override
        {
            return _sockfd;
        }
        void SetSockFd(int sockfd) override
        {
            _sockfd = sockfd;
        }
        void CloseSocket() override
        {
            if (_sockfd > defaultsockfd)
                ::close(_sockfd);
        }
        bool Recv(std::string *buffer, int size) override
        {
            if (buffer == nullptr || size <= 1)
                return false;
            std::vector<char> inbuffer(size);
            ssize_t n = recv(_sockfd, inbuffer.data(), size-1, 0);
            if(n > 0)
            {
                buffer->append(inbuffer.data(), static_cast<size_t>(n)); // 故意拼接的
                return true;
            }
            else if(n == 0) return false;
            else return false;
        }
        void Send(std::string &send_str) override
        {
            // 多路转接我们在统一说
            send(_sockfd, send_str.c_str(), send_str.size(), 0);
        }
    private:
        int _sockfd;
    };

}
cpp 复制代码
//TcpServer.hpp
#pragma once

#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <functional>
#include <memory>
#include "ThreadPool.hpp"
#include "Socket.hpp"
#include "InetAddr.hpp"


using namespace Net_Work;
const static int default_backlog = 6;
using task_t = std::function<void()>;
using handler_t = std::function<std::string(std::string)>;

class TcpServer
{
public:
    TcpServer(uint16_t port, handler_t handler) : _port(port), _listensock(new TcpSocket()), _isrunning(false), _handler(handler)
    {
    }
    // 都是固定套路
    void Init()
    {
        _listensock->BuildListenSocketMethod(_port, default_backlog);
    }
    // 仅仅处理IO问题,具体协议处理,交给上层处理
    void ProcessConnection(std::shared_ptr<Socket> sock, InetAddr)
    {
        std::string request_str;
        if(sock->Recv(&request_str, 4096)) // 仅仅用来测试,不作为实际参考,实际读取要处理字节流问题
        {
            std::string response = _handler(request_str);
            sock->Send(response);
            sock->CloseSocket();
        }
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            std::string clientip;
            uint16_t clientport;
            std::shared_ptr<Socket> sock = _listensock->AcceptConnection(&clientip, &clientport);
            if (sock == nullptr)
            {
                continue;
            }
            InetAddr addr(clientip, clientport);
            task_t task = std::bind(&TcpServer::ProcessConnection, this, sock, addr);
            ThreadPool<task_t>::GetInstance()->Push(task);
        }
        _isrunning = false;
    }
    ~TcpServer()
    {
    }

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    handler_t _handler;
};
cpp 复制代码
//Thread.hpp
#pragma once

#include <string>
#include <functional>
#include <pthread.h>

// 设计方的视角
//typedef std::function<void()> func_t;
template<class T>
using func_t = std::function<void(T&)>;

template<class T>
class Thread
{
public:
    Thread(const std::string &threadname, func_t<T> func, T &data)
    :_tid(0), _threadname(threadname), _isrunning(false), _func(func), _data(data)
    {}

    static void *ThreadRoutine(void *args) // 类内方法,
    {
        // (void)args; // 仅仅是为了防止编译器有告警
        Thread *ts = static_cast<Thread *>(args);

        ts->_func(ts->_data);

        return nullptr;
    }

    bool Start()
    {
        int n = pthread_create(&_tid, nullptr, ThreadRoutine, this/*?*/);
        if(n == 0) 
        {
            _isrunning = true;
            return true;
        }
        else return false;
    }
    bool Join()
    {
        if(!_isrunning) return true;
        int n = pthread_join(_tid, nullptr);
        if(n == 0)
        {
            _isrunning = false;
            return true;
        }
        return false;
    }
    std::string ThreadName()
    {
        return _threadname;
    }
    bool IsRunning()
    {
        return _isrunning;
    }
    ~Thread()
    {}
private:
    pthread_t _tid;
    std::string _threadname;
    bool _isrunning;
    func_t<T> _func;
    T _data;
};
cpp 复制代码
//ThreadPool.hpp
#pragma once

#include <string>
#include <queue>
#include <vector>
#include <pthread.h>
#include <functional>
#include "Log.hpp"
#include "Thread.hpp"
#include "LockGuard.hpp"

static const int defaultnum = 5;

class ThreadData
{
public:
    ThreadData(const std::string &name) : threadname(name)
    {
    }
    ~ThreadData()
    {
    }

public:
    std::string threadname;
};

template <class T>
class ThreadPool
{
private:
    ThreadPool(int thread_num = defaultnum) : _thread_num(thread_num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        // 构建指定个数的线程
        for (int i = 0; i < _thread_num; i++)
        {
            // 待优化
            std::string threadname = "thread-";
            threadname += std::to_string(i + 1);

            ThreadData td(threadname);

            // Thread<ThreadData> t(threadname,
            //                      std::bind(&ThreadPool<T>::ThreadRun, this, std::placeholders::_1), td);
            // _threads.push_back(t);
            _threads.emplace_back(threadname,
                                  std::bind(&ThreadPool<T>::ThreadRun, this,
                                            std::placeholders::_1),
                                  td);
            lg.LogMessage(Info, "%s is created...\n", threadname.c_str());
        }
    }
    ThreadPool(const ThreadPool<T> &tp) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T>) = delete;

public:
    // 有线程安全问题的
    static ThreadPool<T> *GetInstance()
    {
        if (instance == nullptr)
        {
            LockGuard lockguard(&sig_lock);
            if (instance == nullptr)
            {
                lg.LogMessage(Info, "创建单例成功...\n");
                instance = new ThreadPool<T>();
                instance->Start();
            }
        }

        return instance;
    }
    bool Start()
    {
        // 启动
        for (auto &thread : _threads)
        {
            thread.Start();
            lg.LogMessage(Info, "%s is running ...\n", thread.ThreadName().c_str());
        }

        return true;
    }
    void ThreadWait(const ThreadData &td)
    {
        lg.LogMessage(Debug, "no task, %s is sleeping...\n", td.threadname.c_str());
        pthread_cond_wait(&_cond, &_mutex);
    }
    void ThreadWakeup()
    {
        pthread_cond_signal(&_cond);
    }
    void checkSelf()
    {
        // 1. _task_num > _task_num_high_water && _thread_num < _thread_num_high_water
        // 创建更多的线程,并且更新_thread_num

        // 2. _task_num == _task_num_low_water && _thread_num >= _thread_num_high_water
        // 把自己退出了,并且更新_thread_num
    }
    void ThreadRun(ThreadData &td)
    {
        while (true)
        {
            // checkSelf()
            // checkSelf();
            // 取任务
            T t;
            {
                LockGuard lockguard(&_mutex);
                while (_q.empty())
                {
                    ThreadWait(td);
                    lg.LogMessage(Debug, "thread %s is wakeup\n", td.threadname.c_str());
                }
                t = _q.front();
                _q.pop();
            }
            // 处理任务
            t();
            // lg.LogMessage(Debug, "%s handler task %s done, result is : %s\n",
            //               td.threadname, t.PrintTask().c_str(), t.PrintResult().c_str());
        }
    }
    void Push(T &in)
    {
        LockGuard lockguard(&_mutex);
        _q.push(in);
        ThreadWakeup();
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    // for debug
    void Wait()
    {
        for (auto &thread : _threads)
        {
            thread.Join();
        }
    }

private:
    std::queue<T> _q;
    std::vector<Thread<ThreadData>> _threads;
    int _thread_num;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;

    static ThreadPool<T> *instance;
    static pthread_mutex_t sig_lock;
    // 扩展1:
    // int _thread_num;
    // int _task_num;

    // int _thread_num_low_water;  // 3
    // int _thread_num_high_water; // 10
    // int _task_num_low_water;    // 0
    // int _task_num_high_water;   // 30

    // 扩展2: 多进程+多线程

    // int number{1};
};

template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::sig_lock = PTHREAD_MUTEX_INITIALIZER;

12.1测试Cookie写⼊到浏览器


12.2测试⾃动提交


12.3测试写⼊过期时间

已启用60秒过期测试、修正GMT时间格式,编译通过.实测到期后Cookie消失,首次请求不再携带它.到期前:响应包含expires,请求携带Cookie.

到期后:Cookie列表为空.


12.4测试路径path

匹配子路径,请求头包含Cookie

相似前缀/a/bc不匹配,请求头没有 Cookie


十三、HTTP Session

Session 是服务器用于保存用户会话状态的一种机制,主要用于识别用户身份、维持登录状态等.

简单理解:

  • Cookie:通常保存在浏览器中.
  • Session:会话数据通常保存在服务器中.
  • Session ID:连接浏览器和服务器会话的一把"钥匙".

13.1定义

Session(会话)是服务器用来记录某个用户在一段时间内的访问状态和相关信息的机制.

例如,用户登录网站后,服务器创建Session,保存用户ID、登录状态等信息.

复制代码
Session ID: abc123

Session 数据:
userId = 1001
username = zhangsan
isLogin = true

13.2工作原理

工作过程:

  1. 用户登录网站,浏览器发送用户名和密码.
  2. 服务器验证成功后创建 Session,并生成 Session ID.
  3. 服务器通过 Set-Cookie 将 Session ID 返回给浏览器.
  4. 浏览器保存 Cookie,后续请求自动携带 Session ID.
  5. 服务器通过 Session ID 查找对应会话,从而识别用户.

例如:

复制代码
Set-Cookie: JSESSIONID=abc123; HttpOnly; Secure

后续浏览器发送:

复制代码
Cookie: JSESSIONID=abc123

13.3安全性

Session 数据通常保存在服务端,但Session ID一旦泄露,可能导致会话劫持.

常见安全措施:

安全措施 作用
HTTPS 加密传输,防止网络窃听
HttpOnly 防止 JavaScript 直接读取会话 Cookie
Secure 限制 Cookie 通过安全连接发送
SameSite 降低部分 CSRF 攻击风险
登录后更新 Session ID 防止会话固定攻击

13.4超时和失效

Session 通常不会永久有效.

常见失效情况:

情况 说明
超时失效 一定时间内没有操作,会话过期
主动退出 用户点击退出登录,服务器使会话失效
服务器清理 服务器主动删除或清理 Session
绝对过期 即使用户持续操作,到达期限也会失效

例如,服务器设置 Session 的空闲超时时间为 30 分钟.

如果用户连续 30 分钟没有任何刷新会话有效期的操作,服务器可能使 Session 失效,用户需要重新登录.

注意:浏览器 Cookie 过期和服务器 Session 失效不是同一回事.


13.5用途

Session 常用于:

  • 用户登录:保存登录状态,避免每次请求重新输入密码.
  • 身份认证:识别当前请求属于哪个用户.
  • 权限管理:关联用户角色和权限信息.
  • 购物车管理:保存或关联用户的购物车数据.
  • 临时状态存储:保存一次会话中的临时业务数据.

十四、模拟Session⾏为和实验测试Session

Session 基本流程测试通过:

  • 浏览器保存并自动回传相同的 sessionid.
  • 重复登录没有创建新会话.
  • 独立客户端对应 user-1,原浏览器仍对应 user-0.

浏览器保存 Session ID:

后续请求自动携带:

服务端识别不同用户:

总结:HTTP Cookie和Session都是⽤于在Web应⽤中跟踪⽤户状态的机制.Cookie是存储在客户端的,⽽Session是存储在服务器端的.它们各有优缺点,通常在实际应⽤中会结合使⽤,以达到最佳的⽤户体验和安全性.


十五、HTTPS是什么---概念准备

HTTPS(HyperText Transfer Protocol Secure),中文叫作超文本传输安全协议.

简单理解:HTTPS = HTTP + TLS 加密保护

HTTPS 的作用是在客户端(浏览器)和服务器之间建立安全通信,防止传输的数据被窃听或篡改,并帮助客户端验证服务器身份.

1.HTTP与HTTPS的区别

假设用户在网站上输入用户名和密码.

对比 HTTP HTTPS
安全性 默认明文传输 TLS 加密保护
默认端口 80 443
URL 开头 http:// https://
身份验证 默认没有 通过数字证书等机制
数据完整性 无协议层保护 提供完整性保护
典型用途 本地测试、特殊内部环境 网站、登录、支付、API

2.HTTPS的三大核心作用

  1. 数据加密(Confidentiality)

防止第三方在网络传输过程中直接读取 HTTP 请求和响应内容,例如密码、Cookie 和提交的表单数据.

  1. 身份认证(Authentication)

通过数字证书验证服务器身份,降低连接到假冒服务器的风险.

  1. 数据完整性(Integrity)

通过加密协议的完整性校验,检测数据是否在传输过程中被修改.

3.HTTPS的工作原理

HTTPS 的核心是TLS(Transport Layer Security,传输层安全协议).

以常见的 HTTPS over TCP 为例:

这里要理解两个概念:

  • 非对称密码技术:用于身份认证、数字签名以及安全密钥协商.
  • 对称加密:用于高效加密后续传输的数据.

补充:HTTP/3 使用基于 UDP 的 QUIC,而不是TCP,但同样使用TLS 1.3提供安全保护.

4.HTTPS与HTTP请求报文的关系

之前学习的 HTTP 请求格式:

复制代码
POST /login HTTP/1.1
Host: example.com
Content-Type: application/json

{
  "username": "admin",
  "password": "123456"
}

使用 HTTPS 后,HTTP 的请求方法、请求头、请求体等概念不会因此改变.

变化主要发生在网络传输层面:上述HTTP数据经过TLS加密后再传输.

注意:HTTPS也不是隐藏所有网络信息.例如目标IP地址仍可能被观察到,域名也可能通过 DNS或TLS握手信息暴露.

5.HTTPS 是否绝对安全?

不是.

HTTPS 主要保障传输过程中的安全,但不能自动解决所有安全问题.

例如:

  • 网站本身可能存在漏洞.
  • 用户可能遭遇钓鱼网站.
  • 服务器可能发生数据泄露.
  • 用户可能主动泄露账户密码.

因此,HTTPS 不等于网站绝对安全.


15.1什么是"加密"

加密就是通过特定的算法和密钥,将原本能够看懂的数据转换成难以理解的密文,防止未经授权的人读取数据.

简单理解:加密就是给数据"上一把锁",只有拥有正确密钥的人才能把锁打开,看到原始内容.

加密的基本原理

加密中的三个核心概念

名称 含义
明文 加密前的原始数据
密文 加密后的数据
密钥(Key) 控制加密和解密过程的重要参数

例如:

复制代码
明文:HELLO
密钥:3
密文:KHOOR

只知道加密算法,并不一定能够解密数据;现代安全加密依赖密钥的保密性和足够的密码学强度.


15.2为什么要加密

假设用户登录网站,提交用户名和密码:

复制代码
username=admin&password=123456

使用 HTTP:

数据默认以明文形式传输,网络中的第三方可能读取这些信息.

使用HTTPS:

浏览器通过TLS对HTTP数据进行加密,网络中的第三方即使截获传输数据,通常也无法直接读取其中的用户名和密码.

服务器收到数据后,使用相应的会话密钥解密,再处理登录请求.

需要注意:HTTPS 保护的是传输过程,服务器仍然能够读取解密后的登录信息.


15.3常⻅的加密⽅式

1.对称加密(Symmetric Encryption)

采⽤单钥密码系统的加密⽅法,同⼀个密钥可以同时⽤作信息的加密和解密,这种加密⽅法称为对称加密,也称为单密钥加密,加密和解密使用同一把密钥.

例如:

特点:加解密速度快,适合大量数据传输.

常见算法:AES.

对称加密其实就是通过同⼀个 "密钥",把明⽂加密成密⽂,并且也能把密⽂解密成明⽂.

2.非对称加密(Asymmetric Encryption)

使用一对相互关联的密钥:公钥和私钥.

  • 公钥(Public Key):可以公开.
  • 私钥(Private Key):需要保密.

例如,在支持公钥加密的方案中:

使用公钥加密的数据,需要使用对应的私钥解密.

常见算法:RSA.

现代 HTTPS 通常使用非对称密码技术进行身份认证和密钥协商,再使用对称加密保护实际通信内容.

公钥和私钥是配对的.最⼤的缺点就是运算速度⾮常慢,⽐对称加密要慢很多.

• 通过公钥对明⽂加密,变成密⽂

• 通过私钥对密⽂解密,变成明⽂

也可以反着⽤

• 通过私钥对明⽂加密,变成密⽂

• 通过公钥对密⽂解密,变成明⽂


15.4数据摘要和数据指纹

数据摘要(Message Digest)是通过哈希算法,将任意长度的数据转换成固定长度结果的技术.

数据指纹(Fingerprint)通常就是数据摘要的一种表示或应用,可以用来识别数据、检测数据是否发生变化.

简单理解:数据摘要就像文件的"数字指纹":文件内容发生变化,它的数字指纹通常也会发生明显变化.

1.数据摘要的工作原理

数据摘要通常通过 哈希算法(Hash Algorithm)计算得到.

无论原始数据是一个字符串、一张图片,还是一个几 GB 的文件,使用SHA-256算法,最终都会生成长度为 256 位(32字节)的摘要.

2.数据摘要的四个主要特点

1.固定长度

相同的哈希算法,总是生成固定长度的结果.

例如:

  • MD5:128 位
  • SHA-1:160 位
  • SHA-256:256 位
  • SHA-512:512 位

2.确定性

同样的数据使用同一种算法,计算结果始终相同.

例如:

复制代码
SHA256("hello") = 固定的摘要值
SHA256("hello") = 相同的摘要值

3.雪崩效应

原始数据即使只改变一个字符,计算结果也通常会发生巨大变化.

例如:

复制代码
hello → 摘要 A
Hello → 摘要 B

虽然只是首字母大小写不同,但两个摘要通常相差很大.

4.单向性

密码学哈希函数的设计目标之一是:能够容易地从原始数据计算摘要,但难以从摘要反推出原始数据.

注意,哈希并不是绝对无法被猜测.如果原始数据很简单,例如123456,攻击者仍可能通过字典或暴力尝试找到对应输入.

3.数据摘要与数据指纹的区别

这两个概念关系非常密切.

对比 数据摘要 数据指纹
英文 Message Digest Fingerprint
主要含义 哈希算法计算出来的结果 用于标识或比较数据的摘要
主要作用 表示数据的哈希值 识别数据、检查变化
常见算法 SHA-256、SHA-512 通常同样使用哈希算法

可以理解为:

数据摘要强调"计算结果",数据指纹强调"识别和验证用途".

它们很多时候指的是同一个哈希值.

4.数据摘要有什么用?

1.检查文件完整性

假设你从网上下载一个文件.

网站公布的 SHA-256 摘要为:

复制代码
ABC123...(示意)

你下载文件之后,自己计算 SHA-256.

如果结果一致,说明文件内容与该摘要对应的数据一致的可能性极高.

需要注意,官方摘要本身也必须来自可信渠道.如果攻击者同时替换文件和公布的摘要,单纯比较哈希值就无法防止这种攻击.

2.数字签名

数字签名通常结合哈希算法和非对称密码技术使用.

基本过程:

  1. 对原始数据计算摘要.
  2. 使用私钥生成数字签名.
  3. 接收方通过公钥验证签名.
  4. 验证通过后,可以确认数据受到相应私钥持有者的签名保护,且签名覆盖的内容没有被篡改.

这也是 HTTPS 数字证书和 TLS 身份认证涉及的重要密码学知识.

3.密码存储

网站通常不应该直接保存用户的明文密码.

而应当使用专门的密码哈希算法(如 Argon2id、bcrypt、scrypt),配合随机盐值存储密码验证信息.

注意:不能仅使用普通SHA-256或MD5来安全保存密码.

5.数据摘要与加密的区别(重点)

对比 数据摘要(Hash) 加密(Encryption)
主要目的 数据完整性、识别 数据保密
是否需要密钥 普通哈希不需要 通常需要
是否可逆 设计上难以逆推 使用正确密钥可以解密
输出长度 固定长度 取决于算法及数据
常见算法 SHA-256 AES

加密主要解决"别人能不能看懂数据";哈希主要帮助解决"数据是否发生变化".

此外,普通哈希值本身不能证明是谁发送了数据,要验证消息来源,还需要HMAC或数字签名等机制.

6.实际操作:计算文件的数据指纹

如果使用Linux,可以通过下面的命令计算文件摘要:

复制代码
sha256sum test.txt

系统会输出文件的 SHA-256 摘要.

也可以计算字符串:

复制代码
printf 'hello' | sha256sum

得到:

复制代码
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824  -

改变输入内容后再次计算,摘要通常会明显不同.


15.5数字签名

数字签名是一种利用非对称密码技术,验证数据来源和完整性的安全机制.

简单理解:数字签名就像电子文件上的"防伪签名",可以帮助证明数据是谁签署的,以及数据在签署后有没有被篡改.

它广泛应用于HTTPS、数字证书、电子合同、软件发布等场景.

1.为什么需要数字签名?

假设张三通过网络给李四发送一条消息:

复制代码
请向账户 A 转账 100 元

传输过程中可能遇到两个问题:

  1. 身份伪造:李四怎么确定消息确实由张三签署?
  2. 数据篡改:消息会不会被别人修改成"转账 10000 元"?

数字签名就是为了解决这些问题.

2.数字签名的工作原理(重点)

数字签名主要涉及三个概念:

  • 私钥(Private Key):由签名者保管,用于生成数字签名.
  • 公钥(Public Key):用于验证数字签名,可以公开.
  • 数据摘要(Hash):通过哈希算法计算出的数据指纹.

第一步:发送方生成签名

假设张三发送:

复制代码
转账 100 元

操作流程:

  1. 使用 SHA-256 等哈希算法计算数据摘要.
  2. 使用自己的私钥,按照数字签名算法生成签名.
  3. 将原始消息与数字签名一起发送给李四.

注意:数字签名不等于简单地"用私钥加密摘要",这是便于入门的简化说法.RSA-PSS、ECDSA、Ed25519 等算法的具体签名过程有所不同.

第二步:接收方验证签名

李四收到消息后:

  1. 获取原始消息与数字签名.
  2. 获取并确认张三的可信公钥.
  3. 使用公钥、消息和签名执行验证算法.
  4. 得到验证成功或失败的结果.

如果有人把消息修改成:

复制代码
转账 10000 元

原始签名通常就无法通过验证.

3.数字签名的三大作用

核心作用 说明
身份认证 验证签名由对应私钥生成,结合可信公钥确认签名者身份
完整性验证 检测签署后的数据是否被篡改
不可否认性 在密钥可信、保管妥当等条件下,提供签名行为的证据

需要注意:数字签名并不能防止别人读取消息内容,因为它本身不提供数据加密保密功能.

4.数字签名、数据摘要、加密的区别

这是学习 HTTPS 时非常重要的知识点.

对比 数据摘要 数字签名 加密
主要目的 生成数据指纹 验证来源与完整性 保护数据机密性
是否使用密钥 普通哈希不需要 使用公钥和私钥 使用密钥
能否防止篡改不被发现 配合可信摘要可以检测 可以检测 需要具备完整性保护
是否隐藏原文 否 否 是
常见算法 SHA-256 RSA-PSS、ECDSA、Ed25519 AES

形象记忆:

  • 数据摘要:给文件生成一个"指纹".
  • 数字签名:给文件盖上一个可以验证的"防伪印章".
  • 加密:把文件放进一个上锁的保险箱.

5.数字签名在HTTPS中的作用

当浏览器访问:

复制代码
https://www.example.com

HTTPS 会使用 TLS 建立安全连接.

数字签名主要出现在两个重要环节:

  1. 数字证书验证

网站的数字证书包含公钥等信息,并由证书颁发机构(CA)进行数字签名.

浏览器通过证书链和信任规则,验证证书的真实性,以及公钥与网站身份的关联.

  1. TLS 握手身份认证

在常见的 TLS 1.3 证书认证流程中,服务器还会使用私钥对握手相关数据签名,证明自己持有对应的私钥.

随后,双方协商出用于通信的密钥,使用对称加密保护HTTP数据.

因此要区分:

  • 数字签名:主要负责身份认证和完整性验证.
  • 对称加密:主要保护后续传输的数据内容.

十六、HTTPS的⼯作过程探究

16.1⽅案1---只使⽤对称加密

如果通信双⽅都各⾃持有同⼀个密钥X,且没有别⼈知道,这两⽅的通信安全当然是可以被保证的(除⾮密钥被破解)

引⼊对称加密之后,即使数据被截获,由于⿊客不知道密钥是啥,因此就⽆法进⾏解密,也就不知道请求的真实内容是啥了.但事情没这么简单.服务器同⼀时刻其实是给很多客户端提供服务的.这么多客⼾端,每个⼈⽤的秘钥都必须是不同的(如果是相同那密钥就太容易扩散了,⿊客就也能拿到了).因此服务器就需要维护每个客户端和每个密钥之间的关联关系,这也是个很麻烦的事情.

⽐较理想的做法, 就是能在客户端和服务器建⽴连接的时候,双⽅协商确定这次的密钥是啥

但是如果直接把密钥明⽂传输,那么⿊客也就能获得密钥了,此时后续的加密操作就形同虚设了.因此密钥的传输也必须加密传输!但是要想对密钥进⾏对称加密,就仍然需要先协商确定⼀个 "密钥的密钥". 这就成了 "先有鸡还是先有蛋"的问题了.此时密钥的传输再⽤对称加密就⾏不通了.


16.2⽅案2---只使⽤⾮对称加密

鉴于⾮对称加密的机制,如果服务器先把公钥以明⽂⽅式传输给浏览器,之后浏览器向服务器传数据前都先⽤这个公钥加密好再传,从客户端到服务器信道似乎是安全的(有安全问题),因为只有服务器有相应的私钥能解开公钥加密的数据.但是服务器到浏览器的这条路怎么保障安全?

如果服务器⽤它的私钥加密数据传给浏览器,那么浏览器⽤公钥可以解密它,⽽这个公钥是⼀开始通过明⽂传输给浏览器的,若这个公钥被中间⼈劫持到了,那他也能⽤该公钥解密服务器传来的信息了.


16.3⽅案3---双⽅都使⽤⾮对称加密

服务端拥有公钥S与对应的私钥S',客户端拥有公钥C与对应的私钥C'.客户和服务端交换公钥.客户端给服务端发信息:先⽤S对数据加密,再发送,只能由服务器解密,因为只有服务器有私钥

S'.服务端给客户端发信息:先⽤C对数据加密,在发送,只能由客户端解密,因为只有客户端有私钥C'.这样貌似也⾏啊,但是效率太低,依旧有安全问题.


16.4⽅案4---⾮对称加密+对称加密

服务端具有⾮对称公钥S和私钥S',客户端发起https请求,获取服务端公钥S.客户端在本地⽣成对称密钥C,通过公钥S加密,发送给服务器.由于中间的⽹络设备没有私钥,即使截获了数据,也⽆法还原出内部的原⽂,也就⽆法获取到对称密钥.服务器通过私钥S'解密,还原出客户端发送的对称密钥C.并且使⽤这个对称密钥加密给客户端返回的响应数据.后续客户端和服务器的通信都只⽤对称加密即可.由于该密钥只有户端和服务器两个主机知道,其他主机/设备不知道密钥即使截获数据也没有意义.由于对称加密的效率⽐⾮对称加密⾼很多,因此只是在开始阶段协商密钥的时候使⽤⾮对称加密,后续的传输仍然使⽤对称加密.虽然上⾯已经⽐较接近答案了,但是依旧有安全问题⽅案2,⽅案3,⽅案4都存在⼀个问题,如果最开始,中间⼈就已经开始攻击了呢?


①中间⼈攻击---针对上⾯的场景

Man-in-the-MiddleAttack,简称"MITM攻击"(中间人攻击)是一种"间接"的入侵攻击,这种攻击模式是通过各种技术手段将受入侵者控制的一台计算机虚拟放置在网络连接中的两台通信计算机之间,这台计算机就称为"中间人".

确实,在⽅案2/3/4中,客户端获取到公钥S之后,对客户端形成的对称秘钥X⽤服务端给客户端的公钥S进⾏加密,中间⼈即使窃取到了数据,此时中间⼈确实⽆法解出客⼾端形成的密钥X,因为只有服务器有私钥S'但是中间⼈的攻击,如果在最开始握⼿协商的时候就进⾏了,那就不⼀定了,假设hacker已经成功成为中间⼈.

  1. 服务器具有⾮对称加密算法的公钥S,私钥S'.
  2. 中间⼈具有⾮对称加密算法的公钥M,私钥M'.
  3. 客户端向服务器发起请求,服务器明⽂传送公钥S给客户端.
  4. 中间⼈劫持数据报⽂,提取公钥S并保存好,然后将被劫持报⽂中的公钥S替换成为⾃⼰的公钥M,并将伪造报⽂发给客户端.
  5. 客户端收到报⽂,提取公钥M(⾃⼰当然不知道公钥被更换过了),⾃⼰形成对称秘钥X,⽤公钥M加密X,形成报⽂发送给服务器.
  6. 中间⼈劫持后,直接⽤⾃⼰的私钥M'进⾏解密,得到通信秘钥X,再⽤曾经保存的服务端公钥S加密后,将报⽂推送给服务器.
  7. 服务器拿到报⽂,⽤⾃⼰的私钥S'解密,得到通信秘钥X.
  8. 双⽅开始采⽤X进⾏对称加密,进⾏通信.但是⼀切都在中间⼈的掌握中,劫持数据,进⾏窃听甚⾄修改,都是可以的上⾯的攻击⽅案,同样适⽤于⽅案2,⽅案3.

问题本质出在哪⾥了呢?客户端⽆法确定收到的含有公钥的数据报⽂,就是⽬标服务器发送过来的!


②引⼊证书

CA认证

CA认证,即电子认证服务,是指为电子签名相关各方提供真实性、可靠性验证的活动.证书颁发机构(CA, Certificate Authority)即颁发数字证书的机构.是负责发放和管理数字证书的权威机构,并作为电子商务交易中受信任的第三方,承担公钥体系中公钥的合法性检验的责任.

服务端在使⽤HTTPS前,需要向CA机构申领⼀份数字证书,数字证书⾥含有证书申请者信息、公钥信息等.服务器把证书传输给浏览器,浏览器从证书⾥获取公钥就⾏了,证书就如⾝份证,证明服务端.

公钥的权威性

这个证书可以理解成是⼀个结构化的字符串,⾥⾯包含了以下信息:证书发布机构;证书有效期;公钥;证书所有者;签名...需要注意的是:申请证书的时候,需要在特定平台⽣成查,会同时⽣成⼀对⼉密钥对⼉,即公钥和私钥.这对密钥对⼉就是⽤来在⽹络通信中进⾏明⽂加密以及数字签名的.其中公钥会随着CSR⽂件,⼀起发给CA进⾏权威认证,私钥服务端⾃⼰保留,⽤来后续进⾏通信(其实主要就是⽤来交换对称秘钥)

在线⽣成CSR和私钥

形成CSR之后,后续就是向CA进⾏申请认证,不过⼀般认证过程很繁琐,⽹络各种提供证书申请的服务商,⼀般真的需要,直接找平台解决就⾏.


③理解数据签名

签名的形成是基于⾮对称加密算法的,注意,⽬前暂时和https没有关系,不要和https中的公钥私钥搞混了.

当服务端申请CA证书的时候,CA机构会对该服务端进⾏审核,并专⻔为该⽹站形成数字签名,过程如下:

  1. CA机构拥有⾮对称加密的私钥A和公钥A'.
  2. CA机构对服务端申请的证书明⽂数据进⾏hash,形成数据摘要.
  3. 然后对数据摘要⽤CA私钥A'加密,得到数字签名S服务端申请的证书明⽂和数字签名S 共同组成了数字证书,这样⼀份数字证书就可以颁发给服务端了.

16.5⽅案5---⾮对称加密+对称加密+证书认证

在客户端和服务器刚⼀建⽴连接的时候,服务器给客⼾端返回⼀个证书,证书包含了之前服务端的公钥,也包含了⽹站的⾝份信息.


①客户端进⾏认证

当客户端获取到这个证书之后,会对证书进⾏校验(防⽌证书是伪造的).

• 判定证书的有效期是否过期.

• 判定证书的发布机构是否受信任(操作系统中已内置的受信任的证书发布机构).

• 验证证书是否被篡改: 从系统中拿到该证书发布机构的公钥,对签名解密,得到⼀个 hash 值(称为数据摘要),设为hash1.然后计算整个证书的hash值,设为hash2.对⽐hash1和hash2是否相等.如果相等,则说明证书是没有被篡改过的.


②查看浏览器的受信任证书发布机构

Chrome浏览器,点击右上⻆的选择"设置",搜索"证书管理",即可看到以下界⾯.(如果没有,在隐私设置和安全性->安全⾥⾯找找)


③中间⼈有没有可能篡改该证书

中间⼈篡改了证书的明⽂.由于他没有CA机构的私钥,所以⽆法hash之后⽤私钥加密形成签名,那么也就没法办法对篡改后的证书形成匹配的签名.如果强⾏篡改,客户端收到该证书后会发现明⽂和签名解密后的值不⼀致,则说明证书已被篡改,证书不可信,从⽽终⽌向服务器传输信息,防⽌信息泄露给中间⼈.


④中间⼈整个掉包证书

因为中间⼈没有CA私钥,所以⽆法制作假的证书.所以中间⼈只能向CA申请真证书,然后⽤⾃⼰申请的证书进⾏掉包.这个确实能做到证书的整体掉包,但是别忘记,证书明⽂中包含了域名等服务端认证信息,如果整体掉包,客户端依旧能够识别出来.永远记住:中间⼈没有CA私钥,所以对任何证书都⽆法进⾏合法修改,包括⾃⼰的.


十七、常⻅问题

17.1为什么摘要内容在⽹络传输的时候⼀定要加密形成签名?

  1. 性能。非对称运算比哈希慢三到四个数量级.RSA-2048 一次私钥运算约在毫秒级,SHA-256 处理同样的数据是百纳秒级.一个几兆的报文直接签名,在握手、流式传输场景里完全不可接受;摘要固定32字节,签名成本与报文大小无关.
  2. 长度与协议友好。签名值长度固定(RSA-2048为256字节,ECDSA-P256约64字节),便于放进固定长度的头部、证书和 TLV 结构.
  3. 安全性上没有损失。只要哈希满足抗碰撞性(collision resistance),"签名 H(M)"与"签名 M"在语义上等价,因为找到另一个 M′ 使 H(M′)=H(M) 计算上不可行.哈希在这里承担"压缩 + 唯一绑定"两个职责.

摘要负责"内容没变",私钥签名负责"这话是我说的且赖不掉",两者缺一,传输链路就只剩一个可被任意重算的校验和.

但是还有个问题,如果⿊客把hello篡改了,同时也把哈希值重新计算下,客户端就分辨不出来了呀.

所以被传输的哈希值不能传输明⽂,需要传输密⽂.所以,对证书明⽂(这⾥就是"hello")hash形成散列摘要,然后CA使⽤⾃⼰的私钥加密形成签名,将hello和加密的签名合起来形成CA证书,颁发给服务端,当客户端请求的时候,就发送给客户端,中间⼈截获了,因为没有CA私钥,就⽆法更改或者整体掉包,就能安全的证明,证书的合法性.最后,客户端通过操作系统⾥已经存的了的证书发布机构的公钥进⾏解密,还原出原始的哈希值,再进⾏校验.


17.2为什么签名不直接加密,⽽是要先hash形成摘要?

签名并不等于加密:在ECDSA、EdDSA 这类签名算法中,签名运算的输入本身就是一个定长整数,物理上无法直接接受任意长度的原文;而RSA的原始签名满足S(a)·S(b)=S(ab)的乘法同态关系,攻击者只要骗到两个因子的签名并相乘,就能拼出目标消息的合法签名,长消息还必须分块,分块后各块可以被删除或重排.先做哈希,就是把变长消息压成一个定长且不可分割的原子,利用单向性破坏这套代数结构,让伪造难度取决于能否解出哈希原像或找到哈希碰撞;性能上的节省只是顺带得到的好处,并不是这样做的根本原因.


17.3如何成为中间⼈?

成为中间人靠的是控制路径,而非破解密码:同一网段做 ARP 欺骗或假DHCP,伪造同名热点,劫持DNS,或在终端装一张恶意根证书.但今天流量几乎全是TLS,仅仅截获没有用,你转发时必须拿出一张能让客户端验过的证书,而你签不出对方域名的证书.所以真正的门槛不在网络位置,而在让客户端信任你的 CA------恶意根证书、企业 TLS 中间盒、MDM 下发,这才是主流路径.证书和签名的作用也不是阻止你转发,而是让你的篡改暴露出来;因此MITM攻防的胜负手始终是信任锚,而非加密强度.


十八、HTTPS安全连接完整流程

🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容


每日心灵鸡汤: 守住内心的光亮,积极面对生活!

如何在生活中保持积极的心态?遇到烦心事不钻牛角尖,学会及时疏导负面情绪,不任由烦恼不断发酵.接纳生活与学习里的不完美,明白挫折本就是成长路上的常态.多留意身边微小的美好,把每一次小小的进步都当作值得肯定的收获.遇到困难先想解决办法,而不是一味陷入焦虑和自我怀疑.少和他人盲目攀比,专注走好属于自己的成长道路.累的时候适当休息调整,养足精神再继续前行.守住内心的光亮,积极的心态便能长久相伴.

相关推荐
北风toto1 小时前
Linux 服务器通过 tgz 离线部署 Docker
linux·服务器·docker
重生之小比特1 小时前
【Linux系统】13 基础IO(上)
linux·运维·服务器
CCCCCCCCharlie1 小时前
从0到1:一个字节的磁盘之旅
linux·开发语言·c++
启博网络1 小时前
IPv4枯竭、NAT穿透卡顿?用公网IPv6直连搭建异地组网,速度直接起飞
网络·远程安全访问·启博
LuminousCPP1 小时前
数据库学习笔记(二):MySQL 基础操作入门|在 CentOS Stream 10 上连接、建库与建表
数据库·经验分享·笔记·学习·mysql·centos
羑悻的小杀马特1 小时前
超越Bridge:全面掌握Docker网络隔离与通信的底层逻辑!
运维·网络·docker·容器
运维小斌1 小时前
麒麟 V10 ARM64 离线部署 OceanBase:8 步实施手册
linux·数据库·oceanbase·麒麟
着迷不白1 小时前
生产环境标准的Linux系统安装到服务器
linux·云计算
艾莉丝努力练剑1 小时前
【AI大模型接入SDK】ChatServer接口实现
c++·人工智能·学习·ai·架构·大模型·sdk