前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【Linux】:应用层协议 HTTP (超文本传输协议)

【Linux】:应用层协议 HTTP (超文本传输协议)

作者头像
IsLand1314
发布2025-02-03 08:32:26
发布2025-02-03 08:32:26
6000
代码可运行
举报
文章被收录于专栏:学习之路学习之路
运行总次数:0
代码可运行

1. HTTP -- 概述

虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用。HTTP(超文本传输协议) 就是其中之一。

在互联网世界中,HTTP(HyperText Transfer Protocol,超文本传输协议)是一个至关重要的协议。听起来好像是那么回事,实际上超文本传输协议指的是不仅仅可以传输文本,还可以传输图片、音频、视频等文件。它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如 HTML 文档)

  • HTTP 协议是客户端与服务器之间通信的基础。客户端通过 HTTP 协议向服务器发送请求,服务器收到请求后处理并返回响应。
  • HTTP 协议是一个无连接、无状态的协议,即每次请求都需要建立新的连接,且服务器不会保存客户端的状态信息。
1. 历史背景
  • 起源: HTTP 由蒂姆·伯纳斯-李(Tim Berners-Lee)于 1989 年在欧洲核子研究中心(CERN)提出,最初是为了方便研究人员共享文档。
  • 第一个版本(HTTP/0.9): 1991 年发布,极其简单,只支持 GET 方法,没有头部信息,只能传输纯文本(HTML)。
  • 标准化: 随着互联网的快速发展,HTTP 逐渐成为 Web 的基础协议,并由互联网工程任务组(IETF)和万维网联盟(W3C)标准化。
2. 发展历程

HTTP 协议经历了多个版本的演进,每个版本都引入了重要的改进:

HTTP/0.9(1991 年)

  • 只支持 GET 方法。
  • 无头部信息,无状态码。
  • 每次请求完成后,连接立即关闭。

HTTP/1.0(1996 年,RFC 1945)

  • 引入了 HTTP 头部,支持元数据(如内容类型、编码、状态码等)。
  • 支持多种请求方法(如 GETPOSTHEAD)。
  • 支持传输多种类型的数据(如文本、图片、视频等)。
  • 每次请求都需要建立一个新的 TCP 连接。

HTTP/1.1(1997 年,RFC 2068;1999 年更新,RFC 2616)

  • 引入了持久连接(Connection: keep-alive),允许复用 TCP 连接。
  • 支持管道化(Pipelining),允许客户端发送多个请求。
  • 引入了分块传输编码(Transfer-Encoding: chunked)。
  • 支持虚拟主机(Host 头部)。
  • 引入了缓存控制(Cache-Control 头部)。

HTTP/2(2015 年,RFC 7540)

  • 采用二进制协议,取代了 HTTP/1.x 的文本格式。
  • 支持多路复用(Multiplexing),解决了队头阻塞问题。
  • 引入了头部压缩(HPACK 算法)。
  • 支持服务器推送(Server Push)。
  • 支持流优先级(Stream Prioritization)。

HTTP/3(2022 年,RFC 9114)

  • 基于 QUIC 协议,使用 UDP 作为传输层协议。
  • 解决了 TCP 的队头阻塞问题。
  • 内置加密(TLS 1.3)。
  • 支持连接迁移(Connection Migration)。
3. HTTP 是超文本传输协议 -- 理解

HTTP 协议的本质是从服务器获取文件资源。资源可以是:

  • 图片、视频、音频等文件。
  • HTML、CSS、JavaScript 文件。
  • API 接口返回的数据。

无论是哪种资源,它们最终都存储在服务器的某个路径下,而 HTTP 协议则负责根据客户端的请求,定位并返回这些资源。

4. 核心概念
请求-响应模型
  • HTTP 是一个无状态的请求-响应协议。
  • 客户端发送请求,服务器返回响应。
  • 请求和响应由以下部分组成:
    • 请求:请求行(方法、URL、协议版本)、头部、正文。
    • 响应:状态行(协议版本、状态码、状态消息)、头部、正文。
方法(HTTP Methods)
  • 定义客户端请求的操作类型。
  • 常见方法:
    • GET:获取资源。
    • POST:提交数据。
    • PUT:更新资源。
    • DELETE:删除资源。
    • HEAD:获取资源的元数据。
状态码(Status Codes)
  • 表示服务器对请求的处理结果。
  • 常见状态码:
    • 200 OK:请求成功。
    • 404 Not Found:资源未找到。
    • 500 Internal Server Error:服务器内部错误。
头部(Headers)
  • 用于传递元数据。
  • 常见头部:
    • Content-Type:资源的媒体类型(如 text/html)。
    • Cache-Control:缓存控制指令。
    • Authorization:身份验证信息。
Cookie 和 Session
  • Cookie:服务器发送到客户端的小段数据,用于跟踪用户状态。
  • Session:服务器端存储的用户会话数据。
HTTPS
  • HTTP 的安全版本,使用 TLS/SSL 加密数据传输。
  • 防止数据被窃听或篡改。

2. 认识 URL

🔥 我们常说的“网址”,其实就是 URLUniform Resource Locator 统一资源定位符),用于标识互联网上的资源,一个网址通常包含如下部分:

  • 现在一般的登录信息都被隐藏起来了,这个暂时不用管
  • 端口号可以省略,浏览器会根据协议进行缺省端口号填充
  • ?:左边表示访问的资源,右边表示传递的目标服务器参数
  • uid=1:kv值

举个例子:https:://news.qq.com/rain/a/2025

  • https:所采用的协议,虽然我们这篇文章是关于 http 协议,但是现如今用的更广泛的还是 https,因为其多了层加密
  • news.qq.com:域名,会被计算机解析成目标服务的 IP 地址,相当于特定表示了一台主机
  • rain/a/2025:就是一串路径,我们实际访问的就是 2025 这个目标文件,而这个 / 这个就和 我们的 Linux 的分隔符一样,这个就类似于 Linux 下的路径结构,因此其实我们请求的资源本质就是文件
  • 因此这个 http 本质上就是把特定一台主机上的特定路径下对应的某一个文件把它发送给客户端

我们从网络上获取的文字,图片,音视频等等,这些信息本质上都是资源。那么在我们获取这些信息之前,这些文件存储在哪个位置?大多数在Linux服务器中,而在Linux当中,一切皆文件,所以 网络上获取的所有资源本质上都是文件!

  而我们从网络中获取数据本质上就是从 Linux服务器当中获取文件,而每个文件都是有路径的,所以找到一个文件直接通过文件路径即可访问资源。而我们能够找到对应文件的 前提是我们能够找到对应的服务器。 而要想 找到一个服务器就必须要知道该服务器的 IP[ + PORT] ,而IP[PORT] + 服务器中的文件路径 也就在网络中标识了唯一的文件(资源)

