当前位置: 首页 > news >正文

Linux网络HTTP协议(上)

前言

  这一篇是我们网络部分http协议的第一篇内容,在这一篇中我们先对http协议的概念做一定的认识,然后见一见我们http协议请求和应答的实现代码,先对协议格式有一定的了解,在下一篇中我们将会详细讲述http协议的格式内容~

一、HTTP协议概念

虽然我们说, 应用层协议是我们程序员自己定的(像我们上面自己定的实现网络计算器所需的协议);但实际上,已经有大佬们定义了一些现成的,又非常好用的应用层协议,供我们直接参考使用:HTTP(超文本传输协议)就是其中之一

在互联网世界中, HTTP(HyperText Transfer Protocol, 超文本传输协议,超文本除了文本之外还包括音视频等,所以是超文本) 是一个至关重要的协议。 它定义了客户端(如浏览器) 与服务器之间如何通信, 以交换或传输超文本(如 HTML 文档)

HTTP 协议是客户端与服务器之间通信的基础。 客户端通过 HTTP 协议向服务器发送请求, 服务器收到请求后处理并返回响应。 HTTP 协议是一个无连接、 无状态的协议, 即每次请求都需要建立新的连接, 且服务器不会保存客户端的状态信息

URL

我们先来认识一下URL——平时我们俗称的 "网址" 其实就是说的 URL (超链接)

比如说我们访问了一下腾讯新闻网站的某篇新闻

image-20250822134105330

https://news.qq.com/rain/a/20250822A021D200

解读这段URL(网址):

1.首先就是前面https表示获取资源采用的协议

2.其次https:后面接着的news.qq.com其实是域名地址而不是该网站真正的ip地址,因为用ip地址都是数字啥的可读性不高,使用域名地址就可以一眼看出想访问的是哪个网址了(news.qq.com就一眼知道是腾讯新闻了)

我们其实是通过浏览器内置的基础设施域名服务器来根据域名地址映射到对应网址的ip地址,紧接着通过ip地址访问到该网站

image-20250822131833113

我们之前就学过,找到目标服务器需要知道它的ip和port,那么ip有了,port端口号在这个网址没看见呀,欸其实我们成熟的协议呢,端口号都是固定的(这也就是我们前面说前1024的端口号不能给我们自行设置自己的服务器端口号使用的原因)

比如说:

https的port:443

http的port:80

也就是说现在我们网址中协议给出了固定的端口号port,而后面接着的域名会映射出ip地址,就找到这个目标服务器啦

所以我们进行URL(超链接)通信,本质也是socket通信!!

3.那么在域名后面接着的/rain/a/20250822A021D200就是我们访问的资源路径

关于”资源“:

人的上网行为就两种(其实就是IO)

  1. 从远端拿下来数据

  2. 将自己本地的数据上传到远端

数据像视频、网页、图片、音频等就是“资源”

由于这个“/”符号是linux的路径分隔符,根据上面的网页路径/rain/a/20250822A021D200(其中的20250822A021D200就是这个网页的名称),可以推断我们在没有获取这些“资源”的时候,这些资源在linux服务器内部以文件形式存在

所以在http角度:“资源”即文件,文件在linux的特定路径下

总结URL这个超链接的内容来说:

  1. http[s]:知道port

  2. 域名:可以映射目标主机IP地址(具有唯一性)

  3. 路径:表示目标机器上特定路径下的一个文件(具有唯一性)

  4. 域名+路径 -》找到全网内唯一的目标文件

  5. 通过port绑定的服务器就可以将这个文件推送给客户端

ok,可以给出比较权威的较为完整的URL的格式

image-20250822183227531

[^]  其中的登录信息现在已经不用了,服务器端口号也不需要显式写出来了,后面的查询、片段标识后面再讲 

urlencode 和 urldecode

前面的网址被我们称为静态的资源,那也就是说还会有动态的网址,动态网址指的就是在静态网址基础之上需要对这个网站进行交互(如:注册登录、提交代码等等需要交互式的操作)的网站

举个例子:如果我们在百度上搜索:hello ://@world

image-20250822193643320

那么上面的网址就是一个动态的网址

