🌈欢迎来到Linux专栏 ~~ 网络编程
- 🌍博客主页 :张小姐的猫~江湖背景
- 🔥所属专栏 :Linux ~ 不破不立
- 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏

网络编程
- [🌈欢迎来到Linux专栏 ~~ 网络编程](#🌈欢迎来到Linux专栏 ~~ 网络编程)
- 👀再谈协议
- 🍅HTTP协议
-
- ☘️见一见请求
- ☘️协议轮廓
-
- 1️⃣HTTP请求
- [2️⃣HTTP 响应](#2️⃣HTTP 响应)
- 3️⃣HTTP常见方法
- ☘️见一见响应
- 🍅模拟实现HTTP协议
- 🍅简单测试
- 📢写在最后

👀再谈协议
在上一篇文章中,我们了解了 协议 的制定与使用流程(简单结构体 ),不过太过于简陋了,真正的 协议 会复杂得多,也强大得多,比如在网络中使用最为广泛的 HTTP/HTTPS 超文本传输协议
但凡是使用浏览器进行互联网冲浪,那必然离不开这个 协议 ,HTTP/HTTPS 不仅支持传输文本,还支持传输图片、音频、视频等 资源
- 在互联网世界中,
HTTP(HyperTextTransferProtocol,超文本传输协议)是一个至关重要的协议。它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如HTML文档)。 HTTP协议是客户端与服务器之间通信的基础。客户端通过HTTP协议向服务器发送请求,服务器收到请求后处理并返回响应。HTTP协议是⼀个无连接、无状态 的协议,即每次请求都需要建立新的连接,且服务器不会保存客户端的状态信息。

客户端/浏览器上传资源的大小称为 上行流量 ,获取资源的大小称为 下行流量,网速则是单位时间内所能传输的流量大小,所以网速越快,上传/下载的体验就会越好

可以在浏览器中根据 CSDN服务器的 IP 和 Port,以及资源路径 ,基于 HTTPS 协议,获取我们所需要的资源
比如 https://blog.csdn.net/qq_42996461 就表示我的个人主页

🌽认识URL
诸如上面的网址称为 URL -> Uniform Resource Locator 统一资源定位符 ,也就我们熟知的 超链接/链接 ,URL 中包含了 协议、IP地址、端口号、资源路径、参数 等信息

-
http:协议名称 -
登录信息现在已经不使用了,因为不够安全
-
www.bilibili.com:域名 就是IP地址 -
80:服务器端口号(可省略),只是没有显示出来,而不是不存在
-
/qq_42996461:带层次的文件路径 ,所以说网页的本质就是一个文件,服务器怎么保证自己的网页文件能被你找到 ------ 提供文件路径 -
所以
HTTP本质是为了获取服务器上的文件内容 (图片、音频),所以需要IP+Port+ 目标文件路径 = 定位全网内唯一的文件(路径唯一、套接字唯一) -
URL:不仅标识唯一,还可以根据IP + 路径就可以在全网中定位找到这个文件 -
URI:URL是一种特殊的URI。唯一标识一个资源,只负责 "找到这个资源叫什么",不规定怎么访问。 -
uid=1#ch1是提交给服务器的数据、参数 ------URL传参
我们上网行为就只有两种:把我数据给你、把你数据给我(IO)

浏览器会内置域名解析浏览器的地址 (8.8.8.8),如果域名解析服务器找不到,可能会继续向上找跟域名服务器
🌽URL编码、解码
那么如果我输入的数据与URL出现冲突呢?是会怎么样进行处理的?
浏览器为了不让输入的参数干扰
URL的正常解析,会对特殊符合进行编码,后续服务器再对此进行解析
Encode 就是将诸如 分隔符、中文、其他非英语语言 等转换成计算机能认识的符合,比如在浏览器搜索框中输入 //?:: 请求相关资源,实际 URL 中的 参数 为 q=%2F%2F%3F%3A%3A
- 转码规则:将需要转码的字符(
ASCII码值)转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做⼀位,前面加上%,编码成%XY格式

🍅HTTP协议
☘️见一见请求
将浏览器视为客户端 ,编写服务器,浏览器通过 IP+Port 访问服务器时,就会发出 HTTP 请求,服务器在接收后可以进行打印,也就可以看到 HTTP 请求了
接下来只需在上篇序列化与反序列化的代码上进行更改即可
service()函数由长服务 -----> 短服务HttpServer.cc新增一个自定义的HTTP协议模块 ,并且修改对应的handler方法 ------ 链接成协议里的HandlerHttpRequest函数
Http.hpp自定义协议文件
cpp
#pragma once
#include <iostream>
#include <string>
class HttpResquest
{
};
class HttpResponse
{
};
class HttpProtocol
{
public:
HttpProtocol()
{
}
std::string HandlerHttpRequest(std::string &req)
{
std::cout << "####################" << std::endl;
std::cout << req << std::endl;
std::string status_line = "HTTP/1.1 200 OK\r\n";
status_line += "\r\n";
std::string html = "<html><body><h1>LBW NB!</h1></body></html>";
return status_line + html;
}
~HttpProtocol()
{
}
private:
};
HttpServer.cc服务端启动程序
cpp
#include "TcpServer.hpp"
#include "Http.hpp"
#include <memory>
static void Usage(const std::string &proc)
{
std::cout << "Usgae: \t" << proc << "port: " << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
uint16_t server_port = std::stoi(argv[1]);
//0.定义HTTP协议模块
std::unique_ptr<HttpProtocol> protocol = std::make_unique<HttpProtocol>();
//1.定义网络传输对象
std::unique_ptr<TcpServer> tsur = std::make_unique<TcpServer>(
[&protocol](std::string &inbuffer) -> std::string
{
return protocol->HandlerHttpRequest(inbuffer);
}, server_port
);
// 3.启动服务
tsur->Loop();
return 0;
}
最后再改一下Makefile,只需要一个客户端程序即可
cpp
httpserver: HttpServer.cc
g++ -o $@ $^ -std=c++17
.PHONY: clean
clean:
rm -f httpserver
接下来编译并且启动服务器

现在服务器已经准备好了,浏览器输入 IP+Port 发出 HTTP 请求,因为当前服务器并未进行任何响应,所以浏览器无法显示页面
此处如果没有反应,可能是云服务器没有放开对应的端口号,去防火墙里放行对应端口号即可

这就是Http请求

此时的服务器是联通的,只是返回的信息,浏览器不认识!浏览器只认识网页信息:Http应答格式返回
也是我们修改返回内容的格式(后面再详细讲对应是什么)
cpp
std::string HandlerHttpRequest(std::string &req)
{
std::cout << "####################" << std::endl;
std::cout << req << std::endl;
std::string status_line = "HTTP/1.1 200 OK\r\n";
status_line += "\r\n";
std::string html = "<html><body><h1>LBW NB!</h1></body></html>";
return status_line + html;
}
再次进行刷新!

但是网页内容是硬编码进代码里,就没办法开发了。正确做法是把网页内容进行文件化,放到特定路径下,未来如有需要就进行加载读取即可
☘️协议轮廓
HTTP 协议由 Request 请求 和 Response 响应 两部分组成
1️⃣HTTP请求
从宏观角度来看,HTTP 请求 分为这几部分:
- 请求行,包括请求方法(
GET 获取 / POST 发送)、URI(请求资源路径)、协议版本 --- 客户端(http/1.0 http/1.1 http/2.0) - 请求报头,表示请求的详细细节,由多组
k: v结构所组成 - 空行,区分报头和有效载荷
- 有效载荷:账户密码等(也可以没有)

也就说请求行 + 请求报头 = 报头 ,请求正文是有效载荷
在空行之前都是以行为单位!!
在
HTTP协议中是使用\r\n作为 分隔符 的
举例:


如何分离 协议报头 与 有效载荷 ?
- 以空行
\r\n进行分隔,空行之前为协议报头,空行之后为有效载荷
如何进行 序列化与反序列?
- 序列化:使用 \r\n 进行拼接
- 反序列化:根据 \r\n 进行读取
那么在客户端视角 ,如何表达一个HttpResquest呢?
-
把结构化的字段拼成一个字符串 、把一个
<k,v>提取出来后加上\r\n,最后把空行与正文加上 ------ 最终变成了一行行字符串cppGET / HTTP/1.1 Host: 193.112.168.183:9090 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 Upgrade-Insecure-Requests: 1也就是说上述的一大段在我眼里就是一行大字符串而已💥
cppclass HttpResquest { public: //序列化 //反序列化 private: std::string _method; std::string _uri; std::string _version; std::unordered_map<std::string, std::string> _handler; std::string _blank_line; std::string _body; };
现在翻转过来,我们站在接收方的角度出发,收到了一个大字符串怎么样进行处理??
- 以行为单位,一行一行的进行读(直到空行)------ 再提取出第一行(以空格作为分隔符)进行反序列化,拿到对应数据
- 请求报头则是以
:空格作为分隔符,逐个拿到key与value - 其中请求报头里是存在一种属性:
Content-Length,也就说是能知道有效载荷的长度的
此处的反序列最好是我们自己实现,HTTP作为基础协议,是不想依赖于任何的第三方库的

2️⃣HTTP 响应
至于 HTTP 响应 分为这几部分:
- 状态行:协议版本(服务端)、状态码、状态码描述
- 响应报头:表示响应的详细细节,由多组
k: v结构所组成 - 空行:区分报头和有效载荷
- 有效载:即客户端请求的资源: (视频、音频等,也有可能为空)

HTTP 响应 中关于 协议报头与有效载荷的分离、序列化与反序列化 等问题和 HTTP 请求 中的处理方式一致
举个例子:



如何理解协议版本?
- 客户端和服务器可能使用了不同的
HTTP版本(最常用是HTTP/1.1) - 服务器可以根据协议版本的匹配情况进行功能响应
- 可以理解成英雄联盟更新的版本号!服务端每次升级,你客户端太久没打开,上号就会要求你更新客户端
什么是状态码?
- 状态码类似于
C/C++中的错误码 ,可以反应请求的情况, - 常见的状态码:
404,状态码的描述为No Found;请求成功,服务器受理:2xx,状态码的描述为OK;
3️⃣HTTP常见方法

其中最常用的就是GET方法和POST方法
☘️见一见响应
可以通过 telnet 这个工具获取服务器的响应,比如获取 bilibili 服务器的响应
cpp
telnet www.baidu.com 80

输入 ^] 连接服务器(ctrl + ])
此时就表示已经和 百度 的服务器建立了连接