这里IP后面为什么要给PORT打上括号呢?

  • 原因其实在前面文章中也说过,客户端在像服务器端发起请求时,虽然需要绑定IP和端口号,但是不需要用户来绑定,因为这一步操作系统会给你自动绑定,所以在客户端看来,我只需要知道服务器的IP即可。
  • 不同于自定义应用层协议,可以随便绑定端口号,这些成熟的上层协议的端口号一般都是固定的,比如http协议的端口号就是 80。

  在最上方那张图当中,有一点过时了,实际上现在登录信息时,是不需要用户身份信息验证的。并且片段标识符早期可以将图片进行轮播所用,但是现在运用的同样少了,所以这张图实际上应该如下所示:

URL 与 端口号

明明 成熟的应用层协议都是与 端口号强关联的,但是这里的 url 却没有端口号

  • 在 URL 中,端口号通常是可选的,而不是必须的。这是因为大多数应用层协议(如 HTTP、HTTPS、FTP 等)都有默认端口号,如果 URL 中没有显式指定端口号,客户端(如浏览器)会自动使用默认端口号(范围是 0 到 65535)
    • HTTP:默认端口号是 80
    • HTTPS:默认端口号是 443
  • 隐藏端口号可以避免暴露服务器的详细信息,减少被攻击的风险。
  • 大多数 Web 服务都运行在默认端口号上,因此省略端口号已成为一种标准做法。
  • 只有在服务运行在非默认端口号上时,才需要在 URL 中显式指定端口号
URL 编码与解码 -- urlencode 和 urldecode

当我们在某度的搜索框内搜索东西的时候,比如我们搜索C++,浏览器将在资源路径后面出现一个 wd=关键字信息(不同的浏览器解释可能不同,有些浏览器就不会使用wd),如果你仔细观察,其实wd后面跟着的就是我们想要索引的关键字。而这些关键字有时候会发现跟我们在搜索框内搜索的不同:

我们在随便搜索一个问题时,我们会在资源路径后面看到一大堆的字符串,其中包含不少的特殊字符,比如 ‘?’,‘/’, ‘:’ 等字符,而 这些字符实际上有特殊含义的,已经被 url 当做特殊意义理解了。因此这些字符不能随意出现. 比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。

 由上面的例子我们可以看出,其实浏览器对特殊字符的转换是有规则的,在 URL 中,一些特殊字符(如 /, ?, : 等)具有特定的含义,因此不能直接出现在请求的参数中。这些字符在传递时需要进行 URL 编码。编码后的字符以 %XY 的形式表示,其中 XY 是字符的 16 进制表示。

编码规则如下:

  • 将需要编码的字符转换为 16 进制。
  • 每个字符占 8 位,从右到左取 4 位转成 16 进制数。
  • 16 进制数前面加 %,形成 %XY 格式。

解码是将 %XY 转回原始字符。

比如:在上面百度搜索那

  • "+" 被转义成了 "%2B"
  • urldecode 就是 urlencode 的逆过程
  • 把我们搜索的关键词转化成编码,避免我们搜索的这些特殊字符和 URL 的特殊字符冲突进而影响URL 解析失败,因此就会做 urldecodeurlencode

虽然我们可以从头开始实现这个编码和解码过程,但实际上现成的编码和解码函数已经非常成熟,可以直接使用。我们可以在网上查找相关的 URL 解码源码,作为工程师使用即可。

wd -- 解释说明

在 URL 中,wd 通常是 查询参数(query parameter) 的一部分,具体含义取决于上下文和网站的实现。它通常用于传递用户输入的关键字或搜索词,尤其是在搜索引擎或站内搜索功能中。

3. HTTP 协议请求与响应格式

3.1 请求

请求行(首行):[版本号] + [状态码] + [状态码解释]

  • HTTP请求的第一行被称为请求行,它由三部分组成:
  • 请求方法:如GET、POST、PUT等,表示对资源的操作。
  • URL:统一资源定位符,指定请求的资源。
  • HTTP版本:常见的有HTTP/1.0、HTTP/1.1、HTTP/2.0等。
  • 请求行以\r\n或\n作为分隔符。

请求报头(Header)

  • 请求报头是请求的第二大部分,由多行组成,每行包含一个属性,格式为 name:value(冒号分割的键值对)。
  • 这些属性提供了关于请求的额外信息,如客户端类型、请求内容类型等,每组属性之间使用\r\n 分隔
  • 遇到空行表示 Header 部分结束

空行

  • 紧随请求报头之后的是一个空行,这个空行非常重要,它标志着请求头的结束和请求正文的开始

请求正文(Body)

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

一个完整的HTTP请求报文包括上述四大块,并通过TCP连接发送到服务器。

3.2 响应

HTTP响应的格式与请求格式非常相似,也分为四部分。

状态行 响应的第一行是状态行,同样由三部分组成:

  1. HTTP版本:表明使用的HTTP协议版本。
  2. 状态码:如200、400、302、307、500、404等,用于表示请求的结果。
  3. 状态码描述:对状态码的文本描述,如404表示"Not Found",200表示"OK"。

响应报文

  • 响应报文由多行组成,包含了关于响应的额外信息,如内容类型、服务器类型等。

空行:与请求格式一样,响应报文后也跟着一个空行。 响应正文

  • 响应正文中包含了从服务器返回的实际内容,通常是HTML、CSS、JS、图片、视频或音频等。

这四部分构成了HTTP响应报文,并通过TCP连接发送回客户端。

4. 代码实现 -- 简单的HTTP服务器

基本框架,先把我们之前文章写的 Common.hpp、InetAddr.hpp、Log.hpp、Mutex.hpp 导入

InetAddr.hpp 部分修改如下:

Common.hpp 部分修改如下:

Makefile

代码语言:javascript
代码运行次数:0
复制
httpserver:HttpServer.cc
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -rf httpserver
4.1 Socket.hpp
代码语言:javascript
代码运行次数:0
复制
#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#include "Log.hpp"
#include "Mutex.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"


namespace SocketModule
{
    using namespace LogModule;
    class Socket;

    using SockPtr = std::shared_ptr<Socket>;

    class Socket
    {
    public:

