目录
- [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协议向服务器发送请求,服务器收到请求后处理并返回响应。
具体来说:
- 定位:属于网络模型的 "应用层",和我们程序员写的代码处于同一层。
- 核心作用:支撑两种最常见的上网行为 ------ 要么从远程服务器 "拿资源"(比如打开一篇文章、加载一张图片),要么把本地数据 "传上去"(比如发评论、传文件)。
- 关键特点 :
- 是 "无连接" 的:每次请求都要重新建立通信连接;
- 是 "无状态" 的:服务器不会记住客户端的操作(比如刷新页面后,服务器不会默认保留你之前的浏览位置);
- 端口固定:普通 HTTP 用 80 端口,加密的 HTTPS(更安全的版本)用 443 端口。
- 底层本质:靠 socket 通信实现传输,而它传递的 "资源"(网页、视频等),其实就是存放在 Linux 服务器里的文件。
1.2 认识URL
平时我们俗称的 "网址" 其实就是说的 URL。
URL 的全称是Uniform Resource Locator(统一资源定位符) ,它是互联网中唯一标识某一个资源位置的 "地址"------ 相当于资源在网络世界的 "门牌号",让客户端(如浏览器)能精准找到远程服务器上的目标内容(网页、图片、视频等)。
它的核心作用 & 特点:
- 唯一性 :通过 "协议 + 域名 + 资源路径" 的组合,能定位全网唯一的资源(比如
https://blog.csdn.net/xxx/article/details/yyy,对应 CSDN 服务器里某篇文章的文件); - 配合协议工作 :URL 开头的 "协议部分"(如
http://或https://),决定了客户端用哪种应用层协议(HTTP/HTTPS)去连接服务器; - 关联服务器资源: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(了解)
urlencode和urldecode是URL 的编码 / 解码工具,用来解决 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的长度。
- HTTP 报头和有效载荷的分离:HTTP 通过 **"空行(
\r\n\r\n)" 作为固定分隔符 ** 实现分离:
- 空行(连续的两个
\r\n换行符)之前的内容是报头(Header);- 空行之后的内容是有效载荷(Body)。
- HTTP 协议区分不同区域:HTTP 通过协议约定的专用分隔符区分各区域,而非任意特殊字符:
- 首行区域:用空格 分隔「请求方法、URL、HTTP 版本」(如截图首行的
POSThttp://...HTTP/1.1);- Header 区域:用 "冒号 + 空格"分隔 Header 的键和值(如
Host: job.xjtu.edu.cn),用\r\n分隔不同的 Header 字段;- 报头与有效载荷区域:用 ** 空行(
\r\n\r\n)** 分隔。
- HTTP 协议的序列化和反序列化在哪里表现
HTTP 是文本协议,其序列化 / 反序列化通过 "字符串拼接 + 分隔符解析" 实现,不依赖第三方库:
- 序列化(发送方) :将 "请求 / 响应的结构化数据(如方法、Header 键值对、Body 内容)",按照 HTTP 分隔符规则拼接为文本格式的 HTTP 报文(如截图中的请求,就是把 POST 方法、Header、账号密码 Body 按规则拼接成的文本);
- 反序列化(接收方):将收到的 HTTP 文本报文,按协议分隔符规则解析为 "结构化数据"(如服务器收到截图请求后,解析出请求方法是 POST、Cookie 信息、Body 里的账号密码)。
- 总结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.com、urn:isbn:9787506394803) |
是 URL、URN 的统称,是更宽泛的概念 |
| URL | 统一资源定位符(Uniform Resource Locator) | URI 的子集,通过资源的存储位置标识资源 | 通过 "位置" 定位并访问资源 | 日常说的 "网址"(如https://blog.csdn.net/xxx、ftp://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>
- form表单,不指定method时的默认方法是GET
- form表单提交的参数
- 在GET方法中:通过uri进行提交:
- 在POST方法中:通过http request正文提交:
GET和POST的区别:
GET:
获取或静态网页或资源
提交参数以uri方式提交
POST:提交参数以正文进行提交
GET和POST使用上的区别:
GET:
GET提交参数,建议提交的参数不要过长,因为uri一般有长度限制
GET方法会回显参数
POST:正文传参,意味着可以传递长数据
不会回显参数,更加私密
补:
- fiddler工具,抓取报文
- 加密协议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 核心问题
- client && server,是如何保证自己读取到的报文是完整的?
- 分析读取到的字节流是否存在空行
- 提取Content-Length: 获得正文长度,然后再读取或则和截取指定长度的内容
6.2 服务端程序执行流程
- 使用智能指针实例化Http类,并调用Start函数。(Http类中有一个TcpServer类的对象成员tsvrp)
- Http类中的Start函数调用tsvrp的Start函数(即调用TcpServer类中的Start函数),并传入lambda函数作为为Start函数的参数,在TcpServer中的Start函数中名为callback。
- TcpServer中的Start函数使用多进程调用callback,即调用Http类中传入的lambda函数(此lambda函数即位为调用Http类中的HandlerHttpRquest函数)
- HandlerHttpRquest函数中:
- 接收客户端请求信息并进行反序列化
- 将客户端请求的目标信息序列化之后,回传给客户端
6.3 服务端核心代码
-
解析客户端请求
-
对客户端请求作出回应,并将应答消息序列化为http协议的格式,最后可直接发送的字符串
-
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;
};
-
读取目标文件内容
-
读取一行内容: 针对请求报头,读取第一行的请求行。用来获取 请求方法 + URI + HTTP版本 (主要用来获取URI)
-
获取文件大小
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),保证了数据传输的安全性。



