HTTP协议
虽然我们说,应⽤层协议是我们程序猿⾃⼰定的.但实际上,已经有⼤佬们定义了⼀些现成的,⼜⾮常好 ⽤的应⽤层协议,供我们直接参考使⽤.HTTP(超⽂本传输协议)就是其中之⼀。
在互联⽹世界中,HTTP(HyperTextTransferProtocol,超⽂本传输协议)是⼀个⾄关重要的协议。 它定义了客⼾端(如浏览器)与服务器之间如何通信,以交换或传输超⽂本(如HTML⽂档)。
HTTP协议是客⼾端与服务器之间通信的基础。客⼾端通过HTTP协议向服务器发送请求,服务器收到 请求后处理并返回响应。HTTP协议是⼀个⽆连接、⽆状态的协议,即每次请求都需要建⽴新的连接, 且服务器不会保存客⼾端的状态信息。
![]()
我们手机,电脑上的浏览器,访问网页,用的就是HTTP协议。
本质也是socket通信!
先认识一下HTTP结构:


认识URL
URL 全称 统一资源定位符(Uniform Resource Locator),也就是我们常说的「网址」。它是互联网上用来唯一标识、定位资源的地址,就像资源的 “门牌号”—— 浏览器拿到这个地址,才知道该去哪找、取回你要的内容。
URL 与 HTTP 的关系
HTTP 和 URL 是配合工作的一对搭档:
- HTTP:负责「怎么传」—— 规定客户端和服务器之间的对话、数据传输规则。
- URL:负责「找谁 + 拿什么」—— 告诉客户端要访问哪台服务器、哪个资源、附带什么信息。
当你在浏览器输入网址时,浏览器会自动从 URL 里拆解出协议、服务器地址、资源路径等信息,再构建出对应的 HTTP 请求发送给服务器。
URL 的完整结构

