前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【计网】从零开始学习http协议 --- http的请求与应答

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

作者头像
叫我龙翔
发布2024-09-26 08:44:52
750
发布2024-09-26 08:44:52
举报
文章被收录于专栏:就业 C++ 综合学习

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

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

代码语言:javascript
复制
百度 :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文件中获取数据流,将线程的函数方法修改为以下形式:

代码语言:javascript
复制
	// 注意设置为静态函数 , 不然参数默认会有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类来获取客户端申请的数据,目前直接进行简单的打印处理就好!

代码语言:javascript
复制
#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范围。

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

对于CentOS:

代码语言:javascript
复制
# 查看防火墙状态
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

对于Ubuntu:

代码语言:javascript
复制
# 查看防火墙状态
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地址:

使用 curl 命令:

代码语言:javascript
复制
curl ifconfig.me

使用 wget 命令:

代码语言:javascript
复制
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请求的结构化数据! 首先根据其整体的结构我们可以加入四个成员变量:请求行 ,请求报头, 空行 ,请求正文

代码语言:javascript
复制
// 设计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进行分割的字符串,所以十分好处理:

代码语言:javascript
复制
    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);//请求正文
    }

这样就可以将一个字符串切分为四个部分了:

接下来我们可以对数据进行进一步处理:我们加入更加具体的成员变量

代码语言:javascript
复制
		std::string _method; //请求方法
        std::string _url; //请求路径
        std::string _version;//版本
        std::unordered_map<std::string , std::string> _kv_headers;//报头

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

代码语言:javascript
复制
 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进行了反序列化,之后我们再来实现业务逻辑的代码!!!

后续文章,敬请期待!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-09-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 什么是http协议
  • 2 认识URL
  • 3 http的请求和应答
    • 3.1 服务端设计
      • 3.2 如何让外界可以访问Linux云服务器
        • 3.3 运行测试
        • 4 理解http请求与应答
          • 4.1 宏观理解
            • 4.2 http请求反序列化
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档