【计网】从零开始学习http协议 --- http的请求与应答

在这里插入图片描述

如果你不能飞,那就跑;
如果跑不动,那就走;
实在走不了,那就爬。
无论做什么,你都要勇往直前。
--- 马丁·路德·金 ---

1 什么是http协议

虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用。 HTTP(超文本传输协议)就是其中之一。http应用十分的广泛,几乎每一名程序员(无论前后端 无论C++/Java/Go…)都会接触到!

在互联网世界中, HTTP(HyperText Transfer Protocol, 超文本传输协议) 是一个至关重要的协议。 它定义了客户端(如浏览器) 与服务器之间如何通信, 以交换或传输超文本,超文本支持视频,网页 ,图片等等!

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

但是有个疑问?http是基于TCP协议的,也就是面向连接的,为什么http确是无连接的协议呢?
因为http会使用Tcp建立的链接,无需再次建立新的链接。就好比之前我们实现的网络计算器,服务端和客户端的连接是通过TCP建立的,但是通信传输Request和Response直接通过Tcp建立的连接即可,无需再次建立连接!

2 认识URL

平时我们浏览的网站:百度 , 哔哩哔哩 ,力扣…等等网站都有一个域名

百度 :https://www.baidu.com/
哔哩哔哩:https://www.bilibili.com/
力扣:https://leetcode.cn/
...

这些网址都是https协议,这些网址其实就是URL!
在这里插入图片描述
访问时会将网址解析成IP地址!一般成熟的协议名称与端口号是强关联的,称之为知名端口号!

  1. HTTP (Hypertext Transfer Protocol):端口号:80 ,用于在Web服务器和客户端之间传输网页。
  2. HTTPS (HTTP Secure):端口号:443, HTTP的安全版本,通过SSL/TLS加密传输数据。
  3. FTP (File Transfer Protocol):
    • 控制端口:21 ,用于文件传输。
    • 数据端口:20,(主动模式)或随机端口(被动模式)
  4. SSH (Secure Shell):端口号:22 ,用于安全地访问远程服务器。

为什么平时访问网站并没有输入端口号?
只有同时具备IP地址和端口号才可以访问到对应的服务器,浏览器发起请求时会自动拼接端口号80!就类似日常生活中报警会自觉想到拨打110 , 火灾会自然的想到拨打119!

通信中离不开“资源”两个字,通信要么是从别处获取资源,要么是向对方发送资源。http协议下的资源是超文本!
网页,图片,音频,视频都是超文本!在进行通信之前,用户想要获取的资源都在后端的云服务器中,云服务器一般都是Linux系统,那么在Linux视角下不就都是文件吗!

为了将这个文件(资源)发给客户端,就必须要找到这个文件,那么怎么找到这个文件呢?当然是通过文件的唯一标识符 — 路径来实现!在URL中后半部分不就是我们的路径吗!这样通过IP地址确定的唯一主机+唯一的路径就可以标识互联网中的唯一的文件资源!

注意第一个斜杠不是Linux服务器的根目录,而是web根目录,web根目录可以是Linux中的任何目录!

所以URL就叫:统一资源定位符

urlencode 和 urldecode

  • 像 / ? : 等这样的字符,已经被 url 当做特殊意义理解了。因此这些字符不能随意出现。比如,某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义。
  • 转义的规则如下: 将需要转码的字符转为 16 进制, 然后从右到左, 取 4位(不足 4 位直接处理), 每 2 位做一位, 前面加上%,编码成%XY 格式

3 http的请求和应答

3.1 服务端设计

下图是http请求的一个信息:
在这里插入图片描述
接下来我们来通过代码实验,来测试一下是否可以获取到这些信息!
首先我们简化一下代码,在传输层直接进行IO,直接在Socket文件中获取数据流,将线程的函数方法修改为以下形式:

	// 注意设置为静态函数 , 不然参数默认会有TcpServer* this!!!
    static void *Execute(void *args)
    {
        pthread_detach(pthread_self()); // 线程分离!!!
        // 执行Service函数
        TcpServer::ThreadData *td = static_cast<TcpServer::ThreadData *>(args);
        // 直接进行IO
        std::string reqstr;
        // 这里默认读取到的是完整的请求
        ssize_t n = td->_sockfd->Recv(&reqstr);
        if (n > 0)
        {
            std::string resstr = td->_this->_service(reqstr);
            td->_sockfd->Send(resstr);
        }

        td->_sockfd->Close();
        delete td;
        return nullptr;
    }