https://www.baidu.com/s?wd=hello%3A%2F%2F@world&rsv_spt=1&rsv_iqid=0xebb00faf0058a559&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_dl=tb&rsv_enter=1&rsv_sug3=21&rsv_sug1=24&rsv_sug7=101&rsv_n=2&rsv_btype=i&inputT=22109&rsv_sug4=22988

问题:我们可以看到hello和world都是有的,而在中间的 ://这些符号却变成了%3A%2F%2F这种形式

解释:像 / ? : 等这样的字符, 已经被 url 当做特殊意义理解了,因此这些字符不能随意出现.比如, 某个参数中需要带有这些特殊字符,客户端(在这一般是浏览器)就必须先对特殊字符进行转义编码 ,不然会使得url解析失败

转义的规则如下

将需要转码的字符转为 16 进制, 然后从右到左, 取 4 位(不足 4 位直接处理), 每 2 位做一位, 前面加上%, 编码成%XY 格式

这个操作就是urlencode ,urlencode是一种对字符串进行编码的操作,目的是将字符串转换为符合 URL 传输规则的格式

例如:

image-20250822194158983

可以看到其中的 "+" 被转义成了 "%2B"

过程:+号的ascii值是43为十进制,转成16进制是2B,那么url编码从右往左取前四个再在最前加%——也就是%2B了