        virtual ~Socket() = default;
        virtual void SocketOrDie() = 0; // 创建成功/死亡
        virtual void SetSocketOpt() = 0;
        virtual bool BindOrDie(int port) = 0; // 绑定
        virtual bool ListenOrDie() = 0; // 监听
        virtual SockPtr Accepter (InetAddr *client) = 0;
        virtual void Close () = 0; // 关闭
        virtual int Recv(std::string *out) = 0;
        virtual int Send(const std::string &in) = 0;
        virtual int Fd() = 0;

        // 提供一个创建 listensockfd 的固定套路
        void BuildTcpSocketMethod(int port)
        {
            SocketOrDie();
            SetSocketOpt();
            BindOrDie(port);
            ListenOrDie();
        }


    class TcpSocket : public Socket
    {
    public:
        TcpSocket(): _sockfd(gdefaultsockfd)
        {}
        TcpSocket(int sockfd): _sockfd(sockfd)
        {}
        virtual ~TcpSocket()
        {}
        virtual void SocketOrDie() override // 创建成功/死亡
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if(_sockfd < 0)
            {
                LOG(LogLevel::ERROR) << "socket error";
                exit(SOCKET_ERR);
            }
            LOG(LogLevel::DEBUG) << "socket create success: " << _sockfd; 
        }
        virtual void SetSocketOpt() override
        {
        }
        virtual bool BindOrDie(int port) override
        {
            if(_sockfd == gdefaultsockfd) return false;
            InetAddr addr(port);
            int n = ::bind(_sockfd, addr.NetAddr(), addr.NetAddrLen());
            if(n < 0)
            {
                LOG(LogLevel::ERROR) << "bind error";
                exit(BIND_ERR);
            }
            LOG(LogLevel::DEBUG) << "bind success: " << _sockfd; 
            return true;
        }
        virtual bool ListenOrDie() override
        {
            if(_sockfd == gdefaultsockfd) return false;
            int n = ::listen(_sockfd, gbacklog);
            if(n < 0)
            {
                LOG(LogLevel::ERROR) << "listen error";
                exit(LISTEN_ERR);
            }
            LOG(LogLevel::DEBUG) << "listen success: " << _sockfd;
            return true;
        }

        // 拿到 1. 文件描述符 2. client info(客户端信息)
        virtual SockPtr Accepter(InetAddr *client) override
        {
            if(!client) return nullptr;
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int newsockfd = ::accept(_sockfd, CONV(&peer), &len);
            if(newsockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                return nullptr;
            }
            client->SetAddr(peer, len);
            return std::make_shared<TcpSocket>(newsockfd);
        }
        virtual void Close() override
        {
            if(_sockfd == gdefaultsockfd) return ;
            ::close(_sockfd);
        }

        virtual int Recv(std::string *out) override
        {
            char buffer[1024*8];
            auto size = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
            if(size > 0)
            {
                buffer[size] = 0;
                *out = buffer;
            }
            return size;
        }
        virtual int Send(const std::string &in) override
        {
            auto size = ::send(_sockfd, in.c_str(), in.size(), 0);
            return size;
        }

        virtual int Fd() override
        {
            return _sockfd;
        }

        
    private:
        int _sockfd;
    };

}
4.2 TcpServer.hpp
代码语言:javascript
代码运行次数:0
复制
#pragma once

#include <iostream>
#include <memory>
#include <functional>
#include <sys/wait.h>
#include "Socket.hpp"

namespace TcpServerModule
{
    using namespace SocketModule;
    using namespace LogModule;
    using tcphandler_t = std::function<bool(SockPtr, InetAddr)>;

    // 它只负责进行流式 IO,不对协议做任何处理 -- 相当于只负责读写就行
    class TcpServer
    {
    public: 
        TcpServer(int port)
        : _listensockp(std::make_unique<TcpSocket>()),
          _running(false),
          _port(port)
        {}

        void InitServer(tcphandler_t handler)
        {
            _handler = handler;
            _listensockp->BuildTcpSocketMethod(_port);
        }
        void Loop() // 循环
        {
            _running = true;
            while(_running)
            {
                // 1. Accept
                InetAddr clientaddr;
                auto sockfd = _listensockp->Accepter(&clientaddr);
                if(sockfd == nullptr) continue;
                // 2. IO处理
                LOG(LogLevel::DEBUG) << "get a new client, info is: " << clientaddr.Addr();
                
                // sockfd->Recv();
                // sockfd->Send();
                pid_t id = fork();
                if(id == 0)
                {
                    _listensockp->Close();
                    if(fork() > 0) exit(0);
                    _handler(sockfd, clientaddr); // 回调
                    exit(0);
                }
                sockfd->Close();
                waitpid(id, nullptr, 0);
            }
            _running = false;
        }
        ~TcpServer()
        {
            _listensockp->Close();
        }

    private:
        // 一定要有一个 Listensock
        std::unique_ptr<Socket> _listensockp;
        bool _running;

        tcphandler_t _handler;
        int _port;
    };
}
4.3 HttpServer.hpp
代码语言:javascript
代码运行次数:0
复制
#pragma once

#include <iostream>
#include <string>
#include "TcpServer.hpp"

using namespace TcpServerModule;

const std::string Sep = "\r\n";
const std::string BlankLine = Sep;

class HttpServer
{
public:
    HttpServer(int port)
        : _tsvr(std::make_unique<TcpServer>(port))
    {
    }
    void Start()
    {
        _tsvr->InitServer([this](SockPtr sockfd, InetAddr client){ 
            return this->HandlerHttpRequest(sockfd, client); 
        });
        _tsvr->Loop();
    }
    // 就是我们处理http的入口
    bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
    {
        LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();
        return true;
    }
    ~HttpServer() {}

private:
    std::unique_ptr<TcpServer> _tsvr;
};
4.4 HttpServer.cc
代码语言:javascript
代码运行次数:0
复制
#include "HttpServer.hpp"

int main()
{
    auto httpserver = std::make_unique<HttpServer>(8080);
    httpserver->Start();
    return 0;
}

结果运行如下:

打开浏览器进行搜索,相当于用浏览器作客户端 -- B/S 模式

  • B/S 模式(Browser/Server 模式,浏览器/服务器模式)是一种软件架构模式,广泛应用于现代 Web 应用程序开发。与传统的 C/S 模式(Client/Server 模式,客户端/服务器模式)相比,B/S 模式的核心特点是客户端通过浏览器(Browser)与服务器(Server)进行交互,而不需要安装专门的客户端软件。