回调函数单独设计一个HttpServer类来获取客户端申请的数据,目前直接进行简单的打印处理就好!

#include <iostream>
#include <string>

class HttpServer
{
public:
    HttpServer()
    {
    }
    std::string HandlerHelperRequest(std::string& Requeststr)
    {
        std::cout<<"------------------"<<std::endl;
        std::cout << Requeststr << std::endl;

        return std::string();//暂时这样
    }
    ~HttpServer()
    {
    }
};

然后ServerMain中,将HandlerHelperRequest作为回调函数构造TcpServer!进行启动即可!那么接下来我们是不是就可以在外界通过IP地址和端口号就可以访问Linux服务器上启动的进程了呢???还不可以,我们需要对Linux云服务器做一些处理,才能让外界成功的访问!

3.2 如何让外界可以访问Linux云服务器

让外界可以访问Linux云服务器需要两步操作:云服务器的安全组设置和服务器操作系统层面的防火墙设置。

云服务器的安全组设置操作步骤如下,这里以阿里云服务器为例:

  1. 首先在控制台中找到安全组,打开需要操作的实例对象
  2. 在实例中手动添加需要使用什么协议开放哪些端口,手动保存即可:
    • 协议类型:选择 TCP
    • 端口范围:填写“8888/8888”或“8888-8888”
    • 授权对象:可以设置为“0.0.0.0/0”以允许所有IP访问,但出于安全考虑,建议限制为您的IP地址或特定IP范围。
      在这里插入图片描述

云服务器设置好时候,接下来就进行服务器操作系统层面的防火墙设置:

  1. 对于CentOS:
    # 查看防火墙状态
    sudo systemctl status firewalld
    # 如果需要,启动防火墙服务
    sudo systemctl start firewalld
    # 检查端口8888是否开放
    sudo firewall-cmd --zone=public --query-port=8888/tcp
    # 如果端口未开放,添加端口规则
    sudo firewall-cmd --zone=public --add-port=8888/tcp --permanent
    # 重新加载防火墙规则
    sudo firewall-cmd --reload
    
  2. 对于Ubuntu:
    # 查看防火墙状态
    sudo ufw status
    # 如果需要,启用ufw
    sudo ufw enable
    # 检查端口8888是否开放
    sudo ufw allow 8888/tcp
    # 如果端口未开放,添加规则
    sudo ufw allow 8888/tcp
    # 重新加载防火墙规则
    sudo ufw reload
    

这样外界就可以通过IP地址和端口访问到对应的进程了!!!

3.3 运行测试

测试之前我们先获取一个当前机器的IP地址:

  1. 使用 curl 命令:
    curl ifconfig.me
    
  2. 使用 wget 命令:
    wget -qO- ifconfig.me
    

都可以获取到机器的外网IP!
我们启动程序,等待外部的链接:
可以通过手机或者电脑的浏览器通过IP地址和端口号来进行访问:
在这里插入图片描述
进行访问之后,会获取到对应的信息:
在这里插入图片描述

可以看到电脑WIndows系统和手机IPhone都成功的访问了我们的服务器!!!非常cool!!!

4 理解http请求与应答

4.1 宏观理解

请求和应答是http协议中双方都认识的结构化数据:

一个基本的http请求的格式是这个样子的,按行为单位!
在这里插入图片描述

  1. 请求行:指出请求类型(如GET或POST)、资源路径和使用的HTTP版本:
    • 方法(Method):表明对资源的请求类型,如 GET(获取资源)、POST(提交数据)、PUT(更新资源)、DELETE(删除资源)等。
    • URI(Uniform Resource Identifier):请求的资源的路径,例如一个网页的地址的后半部分。
    • HTTP版本(HTTP Version):表明使用的HTTP协议版本,如 HTTP/1.1 或 HTTP/2。
  2. 请求报头:提供关于客户端环境和请求本身的信息,如用户代理、接受的内容类型等。其中是以键值对的方式进行存储。
  3. 空行:请求报头和请求正文之间的分隔符。
  4. 请求正文(可选):包含要发送给服务器的数据,如表单数据。