(依次类推也就是我们上面的://这三个字符就被编码成了%3A%2F%2F了)

而服务器得对客户端发过来的完成编码的字符串进行解码:urldecode就是对 URL 编码后的 的字符串进行解码的操作,还原成原来的字符串

我们管这种由浏览器urlencode,服务器自己进行urldecode的模式叫做B/S模式(浏览器服务器模式),而我们之前写的通信代码客户端和服务端的这种模式叫做C/S模式

UrlEncode编码/UrlDecode解码的工具

二、HTTP 协议请求与应答格式

(http协议也是结构化的)

1.见一见请求和应答

1.请求

那么先来看一下http的请求格式样例:

image-20250822221230626

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

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

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

image-20250822221516671

上面两图的对应:

image-20250822222339423

问题:

  1. http如何做到报头和有效载荷的分离?

    答:通过这个空行(也就是\r\n)来进行分离——欸,我们之前自己定协议的时候用过\r\n的

  2. http协议该如何理解?

    答:协议都是一行一行使用换行符(在第一行【请求行】中是再按空格来区分)来分离区域——也就是说协议采用的是特殊字符来进行区分区域

  3. http协议的序列化和反序列化在哪里表现?

    答:我们可以把这个http请求看作是一个具有多个成员结构化的类,发过去的时候得是一个具有多行的大字符串;那么我们可以通过这个结构化的请求各成员转成大字符串的方法来进行序列化,而反序列化就是将这个大字符串转成结构化的各个成员的过程

    像这样:

    image-20250823203245738

http请求demo

我们自己得先写一份http协议的demo出来,这需要借助我们之前写的Socket.hpp、TcpServer.hpp,因为上面已经说了http协议的底层就是tcp,当然得用上tcpserver的接口啦(当然,我们写的日志代码、Common.hpp等等代码都是要用上的)

我们这里的TcpServer.hpp做一点小小的改变

#include "Socket.hpp"
#include <sys/wait.h>
#include <functional>
​
using namespace SocketModule;
using namespace LogModule;
​
using ioservice_t = std::function<void(std::shared_ptr<Socket> &sock, InetAddr &client)>;
​
class TcpServer
{
public:TcpServer(uint16_t port): _port(port),// 基类指针指向子类,子类重写了基类需要重写的方法_listensocketptr(std::make_unique<TcpSocket>()),_isrunning(false){// 直接完成了tcp服务器的初始化工作_listensocketptr->BuildTcpSocketMethod(_port);}
​void Start(ioservice_t callback){_isrunning = true;while (_isrunning){// 1. 和client通信的sockfd  2.client网络地址InetAddr client;auto sock = _listensocketptr->Accept(&client);if (sock == nullptr){// accept失败了continue;}LOG(LogLevel::DEBUG) << "accept success...";
​// 到这就说明有了客户端套接字以及客户端的地址信息pid_t id = fork();if (id < 0){LOG(LogLevel::FATAL) << "fork error";exit(FOCK_ERR);}else if (id == 0){// 子进程,关闭监听套接字_listensocketptr->Close();if (fork() > 0){exit(OK);}// 孙子进程在执行任务,已经是孤儿了callback(sock, client);// 回调方法执行结束,回到这说明获取请求结束了,我们可以套接字关闭了sock->Close();exit(OK);}else{// 父进程,关闭accept的套接字sock->Close();pid_t rid = ::waitpid(id, nullptr, 0);(void)rid;}}_isrunning = false;}
​~TcpServer() {}
​
private:uint16_t _port; // 端口号std::unique_ptr<Socket> _listensocketptr;bool _isrunning;
};

我们写的暂时的Http.hpp和Main.cc的代码,这样可以先见一见发出来的请求

Http.hpp

#pragma once
#include "TcpServer.hpp"
#include "Socket.hpp"
#include <iostream>
#include <string>
#include <memory>
#include <unordered_map>
​
using namespace SocketModule;
​
std::string gspace = " ";        // 空格
std::string glinespace = "\r\n"; // 换行符
​
// http的请求协议报头
class HttpRquest
{
public:HttpRquest(){}
​// 把结构化成员转成大字符串的方法// 也就是序列化std::string Serialize(){return std::string();}
​// 把大字符串转成下面各个成员的方法// 也就是反序列化bool Deserialize(std::string &reqstr){return true;}
​~HttpRquest(){}
​
private:// http请求协议结构化成员// 1.请求行std::string _method;  // 请求方法std::string _url;     // 超链接urlstd::string _version; // http版本// 2.请求报头// htpp的报头属性 key valuestd::unordered_map<std::string, std::string> _headers;
​// 3.空行std::string _blankline;
​// 4.请求正文std::string _text;
};
​
class Http
{
public:Http(uint16_t port): tsvrp(std::make_unique<TcpServer>(port)){}
​// 请求方法void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client){
#ifndef DEBUG
#define DEBUGstd::string httpreqstr;sock->Recv(&httpreqstr); // 浏览器给我发过来的是一个大的http字符串std::cout << httpreqstr;
#endif// 对请求字符串进行反序列化}
​void Start(){tsvrp->Start([this](std::shared_ptr<Socket> &sock, InetAddr &client){ this->HandlerHttpRquest(sock, client); });}
​
private:std::unique_ptr<TcpServer> tsvrp;
};

Main.cc

#include "Http.hpp"
​
// http port
int main(int argc, char *argv[])
{if (argc != 2){std::cout << "Usage: " << argv[0] << " port" << std::endl;exit(USAGE_ERR);}
​uint16_t port = std::atoi(argv[1]);
​std::unique_ptr<Http> httpsvr = std::make_unique<Http>(port);httpsvr->Start();return 0;
}

ok,我们绑定一下我们的端口号8080,然后运行我们的http服务器

image-20250823215708183

然后我们就可以使用浏览器进行访问了,可以直接输入网址:该服务的ip:8080(比如说我的就是192.168.85.128:8080)就可以看到服务器这边输出的读到的请求结构化的字符串了

image-20250823220736237

2.应答

那么先来看一下http的应答格式样例:

image-20250824115456502

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

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

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

image-20250824120056040

请求和应答(响应)格式对应

image-20250824120227130

http应答(响应)demo

成熟的http的响应做序列化,不需要依赖任何第三方库,自己做结构化成员拼接转成大字符串就好

像:

// 1.状态行拼接
std::string status_line = _version + gspace + std::to_string(_code) + gspace + _desc + glinespace;//  状态行结构化成员std::string _version; // http版本int _code;            // 状态码:用编号告诉客户端请求成功还是出错,出错原因,比如404std::string _desc;    // 状态码描述:对状态码返回出错信息的文字版描述,比如“Not Found”

那么响应报头类我们可以先这样写:

// http的响应报头
class HttpResponse
{
public:HttpResponse(): _blankline(glinespace){}
​// 把结构化成员转成大字符串的方法// 也就是序列化// 响应需要实现// 成熟的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 + ::gsep + header.second + glinespace;resp_header += line;}
​return status_line + resp_header + _blankline + _text;}
​// 把大字符串转成下面各个成员的方法// 也就是反序列化bool Deserialize(std::string &reqstr){return true;}
​~HttpResponse() {}
​// private://  http应答协议结构化成员//  1.状态行std::string _version; // http版本int _code;            // 状态码:用编号告诉客户端请求成功还是出错,出错原因,比如404std::string _desc;    // 状态码描述:对状态码返回出错信息的文字版描述,比如“Not Found”
​// 2.响应报头//  http的报头属性 key valuestd::unordered_map<std::string, std::string> _headers;
​// 3.空行std::string _blankline;
​// 4.响应正文std::string _text;
};

ok,我们的http类就先改成了这样:

class Http
{
public:Http(uint16_t port): tsvrp(std::make_unique<TcpServer>(port)){}
​// 请求方法void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client){
#ifndef DEBUG
#define DEBUG// 收到请求std::string httpreqstr;// 假设:读到了完整的请求sock->Recv(&httpreqstr); // 浏览器给我发过来的是一个大的http字符串// (这里的recv也是有问题的,tcp是面向字节流的)std::cout << httpreqstr;
​// 直接构建http应答,内存级别+固定HttpResponse resp;resp._version = "HTTP/1.1";resp._code = 200;  // 状态码位200表示成功resp._desc = "OK"; // 状态码描述表示成功resp._text = "<!DOCTYPE html>\<html lang=\"en\">\<head>\<meta charset=\"UTF-8\">\<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\<title>Hello World</title>\</head>\<body>\<h1>Hello World</h1>\<h1>Hello World</h1>\<h1>Hello World</h1>\<h1>Hello World</h1>\<h1>Hello World</h1>\</body>\</html>";
​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); });}
​
private:std::unique_ptr<TcpServer> tsvrp;
};