搜索格式:http:://云服务器ip:端口号/,然后结果如下:

  • 但是此时浏览器并没有任何反应和显示,因为此时我们的 Http 还没有做任何处理,代码是直接 return 的,如果我们想要打开网页显示一个东西应该怎么做呢?
  • 我们处理 http 的入口 就是在 HttpServer.hpp 文件的 HandlerHttpRequest 函数中 ,假如我们想要显示一个 Hello World,修改如下:

结果如下:

我们可以发现,客户端发来的请求报头一共有11行,其中最后一行为空行,但实际上它们就只有一行,只不过每一行的末尾都有 \r\n 将它们连接到一起。我们之前说过,无论是什么协议,都要做到 将报头 和 有效载荷进行分离,同样,http协议也是如此。

  在上图中,空行以上实际上全部都是有效载荷的部分,空行之后就是正文,只不过这里我们没有做任何处理,所以空行下面就啥也没有。


如果这里无法访问的话,我们就需要去服务器上开放一下端口号的,这个需要注意一下

这里说一下:网页不会写的话,可以直接要 AI 跑一个,或者可以搜一下 w3cschool html教程

4.5 HttpProtocol.hpp

现在我们基本可以知道 我们处理 Http 的入口基本都在 HandlerHttpRequest 中,那么我们需要改一下我们之前的代码,因为那个直接写 HTML 代码在string 中,还是过于冗长,而且不方便,因此修改如下:

代码语言:javascript
代码运行次数:0
复制
// 就是我们处理http的入口
bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
{
    LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();
    std::string http_request;
    sockfd->Recv(&http_request); // 在 Http 这里暂时不做报文完整性处理
    HttpRequest req;
    req.Deserialize(http_request);

    return true;
}

Common.http 修改如下:

HttpProtocol.hpp

代码语言:javascript
代码运行次数:0
复制
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <unordered_map>
#include "Common.hpp"
#include "Log.hpp"

const std::string Sep = "\r\n";
const std::string LineSep = " "; // 行分隔符
const std::string HeaderLineSep = ": "; // 属性分隔符
const std::string BlankLine = Sep;
const std::string defaulthomepage = "wwwroot"; // 自定义的 web 根目录
const std::string http_version = "HTTP/1.0";
const std::string page404 = "wwwroot/404.html";
const std::string firstpage = "index.html";

using namespace LogModule;

// B/S 模式 -- 由浏览器当客户端了

class HttpRequest
{
public:
    HttpRequest(){}
    ~HttpRequest(){}

// GET /favicon.ico HTTP/1.1
// Host: 1.12.51.69:8080
// Connection: keep-alive
// User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 SLBrowser/9.0.5.12181 SLBChan/105 SLBVPV/64-bit
// Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
// Referer: http://1.12.51.69:8080/
// Accept-Encoding: gzip, deflate
// Accept-Language: zh-CN,zh;q=0.9


    void Deserialize(std::string &request_str)
    {
        std::cout << "########################" << std::endl;
        std::cout << "之前: request:\n" << request_str;
        
        if(ParseOneLine(request_str, &_req_line, Sep)) // _req_line 在这里被初始值
        {
            // 提取请求行中的详细字段
            ParReqLine(_req_line, LineSep);
        }
        std::cout << "之后: request:\n" << request_str;
        Print();
        std::cout << "########################" << std::endl;
    }
  
    void Print()
    {
        std::cout << "_method: " << _method << std::endl;
        std::cout << "_uri: " << _uri << std::endl;
        std::cout << "_version: " << _version << std::endl;

    }
    std::string Uri()
    {
        return _uri;
    }

    void SetUri(const std::string newuri)
    {
        _uri = newuri;
    }

private:
// 从 HTTP 请求行中提取方法(_method)、URI(_uri)和版本(_version),并将 _uri 与一个默认主页路径(defaulthomepage)拼接
    void ParReqLine(std::string &_req_line, const std::string &Sep)
    {
        (void)Sep;
        std::stringstream ss(_req_line); // 流式分隔
        ss >> _method >> _uri >> _version;
    }

    std::string _req_line;                  // 首行
    std::vector <std::string> _req_header;  // 请求报头
    std::string _blank_line;                // 空行
    std::string _body;                      // 请求正文

    // 在反序列化的过程中,细化我们解析出来的字段
    std::string _method;
    std::string _uri;
    std::string _version;
    std::unordered_map<std::string, std::string> _headerkv;

};

// 对于 http 任何请求都要有应答
class HttpResponse
{
public:
    HttpResponse(): _verion(http_version), _blank_line(Sep)
    {}
    ~HttpResponse()
    {}
};

如果这次我们在请求的后面加上一些路径,比如: /a/b/c/d.html,结果如下:

  • 为什么这里我们只有协议的处理方法与协议的版本信息呢?因为我们写的程序并没有向服务器发起任何的资源访问,所以这里 访问的资源路径默认为 ‘/’ 。而剩下的所有行,也都符合上图中的 Key: Value 的形式,而我们要学习的就是HTTP请求报文中的请求行以及请求报头信息。
  • 说明在 Http 请求中,在端口号(即 /)之后的都会作为 Url 的一部分,这个表明我们要请求的资源,因此我们做 Http 请求的时候,浏览器会自动把我们 / 之后的所有内容全部作为 http 轻轻的第二部分(uri)来统一构建 http 请求发送
  • 我们知道,任何协议都需要有将 报头 和 有效载荷进行分离的能力,那么HTTP协议是根据什么对报头和载荷进行分离的呢?我们发现http请求格式中,报头部分与正文部分实际上是有一个空行作为分隔的,所以 HTTP协议就可以通过空行(“\n”)来分离报头和有效载荷部分。

在http请求报头当中,存在一个 key为 Content-Length value为 正文长度 的key: value请求行,如果发送的请求当中没有正文,那么这个字段也就不会存在。HttpRequest 类修改如下:

代码语言:javascript
代码运行次数:0
复制
bool ParseHeader(std::string& request_str)
{
    std::string line;
    while (true)
    {
        bool r = ParseOneLine(request_str, &line, Sep);
        if (r && !line.empty())
        {
            _req_header.push_back(line);
        }
        else if (r && line.empty()) // 读到了 空行
        {
            _blank_line = Sep;
            break;
        }
        else return false; // 解析失败
    }
    return true;
}

void Deserialize(std::string& request_str)
{
    if (ParseOneLine(request_str, &_req_line, Sep)) // 1. 解析请求报头
    {
        // 提取请求行详细字段
        ParReqLine(_req_line, LineSep);
        ParseHeader(request_str);   // 2. 报头继续请求
        _body = request_str;        // 3. 获取正文
    }
}