http的应答与请求的格式很类似:在这里插入图片描述

  1. 状态行:包含HTTP版本、状态码和状态消息。例如,HTTP/1.1 200 OK 表示服务器成功处理了请求。
  2. 响应报头:提供关于响应的信息,如内容类型、内容长度、服务器类型、设置Cookie等。例如:
  3. 空行:响应报头和响应正文之间的分隔符。
  4. 响应正文(可选):包含从服务器返回的实际内容,如HTML页面、图片或其他数据。

知道了请求和报文的结构,其本质上还是报文,那么如何将其报头与有效载荷进行分离呢?
我们看到的请求和应答的结构可以看到,报头和报文是通过换行符进行分割的!巧了我们之前不也是这样进行操作的吗!而且只要有正文,就会有对应的content-length:xxx来帮我我们判断正文的是否完整!

4.2 http请求反序列化

接下来我们简单设计一下HttpRequesthttp请求的结构化数据!
首先根据其整体的结构我们可以加入四个成员变量:请求行 ,请求报头, 空行 ,请求正文

// 设计http协议
class HttpRequest
{
public:
    HttpRequest()
    {
    }
    void Serialization(std::string &reqstr)
    {
    }
    void Deserialization(std::string &reqstr)
    {
    }
    ~HttpRequest()
    {
    }

private:
    std::string _req_line;                 // 请求行
    std::vector<std::string> _req_headers; // 请求报头
    std::string _blank_line;               // 分割行
    std::string _req_body_text;            // 正文
};

这是最基本的四块数据,我们先对这四部分进行反序列化。因为他们都是根据分隔符\r\n进行分割的字符串,所以十分好处理:


    std::string GetLine(std::string &reqstr)
    {
        // 寻找分隔符
        auto pos = reqstr.find(base_sep);
        if (pos == std::string::npos)
            return std::string();
        std::string line = reqstr.substr(0, pos);
        if (line.empty())
            return base_sep;
        // 在原字符串中删除
        reqstr.erase(0, base_sep.size() + line.size());
        return line;
    }
    void Deserialization(std::string &reqstr)
    {
        // 进行反序列化
        _req_line = GetLine(reqstr);//请求行
        do
        {
            std::string header = GetLine(reqstr);
            if (header == "")
                break;
            else if (header == base_sep)
                break;
            else
                _req_headers.push_back(header);

        } while (true);//请求报头
        _blank_line = GetLine(reqstr);//空行
        _req_body_text = GetLine(reqstr);//请求正文
    }

这样就可以将一个字符串切分为四个部分了:
在这里插入图片描述
接下来我们可以对数据进行进一步处理:我们加入更加具体的成员变量

		std::string _method; //请求方法
        std::string _url; //请求路径
        std::string _version;//版本
        std::unordered_map<std::string , std::string> _kv_headers;//报头

处理很简单:按照字符串结构编写代码即可!

 void ParseReqLine()
    {
        // 优雅的操作!!!
        std::stringstream ss(_req_line);
        ss >> _method >> _url >> _version;
    }
    void ParseReqHeader()
    {
        for (auto &header : _req_headers)
        {
            auto pos = header.find(line_sep);
            if (pos == std::string::npos)
                continue;
            std::string k = header.substr(0, pos);
            std::string v = header.substr(pos + line_sep.size());
            if (k.empty() || v.empty())
                continue;
            _kv_headers.insert(std::make_pair(k, v));
        }
    }

    void Deserialization(std::string &reqstr)
    {
        // 进行反序列化
		//...
        //----------------具体数据的处理-------------------
        ParseReqLine();   // 请求行的处理!!!
        ParseReqHeader(); // 处理报头
    }

我们可以将结果打印出来看看:

在这里插入图片描述
非常好,我们成功将reqstr进行了反序列化,之后我们再来实现业务逻辑的代码!!!

后续文章,敬请期待!

评论 40
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叫我龙翔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值