那么绑定端口号8080启动我们的服务器,再在浏览器使用192.168.85.182:8080进行访问就会出现:

服务端获得了我们浏览器访问请求:

image-20250824134809351

而下面这样的网页,这正是我们浏览器(客户端)给服务端发送请求之后,服务端发回给我们的响应

image-20250824134454791

问题:

  1. 请求怎么表示自己要请求什么资源?

    答:通过URI表示,当我们网址后不跟路径时,像上面的请求样例的URI是 /,这个/不是linux根目录,而是 web 根目录(是web/http服务器所对应的一个最简单的定好的目录,将来http乱码所有的资源都放在这个web根目录下),在URI是/的时候,服务器并不能给我们在web根目录下的所有资源,而是服务器自动拼接一个首页:index.html/index.htm给我们呈现;但如果我们这样写192.168.85.128:8080/a/b/c.html,那么URI就变成了/a/b/c.html这个路径

    image-20250824164412209

    根据规定:网页的内容必须是在服务器特定路径下的文件(图片、视频...,css,js...),也就是说我们上面所做的测试中呈现的网页是硬编码的,内容是直接写出来存在内存中的,不符合这个规定!

    我们需要新建一个wwwroot目录,这个目录就代表web根目录,然后在这个目录下创建一个index.html文件,这个文件就是我们默认的首页,我们将原来硬编码的html代码放到这个文件中(当然这个首页也是我们为了做实验无脑拼接的,这样无论URI是啥都会访问到这个首页,但实际当中应该是用户输入想去的这个服务器网页路径,URI就是这个路径,跳转到的也应是这个路径下的文件

    image-20250824184236286

    也就是说我们的resp._text应该是从这个文件中读取到的内容,我们可以在这里新增一个工具类,先写一个从文件中读取数据的静态方法,设置参数1为所要读取的文件路径,参数2为输出型参数,用来接收读取到的数据(也就是resp. _text),再在http类请求方法中调用这个读取文件的静态方法,把数据设置进resp. _text中

    Util.hpp

    #pragma once
    #include <iostream>
    #include <string>
    #include <fstream> //文件操作
    ​
    // 工具类
    class Util
    {
    public:// 读取文件内容的方法static bool ReadFileContent(const std::string &filename, std::string *out){// version1out->clear();std::ifstream in(filename);if (!in.is_open()){return false;}// 文件打开成功// 对文件进行读取std::string line;while (getline(in, line)){*out += line;}
    ​in.close();return true;}
    ​
    private:
    };

    我们修改之后的Http.hpp中http类的请求方法

    const std::string webroot = "../wwwroot";   // web根目录
    const std::string homepage = "/index.html"; // 首页
    ​
    ​
    // 请求方法void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client){
    #ifndef DEBUG
    #define DEBUG// 收到请求std::string httpreqstr;// 假设:读到了完整的请求sock->Recv(&httpreqstr); // 浏览器给我发过来的是一个大的http字符串// (这里的recv也是有问题的,tcp是面向字节流的)std::cout << httpreqstr;
    ​// 直接构建http应答,内存级别+固定HttpResponse resp;resp._version = "HTTP/1.1";resp._code = 200;  // 状态码位200表示成功resp._desc = "OK"; // 状态码描述表示成功
    ​std::string filename = webroot + homepage; // 要访问的文件:"../wwwroot/index.html"// 调用我们工具类中的静态从文件中读取内容的方法// 参数1为文件路径,参数2为输出型参数,用来接受从文件中读取的内容bool res = Util::ReadFileContent(filename, &(resp._text));(void)res;
    ​std::string response_str = resp.Serialize();sock->Send(response_str);
    ​
    #endif// 对请求字符串进行反序列化}

    再接着启动服务器,用浏览器访问时端口号后接的路径无论现在是什么,服务器返回的都是这个在wwwroot根目录下首页index.html文件的内容(并且网页的内容会随着在这个文件中的代码更改而更改)

    image-20250824185402434

    通过上面的演示,我们可以给出这个问题的结论了:请求通过URI决定,我们就根据这个URI在/后面的路径再加上这个/转化成的web根目录所处位置,进而来确定客户端要访问的网页是处在web根目录下的哪个文件的(如果只有/,那么就是在/后拼上首页路径默认访问我们设置的网站首页)!——http请求的本质其实就是请求我们代码中的../wwwroot目录下的特定路径下的资源,而URI中的路径就是我们要找的这个资源路径