void Print()
{
    std::cout << "_method: " << _method << std::endl;
    std::cout << "_uri: " << _uri << std::endl;
    std::cout << "_version: " << _version << std::endl;

    for (auto& line : _req_header) {
        std::cout << line << std::endl;
    }

    std::cout << "_blank_line: " << _blank_line << std::endl;
    std::cout << "_body: " << _body << std::endl;
}

然后再在 HttpServer.cc 那加个 Print 打印即可,此时我们要的东西是可以正常获得的就可以出来了

但是我们的解析还是不够彻底,还需要继续打散,因此我们这里就需要用到 哈希 来进行

代码语言:javascript
代码运行次数:0
复制
bool ParseHeaderkv()
{
    std::string key, value;
    for (auto& header : _req_header)
    {
        // Connection: keep-alive -> 以 ": " 分割字符串为 Kv
        if (SplitString(header, HeaderLineSep, &key, &value))
        {
            _headerkv.insert(std::make_pair(key, value));
        }
    }
    return true;
}

bool ParseHeader(std::string& request_str)
{
    std::string line;
    while (true)
    {
        bool r = ParseOneLine(request_str, &line, Sep);
        if (r && !line.empty())
        {
            _req_header.push_back(line);
        }
        else if (r && line.empty()) // 读到了 空行
        {
            _blank_line = Sep;
            break;
        }
        else return false; // 解析失败
    }
    ParseHeaderkv();
    return true;
}

void Print()
{
    std::cout << "_method: " << _method << std::endl;
    std::cout << "_uri: " << _uri << std::endl;
    std::cout << "_version: " << _version << std::endl;

    for (auto& kv : _headerkv) {
        std::cout << kv.first << " # " << kv.second << std::endl;
    }

    std::cout << "_blank_line: " << _blank_line << std::endl;
    std::cout << "_body: " << _body << std::endl;
}



Common.hpp 下的 SplitString实现如下:
// Connection: keep-alive
bool SplitString(const std::string &header, const std::string &sep, std::string *key, std::string *value)
{
    auto pos = header.find(sep);
    if(pos == std::string::npos) return false;

    *key = header.substr(0, pos);
    *value = header.substr(pos + sep.size());
    
    return true;
}

此时结果如下:

5. 服务器 和 网页分离

代码实现 -- URI 资源

🚩 URI是客户端访问服务器的资源路径,当服务器端收到客户端发来的请求时,如果没有访问任何资源路径,那么默认就是 ‘/’ ,而默认的’/’ 需要有一个默认的首页,以后只要对服务器进行访问,如果没有访问具体的服务器资源,就会默认跳转到给出的默认首页,通常默认首页是一个 index.html 文件。而这些文件,通常存在于一个名为 wwwroot 的目录结构下,服务器默认访问资源路径实际上就是wwwroot目录下的 index.html文件。

后续我们在资源访问的时候,都需要默认带上 wwwroot或者我们自定义的 web 根目录,如下:

代码语言:javascript
代码运行次数:0
复制
void ParReqLine(std::string& _req_line, const std::string& Sep)
{
    (void)Sep;
    std::stringstream ss(_req_line); // 流式分隔
    ss >> _method >> _uri >> _version;
    _uri = defaulthomepage + _uri;    // defaulthomepage 全局定义的web根目录
}

运行如下:

因此我们可以知道所有的请求都会被转化到 从 web 根目录下找的内容,而不是从 Linux 根目录下找了,这个很关键,后面我们的 网页请求链接 就会设计到这个

继续往后,我们如果想要得到文件内容的话

代码语言:javascript
代码运行次数:0
复制
std::string GetContent()
{
     std::string content;
     std::ifstream in(_uri);
     if(!in.is_open()) return std::string();
     std::string line;
     while(std::getline(in, line))
     {
         content += line;
     }
     in.close();
    return content;
}

响应 HttpResponse 类实现如下:

代码语言:javascript
代码运行次数:0
复制
// 对于 http 任何请求都要有应答
class HttpResponse
{
public:
    HttpResponse(): _verion(http_version), _blank_line(Sep)
    {}
    ~HttpResponse()
    {}

    void Build(HttpRequest &req)
    {
        _content = req.GetContent();
        if(_content.empty())
        {
            // 用户请求的资源并不存在
            _status_code = 404;
            req.SetUri(page404);
            _content = req.GetContent();
        }
        else
        {
            _status_code = 200;
        }
        LOG(LogLevel::DEBUG) << "客户端在请求:" << req.Uri();
        _status_desc = Code2Desc(_status_code); // 和状态码强相关
    }
    
    void Serialize(std::string *resp_str)
    {
        // 首行: 版本+ 空格 + 状态码 + 空格 + 状态码描述 + 换行符 
        _resp_line = _verion + LineSep + std::to_string(_status_code) + LineSep + _status_desc + Sep; 
        _body = _content; // 正文

        // 序列化
        *resp_str = _resp_line;
        for(auto &line : _resp_header)
        {
            *resp_str += (line + Sep);
        }
        *resp_str += _blank_line;
        *resp_str += _body;
    }

private:
    std::string Code2Desc(int code)
    {
        switch(code)
        {
            case 200: return "OK";
            case 404: return "Not Found";
            default: return std::string();
        }
    }

private:

    // 必备要素
    std::string _verion;
    int _status_code; // 状态码
    std::string _status_desc; // 状态码描述
    std::unordered_map<std::string, std::string> _header_kv;

    std::string _resp_line;
    std::vector<std::string> _resp_header;
    std::string _blank_line;
    std::string _body;
    std::string _content; 
};

然后我们再用AI 写个 404 界面(404.html),将其放在 wwwroot 目录下,方便我们等下使用,然后,此时 Http 入口函数修改如下:

代码语言:javascript
代码运行次数:0
复制
// 就是我们处理http的入口
bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
{
    LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();
    std::string http_request;
    sockfd->Recv(&http_request); // 在 Http 这里暂时不做报文完整性处理
    HttpRequest req;
    req.Deserialize(http_request);

    HttpResponse resp;
    resp.Build(req);
    std::string resp_str;
    resp.Serialize(&resp_str);
    sockfd->Send(resp_str);

    return true;
}

结果如下:

浏览器此时也可以正常显示出对应的 HTML 代码所对应的 页面了

然后我们再对 HttpResponse 做修改,如下:

