HTTP 协议深度解析:请求/响应、报头、正文的核心原理与实战

目录

  • [1. 基础概念](#1. 基础概念)
    • [1.1 http协议](#1.1 http协议)
    • [1.2 认识URL](#1.2 认识URL)
    • [1.3 urlencode和urldecode(了解)](#1.3 urlencode和urldecode(了解))
      • [1. urlencode(URL 编码)](#1. urlencode(URL 编码))
      • [2. urldecode(URL 解码)](#2. urldecode(URL 解码))
      • [3. 编码规则](#3. 编码规则)
  • [2. HTTP协议请求与响应](#2. HTTP协议请求与响应)
    • [2.1 HTTP请求格式](#2.1 HTTP请求格式)
    • [2.2 HTTP响应格式](#2.2 HTTP响应格式)
    • [补:URI、URL、URN 的核心区分(重点)](#补:URI、URL、URN 的核心区分(重点))
    • [2.3 HTTP](#2.3 HTTP)
  • [3. HTTP的方法](#3. HTTP的方法)
    • [3.1 GET方法(重点)](#3.1 GET方法(重点))
    • [3.2 POST方法(重点)](#3.2 POST方法(重点))
    • [补:GET和POST方法的区别 - 针对form表单](#补:GET和POST方法的区别 - 针对form表单)
    • [3.3 PUT方法(不常用)](#3.3 PUT方法(不常用))
    • [3.4 HEAD方法](#3.4 HEAD方法)
    • [3.5 DELETE方法(不常用)](#3.5 DELETE方法(不常用))
    • [3.6 OPTIONS方法](#3.6 OPTIONS方法)
  • [4. HTTP的状态码](#4. HTTP的状态码)
  • [5. HTTP常见Header](#5. HTTP常见Header)
    • [5.1 关于connection报头](#5.1 关于connection报头)
    • [5.2 HTTP常见header的表格](#5.2 HTTP常见header的表格)
  • [6. 简单HTTP服务器实现](#6. 简单HTTP服务器实现)
    • [6.1 核心问题](#6.1 核心问题)
    • [6.2 服务端程序执行流程](#6.2 服务端程序执行流程)
    • [6.3 服务端核心代码](#6.3 服务端核心代码)
    • [6.4 功能验证](#6.4 功能验证)
    • [6.5 完整版代码](#6.5 完整版代码)
  • 附录:HTTP历史及版本核心技术与时代背景

1. 基础概念

1.1 http协议

HTTP(HyperText Transfer Protocol,超文本传输协议)是一个至关重要的协议。它是应用层的超文本传输协议 ,是互联网里客户端(比如浏览器)和服务器之间通信的 "规则手册"------ 专门用来约定双方怎么交换超文本(像 HTML 网页这类内容),是网页访问、数据交互的基础。客户端通过HTTP协议向服务器发送请求,服务器收到请求后处理并返回响应。

具体来说:

  • 定位:属于网络模型的 "应用层",和我们程序员写的代码处于同一层。
  • 核心作用:支撑两种最常见的上网行为 ------ 要么从远程服务器 "拿资源"(比如打开一篇文章、加载一张图片),要么把本地数据 "传上去"(比如发评论、传文件)。
  • 关键特点
    1. 是 "无连接" 的:每次请求都要重新建立通信连接;
    2. 是 "无状态" 的:服务器不会记住客户端的操作(比如刷新页面后,服务器不会默认保留你之前的浏览位置);
    3. 端口固定:普通 HTTP 用 80 端口,加密的 HTTPS(更安全的版本)用 443 端口。
  • 底层本质:靠 socket 通信实现传输,而它传递的 "资源"(网页、视频等),其实就是存放在 Linux 服务器里的文件。

1.2 认识URL

平时我们俗称的 "网址" 其实就是说的 URL。

URL 的全称是Uniform Resource Locator(统一资源定位符) ,它是互联网中唯一标识某一个资源位置的 "地址"------ 相当于资源在网络世界的 "门牌号",让客户端(如浏览器)能精准找到远程服务器上的目标内容(网页、图片、视频等)。

它的核心作用 & 特点:

  1. 唯一性 :通过 "协议 + 域名 + 资源路径" 的组合,能定位全网唯一的资源(比如https://blog.csdn.net/xxx/article/details/yyy,对应 CSDN 服务器里某篇文章的文件);
  2. 配合协议工作 :URL 开头的 "协议部分"(如http://https://),决定了客户端用哪种应用层协议(HTTP/HTTPS)去连接服务器;
  3. 关联服务器资源:URL 里的 "域名 + 路径",会对应到服务器(通常是 Linux 服务器)内的具体文件路径 ------ 也就是说,URL 定位的 "资源",本质是服务器里的某个文件。

举个例子:https://news.qq.com/rain/a/20250418A05TE500这个 URL,就通过https协议,找到news.qq.com服务器里、/rain/a/路径下名为20250418A05TE500的文件资源。

  • url参数解析:
组件 对应内容 说明
Scheme(协议) https 现代 Web 安全协议,加密传输数据,替代早期未加密的http
Host(主机 / 域名) blog.csdn.net CSDN 博客平台的核心服务域名,指向存储博客文章的服务器集群。
Path(资源路径) /qq_41854911/article/details/132875859 精准定位具体资源。
Query(查询参数) spm=1001.2014.3001.5502 CSDN 内部流量统计参数.。
Fragment(锚点) #heading-3 页面内定位标识

1.3 urlencode和urldecode(了解)

urlencodeurldecodeURL 的编码 / 解码工具,用来解决 URL 中 "特殊字符无法直接传输" 的问题,是 URL 传输过程中的 "翻译工具"。

1. urlencode(URL 编码)

  • 作用 :把 URL 中不能直接传输的字符(比如空格、中文、特殊符号&/?#等),转换成 "% + 字符的十六进制ASCII值" 的格式,让这些字符能安全在网络中传输(避免 URL 解析出错)。
  • 示例
    • 空格 → 编码为%20
    • 中文 "你好" → 编码为%E4%BD%A0%E5%A5%BD
    • 原始内容 "搜索 & 你好" → 编码后变成%E6%90%9C%E7%B4%A2%26%E4%BD%A0%E5%A5%BD

2. urldecode(URL 解码)

  • 作用 :是urlencode逆操作 ------ 把编码后的 "%+十六进制" 格式字符,还原成原来的原始字符,方便服务器 / 客户端读取内容。
  • 示例
    • 编码后的%20 → 解码回 "空格";
    • 编码后的%E4%BD%A0%E5%A5%BD → 解码回 "你好"。

应用场景

比如你在浏览器搜索 "苹果 手机":

  • 浏览器会自动对 "苹果 手机" 做urlencode,变成%E8%8B%B9%E6%9E%9C%20%E6%89%8B%E6%9C%BA
  • 服务器收到这个编码后的内容,用urldecode还原成 "苹果 手机",再执行搜索。

3. 编码规则

字符类型 处理方式
字母 / 数字 /-/_ /./ ~ 直接保留
其他字符(含中文、空格) 转成 %+UTF-8编码格式的两位十六进制(小写)
保留字符(作普通内容时) 同上编码规则

例如下图:"+" 被转义成了 "%2B"

2. HTTP协议请求与响应

2.1 HTTP请求格式

  • 首:[方法] + [url] + [版本]

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

  • Body:空行后面的内容都是Body。Body允许为空字符串。如果Body存在,则在Header中会有一个Content-Length属性来标识Body的长度。

  1. HTTP 报头和有效载荷的分离:HTTP 通过 **"空行(\r\n\r\n)" 作为固定分隔符 ** 实现分离:
  • 空行(连续的两个\r\n换行符)之前的内容是报头(Header);
  • 空行之后的内容是有效载荷(Body)。
  1. HTTP 协议区分不同区域:HTTP 通过协议约定的专用分隔符区分各区域,而非任意特殊字符:
  • 首行区域:用空格 分隔「请求方法、URL、HTTP 版本」(如截图首行的POST http://... HTTP/1.1);
  • Header 区域:用 "冒号 + 空格"分隔 Header 的键和值(如Host: job.xjtu.edu.cn),用 \r\n 分隔不同的 Header 字段;
  • 报头与有效载荷区域:用 ** 空行(\r\n\r\n)** 分隔。
  1. HTTP 协议的序列化和反序列化在哪里表现

HTTP 是文本协议,其序列化 / 反序列化通过 "字符串拼接 + 分隔符解析" 实现,不依赖第三方库:

  • 序列化(发送方) :将 "请求 / 响应的结构化数据(如方法、Header 键值对、Body 内容)",按照 HTTP 分隔符规则拼接为文本格式的 HTTP 报文(如截图中的请求,就是把 POST 方法、Header、账号密码 Body 按规则拼接成的文本);
  • 反序列化(接收方):将收到的 HTTP 文本报文,按协议分隔符规则解析为 "结构化数据"(如服务器收到截图请求后,解析出请求方法是 POST、Cookie 信息、Body 里的账号密码)。
  1. 总结HTTP协议的格式即为右上图

2.2 HTTP响应格式

  • 首行:[版本号] + [状态码] + [状态码解释]

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

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

补:URI、URL、URN 的核心区分(重点)

URI就是客户端要访问的服务器的目录,默认是外部跟目录,可以根据需求,设置具体目录

类别 全称 定义 核心作用 示例 关键特点
URI 统一资源标识符(Uniform Resource Identifier) 唯一标识某一资源的字符串 唯一标识资源(不限制标识方式) 包含 URL 和 URN(如https://www.baidu.comurn:isbn:9787506394803 是 URL、URN 的统称,是更宽泛的概念
URL 统一资源定位符(Uniform Resource Locator) URI 的子集,通过资源的存储位置标识资源 通过 "位置" 定位并访问资源 日常说的 "网址"(如https://blog.csdn.net/xxxftp://ftp.example.com/file.zip 依赖资源的位置,可直接访问资源
URN 统一资源名称(Uniform Resource Name) URI 的子集,通过资源的唯一名称标识资源 通过 "名称" 唯一标识资源(与位置无关) urn:isbn:9787506394803(某本书的 ISBN 号)、urn:mail:user@example.com 不依赖位置,仅靠名称标识;实际场景中使用较少

2.3 HTTP

请求与响应示例:

3. HTTP的方法

其中最常用的就是GET方法和POST方法。

3.1 GET方法(重点)

用途:用于请求URL指定的资源。

示例: GET /index.html HTTP/1.1

特性:指定资源经服务器端解析后返回响应内容。

form表单:https://www.runoob.com/html/html-forms.html

cpp 复制代码
// 要通过历史写的http服务器,验证GET方法, 这里需要了解一下FORM表单的问题
// 这里就要引入web根目录,文件读取的基本操作了
std::string GetFileContentHelper(const std::string& path)
{
	// 一份简单的读取二进制文件的代码
	std::ifstream in(path, std::ios::binary);
	if (!in.is_open())
		return "";
	in.seekg(0, in.end);
	int filesize = in.tellg();
	in.seekg(0, in.beg);

	std::string content;
	content.resize(filesize);
	in.read((char*)content.c_str(), filesize);

	// std::vector<char> content(filesize);
	// in.read(content.data(), filesize);

	in.close();
	return content;
}

3.2 POST方法(重点)

用途:用于传输实体的主体,通常用于提交表单数据。

示例: POST /submit.cgi HTTP/1.1

特性:可以发送大量的数据给服务器,并且数据包含在请求体中。

form表单:https://www.runoob.com/html/html-forms.html

cpp 复制代码
// 要通过历史写的http服务器,验证POST方法,这里需要了解一下FORM表单的问题

补:GET和POST方法的区别 - 针对form表单

form表单使用示例:

html 复制代码
<form action="/login" method="GET">
    <div class="form-group">
        <label for="username">Username</label>
        <input type="text" id="username" name="username" required>
    </div>
    <div class="form-group">
        <label for="password">Password</label>
        <input type="password" id="password" name="password" required>
    </div>
    <div class="form-group">
        <input type="submit" value="Login">
    </div>
    <a href="Register.html">Login</a> <!-- 跳转到登录页面 -->
    <a href="index.html">Register</a> <!-- 跳转到注册页面 -->
</form>
  1. form表单,不指定method时的默认方法是GET
  2. form表单提交的参数
    • 在GET方法中:通过uri进行提交:
    • 在POST方法中:通过http request正文提交:

GET和POST的区别:

GET:

  1. 获取或静态网页或资源

  2. 提交参数以uri方式提交
    POST:

  3. 提交参数以正文进行提交

GET和POST使用上的区别:

GET:

  1. GET提交参数,建议提交的参数不要过长,因为uri一般有长度限制

  2. GET方法会回显参数
    POST:

  3. 正文传参,意味着可以传递长数据

  4. 不会回显参数,更加私密

补:

  1. fiddler工具,抓取报文
  2. 加密协议https

3.3 PUT方法(不常用)

用途:用于传输文件,将请求报文主体中的文件保存到请求URL指定的位置。

示例: PUT /example.html HTTP/1.1

特性:不太常用,但在某些情况下,如RESTful API中,用于更新资源。

3.4 HEAD方法

用途:与GET方法类似,但不返回报文主体部分,仅返回响应头。

示例: HEAD /index.html HTTP/1.1

特性:用于确认URL的有效性及资源更新的日期时间等。

bash 复制代码
// curl -i 显示
$ curl - i www.baidu.com
HTTP / 1.1 200 OK
Accept - Ranges: bytes
Cache - Control : private, no - cache, no - store, proxy - revalidate, no - transform
Connection : keep - alive
Content - Length : 2381
Content - Type : text / html
Date : Sun, 16 Jun 2024 08 : 38 : 04 GMT
Etag : "588604dc-94d"
Last - Modified : Mon, 23 Jan 2017 13 : 27 : 56 GMT
Pragma : no - cache
Server : bfe / 1.0.8.18
Set - Cookie : BDORZ = 27315; max - age = 86400; domain = .baidu.com; path = /

<!DOCTYPE html>
...

// 使用head方法,只会返回响应头
$ curl --head www.baidu.com
HTTP / 1.1 200 OK
Accept - Ranges: bytes
Cache - Control : private, no - cache, no - store, proxy - revalidate, no - transform
Connection : keep - alive
Content - Length : 277
Content - Type : text / html
Date : Sun, 16 Jun 2024 08 : 43 : 38 GMT
Etag : "575e1f71-115"
Last - Modified : Mon, 13 Jun 2016 02 : 50 : 25 GMT
Pragma : no - cache
Server : bfe / 1.0.8.18

3.5 DELETE方法(不常用)

用途:用于删除文件,是PUT的相反方法。

示例:DELETE /example.html HTTP/1.1

特性:按请求URL删除指定的资源。

3.6 OPTIONS方法

用途:用于查询针对请求URL指定的资源支持的方法。

示例: OPTIONS * HTTP/1.1

特性:返回允许的方法,如GET、POST等。

不支持的效果

bash 复制代码
// 搭建一个nginx用来测试
// sudo apt install nginx
// sudo nginx -- 开启
// ps ajx | grep nginx -- 查看
// sudo nginx -s stop -- 停止服务

$ sudo nginx - s stop
$ ps ajx | grep nginx
2944845 2945390 2945389 2944845 pts / 1 2945389 S + 1002 0:00 grep --
color = auto nginx
$ sudo nginx
$ ps axj | grep nginx
	  1    2945393 2945393 2945393 ? -1 Ss 0 0 : 00 nginx :
	master process nginx
	2945393 2945394 2945393 2945393 ? -1 S 33 0 : 00 nginx :
	worker process
	2945393 2945395 2945393 2945393 ? -1 S 33 0 : 00 nginx :
	worker process
	2944845 2945397 2945396 2944845 pts / 1 2945396 S + 1002 0 : 00 grep --
	color = auto nginx
	
// -X(大x) 指明方法
$ curl - X OPTIONS - i http ://127.0.0.1/
HTTP / 1.1 405 Not Allowed
Server : nginx / 1.18.0 (Ubuntu)
Date : Sun, 16 Jun 2024 08 : 48 : 22 GMT
Content - Type : text / html
Content - Length : 166
Connection : keep - alive

<html>
<head> < title>405 Not Allowed< / title>< / head>
<body>
<center> < h1>405 Not Allowed< / h1>< / center>
<hr><center>nginx / 1.18.0 (Ubuntu) < / center >
< / body>
< / html>

支持的效果

bash 复制代码
HTTP/1.1 200 OK
Allow: GET, HEAD, POST, OPTIONS
Content-Type: text/plain
Content-Length: 0
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 16 Jun 2024 09:04:44 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
// 注意:这里没有响应体,因为Content-Length为0

4. HTTP的状态码

最常见的状态码,比如 200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向),504(Bad Gateway)

状态码 含义
100 Continue 上传大文件时,服务器告诉客户端可以继续上传
200 OK 访问网站首页,服务器返回网页内容
201 Created 发布新文章,服务器返回文章创建成功的信息
204 No Content 删除文章后,服务器返回"无内容"表示操作成功
301 Moved Permanently 网站换域名后,自动跳转到新域名;搜索引擎更新网站链接时使用
302 Found 或 See Other 用户登录成功后,重定向到用户首页
304 Not Modified 浏览器缓存机制,对未修改的资源返回304状态码
400 Bad Request 填写表单时,格式不正确导致提交失败
401 Unauthorized 访问需要登录的页面时,未登录或认证失败
403 Forbidden 尝试访问你没有权限查看的页面
404 Not Found 访问不存在的网页链接
500 Internal Server Error 服务器崩溃或数据库错误导致页面无法加载
502 Bad Gateway 使用代理服务器时,代理服务器无法从上游服务器获取有效响应
503 Service Unavailable 服务器维护或过载,暂时无法处理请求

仅包含重定向相关状态码的表格:

状态码 含义 是否为临时重定向 应用样例
301 Moved Permanently 否(永久重定向) 网站换域名后,自动跳转到新域名;搜索引擎更新网站链接时使用
302 Found 或 See Other 是(临时重定向) 用户登录成功后,重定向到用户首页
307 Temporary Redirect 是(临时重定向) 临时重定向资源到新的位置(较少使用)
308 Permanent Redirect 否(永久重定向) 永久重定向资源到新的位置(较少使用)

关于重定向的验证,以301为代表

**HTTP状态码301(永久重定向)和302(临时重定向)都依赖Location选项。**以下是关于两者依赖Location选项的详细说明:

HTTP状态码301(永久重定向)

  • 当服务器返回HTTP 301状态码时,表示请求的资源已经被永久移动到新的位置。

  • 在这种情况下,服务器会在响应中添加一个Location头部,用于指定资源的新位置。这个Location头部包含了新的URL地址,浏览器会自动重定向到该地址。

  • 例如,在HTTP响应中,可能会看到类似于以下的头部信息:

bash 复制代码
HTTP/1.1 301 Moved Permanently\r\n
Location: https://www.new-url.com\r\n

HTTP状态码302(临时重定向)

  • 当服务器返回HTTP 302状态码时,表示请求的资源临时被移动到新的位置。

  • 同样地,服务器也会在响应中添加一个Location头部来指定资源的新位置。浏览器会暂时使用新的URL进行后续的请求,但不会缓存这个重定向。

  • 例如,在HTTP响应中,可能会看到类似于以下的头部信息:

bash 复制代码
HTTP/1.1 302 Found\r\n
Location: https://www.new-url.com\r\n

总结:无论是HTTP 301还是HTTP 302重定向,都需要依赖Location选项来指定资源的新位置。这个Location选项是一个标准的HTTP响应头部,用于告诉浏览器应该将请求重定向到哪个新的URL地址。

5. HTTP常见Header

  • Content-Type: 数据类型(text/html等)

  • Content-Length: Body的长度

  • Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;

  • User-Agent: 声明用户的操作系统和浏览器版本信息;

  • Referer: 当前页面是从哪个页面跳转过来的;

  • Location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;

  • Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;

5.1 关于connection报头

HTTP中的Connection 字段是HTTP报文头的一部分,它主要用于控制和管理客户端与服务器之间的连接状态

核心作用

  • 管理持久连接: Connection 字段还用于管理持久连接(也称为长连接)。持久连接允许客户端和服务器在请求/响应完成后不立即关闭TCP连接,以便在同一个连接上发送多个请求和接收多个响应。

持久连接(长连接)

  • HTTP/1.1:在HTTP/1.1协议中,默认使用持久连接。当客户端和服务器都不明确指定关闭连接时,连接将保持打开状态,以便后续的请求和响应可以复用同一个连接。

  • HTTP/1.0:在HTTP/1.0协议中,默认连接是非持久的。如果希望在HTTP/1.0上实现持久连接,需要在请求头中显式设置Connection: keep-alive 。

语法格式

  • Connection: keep-alive :表示希望保持连接以复用TCP连接。

  • Connection: close :表示请求/响应完成后,应该关闭TCP连接。

5.2 HTTP常见header的表格

字段名 含义 样例
Accept 客户端可接受的响应内容类型 Accept: <btext/html,application/xhtml+xml,application/xml; q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding 客户端支持的数据压缩格式 Accept-Encoding: gzip, deflate, br
Accept-Language 客户端可接受的语言类型 Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Host 请求的主机名和端口号 Host: www.example.com:8080
User-Agent 客户端的软件环境信息 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
Cookie 客户端发送给服务器的HTTP cookie信息 Cookie: session_id=abcdefg12345; user_id=123
Referer 请求的来源URL Referer: http://www.example.com/previous_page.html
Content-Type 实体主体的媒体类型 Content-Type: application/x-www-formurlencoded (对于表单提交) 或 Content-Type: application/json (对于JSON数据)
Content-Length 实体主体的字节大小 Content-Length: 150
Authorization 认证信息,如用户名和密码 Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== (Base64编码后的用户名:密码)
Cache-Control 缓存控制指令 请求时: Cache-Control: no-cache 或 Cache- Control: max-age=3600 ;响应时: Cache- Control: public, max-age=3600
Connection 请求完后是关闭还是保持连接 Connection: keep-alive 或 Connection: close
Date 请求响应的日期和时间 Date: Wed, 21 Oct 2023 07:28:00 GMT
Location 重定向的目标URL(与3xx状态码配合使用) Location: http://www.example.com/new_location.html (与302状态码配合使用)
Server 服务器类型 Server: Apache/2.4.41 (Unix)
Last-Modified 资源的最后修改时间 Last-Modified: Wed, 21 Oct 2023 07:20:00 GMT
ETag 资源的唯一标识符,用于缓存 ETag: "3f80f-1b6-5f4e2512a4100"
Expires 响应过期的日期和时间 Expires: Wed, 21 Oct 2023 08:28:00 GMT

6. 简单HTTP服务器实现

  • 复用之前写的TcpServer.hpp和Socket.hpp

客户端直接使用浏览器

6.1 核心问题

  1. client && server,是如何保证自己读取到的报文是完整的?
  1. 分析读取到的字节流是否存在空行
  2. 提取Content-Length: 获得正文长度,然后再读取或则和截取指定长度的内容

6.2 服务端程序执行流程

  1. 使用智能指针实例化Http类,并调用Start函数。(Http类中有一个TcpServer类的对象成员tsvrp)
  2. Http类中的Start函数调用tsvrp的Start函数(即调用TcpServer类中的Start函数),并传入lambda函数作为为Start函数的参数,在TcpServer中的Start函数中名为callback。
  3. TcpServer中的Start函数使用多进程调用callback,即调用Http类中传入的lambda函数(此lambda函数即位为调用Http类中的HandlerHttpRquest函数)
  4. HandlerHttpRquest函数中:
  1. 接收客户端请求信息并进行反序列化
  2. 将客户端请求的目标信息序列化之后,回传给客户端

6.3 服务端核心代码

  1. 解析客户端请求

  2. 对客户端请求作出回应,并将应答消息序列化为http协议的格式,最后可直接发送的字符串

  3. Http协议调用

Http.hpp

cpp 复制代码
#pragma once

#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Util.hpp"
#include "Log.hpp"
#include <iostream>
#include <string>
#include <memory>
#include <sstream>
#include <unordered_map>

using namespace SocketModule;
using namespace LogModule;

const std::string gspace = " ";
const std::string glinespace = "\r\n";
const std::string glinesep = ": ";

const std::string webroot = "./wwwroot";
const std::string homepage = "index.html";
const std::string page_404 = "404.html";

// 1.解析客户端请求
// 主要解析第一行"请求行",获取: 请求方法 + URI + HTTP版本(主要是URI,指定访问的服务端目录)
// 1.1 对第一行根据空格进行分割,得到 请求方法 + URI + HTTP版本
// 1.2 实现反序列化: 提取第一行 - 分割字符串 - 拼接uri为完整的服务端目录
class HttpRequest
{
public:
    HttpRequest()
    {
    }
    std::string Serialize()
    {
        return std::string();
    }
    // 1.1 对第一行根据空格进行分割,得到 请求方法 + URI + HTTP版本
    // sstream 根据空格分割字符串
    void ParseReqLine(std::string &reqline)
    {
        std::stringstream ss(reqline);
        ss >> _method >> _uri >> _version;
    }
    // 1.2 实现反序列化: 提取第一行 - 分割字符串 - 拼接uri为完整的服务端目录
    // 反序列化:将请求行和请求报头进行序列化(将字符串信息进行解析)
    // 此处只反序列化了首行,将 请求方法+URI+HTTP版本 提取(根据URI寻找需要请求的目标目录)
    bool Deserialize(std::string &reqstr)
    {
        // 1.提取请求行
        std::string reqline;
        bool res = Util::ReadOneLine(reqstr, &reqline, glinespace);
        LOG(LogLevel::DEBUG) << reqline;

        // 2.对请求行进行反序列化
        ParseReqLine(reqline);

        LOG(LogLevel::DEBUG) << "_method: " << _method;
        LOG(LogLevel::DEBUG) << "_uri: " << _uri;
        LOG(LogLevel::DEBUG) << "_version: " << _version;

        // 3.拼接完整路径给到_uri(即为客户端发送来的完整路径uri)
        if (_uri == "/")
            _uri = webroot + _uri + homepage;
        else
            _uri = webroot + _uri;

        LOG(LogLevel::DEBUG) << "path: " << _uri;

        return true;
    }
    std::string Uri() { return _uri; }
    ~HttpRequest()
    {
    }

private:
    std::string _method;
    std::string _uri;
    std::string _version;

    std::unordered_map<std::string, std::string> _headers;
    std::string _blankline;
    std::string _text;
};

// 2.对客户端请求作出回应,并将应答消息序列化为http协议的格式,最后可直接发送的字符串
// 实现完整的应答信息
// 2.1: 拼接 状态行 + 报头 + 内容 成为一个完整的字符串
//  2.2: 设置目标目录
//  2.3: 根据状态码设置状态码描述
//  2.4: 设置响应报头
//  2.5: 根据文件后缀,设置http格式的扩展名(Content-Type)
// 2.6: 制作应答信息: 读取目标文件内容,调用3&4&5
class HttpResponse
{
public:
    HttpResponse()
        : _blankline(glinespace),
          _version("HTTP/1.0")
    {
    }
    // 2.1: 序列化: 拼接 状态行 + 报头 + 内容 成为一个完整的字符串
    // 实现:成熟的http,应答序列化,不需要以来任何第三方库
    std::string Serialize()
    {
        // 1.状态行
        std::string status_line = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace;
        // 2.报头
        std::string resp_header;
        for (auto &header : _headers)
        {
            std::string line = header.first + glinesep + header.second + glinespace;
            resp_header += line;
        }
        // 3.将: 状态行 + 报头 + 内容 整合返回一个字符串
        return status_line + resp_header + _blankline + _text;
    }
    //  2.2: 设置目标目录
    void SetTargetFile(const std::string &target)
    {
        _targetfile = target;
    }
    //  2.3: 根据状态码设置状态码描述
    void SetCode(int code)
    {
        _code = code;
        switch (_code)
        {
        case 200:
            _desc = "OK";
            break;
        case 404:
            _desc = "Not Found";
            break;
        default:
            break;
        }
    }
    // 2.4: 设置一条响应报头
    void SetHeader(const std::string &key, const std::string &value)
    {
        auto iter = _headers.find(key);
        if (iter != _headers.end())
            return;
        _headers.insert(std::make_pair(key, value));
    }
    //  2.5: 根据文件后缀,设置http格式的扩展名(Content-Type)
    // 文件扩展名 转 Content-Type
    std::string Uri2Suffix(const std::string &targetfile)
    {
        // ./wwwroot/a/b/c.html
        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 "";
    }

    // 2.6: 制作应答信息: 读取目标文件内容,调用3&4&5,设置状态行和响应报头(包括扩展名转换Content-Type)
    bool MakeResponse()
    {
        // 忽略图标请求
        if (_targetfile == "./wwwroot/favicon.ico")
        {
            LOG(LogLevel::DEBUG) << "用户请求: " << _targetfile << "忽略它";
            return false;
        }

        int filesize = 0;
        bool res = Util::ReadFileContent(_targetfile, &_text); // 浏览器请求的资源,一定会存在吗?出错呢?
        if (!res)
        {
            _text = "";
            LOG(LogLevel::WARNING) << "client want get : " << _targetfile << " but not found";
            // 1.设置状态码 + 状态码描述
            SetCode(404);
            _targetfile = webroot + page_404;
            filesize = Util::FileSize(_targetfile);
            Util::ReadFileContent(_targetfile, &_text);
            // 2.设置Content-Type
            std::string suffix = Uri2Suffix(_targetfile);
            // 3.根据目标文件属性设置响应报头
            SetHeader("Content-Type", suffix);
            SetHeader("Content-Length", std::to_string(filesize));
        }
        else
        {
            LOG(LogLevel::DEBUG) << "读取文件: " << _targetfile;
            // 1.设置状态码 + 状态码描述
            SetCode(200);
            filesize = Util::FileSize(_targetfile);
            // 2.设置Content-Type
            std::string suffix = Uri2Suffix(_targetfile);
            // 3.根据目标文件属性设置响应报头
            SetHeader("Content-Type", suffix);
            SetHeader("Content-Length", std::to_string(filesize));
        }
        return true;
    }
    bool Deserialize(std::string &reqstr)
    {
        return true;
    }

    ~HttpResponse() {}

public:
    std::string _version;
    int _code;
    std::string _desc;

    std::unordered_map<std::string, std::string> _headers;
    std::string _blankline;
    std::string _text;

    // 其他属性
    std::string _targetfile;
};


// 3.Http协议调用
//  1.HandlerHttpRquest函数: 接收客户端请求,并进行反序列化,获取请求信息;
//                          根据请求信息制作应答信息,进行序列化,并发送给客户端
//  2.Start函数调用HandlerHttpRquest
class Http
{
public:
    Http(uint16_t port) : tsvrp(std::make_unique<TcpServer>(port))
    {
    }
    void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client)
    {

        // 收到请求
        std::string httpreqstr;
        int n = sock->Recv(&httpreqstr); // 浏览器给我发过来的是一个大的http字符串, 其实我们的recv也是有问题的。tcp是面向字节流的.
        if (n > 0)
        {

            // 对报文完整性进行审核 -- 缺
            // 1.接收客户端请求,并进行反序列化,获取请求信息;
            HttpRequest req;
            req.Deserialize(httpreqstr);
            // 2.根据请求信息制作应答信息,进行序列化,并发送给客户端
            HttpResponse resp;
            resp.SetTargetFile(req.Uri());
            if (resp.MakeResponse())
            {
                std::string response_str = resp.Serialize();
                sock->Send(response_str);
            }
        }

// #ifndef DEBUG
// #define DEBUG
#ifdef DEBUG
        // 收到请求
        std::string httpreqstr;
        sock->Recv(&httpreqstr); // 浏览器给我发过来的是一个大的http字符串, 其实我们的recv也是有问题的。tcp是面向字节流的.
        std::cout << httpreqstr;

        // 直接构建http应答,内存级别+固定
        HttpResponse resp;
        resp._version = "HTTP/1.1";
        resp._code = 200; // success
        resp._desc = "OK";

        std::string filename = webroot + homepage; // "./wwwroot/index.html";
        bool res = Util::ReadFileContent(filename, &(resp._text));
        std::string response_str = resp.Serialize();
        sock->Send(response_str);
#endif
    }
    // 对请求字符串,进行反序列化
    void Start()
    {
        tsvrp->Start(
            [this](std::shared_ptr<Socket> &sock, InetAddr &client)
            {
                this->HandlerHttpRquest(sock, client);
            });
    }
    ~Http()
    {
    }

private:
    std::unique_ptr<TcpServer> tsvrp;
};
  1. 读取目标文件内容

  2. 读取一行内容: 针对请求报头,读取第一行的请求行。用来获取 请求方法 + URI + HTTP版本 (主要用来获取URI)

  3. 获取文件大小

Util.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <fstream>
#include <string>

// 工具类

class Util
{
public:
    // 1.读取目标文件内容
    static bool ReadFileContent(const std::string &filename, std::string *out)
    {
        // version1
        // std::ifstream in(filename);
        // if(!in.is_open())
        // {
        //     return false;
        // }
        // std::string line;
        // while(std::getline(in, line))
        // {
        //     *out += line;
        // }
        // in.close();
        // return true;

        // version2 // 以二进制方式进行读取
        int filesize = FileSize(filename);
        if (filesize > 0)
        {
            std::ifstream in(filename);
            if (!in.is_open())
                return false;
            out->resize(filesize);
            in.read((char *)(out->c_str()), filesize);
            in.close();
        }
        else
        {
            return false;
        }
        return true;
    }
    // 2.读取一行内容: 针对请求报头,读取第一行的请求行。用来获取 请求方法 + URI + HTTP版本 (主要用来获取URI)
    static bool ReadOneLine(std::string &bigstr, std::string *out, const std::string &sep)
    {
        auto pos = bigstr.find(sep);
        if (pos == std::string::npos)
            return false;

        *out = bigstr.substr(0, pos);
        bigstr.erase(0, pos + sep.size());
        return true;
    }
    // 3.获取文件大小
    static int FileSize(const std::string &filename)
    {
        std::ifstream in(filename, std::ios::binary);
        if(!in.is_open())
            return false;
        in.seekg(0, in.end);
        int filesize = in.tellg();
        in.seekg(0,in.beg);
        in.close();
        return filesize;
    }
};

6.4 功能验证

编译成功之后在终端运行服务端

bash 复制代码
./myhttp 8080

使用浏览器直接访问

bash 复制代码
IP:port		 # 默认外部根目录
IP:port/目录	# 指定目录


6.5 完整版代码

【免费】网络-http协议-http协议简单服务器实现资源-CSDN下载

附录:HTTP历史及版本核心技术与时代背景

HTTP(Hypertext Transfer Protocol,超文本传输协议)作为互联网中浏览器和服务器间通信的基石,经历了从简单到复杂、从单一到多样的发展过程。以下将按照时间顺序,介绍HTTP的主要版本、核心技术及其对应的时代背景。

HTTP/0.9

核心技术:

  • 仅支持GET请求方法。

  • 仅支持纯文本传输,主要是HTML格式。

  • 无请求和响应头信息。

时代背景:

  • 1991年,HTTP/0.9版本作为HTTP协议的最初版本,用于传输基本的超文本HTML内容。

  • 当时的互联网还处于起步阶段,网页内容相对简单,主要以文本为主。

HTTP/1.0

核心技术:

  • 引入POST和HEAD请求方法。

  • 请求和响应头信息,支持多种数据格式(MIME)。

  • 支持缓存(cache)。

  • 状态码(status code)、多字符集支持等。

时代背景:

  • 1996年,随着互联网的快速发展,网页内容逐渐丰富,HTTP/1.0版本应运而生。

  • 为了满足日益增长的网络应用需求,HTTP/1.0增加了更多的功能和灵活性。

  • 然而,HTTP/1.0的工作方式是每次TCP连接只能发送一个请求,性能上存在一定局限。

HTTP/1.1

核心技术:

  • 引入持久连接(persistent connection),支持管道化(pipelining)。

  • 允许在单个TCP连接上进行多个请求和响应,提高了性能。

  • 引入分块传输编码(chunked transfer encoding)。

  • 支持Host头,允许在一个IP地址上部署多个Web站点。

时代背景:

  • 1999年,随着网页加载的外部资源越来越多,HTTP/1.0的性能问题愈发突出。

  • HTTP/1.1通过引入持久连接和管道化等技术,有效提高了数据传输效率。

  • 同时,互联网应用开始呈现出多元化、复杂化的趋势,HTTP/1.1的出现满足了这些需求。

HTTP/2.0

核心技术:

  • 多路复用(multiplexing),一个TCP连接允许多个HTTP请求。

  • 二进制帧格式(binary framing),优化数据传输。

  • 头部压缩(header compression),减少传输开销。

  • 服务器推送(server push),提前发送资源到客户端。

时代背景:

  • 2015年,随着移动互联网的兴起和云计算技术的发展,网络应用对性能的要求越来越高。

  • HTTP/2.0通过多路复用、二进制帧格式等技术,显著提高了数据传输效率和网络性能。

  • 同时,HTTP/2.0还支持加密传输(HTTPS),提高了数据传输的安全性。

HTTP/3.0

核心技术:

  • 使用QUIC协议替代TCP协议,基于UDP构建的多路复用传输协议。

  • 减少了TCP三次握手及TLS握手时间,提高了连接建立速度。

  • 解决了TCP中的线头阻塞问题,提高了数据传输效率。

时代背景:

  • 2022年,随着5G、物联网等技术的快速发展,网络应用对实时性、可靠性的要求越来越高。

  • HTTP/3.0通过使用QUIC协议,提高了连接建立速度和数据传输效率,满足了这些需求。

  • 同时,HTTP/3.0还支持加密传输(HTTPS),保证了数据传输的安全性。

相关推荐
wechat_Neal2 小时前
车载以太网技术全景-网络基础理论篇
网络
水境传感 张园园2 小时前
便携式光透过率检测仪:如何成为安全“守门人”?
网络
Mintopia3 小时前
🚀 HTTP/2 多路复用技术全透视
网络协议·http·https
做萤石二次开发的哈哈3 小时前
萤石开放平台 萤石可编程设备 | 设备 Python SDK 使用说明
开发语言·网络·python·php·萤石云·萤石
nvd113 小时前
从 SSE 到 Streamable HTTP:MCP Server 的现代化改造之旅
网络·网络协议·http
小蜗的房子4 小时前
Oracle 19C RAC Public IP单网卡改为bond模式操作指南
运维·网络·数据库·sql·tcp/ip·oracle·oracle rac
无忧智库4 小时前
国家级算力枢纽节点(东数西算)跨区域调度网络与绿色节能数据中心建设:深度解析“数字新基建”的战略落地
网络
网络工程师_ling4 小时前
【阿里云多地域混合云网络架构】
网络·阿里云·架构
sun0077004 小时前
androd和qnx判断实网卡还是虚网卡
运维·服务器·网络