http://www.dtcms.com/a/399368.html

相关文章:

  • 开源 java android app 开发(十四)自定义绘图控件--波形图
  • umijs 4.0学习 - umijs 的项目搭建+自动化eslint保存+项目结构
  • 汇天网络科技有限公司苏州关键词优化软件
  • 制冷剂中表压对应温度值的获取(Selenium)
  • 建什么网站访问量高seo优化
  • 小型网站建设参考文献word超链接网站怎样做
  • 可视化 GraphRAG 构建的知识图谱 空谈版
  • 安装gitlab并上传本地项目
  • 黄页88网站免费推广网站大全网
  • 深圳附近建站公司做企业网站有什么工作内容
  • 新能源知识库(104)什么是FAT和SAT?
  • 多元函数可微性的完整证明方法与理解
  • 长春建站培训wordpress广告先显示
  • 怎么做网页版手机版网站百度竞价托管公司
  • 【寰宇光锥舟】Bash 脚本详细解释
  • 如何高效解析复杂表格
  • glog使用: 07-错误信号处理(Failure Signal Handler)
  • Netty从0到1系列之内置Handler【下】
  • java服务注册到 Nacos 及相关配置
  • 设计网站与建设wordpress网站部署
  • 扬州鼎盛开发建设有限公司网站简单的ps网页设计教程
  • 本地AI部署成趋势:LocalAl+cpolar安全指南
  • 概率编程实战:使用Pyro/PyMC3构建贝叶斯模型
  • 数据结构系列之链表
  • 194-基于Python的脑肿瘤患者数据分析可视化
  • 在 Mac 上无线挂载 Android /sdcard
  • Nature论文解读DeepSeek R1:MoE架构如何重构高效推理的技术范式
  • 拆炸弹-定长滑动窗口/环形数组
  • 成都市城乡建设局网站重庆市建设施工安全网站
  • 力扣1003