代码语言:javascript
代码运行次数:0
复制
void Build(HttpRequest& req)
{
    std::string uri = req.Uri(); // wwwroot/ wwwroot/a/b/
    if (uri.back() == '/')
    {
        uri += firstpage; // wwwroot/index.html
        req.SetUri(uri);
    }

    _content = req.GetContent();
    LOG(LogLevel::DEBUG) << "content length: " << _content.size();

    if (_content.empty())
    {
        // 用户请求的资源并不存在
        _status_code = 404;
        req.SetUri(page404);
        _content = req.GetContent();
    }
    else
    {
        _status_code = 200;
    }
    LOG(LogLevel::DEBUG) << "客户端在请求:" << req.Uri();

    _status_desc = Code2Desc(_status_code); // 和状态码强相关
}

void Serialize(std::string* resp_str)
{
    // 首行: 版本+ 空格 + 状态码 + 空格 + 状态码描述 + 换行符 
    _resp_line = _verion + LineSep + std::to_string(_status_code) + LineSep + _status_desc + Sep;
    _body = _content; // 正文

    // 序列化
    *resp_str = _resp_line;
    for (auto& line : _resp_header)
    {
        *resp_str += (line + Sep);
    }
    *resp_str += _blank_line;
    *resp_str += _body;
}

此时进入我们的服务器链接,就可以看到默认主页了,如下:

然后我们再在我们的链接之后,输入我们登录界面对应的 login.html,则跳转如下:

网页跳转

网页页面之间跳转的功能,是通过链接来进行切换页面的,如下图:

然后对应应答再做一些修改,如下:

代码语言:javascript
代码运行次数:0
复制
void Build(HttpRequest& req)
{
    std::string uri = req.Uri(); // wwwroot/ wwwroot/a/b/
    if (uri.back() == '/'){
        uri += firstpage; // wwwroot/index.html
        req.SetUri(uri);
    }

    _content = req.GetContent();
    LOG(LogLevel::DEBUG) << "content length: " << _content.size();

    if (_content.empty()){
        // 用户请求的资源并不存在
        _status_code = 404;
        req.SetUri(page404);
        _content = req.GetContent();
    }
    else{
        _status_code = 200;
    }
    LOG(LogLevel::DEBUG) << "客户端在请求:" << req.Uri();

    _status_desc = Code2Desc(_status_code); // 和状态码强相关
    
     if(!_content.empty()){
        SetHeader("Content-Length", std::to_string(_content.size()));
     }
    
    for (auto& header : _header_kv){
        _resp_header.push_back(header.first + HeaderLineSep + header.second);
    }
}

// ...


void SetHeader(const std::string& k, const std::string& v){
    _header_kv[k] = v;
}
  • 上面我们给 httpresponse 添加了一个新的容器来保存其 kv,然后在以后我们想要什么属性,就可以在 SetHeader 里加入,然后对应的在 build 里面将内容提取处理出来,再 SetHeader 把我们添加的属性加上,然后在 for 循环里面进行构建,拼接到对应的响应报告里
  • 当然不仅仅如此,我们在页面中访问网页,进行页面跳转,就是每一次http请求:

为了好看我们把图片也导入,如下:

但是其实服务器是访问了图片的,如下:

因此我们还要对我们的代码进行修改,如下:

此时图片就可以正常显示了,如下:

然后由于图片一般在代码中是二进制的形式显示,因此我们需要设置 HTTP 响应头中的 Content-Type 字段,用于将文件后缀映射到对应的 MIME 类型描述。这个功能在 Web 开发中非常常见,通常用,如下:

现在我们的 服务器-- 网页 代码就基本写好了

什么是网站?站在我们程序员的角度来说,网站其实就是一堆特定的目录和文件所构成的目录结构!前端程序员并不需要管后端的各种服务是怎么实现的,只需要将wwwroot里的内容做好,进行页面之间的跳转即可。


HTTP 常见 Header
  • Content-Type:数据类型(text/html 等)

🔥 Content-Type 表示资源的数据类型,资源类型必须得提前在代码当中定义好,如果你拿着一个js文件返回给浏览器,但是却告诉浏览器这个文件是html文件,这就可能会导致一些无法预测的问题,不过浏览器还是很聪明的,如果你发来的是js文件,那么浏览器不会解释为html文件的,但是文件类型有非常多,浏览器不能保证每次都能帮你纠正错误,在服务器的代码中,我们有必要将文件后缀做解析,发送正确的文件后缀,至于如何做,我们可以使用 Content-Type对照表 来定位文件类型:

  • Content-Length:Body 的长度
  • Host:客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
  • User-Agent:声明用户的操作系统和浏览器版本信息;
  • referer:当前页面是从哪个页面跳转过来的;
  • Location:搭配 3xx 状态码使用, 告诉客户端接下来要去哪里访问;
  • Cookie:用于在客户端存储少量信息. 通常用于实现会话(session)的功能;

但是这里有个问题:明明请求的 服务器和端口号就是我,为啥 host 需要把我的 ip 和 端口号带上,这里其实是因为 服务器 作为了 代理服务器,如下:

但是对于公司而言,它不可能只有一台服务器,那么我们把这么多台提供资源的 http 服务器 称为(集群或者机房),此时就需要在我的 Http 服务器内维护一张资源服务器的清单 / 列表,然后从这里获取服务器信息,此时代理服务器就需要使用策略动态选择后端服务器,维护了每个后端服务器的压力,此时就做了 转发 和 负载均衡


HTTP 状态码

上面我们在 Code2Desc 那用到了状态码,这里我们补充一下 Http 状态码知识:

最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定 向), 504(Bad Gateway)

状态码

含义

应用案例

100

Continue

上传大文件时,服务器告诉客户端可以 继续上传

200

OK

访问网站首页,服务器返回网页内容

201

Created

发布新文章,服务器返回文章创建成功的信息

204

No Content

删除文章后,服务器返回“无内容”表示操作成功

301

Moved Permanently

网站换域名后,自动跳转到新域名;搜索引擎更新网站链接时使用

302

Found 或 See Other

用户登录成功后,重定向到用户首页

304

Not Modified

浏览器缓存机制,对未修改的资源返回304 状态码

400

Bad Request

填写表单时,格式不正确导致提交失败

401

Unauthorized

访问需要登录的页面时,未登录或认证失败

403

Forbidden

尝试访问你没有权限查看的页面

404

Not Found

访问不存在的网页链接

500

Internal Server Error

服务器崩溃或数据库错误导致页面无法加载

502

Bad Gateway

使用代理服务器时,代理服务器无法从上游服务器获取有效响应

503

Service Unavailable

