【Linux】网络编程 —— HTTP协议(上)

🌈欢迎来到Linux专栏 ~~ 网络编程

网络编程

👀再谈协议

在上一篇文章中,我们了解了 协议 的制定与使用流程(简单结构体 ),不过太过于简陋了,真正的 协议 会复杂得多,也强大得多,比如在网络中使用最为广泛的 HTTP/HTTPS 超文本传输协议

但凡是使用浏览器进行互联网冲浪,那必然离不开这个 协议HTTP/HTTPS 不仅支持传输文本,还支持传输图片、音频、视频等 资源

  • 在互联网世界中,HTTP(HyperTextTransferProtocol,超文本传输协议)是一个至关重要的协议。它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如HTML文档)。
  • HTTP协议是客户端与服务器之间通信的基础。客户端通过HTTP协议向服务器发送请求,服务器收到请求后处理并返回响应。HTTP协议是⼀个无连接、无状态 的协议,即每次请求都需要建立新的连接,且服务器不会保存客户端的状态信息。

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

可以在浏览器中根据 CSDN服务器的 IPPort,以及资源路径 ,基于 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 + 路径就可以在全网中定位找到这个文件

  • URIURL是一种特殊的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格式

URL编码工具

🍅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 ,最后把空行与正文加上 ------ 最终变成了一行行字符串

    cpp 复制代码
    GET / 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

    也就是说上述的一大段在我眼里就是一行大字符串而已💥

    cpp 复制代码
    class 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;
    };

现在翻转过来,我们站在接收方的角度出发,收到了一个大字符串怎么样进行处理??

  • 以行为单位,一行一行的进行读(直到空行)------ 再提取出第一行(以空格作为分隔符)进行反序列化,拿到对应数据
  • 请求报头则是以:空格 作为分隔符,逐个拿到 keyvalue
  • 其中请求报头里是存在一种属性: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
  • 字符串提取出keyvalue后,成对的插入到_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-Length
  • Host客户端告知服务器,所请求的资源是在哪个主机的哪个端口上
  • 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(下)

相关推荐
国科安芯1 小时前
星间光链路:AS32S601型抗辐射MCU在空间激光通信终端控制中的技术实现
服务器·网络·单片机·嵌入式硬件·物联网·安全·信息与通信
小稻穗1 小时前
市场调研样本选型坐标系:2026六家国内样本平台能力横向校验
大数据·运维·数据分析
栩栩云生2 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
AOwhisky2 小时前
Python 学习笔记(第十三期)——运维自动化(下·前篇):远程命令执行——paramiko基础篇
运维·python·学习·云原生·自动化·运维开发·paramiko
可视化运维管理爱好者2 小时前
nVisual Studio:数据中心网络智能规划设计工具
网络·ide·visual studio
天行健,君子而铎2 小时前
知影-API风险监测系统:数据流转安全的核心防护方案
网络
AOwhisky2 小时前
Python 学习笔记(第十四期)——运维自动化(下·中篇):远程文件传输——paramiko进阶篇
运维·python·学习·云原生·自动化·文件传输·paramiko
其实防守也摸鱼3 小时前
补天SRC新手入门指南:从0到1的漏洞挖掘之路
网络·python·学习·安全·web安全·数据挖掘·挖洞
imc.113 小时前
linux基础IO
linux·运维·服务器