1. 协议(方案名)
- 示例内容:
http:// - 核心作用:规定客户端和服务器之间的通信规则,也就是 “用什么方式访问资源”
- 常见类型:网页场景主流是
http/https,还有文件传输用的ftp、邮件用的mailto等
2. 登录信息(认证)
- 示例内容:
user:pass@ - 核心作用:给服务器传递基础认证信息,格式为
用户名:密码@ - 补充说明:这部分是可选的,现在几乎不再使用,因为明文传输账号密码存在严重安全隐患,现代网站会用更安全的方式做登录验证。
3. 服务器地址 + 端口号
- 服务器地址示例:
www.example.jp- 作用:服务器的地址标识,可以是域名(会被 DNS 解析为 IP)或直接的 IP 地址
- 端口号示例:
:80- 作用:区分服务器上的不同服务,相当于服务的 “房间号”
- 补充:HTTP 默认端口是 80,HTTPS 默认是 443,使用默认端口时浏览器会自动补全,地址栏不会显示
4. 带层次的文件路径
- 示例内容:
/dir/index.htm - 核心作用:资源在服务器上的具体位置,类似文件系统的目录结构,告诉服务器你要访问哪个文件
- 补充说明:如果路径省略具体文件,服务器通常会返回默认的首页文件(比如
index.html)
5. 查询字符串
- 示例内容:
?uid=1 - 核心作用:给服务器传递附加参数,常用于搜索、筛选、分页等场景
- 格式规则:以
?开头,多个参数用&分隔,格式为key=value
6. 片段标识符(锚点)
- 示例内容:
#ch1 - 核心作用:定位页面内部的某个位置,让浏览器直接滚动到对应的章节
- 补充说明:这部分仅在浏览器本地生效,不会被发送到服务器
上面这种能完整展示URL所有组成部分的公开网站几乎不存在;
现在都是这种:![]()
为什么公开网站不会用完整 URL?
- 认证信息不安全:
user:pass@会把账号密码明文写在 URL 里,会被服务器日志、浏览器历史记录保存,极易泄露,现在所有网站都用 Cookie/Token 代替。 - 默认端口没必要写:HTTPS 默认用 443 端口,HTTP 默认用 80 端口,浏览器会自动补全,地址栏写出来反而不简洁。
HTTP常见请求方法
HTTP 方法(也叫 “动作 / 谓词”)是客户端向服务器发送的「操作指令」,它和 URL 一起,明确告诉服务器「你要对哪个资源、做什么操作」。
举个直观例子:POST /api/users HTTP/1.1,这里的POST就是方法,/api/users是资源路径,两者结合,服务器就知道客户端要创建新用户了。
5 个开发中最常用的 HTTP 方法
这 5 个方法正好对应了数据的「增删改查」操作,每个都有明确的语义和使用场景:
| 方法 | 核心语义 | 典型使用场景 | 是否带请求体 | 安全(是否只读) | 幂等(重复执行结果不变) |
|---|---|---|---|---|---|
| GET | 获取资源 | 访问网页、查询列表、请求图片 / JSON 数据 | ❌ 无(参数放在 URL 里) | ✅ 安全 | ✅ 幂等 |
| POST | 创建 / 提交数据 | 提交表单、用户登录、上传文件、创建新订单 | ✅ 有 | ❌ 不安全 | ❌ 非幂等 |
| PUT | 整体更新(替换)资源 | 更新用户全部信息、上传文件到指定路径 | ✅ 有 | ❌ 不安全 | ✅ 幂等 |
| PATCH | 部分修改资源 | 只改用户密码、只修改昵称 | ✅ 有 | ❌ 不安全 | ❌ 通常非幂等 |
| DELETE | 删除资源 | 删除文章、删除用户账号、取消订单 | ❌ 一般无(也可带) | ❌ 不安全 | ✅ 幂等 |
两个关键补充概念
这两个属性是区分不同方法的核心,也是开发中容易踩坑的点:
- 安全:指请求不会改变服务器的状态,只是纯粹读取数据。
- 只有
GET是安全的,POST/PUT/DELETE都会修改服务器数据,所以不安全。
- 只有
- 幂等:指同一个请求执行多次,和执行一次的结果完全相同。
GET/PUT/DELETE是幂等的:比如多次 GET 同一个文章,结果都一样;多次 PUT 更新用户信息,结果也一样;多次 DELETE 同一个用户,第一次删除成功,后面再删都是 “已删除”,不会重复删除。POST不是幂等的:重复提交两次订单,会生成两个不同的订单,结果会变。
GET 方法
- 语义:请求获取服务器上的指定资源,纯读取操作
- 本质:安全、幂等,多次请求无副作用,结果始终一致
- 典型请求(查询用户信息):
GET /api/users/123 HTTP/1.1
Host: api.example.com
常见响应:200 OK(返回资源数据)、304 Not Modified(使用缓存)
POST 方法
- 语义:向服务器提交数据,用于创建新资源或处理业务逻辑
- 本质:非安全、非幂等,多次请求可能创建多个独立资源
- 典型请求(创建新用户):
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"name":"张三","email":"zs@example.com"}
常见响应:201 Created(创建成功)、200 OK
PUT 方法
- 语义:用请求体完整替换指定资源(资源不存在时,可创建新资源)
- 本质:幂等,发送一次和多次,最终资源状态一致(每次都是整体覆盖)
- 典型请求(更新整个用户):
PUT /api/users/123 HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"name": "李四", "email": "lisi@example.com", "age": 30}
常见响应:
200 OK:返回更新后的资源数据204 No Content:更新成功,不返回响应体201 Created:资源不存在,创建成功
PATCH 方法
- 语义:对资源进行部分更新,请求体中仅包含要修改的内容
- 本质:通常非幂等(也可设计为幂等,取决于补丁格式)
- 两种常见补丁格式:
- JSON Merge Patch (RFC 7396):直接发送要合并的 JSON 对象,如
{"email": "new@ex.com"},后端自动合并到已有对象 - JSON Patch (RFC 6902):通过一组操作指令描述修改,更精准
- JSON Merge Patch (RFC 7396):直接发送要合并的 JSON 对象,如
- 典型请求(JSON Patch 修改用户信息):
PATCH /api/users/123 HTTP/1.1
Content-Type: application/json-patch+json
[
{"op": "replace", "path": "/email", "value": "new@ex.com"},
{"op": "remove", "path": "/age"}
]
常见响应:200 OK、204 No Content
DELETE 方法
- 语义:请求服务器删除指定资源
- 本质:幂等,删除一次和多次,资源最终状态都是不存在(第二次请求可能返回 404,但结果一致)
- 典型请求(删除指定用户):
DELETE /api/users/123 HTTP/1.1
Host: api.example.com
常见响应:204 No Content、404 Not Found
HTTP 状态码
HTTP 状态码是服务器在响应中返回的一个三位数字,用来简明扼要地告诉客户端请求的结果。它配合之前讲解的请求方法(GET、POST 等),构成了请求 - 处理 - 结果的完整闭环。不论是打开网页、调用 API 还是抓取数据,读懂状态码都是排查问题的第一步。
状态码的分类
| 类别 | 范围 | 含义 | 典型场景 |
|---|---|---|---|
| 1xx | 100 - 199 | 信息响应 | 请求已接收,告知客户端可继续后续处理 |
| 2xx | 200 - 299 | 成功 | 请求被服务器成功接收、理解并处理完毕 |
| 3xx | 300 - 399 | 重定向 | 需要客户端额外跳转操作,才能完成最终请求 |
| 4xx | 400 - 499 | 客户端错误 | 请求存在格式 / 权限等问题,服务器无法完成请求 |
| 5xx | 500 - 599 | 服务器错误 | 服务器处理请求时发生故障,属于服务端内部问题 |
1xx 信息响应类
100 Continue
- 含义:服务器已接收并认可你的请求头部,允许客户端继续发送后续的请求体数据
- 适用:POST/PUT 上传大文件这类需要传输大容量请求体的场景
- 典型场景:客户端先发送请求头试探连通性,收到 100 后再推送几 MB 级别的文件内容
2xx 成功类
200 OK
- 含义:请求成功,服务器正常返回本次请求对应的资源数据
- 适用:GET 拉取页面 / 接口数据、PUT 更新资源、PATCH 修改资源等绝大多数成功场景
- 注意:POST 创建新资源行业惯例返回 201,使用 200 也符合规范
201 Created
- 含义:请求成功执行,且服务器新建了一个资源
- 典型:POST 创建用户、发布文章,响应必须携带
Location头部,指向新创建资源的 URL - 示例:
HTTP/1.1 201 Created
Location: /api/users/456
204 No Content
- 含义:请求成功处理,但响应主体不携带任何内容(空 Body)
- 典型:DELETE 删除资源、PUT 更新资源且不需要回传数据的场景
- 后果:浏览器收到该状态会停留在原页面,不会自动刷新跳转,常用于 AJAX 无刷新操作
206 Partial Content
- 含义:服务器仅返回了请求的部分资源,多用于分块下载、断点续传
- 条件:客户端请求携带
Range: bytes=1000-分段请求头,服务器响应 206 并配套Content-Range头部标注分段范围
3xx 重定向类
301 Moved Permanently
- 含义:资源永久迁移到新 URL(新地址在响应
Location头部标明) - 浏览器行为:GET 方法会自动跳转,搜索引擎会更新收录链接、替换旧 URL;POST 跳转大多会转为 GET 请求跳转
- 典型场景:网站永久更换域名、旧接口长期下线迁移
302 Found
- 含义:资源临时迁移到新地址,后续请求仍可使用原 URL
- 浏览器行为:GET 自动跳转,POST 跳转通常会改成 GET 请求
- 典型场景:未登录临时跳转到登录页、活动页面临时更换地址
303 See Other
- 含义:请求已处理完成,需要用 GET 方法访问
Location里的新地址获取结果 - 典型场景:POST 提交表单后,引导用户用 GET 查看结果页,避免刷新重复提交
304 Not Modified
- 含义:客户端本地缓存的资源没有更新,可以直接复用缓存,服务器不重复返回资源内容
- 典型场景:静态图片、JS/CSS 文件的缓存协商,节省带宽开销
307 Temporary Redirect
- 含义:资源临时跳转,严格保留原请求方法跳转(POST 跳转仍用 POST 请求新地址)
- 典型场景:POST 接口临时迁移,需要保留请求方法和请求体完成跳转
4xx 客户端错误类
400 Bad Request
- 含义:客户端请求格式错误、参数非法,服务器无法解析请求
- 典型场景:JSON 请求体语法写错、URL 参数类型不匹配、传参超出接口规则限制
401 Unauthorized
- 含义:请求缺少合法身份凭证,需要完成登录认证才能访问
- 典型场景:接口未携带 Token 令牌、登录 Session 过期,前端一般会引导跳转登录页
403 Forbidden
- 含义:身份认证通过,但当前账号没有访问该资源的权限
- 典型场景:普通用户尝试进入管理员后台、修改其他用户的私有数据
404 Not Found
- 含义:请求的 URL 对应的资源不存在、已删除或路径书写错误
- 典型场景:网址拼写错误、接口路径写错、访问已下架的文章
405 Method Not Allowed
- 含义:当前接口不支持你使用的 HTTP 请求方法
- 典型场景:对只接受 GET 的接口发送 POST 请求、对只读接口使用 DELETE 方法
5xx 服务器错误类
500 Internal Server Error
- 含义:服务器内部出现未捕获的异常、代码逻辑崩溃
- 典型场景:接口代码出现 BUG、数据库查询报错、业务逻辑处理异常
502 Bad Gateway
- 含义:网关 / 反向代理从上游业务服务器拿到了无效的非法响应
- 典型场景:Nginx 代理的后端服务宕机、上游服务崩溃无法返回合法数据
503 Service Unavailable
- 含义:服务器暂时无法提供服务,多为维护停机、高并发限流
- 典型场景:网站定时运维维护、大促活动流量过载触发限流保护
504 Gateway Timeout
- 含义:网关等待上游服务器响应超时
- 典型场景:后端接口处理耗时过长、数据库查询卡死导致代理等待超时
服务器出问题,一般不直接返回 5xx 状态码
核心原因有两点:
- 暴露风险:直接返回 5xx,相当于把 “服务器内部故障” 的状态公开给用户,会引来攻击者的恶意扫描、攻击,让服务面临更大风险。
- 安全伪装:服务器崩溃 / 故障时,通常会伪装成
4xx客户端错误(比如 404、403),让用户误以为是 “自己的请求有问题”,既不暴露服务状态,也不泄露故障细节。
HTTP 版本
HTTP 协议从诞生至今经历了多个版本演进,每次大版本更新都为了解决上一代的核心痛点 —— 尤其是传输速度和连接效率。目前共有五个主要版本:0.9、1.0、1.1、2、3,其中 HTTP/1.1 是统治时间最长的经典版本,HTTP/2 和 HTTP/3 已是现代主流,如今实际使用最广泛的仍是 HTTP/1.1。
HTTP/0.9
- 含义:HTTP 的最初极简版本,仅支持基础网页传输
- 核心特点:只支持 GET 请求,无请求头、状态码,响应也只有纯 HTML 内容,连接用完立即断开,只能传输简单文本页面,现已完全淘汰
HTTP/1.0
- 含义:第一个标准化的 HTTP 版本
- 核心改进:新增状态码、请求头、POST/HEAD 等方法,支持传输图片、文本等多种类型资源
- 局限:每个请求都需新建独立 TCP 连接,并发请求时会严重卡顿,现在已基本不用
HTTP/1.1
- 含义:HTTP 的经典版本,也是目前使用最广泛的版本
- 核心改进:
- 支持长连接(Keep-Alive),一个 TCP 连接可复用处理多个请求
- 新增缓存控制、断点续传、Host 头部,解决了虚拟主机多域名共用 IP 的问题
- 现状:至今仍是绝大多数网站的基础协议
HTTP/2
- 含义:基于 HTTP/1.1 语义优化的高性能版本
- 核心改进:
- 改用二进制传输,支持多路复用,同一连接可并行处理多个请求,解决了 HTTP/1.1 的 “队头阻塞” 问题
- 新增头部压缩、服务器推送,大幅降低传输开销
- 现状:现代网站广泛支持,是当前主流版本之一
HTTP/3
- 含义:最新一代 HTTP 协议,基于 QUIC 协议实现
- 核心改进:底层抛弃 TCP 改用 UDP,解决了 TCP 握手延迟和队头阻塞问题,连接建立更快、丢包对整体传输影响更小,弱网环境下体验大幅提升
- 现状:逐步普及中,是未来 HTTP 协议的主流方向
HTTP报头
HTTP 报头(Headers)是 HTTP 请求和响应中携带的元数据,用来传递额外的控制信息 —— 客户端通过它告诉服务器自己的身份、能接受的数据格式;服务器也用它告诉浏览器返回内容的类型、处理方式。
通用报头
1. Connection
- 作用:控制 TCP 连接的复用策略
- 说明:HTTP/1.1 默认开启
keep-alive(长连接,可复用同一连接处理多个请求);HTTP/1.0 默认close(请求完成立即断开);也可用于发起协议升级请求,如Upgrade: h2c升级到 HTTP/2
2. Transfer-Encoding
- 作用:说明消息体的传输编码方式
- 说明:最核心的取值是
chunked(分块传输),多用于动态生成的响应场景,服务器无需提前知道响应总长度,可边生成边分块返回内容
3. Date
- 作用:标识响应生成的服务器时间
- 说明:所有服务器响应都必须携带,格式遵循标准 HTTP-date 规范,例如
Date: Tue, 15 Nov 2023 08:12:31 GMT
请求报头(客户端→服务器)
1. Host(HTTP/1.1 强制要求)
- 作用:声明目标服务器的域名与端口(非默认端口需明确写出)
- 说明:同一 IP 可托管多个不同站点,服务器依靠 Host 头区分请求对应的目标站点;示例:
Host: www.example.com:8080
2. User-Agent
- 作用:标识客户端的软件类型、版本及运行环境
- 说明:服务器可通过它做页面兼容适配、爬虫识别与限制;示例:
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0
3. Accept 系列(内容协商)
- 作用:客户端向服务器声明自己能接受的响应格式,由服务器决定返回最合适的内容
- 包含三类:
Accept:可处理的 MIME 数据类型,如text/html, application/jsonAccept-Encoding:支持的压缩算法,如gzip, deflate, brAccept-Language:偏好的语言类型,可带优先级权重,如zh-CN,zh;q=0.9,en;q=0.8
4. Referer
- 作用:标识请求的来源页面 URL
- 说明:从 A 页面跳转到 B 页面时,B 的请求会携带 Referer 为 A 的地址;常被用于防盗链校验、访问溯源分析;注意正确拼写为
Referer,而非Referrer
5. Authorization
- 作用:向服务器传递用户的身份认证信息,用于接口鉴权、身份校验
- 说明:主流有两种实现方式:
- 基础认证:
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==,值为用户名:密码的 Base64 编码 - Bearer Token:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...,值为服务端下发的身份令牌,常用于 API 接口的权限验证
- 基础认证:
6. Cookie(重要)
- 作用:将服务器之前通过
Set-Cookie下发的会话信息回传,维持客户端与服务器的会话状态 - 说明:示例
Cookie: session_id=abc123; theme=dark;网站的自动登录、个性化设置、会话保持都依赖它,浏览器会自动携带域名对应的 Cookie,过期或被清除后则无法维持登录状态
7. Cache-Control
- 作用:向服务器和中间代理发送缓存控制指令,约定本次请求响应的缓存规则
- 常见取值说明:
no-cache:使用缓存前,必须先向服务器验证资源是否更新no-store:完全不缓存本次请求和响应的任何内容max-age=3600:设置本次响应的缓存有效期为 3600 秒(1 小时)
响应报头(服务器→客户端)
1. Content-Type
- 作用:告知客户端响应正文的 MIME 数据类型与字符编码
- 说明:示例
Content-Type: application/json; charset=utf-8;这个报头至关重要,若缺失或错误,浏览器可能会把 JSON 当成普通文本解析,或出现中文乱码问题
2. Content-Length
- 作用:告知客户端响应正文的总字节长度
- 说明:用于非分块传输的场景,客户端可根据该值判断响应是否接收完整;当响应使用
Transfer-Encoding: chunked分块传输时,无需携带该报头
3. Content-Encoding
- 作用:告知客户端响应正文使用的压缩编码方式,客户端需按此规则解压解析
- 说明:示例
Content-Encoding: gzip;需注意和Transfer-Encoding的区别:前者是内容本身的压缩编码,用于减少传输体积;后者是传输过程中的分块编码,用于动态响应的分段传输,两者作用层级不同
4. Set-Cookie
- 作用:服务器向客户端写入会话 Cookie,用于后续请求的会话状态保持
- 说明:可携带过期时间、域名、路径、安全属性等控制参数,示例
Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/;客户端会自动将符合规则的 Cookie 携带到后续请求中
5. Location
- 作用:在 3xx 重定向或 201 Created 响应中,指定目标资源的新 URL
- 说明:示例
Location: /users/456;客户端收到该报头后,会根据状态码自动跳转或访问新地址
HTTP 正文
HTTP 正文(Message Body)是请求或响应中真正传输的数据。如果说请求行、状态行和头部像快递信封上的地址说明,正文就是信封里的货物,可承载网页、JSON、表单数据、图片、视频、二进制文件等任意形式的内容,其具体含义由Content-Type头部声明。
补充说明:并非所有请求 / 响应都有正文:
GET/HEAD请求本身无正文,参数放在 URL 中204 No Content/304 Not Modified响应无正文
| 类别 | Content-Type 示例 | 用途说明 |
|---|---|---|
| 纯文本 | text/plain | 简单文本消息 |
| HTML | text/html | 网页主体内容 |
| JSON | application/json | API 数据交互(REST 接口最常用格式) |
| XML | application/xml / text/xml | SOAP 协议、RSS 订阅、配置数据 |
| URL 编码表单 | application/x-www-form-urlencoded | 传统表单提交,格式类似key1=val1&key2=val2 |
| 多部分表单 | multipart/form-data | 文件上传场景,可包含多个独立部分(文本 + 文件) |
| 二进制数据 | application/octet-stream | 通用二进制流,常用于文件下载 |
| 媒体资源 | image/png、video/mp4、audio/mpeg | 图片、视频、音频等多媒体内容 |
如何设置正文?
处理正文的关键在于理解这几个核心头部:
Content-Type— 决定内容格式告知接收方 “发送的是什么类型的数据、字符集是什么”,例如Content-Type: application/json; charset=utf-8;若缺失,浏览器可能会通过 MIME 嗅格猜测类型,易出现解析错误,建议明确指定。Content-Length— 标记正文大小声明正文的未压缩字节数,接收方可据此判断数据是否接收完整,是持久连接中界定响应边界的关键方式之一。Transfer-Encoding: chunked— 分块传输正文长度无法提前确定时,服务器将正文分成多个块发送,每块前标注十六进制长度,最后以大小为 0 的块结束,与Content-Length是两种互斥的界定方式。Content-Encoding— 压缩标记正文经过压缩时(如gzip/deflate/brotli),需通过该头部声明,接收方需先解压再解析内容;注意它是内容本身的压缩编码,与分块传输编码作用层级不同。
如何处理正文?
1. 客户端处理响应正文
- 浏览器端:根据
Content-Type决定行为 ——text/html直接渲染页面、image/png显示图片、application/json不直接展示;下载文件时,会通过Content-Disposition: attachment触发下载流程。 - JavaScript 中:使用
fetch/XMLHttpRequest时,可通过对应方法自动解析:const response = await fetch('/api/data'); const data = await response.json(); // 自动按JSON解析 const text = await response.text(); // 读取为纯文本 const blob = await response.blob(); // 读取为二进制Blob
2. 服务器处理请求正文
服务器接收POST/PUT/PATCH等请求时,需根据Content-Type解析正文:
- URL 编码表单:读取字节流,按
&拆分键值对,再做 URL 解码 - JSON:合并字节流为字符串,用 JSON 解析器转换为对象
- 多部分表单:解析
boundary分隔的各个部分,区分文件字段和普通字段 - 原始二进制:原样保存为文件或直接传递给后续处理现代 Web 框架通常内置解析功能(如 Node.js 的
express.json()、Python Flask 的request.get_json()),无需手动处理流数据。
注意事项
- 忘记设置
Content-Type:POST 提交 JSON 时,若未添加Content-Type: application/json,服务器无法识别数据格式,可能返回400 Bad Request或415 Unsupported Media Type。 Content-Type与实际内容不匹配:声明发送application/json却传递纯文本,服务器解析 JSON 失败,会返回400 Bad Request。- 压缩链条问题:代理或 CDN 自动添加
Content-Encoding: gzip时,读取数据未解压会导致乱码;浏览器和fetch等工具通常会自动透明解压,需注意后端手动处理时的解压逻辑。 - 大文件上传风险:超大正文直接读取到内存会导致服务崩溃,需使用流式处理,后端按块读写磁盘,
multipart/form-data格式能有效处理文件边界。 - 安全相关问题:
- 验证请求体长度,设置限制防止恶意超大请求耗尽内存
- 不要仅依赖
Content-Type或文件扩展名判断文件类型,上传文件需读取魔数检查真实类型 - 避免 XSS 攻击,用户上传的 HTML 内容不应被设置为
text/html直接渲染
实现简单的HTTP服务器
实现⼀个最简单的HTTP服务器,只在⽹⻚上输出"helloworld";只要我们按照HTTP协议的要求构造数 据,就很容易能做到;
思路:
一个HTTP服务器需要包含以下模块
1.底层TCP服务器:TCP服务器接收到浏览器发送的请求后,需要执行特定的处理,这个处理就是对HTTP协议的处理
2.HTTP Request:TCP服务器接受到的数据,我们当作Request请求处理,一个HTTP Request包含以下结构,我们需要从字符串中提取这些结构
1.请求方法
2.URL
3.HTTP版本
4.报头
5.正文
3.HTTP Response:服务器解析完Request后,根据请求数据做出相应的处理,并制作Response返回给客户端,一个Response包含以下结构
1.HTTP版本
2.状态码
3.状态码描述
4.报头
5.正文
因此我们需要用C++的类来封装这些方法
com.hpp
com.hpp是一个公共头文件,其他的模块都需要包含这个头文件
#pragma once
#include "logstrategy.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include "InetAddr.hpp"
#include <cstdlib>
#include <functional>
#include <unistd.h>
#include <string>
#include <jsoncpp/json/json.h>
#include <memory>
#include <sys/wait.h>
#define CONV(x) ((struct sockaddr*)(&x))
#define DEFAULT_BACKLOG 8
#define MAXNUM 1024
#define DEFAULT_PORT 8080
#define DEFAULT_SOCKFD -1
#define DEFAULT_IP "127.0.0.1"
enum ExitCode
{
NORMAL = 0,
SOCKET,
BIND,
LISTEN,
ACCEPT,
FORMAT,
CONNECT,
FORK
};
enum ResultCode
{
OK = 0,
};
class nocopy{
public:
nocopy()
{}
~nocopy()
{}
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
socket.hpp
socket.hpp是一个封装socket套接字的头文件
#pragma once
#include "com.hpp"
class Socket
{
public:
Socket()
{}
~Socket()
{}
virtual void create_socket() = 0;
virtual void Bind(uint16_t) = 0;
virtual void Listen(int) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr&) = 0;
virtual bool Connect(const std::string&,const uint16_t&) = 0;
virtual int Recv(std::string&) = 0;
virtual void Send(const std::string&) = 0;
virtual int get_sockfd() = 0;
void InitTcpServer(uint16_t port = DEFAULT_PORT,int backlog = DEFAULT_BACKLOG)
{
create_socket();
Bind(port);
Listen(backlog);
}
void InitTcpClient(std::string ip = DEFAULT_IP,uint16_t port = DEFAULT_PORT)
{
create_socket();
Connect(ip,port);
}
};
class TcpSocket : public Socket
{
private:
using func_t = std::function<void()>;
public:
TcpSocket(int sockfd = DEFAULT_SOCKFD)
:_sockfd(sockfd)
{}
void create_socket() override
{
_sockfd = socket(AF_INET,SOCK_STREAM,0);
if(_sockfd < 0)
{
logger(LogLevel::FATAL)<<"socket error";
exit(ExitCode::SOCKET);
}
logger(LogLevel::INFO)<<"sockect create success : "<<_sockfd;
}
void Bind(uint16_t port)override
{
InetAddr addr(port);
int n = bind(_sockfd,CONV(addr.get_addr()),sizeof(addr.get_addr()));
if(n < 0)
{
logger(LogLevel::FATAL)<<"bind error";
exit(ExitCode::BIND);
}
logger(LogLevel::INFO)<<"bind success";
}
void Listen(int backlog)override
{
int n = listen(_sockfd,backlog);
if(n < 0)
{
logger(LogLevel::FATAL)<<"listen error";
exit(ExitCode::LISTEN);
}
logger(LogLevel::INFO)<<"listen success";
}
std::shared_ptr<Socket> Accept(InetAddr& addr)override
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = accept(_sockfd,CONV(peer),&len);
if(sockfd < 0)
{
//logger(LogLevel::FATAL,__LINE__,__FILE__)<<"accept error";
return nullptr;
}
InetAddr tmp(peer);
addr = tmp;
logger(LogLevel::INFO)<<"accpet success";
std::shared_ptr<Socket> p = std::make_shared<TcpSocket>(sockfd);
return p;
}
bool Connect(const std::string& ip,const uint16_t& port)override
{
InetAddr addr(ip,port);
int n = connect(_sockfd,CONV(addr.get_addr()),sizeof(addr.get_addr()));
if(n < 0)
{
logger(LogLevel::FATAL)<<"connect error";
return false;
}
logger(LogLevel::INFO)<<"connect success";
return true;
}
int Recv(std::string& out)override
{
char buffer[MAXNUM];
ssize_t n = recv(_sockfd,buffer,sizeof(buffer) - 1,0);
if(n > 0)
{
buffer[n] = '\0';
out += buffer;
return n;
}
else if(n == 0)
return 0;
else
return -1;
}
void Send(const std::string& out)override
{
//logger(LogLevel::INFO)<<"send "<<std::to_string(_sockfd)<<" "<<out;
send(_sockfd,out.c_str(),out.size(),0);
//logger(LogLevel::INFO)<<"success";
}
void Start()
{
}
int get_sockfd()override
{
return _sockfd;
}
private:
int _sockfd;
};
TcpServer.hpp
TcpServer.hpp在socket.hpp的基础上,封装成TCP服务器
#pragma once
#include "socket.hpp"
class TcpServer : public nocopy
{
private:
using func_t = std::function<void(std::shared_ptr<Socket>&,InetAddr&)>;
public:
TcpServer(uint16_t port,func_t func)
:_port(port),_ioserver(func),_listensock(std::make_shared<TcpSocket>())
{}
void init(int backlog = DEFAULT_BACKLOG)
{
_listensock->InitTcpServer(_port,backlog);
_listensockfd = _listensock->get_sockfd();
}
void run()
{
_isrunning = true;
while(_isrunning)
{
InetAddr addr;
std::shared_ptr<Socket> sock = _listensock->Accept(addr);
if(sock == nullptr)
continue;
pid_t pid = fork();
if(pid > 0)
{
close(sock->get_sockfd());
waitpid(pid,nullptr,0);
}
else if(pid == 0)
{
if(fork() > 0)
exit(ExitCode::NORMAL);
close(_listensock->get_sockfd());
_ioserver(sock,addr);
}
else
{
logger(LogLevel::FATAL)<<"fork error";
exit(ExitCode::FORK);
}
}
}
private:
uint16_t _port;
int _listensockfd;
bool _isrunning = false;
std::shared_ptr<Socket> _listensock;
func_t _ioserver;
};
http.hpp
http.hpp是一个专门封装http处理方法的头文件,其中包含:HttpServer,HttpRequest,HttpResponse
HttpServer:
HttpServer需要提供TCP服务器接收到数据后,对数据的处理方法
具体处理方法:将数据解析成Request,经过处理后返还Response
class HttpServer
{
public:
HttpServer(uint16_t port)
: _server(std::make_unique<TcpServer>(port, [this](std::shared_ptr<Socket> &sock, InetAddr &addr)
{ this->handler_http_request(sock, addr); }))
{
}
void init()
{
_server->init();
}
void run()
{
_server->run();
}
static void handler_http_request(std::shared_ptr<Socket> &sock, InetAddr &addr)
{
std::string in;
sock->Recv(in);
logger(LogLevel::INFO)<<"get a request from "<<addr.get_string();
HttpRequest req;
req.Deserialize(in);
HttpResponse rep;
rep.MakeReponse(req.get_uri());
std::string out = rep.Serialize();
sock->Send(out);
}
private:
std::unique_ptr<TcpServer> _server;
};
HttpRequest:
HttpRequest需要将数据解析成对应的结构
struct Util
{
static bool ReadOneLine(std::string &line, std::string &out, const std::string &sep)
{
int pos = line.find(sep);
if (pos == std::string::npos)
return false;
out = line.substr(0, pos);
line.erase(0, pos + sep.size());
return true;
}
static int FileSize(const std::string& file)
{
std::ifstream f(file, std::ios::binary);
if(!f.is_open())
return -1;
f.seekg(0,f.end);
int filesize = f.tellg();
f.seekg(0,f.beg);
f.close();
return filesize;
}
static bool ReadFile(const std::string& file,std::string& out)
{
int filesize = FileSize(file);
if(filesize > 0)
{
std::ifstream f(file);
if(!f.is_open())
return false;
out.resize(filesize);
f.read((char*)out.c_str(),filesize);
f.close();
return true;
}
else
return false;
}
};
class HttpRequest
{
public:
bool Deserialize(std::string &reqstr)
{
std::string req = reqstr;
#ifdef Debug
logger(LogLevel::DEBUG) << "get a http request: " << req;
#endif
if (!prase_request_line(req) || !prase_request_head(req) || !prase_blank_line(req) || !prase_text(req))
return false;
return true;
}
std::string get_uri()
{
return _uri;
}
private:
bool prase_request_line(std::string &req)
{
std::string reqline;
if (!Util::ReadOneLine(req, reqline, glinespace))
return false;
std::stringstream s(reqline);
s >> _method >> _uri >> _version;
if (_uri == "/")
_uri = webroot + _uri + homepage;
else
_uri = webroot + _uri;
#ifdef Debug
logger(LogLevel::DEBUG) << "_method: " << _method;
logger(LogLevel::DEBUG) << "_uri: " << _uri;
logger(LogLevel::DEBUG) << "_version: " << _version;
#endif
return true;
}
bool prase_request_head(std::string &req)
{
std::string header;
int pos = req.find(glinespace + glinespace);
if (pos == std::string::npos)
return false;
header = req.substr(0, pos + glinespace.size());
req.erase(0, pos + glinespace.size());
std::string line;
while (Util::ReadOneLine(header, line, glinespace))
{
int pos = line.find(glinesep);
if (pos == std::string::npos)
break;
_headers[line.substr(0, pos)] = line.substr(pos + glinesep.size());
}
#ifdef Debug
logger(LogLevel::DEBUG) << "headers:";
for (auto &it : _headers)
{
logger(LogLevel::DEBUG) << it.first << glinesep << it.second;
}
#endif
return true;
}
bool prase_blank_line(std::string &req)
{
int pos = req.find(glinespace);
if (pos == std::string::npos)
return false;
_blankline = req.substr(0, pos);
req.erase(0, pos + glinespace.size());
#ifdef Debug
logger(LogLevel::INFO) << "blankline: " << _blankline;
#endif
return true;
}
bool prase_text(std::string &req)
{
_text = req;
#ifdef Debug
logger(LogLevel::DEBUG) << "text: " << _text;
#endif
return true;
}
std::string _method;
std::string _uri;
std::string _version;
std::unordered_map<std::string, std::string> _headers;
std::string _blankline;
std::string _text;
};
HttpResponse:
HttpResonse需要返回给客户端
class HttpResponse
{
public:
std::string Serialize()
{
std::string status_line = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace;
std::string headers;
for(auto & it : _headers)
{
headers = headers + it.first + glinesep + it.second + glinespace;
}
#ifdef Debug
logger(LogLevel::DEBUG)<<"response : "<< status_line + headers + _blankline + _text;
#endif
return status_line + headers + _blankline + _text;
}
void MakeReponse(std::string uri)
{
int filesize = 0;
if(Util::ReadFile(uri,_text))
{
_targetfile = uri;
_desc = "OK";
set_code(200);
}
else
{
_desc = "FATAL";
uri = webroot + "/404.html";
_targetfile = uri;
set_code(404);
Util::ReadFile(uri,_text);
}
filesize = Util::FileSize(uri);
set_header("Content-Type",get_suffix(uri));
set_header("Content-Length",std::to_string(filesize));
}
private:
std::string get_suffix(std::string targetfile)
{
auto pos = targetfile.rfind(".");
if(pos == std::string::npos)
return "text/html";
std::string suffix = targetfile.substr(pos);
if(suffix == ".html" || suffix == ".htm")
return "text/html";
else if(suffix == ".jpg")
return "image/jpeg";
else if(suffix == "png")
return "image/png";
else
return "";
}
void set_header(const std::string &key, const std::string &value)
{
if (_headers.find(key) != _headers.end())
return;
_headers[key] = value;
}
void set_code(int code)
{
_code = code;
}
std::string _version = "HTTP/1.0";
int _code;
std::string _desc;
std::unordered_map<std::string, std::string> _headers;
std::string _blankline = glinespace;
std::string _text;
std::string _targetfile;
};
index.html:
我们假设浏览器只访问wwwroot/index.html这个网页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>hello world</p>
</body>
</html>
测试

1502

被折叠的 条评论
为什么被折叠?