服务器维护或过载,暂时无法处理请求

  • 其中上面的 301、308 是永久重定向,302、307是临时重定向

关于重定向的验证,以 301 为代表 HTTP 状态码 301(永久重定向)和 302(临时重定向)都依赖 Location 选项。以下 是关于两者依赖 Location 选项的详细说明:

HTTP 状态码 301(永久重定向):

  • 当服务器返回 HTTP 301 状态码时,表示请求的资源已经被永久移动到新的位置。
  • 在这种情况下,服务器会在响应中添加一个 Location 头部,用于指定资源的新位置。这个 Location 头部包含了新的 URL 地址,浏览器会自动重定向到该地址。

例如,在 HTTP 响应中,可能会看到类似于以下的头部信息:

代码语言:javascript
代码运行次数:0
复制
HTTP/1.1 301 Moved Permanently\r\n
Location: https://www.new-url.com\r\n

HTTP 状态码 302(临时重定向):

  • 服务器返回 HTTP 302 状态码时,表示请求的资源临时被移动到新的位置。
  • 同样地,服务器也会在响应中添加一个 Location 头部来指定资源的新位置。
  • 浏览器会暂时使用新的 URL 进行后续的请求,但不会缓存这个重定向。

例如,在 HTTP 响应中,可能会看到类似于以下的头部信息

代码语言:javascript
代码运行次数:0
复制
HTTP/1.1 302 Found\r\n
Location: https://www.new-url.com\r\n

我们对上面代码做些修改,如下:

代码语言:javascript
代码运行次数:0
复制
void Build(HttpRequest& req)
{
#define TestRedir 1
#ifdef TestRedir
    _status_code = 302;
    _status_desc = Code2Desc(_status_code); // 和状态码强相关
    SetHeader("Location", "https://www.qq.com/");
    for (auto& header : _header_kv)
    {
        _resp_header.push_back(header.first + HeaderLineSep + header.second);
    }
#else
    std::string uri = req.Uri(); // wwwroot/ wwwroot/a/b/
    if (uri.back() == '/')
    {
        uri += firstpage; // wwwroot/index.html
        req.SetUri(uri);
    }

    _content = req.GetContent();
    LOG(LogLevel::DEBUG) << "content length: " << _content.size();

    if (_content.empty())
    {
        // 用户请求的资源并不存在
        _status_code = 404;
        req.SetUri(page404);
        _content = req.GetContent();
    }
    else
    {
        _status_code = 200;
    }
    LOG(LogLevel::DEBUG) << "客户端在请求:" << req.Uri();

    _status_desc = Code2Desc(_status_code); // 和状态码强相关

    if (!_content.empty())
    {
        SetHeader("Content-Length", std::to_string(_content.size()));
    }
    std::string mime_type = Suffix2Desc(req.Suffix());

    SetHeader("Content-Type", mime_type);

    for (auto& header : _header_kv)
    {
        _resp_header.push_back(header.first + HeaderLineSep + header.second);
    }
#endif
}

此时结果如下:

然后再做点修改,

此时访问结果如下:

总结:无论是 HTTP 301 还是 HTTP 302 重定向,都需要依赖 Location 选项来指定资 源的新位置。这个 Location 选项是一个标准的 HTTP 响应头部,用于告诉浏览器应该 将请求重定向到哪个新的 URL 地址。


在发送报文或者相应报文的报头当中,都在一个叫做 Connection 的字段,HTTP 中的 Connection 字段是 HTTP 报文头的一部分,它主要 用于控制和管理客户端与服务器之间的连接状态

关于 connection 报头

HTTP 中的 Connection 字段是 HTTP 报文头的一部分,它主要用于控制和管理客户端与服务器之间的连接状态

核心作用

  • 管理持久连接:Connection 字段还用于管理持久连接(也称为长连接)。
  • 持久连接允许客户端和服务器在请求/响应完成后不立即关闭 TCP 连接,以便在同一个连接上发送多个请求和接收多个响应。

持久连接(长连接)

  • HTTP/1.1:在 HTTP/1.1 协议中,默认使用持久连接。当客户端和服务器都不明确指定关闭连接时,连接将保持打开状态,以便后续的请求和响应可以复用同一个连接HTTP/1.0:
  • 在 HTTP/1.0 协议中,默认连接是非持久的。如果希望在 HTTP/1.0上实现持久连接,需要在请求头中显式设置 Connection: keep-alive。

语法格式

  • Connection: keep-alive:表示希望保持连接以复用 TCP 连接。
  • Connection: close:表示请求/响应完成后,应该关闭 TCP 连接
HTTP 常见 header 表格

字段名

含义

样例

Accept

客户端可接受的响应内容类型

