Linux 应用层协议HTTP

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?

  1. 认证信息不安全user:pass@会把账号密码明文写在 URL 里,会被服务器日志、浏览器历史记录保存,极易泄露,现在所有网站都用 Cookie/Token 代替。
  2. 默认端口没必要写: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删除资源删除文章、删除用户账号、取消订单❌ 一般无(也可带)❌ 不安全✅ 幂等

两个关键补充概念

这两个属性是区分不同方法的核心,也是开发中容易踩坑的点:

  1. 安全:指请求不会改变服务器的状态,只是纯粹读取数据。
    • 只有GET是安全的,POST/PUT/DELETE都会修改服务器数据,所以不安全。
  2. 幂等:指同一个请求执行多次,和执行一次的结果完全相同。
    • 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 方法

  • 语义:对资源进行部分更新,请求体中仅包含要修改的内容
  • 本质:通常非幂等(也可设计为幂等,取决于补丁格式)
  • 两种常见补丁格式
    1. JSON Merge Patch (RFC 7396):直接发送要合并的 JSON 对象,如{"email": "new@ex.com"},后端自动合并到已有对象
    2. JSON Patch (RFC 6902):通过一组操作指令描述修改,更精准
  • 典型请求(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 OK204 No Content

DELETE 方法

  • 语义:请求服务器删除指定资源
  • 本质:幂等,删除一次和多次,资源最终状态都是不存在(第二次请求可能返回 404,但结果一致)
  • 典型请求(删除指定用户)
DELETE /api/users/123 HTTP/1.1
Host: api.example.com

常见响应204 No Content404 Not Found

HTTP 状态码

HTTP 状态码是服务器在响应中返回的一个三位数字,用来简明扼要地告诉客户端请求的结果。它配合之前讲解的请求方法(GET、POST 等),构成了请求 - 处理 - 结果的完整闭环。不论是打开网页、调用 API 还是抓取数据,读懂状态码都是排查问题的第一步。

状态码的分类

类别范围含义典型场景
1xx100 - 199信息响应请求已接收,告知客户端可继续后续处理
2xx200 - 299成功请求被服务器成功接收、理解并处理完毕
3xx300 - 399重定向需要客户端额外跳转操作,才能完成最终请求
4xx400 - 499客户端错误请求存在格式 / 权限等问题,服务器无法完成请求
5xx500 - 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 状态码

核心原因有两点:

  1. 暴露风险:直接返回 5xx,相当于把 “服务器内部故障” 的状态公开给用户,会引来攻击者的恶意扫描、攻击,让服务面临更大风险。
  2. 安全伪装:服务器崩溃 / 故障时,通常会伪装成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/json
    • Accept-Encoding:支持的压缩算法,如gzip, deflate, br
    • Accept-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简单文本消息
HTMLtext/html网页主体内容
JSONapplication/jsonAPI 数据交互(REST 接口最常用格式)
XMLapplication/xml / text/xmlSOAP 协议、RSS 订阅、配置数据
URL 编码表单application/x-www-form-urlencoded传统表单提交,格式类似key1=val1&key2=val2
多部分表单multipart/form-data文件上传场景,可包含多个独立部分(文本 + 文件)
二进制数据application/octet-stream通用二进制流,常用于文件下载
媒体资源image/pngvideo/mp4audio/mpeg图片、视频、音频等多媒体内容

如何设置正文?

处理正文的关键在于理解这几个核心头部:

  1. Content-Type — 决定内容格式告知接收方 “发送的是什么类型的数据、字符集是什么”,例如Content-Type: application/json; charset=utf-8;若缺失,浏览器可能会通过 MIME 嗅格猜测类型,易出现解析错误,建议明确指定。
  2. Content-Length — 标记正文大小声明正文的未压缩字节数,接收方可据此判断数据是否接收完整,是持久连接中界定响应边界的关键方式之一。
  3. Transfer-Encoding: chunked — 分块传输正文长度无法提前确定时,服务器将正文分成多个块发送,每块前标注十六进制长度,最后以大小为 0 的块结束,与Content-Length是两种互斥的界定方式。
  4. 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()),无需手动处理流数据。

注意事项

  1. 忘记设置Content-Type:POST 提交 JSON 时,若未添加Content-Type: application/json,服务器无法识别数据格式,可能返回400 Bad Request415 Unsupported Media Type
  2. Content-Type与实际内容不匹配:声明发送application/json却传递纯文本,服务器解析 JSON 失败,会返回400 Bad Request
  3. 压缩链条问题:代理或 CDN 自动添加Content-Encoding: gzip时,读取数据未解压会导致乱码;浏览器和fetch等工具通常会自动透明解压,需注意后端手动处理时的解压逻辑。
  4. 大文件上传风险:超大正文直接读取到内存会导致服务崩溃,需使用流式处理,后端按块读写磁盘,multipart/form-data格式能有效处理文件边界。
  5. 安全相关问题
    • 验证请求体长度,设置限制防止恶意超大请求耗尽内存
    • 不要仅依赖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>

测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值