接着发出一个最简单的请求,看看 百度 服务器的响应结果
注意: 需要先按回车后,再发出请求,请求发出后需要再次回车表示空行,同时回车发送 (三次回车)
下面这个就是 百度 服务器对于请求资源路径为 / 时的响应结果,也就是前端页面信息

而这就是 百度 的默认页面,它的响应结果也得遵循 HTTP 协议的响应格式

状态行中包括了 HTTP 版本、状态码、状态描述 ,响应报头中是各种 属性 ,重要字段后面再谈,有效载荷中则是请求的 资源
🍅模拟实现HTTP协议
今天我们重点放在HTTP协议本身 ,TcpServer处理时回调到协议里的函数HandlerHttpRequest去完成执行
cpp
//0.定义HTTP协议模块
std::unique_ptr<HttpProtocol> protocol = std::make_unique<HttpProtocol>();
//1.定义网络传输对象
std::unique_ptr<TcpServer> tsur = std::make_unique<TcpServer>(
[&protocol](std::string &inbuffer) -> std::string
{
return protocol->HandlerHttpRequest(inbuffer);
}, server_port
);
之前说过,协议本身就是双方都具有共识的结构体变量,在CPP里就是一个类!
分为请求 和应答两个类
当用浏览器访问你的服务器(如 http://127.0.0.1:8080)时,Recv 收到的 inbuffer 是一个标准 HTTP 请求报文,类似这样:
json
GET / HTTP/1.1\r\n
Host: 127.0.0.1:8080\r\n
Connection: keep-alive\r\n
User-Agent: Mozilla/5.0 (X11; Linux x86_64)...\r\n
Accept: text/html,application/xhtml+xml,...\r\n
Accept-Encoding: gzip, deflate\r\n
Accept-Language: zh-CN,zh;q=0.9\r\n
\r\n
此处为了简化工作,我们假设是读取到了一个完整的
HTTP报文请求
🌳HTTP协议类
也就是传入的req字符串已经是一个大字节流了
- 1️⃣首先构建一个请求类
httpreq,接着对req进行反序列化 ,填充好对应的请求类(字符流信息转化成结构化信息) - 2️⃣根据不用的请求方法和请求URI,给用户返回不同的报文(根据结构化请求
httpreq转化成为结构化的httpresq------ 结构体之间的转化) - 3️⃣再把应答类的结构化数据进行序列化 返回
cpp
class HttpProtocol
{
public:
HttpProtocol()
{
}
std::string HandlerHttpRequest(std::string &req)
{
// 1. 保证报文完整性 --- 忽略
// 2. 反序列化
HttpResquest httpreq;
httpreq.Deserialize(req);
// 3.根据不用的请求方法和请求URI,给用户返回不同的报文
// 根据结构化请求httpreq 转化成为结构化的 httpresq ------ 结构体之间的转化
HttpResponse httpresq;
httpresq.Build(httpreq);
return httpresq.Serialize();
}
~HttpProtocol()
{
}
private:
};
接下来见一下这个/是什么??
cpp
https://gamesci.cn/
/ : web根目录,默认请求的时候URI = /
web根目录一般是在项目同级的路径下!并且任何一个站点都要有一个默认的首页index.html
-
web根目录在下面的列表里指的就是wwwroot,默认请求时服务器会给你添加上一个默认首页!cpp/ + index.html

以上的文件就是给你返回资源的网络服务器,资源就存在于某个工作目录里:wwwroot
介绍一个函数:
wget:在命令行中模拟浏览器下载文件
cpp
wget https://example.com/
执行后会:向服务器发送 HTTP 请求,获取根目录,保存到当前目录 ,实际上会返回默认首页:/ + 默认首页 。
所以HTTP请求的本质 :就是通过网络服务器把Linux机器上,特定目录下的文件以HTTP应答的方式返回给Client(浏览器或者app)
- 因为文件有可能是网页、视频等,超越了文本 ------
HTTP叫做超文本传输协议
🌳请求类
后续我们会读到下面数据格式的字符流,紧接着对其进行反序列化,来填充请求类的结构化数据

接下来就上述格式的字符串进行反序列化,因为HTTP不想依赖任何第三方的东西,所以它的反序列化是自己搓出来的!
反序列化分为了三个模块:解析请求行ParseReqLine、解析报头ParseHeadkv、解析正文部分PraseText ------ 为了提高耦合度,在反序列化函数外实现这三个函数
接下来逐个分析:
1️⃣解析请求行 ParseReqLine
- 调用工具类
Util提取出首行! - 因为以空格为单位,所以用
stringstream流提取到_method、 _uri 、_version - 如果请求的资源单单是
/,就需要在此基础上加上首页g_first_page,如果单纯这样写,不就是在根目录区寻找吗?? - 为了防止上述情况发生,需要在前拼接上
web根目录 :wwwroot。后续访问/的话,就会转化成:web根目录 +/+g_first_page
cpp
bool ParseReqLine(std::string &httpstr)
{
std::string req_line = Util::ReadLine(httpstr);
if (req_line.empty() || req_line == linesep)
return false;
std::stringstream ss(req_line);
ss >> _method >> _uri >> _version;
if (_uri == "/")
_uri += g_first_page; // uri最终会被转化成:/index.html
_uri = g_wwwroot + _uri; // web根目录
return true;
}

测试成功!
2️⃣解析报头 ParseHeadkv
第二步就是解析剩下的报头!我们知道请求报头此处是key:[空格]value
- 逐行逐行的读取:
do while - 字符串提取出
key和value后,成对的插入到_handler里
cpp
bool ParseHeadkv(std::string &httpstr)
{
std::string head_line;
// 不断读取请求报头
do
{
head_line = Util::ReadLine(httpstr);
if (head_line.empty()) // 读到空行
return false;
if (head_line != linesep) // 读到内容
{
auto pos = head_line.find(headsep);
if (pos == std::string::npos)
return false;
std::string key = head_line.substr(0, pos);
std::string value = head_line.substr(pos + headsep.size());
_handler.insert(std::make_pair(key, value));
// _handler.insert({key, value});
}
} while (head_line != linesep); // 读到具体一行
return true;
}
测试是否解析报头成功,且放进类的map里
cpp
for(auto& header: _handler)
{
std::cout << header.first << ": " << header.second<< std::endl;
}
std::cout << "start|"<< httpstr << "|end";

3️⃣解析正文部分 PraseText
此部分主要解决两个问题:有没有正文 、正文多大?从哪开始
这就引出了HTTP常见Header报头
Content-Type:数据类型(text/html等)Content-Length:正文Body的长度,没有正文就不需要Content-LengthHost:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上User-Agent:声明用户的操作系统和浏览器版本信息 ------ 仅出现在请求类里Referer:当前页面是从哪个页面跳转过来的Location:搭配3xx状态码使用,告诉客户端接下来要去哪里访问;Cookie:用于在客户端存储少量信息。通常用于实现会话(session)的功能
也就说去_handler里找是否存在Content-Length,如果不存在:没有正文。存在就提取出对应长度的子串,并且删除此正文。
cpp
bool PraseText(std::string &httpstr)
{
// 1.有没有正文
if (_handler.find("Content-Length") == _handler.end())
{
_body = "";//不存在
}
else
{
// 2.正文多大?
int Content_Length = std::stoi(_handler["Content-Length"]);
_body = httpstr.substr(0, Content_Length);
httpstr.erase(0, Content_Length);
}
return true;
}
🌳工具类
工具类实现了两个功能函数:
ReadLine:读取出一行数据(字符串查找),分为三种情况:
- 空 :没有完整行 ------
pos == std::string::npos \r\n:读取到了空行- 其他情况,读到了一行具体内容
注意:
erase函数应该在line的判空之前,如果反过来放,导致并没有删除掉对应的空行就返回了
ReadUri:根据文件路径,读取文件全部内容返回为字符串
作用:
- 把
HttpResquest解析出的文件路径(如wwwroot/index.html)打开并且进行逐行读取 - 最后返回一个大字符串
content
cpp
// 工具类
class Util
{
public:
static std::string ReadLine(std::string &str)
{
auto pos = str.find(linesep);
if (pos == std::string::npos)
return std::string(); // 没有完整行
std::string line = str.substr(0, pos);
str.erase(0, line.size() + linesep.size()); // 也删除了分隔行
if (line.empty())
return linesep; // 读到分隔行
return line;
}
static std::string ReadUri(std::string &filename)
{
// 超文本协议 bug
std::ifstream in(filename);
if (!in.is_open())
{
return std::string();
}
std::string content;
std::string line;
while(std::getline(in, line))
{
content += line;
}
in.close();
return content;
}
};
🌳应答类
这个类实现了两个功能:通过请求类来构建Build应答类 、对应答类进行序列化发送出去
1️⃣构建应答类:
- 获取到对应的内容
_body,并且设置状态码和状态码描述
了解一下状态码 。
最常见的状态码,比如200(OK)、404(NotFound)、403(Forbidden)、302(Redirect,重定向)、504(BadGateway)

cpp
bool Build(HttpResquest req)
{
std::string target_file = req.Uri();
_body = Util::ReadUri(target_file);
if (_body.empty())
{
// 没读取到
_code = 404;
return false;
}
else
{
_code = 200; // 表示ok
_code_desc = "OK";
return true;
}
}
2️⃣序列化
此处序列化无非就是对字符串进行拼接! (注意此处我们还没有对报头进行赋值,但先默认处理了)
cpp
std::string Serialize()
{
// 构建状态行
std::string respstr = _version;
respstr += spacesep;
respstr += std::to_string(_code);
respstr += spacesep;
respstr += _code_desc;
respstr += linesep;
// 添加报头
for (auto &handler : _handler)
{
std::string line = handler.first + headsep + handler.second + linesep;
respstr += line;
}
// 添加换行符
respstr += _blank_line;
// 添加正文
respstr += _body;
return respstr;
}
🍅简单测试
上面算是写完了一个简易的HTTP的框架了,接着我们进行一下测试,对于index.html的内容是用ai生成了一个网页内容
我们用浏览器去访问服务器:http://193.112.168.183:8080/,发现浏览器能接收到默认首页index.html的内容(尽管我们没有请求报头)

📢写在最后
接下来登场的是 HTTP(下)