Accept:text/html,application/xhtml+xml,application/xml;q=0.9, image/webp,image/apng,*/*;q=0.8

Accept-Encoding

客户端支持的数据压缩格式

Accept-Encoding: gzip, deflate, br

Accept- Language

客户端可接受的语言类型

Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

Host

请求的主机名和端口号

Host: www.example.com:8080

User-Agent

客户端的软件环 境信息

User-Agent: Mozilla/5.0 (Windows NT10.0; Win64; x64)AppleWebKit/537.36 (KHTML, likeGecko)Chrome/91.0.4472.124Safari/537.36

Cookie

客户端发送给服 务器的 HTTP cookie 信息

Cookie: session_id=abcdefg12345;user_id=123

Referer

请求的来源 URL

Referer:http://www.example.com/previous_page.html

Content-Type

实体主体的媒体类型

Content-Type: application/x-www-form-urlencoded (对于表单提交) 或Content-Type: application/json (对于JSON 数据)

Content-Length

实体主体的字节大小

Content-Length: 150

Authorization

认证信息,如用户名和密码

Authorization: BasicQWxhZGRpbjpvcGVuIHNlc2FtZQ== (Base64 编码后的用户名:密码)

Cache-Control

缓存控制指令

请求时:Cache-Control: no-cache 或Cache-Control: max-age=3600; 响应时:Cache-Control: public, max-age=3600

Connection

请求完后是关闭还是保持连接

Connection: keep-alive 或 Connection: close

Date

请求或响应的日期和时间

Date: Wed, 21 Oct 2023 07:28:00 GMT

Location

重定向的目标URL(与 3xx 状态码配合使用)

Location: http://www.example.com/new_location.html (与 302 状态码配合使用)

Server

服务器类型

Server: Apache/2.4.41 (Unix)

Last-Modified

资源的最后修改 时间

Last-Modified: Wed, 21 Oct 2023 07:20:00 GMT

ETag

资源的唯一标识符,用于缓存

ETag: "3f80f-1b6-5f4e2512a4100"

Expires

响应过期的日期和时间

Expires: Wed, 21 Oct 2023 08:28:00GMT

HTTP 常见方法

GET 方法:

  • 用途:用于请求 URL 指定的资源。
  • 示例:GET /index.html HTTP/1.1
  • 特性:指定资源经服务器端解析后返回响应内容。

POST 方法:

  • 用途:用于传输实体的主体,通常用于提交表单数据。
  • 示例:POST /submit.cgi HTTP/1.1
  • 特性:可以发送大量的数据给服务器,并且数据包含在请求体中。

PUT 方法(不常用):

  • 用途:用于传输文件,将请求报文主体中的文件保存到请求 URL 指定的位置。
  • 示例:PUT /example.html HTTP/1.1
  • 特性:不太常用,但在某些情况下,如 RESTful API 中,用于更新资源。

HEAD 方法:

  • 用途:与 GET 方法类似,但不返回报文主体部分,仅返回响应头。
  • 示例:HEAD /index.html HTTP/1.1
  • 特性:用于确认 URL 的有效性及资源更新的日期时间等。

DELETE 方法(不常用):

  • 用途:用于删除文件,是 PUT 的相反方法。
  • 示例:DELETE /example.html HTTP/1.1
  • 特性:按请求 URL 删除指定的资源。

OPTIONS 方法:

  • 用途:用于查询针对请求 URL 指定的资源支持的方法。
  • 示例:OPTIONS * HTTP/1.1
  • 特性:返回允许的方法,如 GET、POST 等。

  以上这些方法一般都不是由后端代码来完成的,不过如果想要处理这些请求后端也可以处理,一般这些都属于前端页面的请求方法,我们可以通过 HTML表单 来获取简单的前段页面,而以上方法中最重要的莫过于 GET 和 POST方法了。

其中GET方法我们如何理解呢?就拿简单的登录页面来说,我们登录页面实际上在前端代码中,就是一个form表单,我们对我们上面的代码进行修改,方便理解:

代码语言:javascript
代码运行次数:0
复制
void Build(HttpRequest& req)
{
    std::string uri = req.Uri(); // wwwroot/ wwwroot/a/b/
    if (uri.back() == '/')
    {
        uri += firstpage; // wwwroot/index.html
        req.SetUri(uri);
    }
    LOG(LogLevel::DEBUG) << "----- 客户端在请求:" << req.Uri();
    req.Print();
    LOG(LogLevel::DEBUG) << "--------------------------";

    _content = req.GetContent();
    // LOG(LogLevel::DEBUG) << "content length: " << _content.size();

    if (_content.empty())
    {
        // 用户请求的资源并不存在
        _status_code = 404;
        req.SetUri(page404);
        _content = req.GetContent();
    }
    else
    {
        _status_code = 200;
    }

    _status_desc = Code2Desc(_status_code); // 和状态码强相关

    if (!_content.empty())
    {
        SetHeader("Content-Length", std::to_string(_content.size()));
    }
    std::string mime_type = Suffix2Desc(req.Suffix());

    SetHeader("Content-Type", mime_type);

    for (auto& header : _header_kv)
    {
        _resp_header.push_back(header.first + HeaderLineSep + header.second);
    }
}

把我们的方法换成 GET,如下:

然后登录输入账号密码,如下:

  • 此时 _body 没有数据
  • 我们可以看到在搜索框内出现的网址,在前端页面上我们指定了要上传资源的路径,以及使用方法,在网址中,我们看到除了前端页面以外,后面紧跟着的字符是 “?” ,问号后面跟着的起始就是用户的用户名信息,以及用户的密码信息。
  • 所以由此我们就可以看到,使用HTTP协议实际上是不安全的,所以催生出了HTTPS协议(后面再谈),更加注重用户的隐私性。

如果我们再将方法换成 POST,如下:

  • 此时我们输入的账号和密码就在 _body 里面了

因此我们可以知道 GET 通常获取网页内容,是通过url来上传资源,而POST方法上传参数,是以正文形式进行参数上传的, content-length + request body 传参,POST 传参更私密,但是并不是更安全

  • 两个方法都不安全,因为POST方法虽然在浏览器层面看不到用户信息,但是如果你会抓包,实际上所有的用户信息都会被暴漏出去,所以这两种方法都不能称为安全方法。如果要安全的话,需要加一层加密层,这个就涉及到 https 的知识了
动静态
  • 但是看了这么多,我们的服务器也只有静态的功能,无法处理动态的

静态内容

  • 定义:服务器直接返回预先存储的文件,内容固定。
  • 特点
    • 内容不变,每次请求返回相同结果。
    • 通常包括 HTML、CSS、JavaScript、图片、视频等。
    • 处理速度快,服务器负担小。
  • 示例
    • 请求:GET /index.html
    • 响应:直接返回 index.html 文件内容。

动态内容

  • 定义:服务器根据请求生成内容,通常涉及后端处理。
  • 特点
    • 内容可变,每次请求可能返回不同结果。
    • 依赖服务器端脚本(如 PHP、Python、Node.js)和数据库。
    • 处理较慢,服务器负担较大。
  • 示例
    • 请求:GET /user?id=123
    • 响应:服务器查询数据库后生成并返回用户信息页面。

6. 共勉

上面完整代码:服务器-网页

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. HTTP -- 概述
    • 1. 历史背景
    • 2. 发展历程
    • 3. HTTP 是超文本传输协议 -- 理解
    • 4. 核心概念
      • 请求-响应模型
      • 方法(HTTP Methods)
      • 状态码(Status Codes)
      • 头部(Headers)
      • Cookie 和 Session
      • HTTPS
  • 2. 认识 URL
    • URL 与 端口号
    • URL 编码与解码 -- urlencode 和 urldecode
  • 3. HTTP 协议请求与响应格式
    • 3.1 请求
    • 3.2 响应
  • 4. 代码实现 -- 简单的HTTP服务器
    • 4.1 Socket.hpp
    • 4.2 TcpServer.hpp
    • 4.3 HttpServer.hpp
    • 4.4 HttpServer.cc
    • 4.5 HttpProtocol.hpp
  • 5. 服务器 和 网页分离
    • 代码实现 -- URI 资源
    • 网页跳转
    • HTTP 常见 Header
    • HTTP 状态码
    • 关于 connection 报头
    • HTTP 常见 header 表格
    • HTTP 常见方法
    • 动静态
  • 6. 共勉
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档