Linux网络:应用层协议http

发布于:2025-09-14 ⋅ 阅读:(19) ⋅ 点赞:(0)

前言

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

我们之前已经学了UDP与TCP套接字的简单使用,以及讲解了进程间的各种关系,以及守护进程的概念。

今天,就让我们继续来学习一下http协议吧!!

HTTP 是什么?—— 核心定义

在互联网世界中,HTTP(HyperTextTransferProtocol,超文本传输协议)是一个至关重要的协议。它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如HTML文档)。

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

你可以把它想象成一种约定俗成的“问答”规则

  • 客户端问(Request):“请把 /index.html 这个文件发给我。”
  • 服务器答(Response):“好的,这是文件内容(状态码 200),它的类型是文本。” 或者 “抱歉,你要的文件我没找到(状态码 404)。”

它的几个关键特性:

  • 无连接(Connectionless):在 HTTP/1.0 及之前,每次请求完成后就断开 TCP 连接。这意味着服务器处理完一个请求并发送响应后,就“忘记”了这个客户端。新的请求需要建立新的连接。虽然 HTTP/1.1 的默认持久连接(Keep-Alive)缓和了这个问题,但其设计哲学仍是“无状态”的。

  • 无状态(Stateless):服务器不会记住之前的任何请求。每一次请求都是独立的,与之前或之后的请求没有关系。这简化了服务器设计,但导致了一些问题(例如,需要登录的网站如何记住用户?这就引入了 Cookie 等技术来“模拟”状态)。

  • 基于请求/响应模型:通信总是由客户端发起,服务器被动响应。

  • 媒体独立(Media Independent):只要客户端和服务器知道如何处理数据内容,任何类型的数据(文本、图片、视频、JSON 等)都可以通过 HTTP 传输。这是通过 Content-Type 头字段来实现的。

我们可以这样理解:

http的本质上是全网唯二两个进程在通信,而网络通信的本质就是进程间通信。只不过因为距离变远了,所以需要加上网络。

而由于用户通信时,不只是传文字,还会有很多超文本的内容。所以我们就有了 http这个协议,该协议通过规范请求与应答、使通信双方通过序列化与反序列化就能拿到需提取的内容。

而所谓超文本传输的本质也是读文件!比如一张图片、我们用二进制方式读取文件内容并存在string 中,最后将该 string 序列化并加入相应字段,比如状态码,状态描述符,版本,content-length, content-Type 等等,发送给客户端,如此一来,对方就知道传过来的文件类型并获取内容读文件、这就实现了基于 htp的网络通信了。

光是说概念肯定不会理解,我们之后将会手撸一个简单的http的实现帮助大家理解。


认识URL:

什么是URL:

URL(Uniform Resource Locator,统一资源定位符),俗称网页地址网址。它是互联网上标准资源的地址,用于指定互联网上资源(Resource) 的位置以及访问该资源所用的协议(Protocol)

你可以把它想象成现实生活中一个文件的完整地址

  • 协议(Protocol):相当于运输方式(例如:快递、空运、海运)。

  • 域名(Domain Name):相当于城市和街道名。

  • 路径(Path):相当于具体的门牌号和文件柜。

  • 参数(Parameters):相当于对文件的特殊要求(例如:要文件的第几页、排序方式)。

核心目的:URL 的目的是让浏览器(或其他客户端)能够通过一种标准化的方式,准确地定位并检索互联网上的任何资源,无论是网页、图片、视频、API 数据还是可下载文件。

平时我们说的网址就是URL:

在这里插入图片描述

这个跟我们之前学习的路径结构是一致的,我们请求的资源,本质上就是文件。

为什么是路径呢?

请大家思考:

  • 我把我的数据交给别人,别人把他的数据交给我,这本质不就是在做IO吗?所以我们上网的所有行为,都是在做IO。
  • 我们所看见的图片,视频,文本,这些都是一个一个的资源。
  • 所以我们想要看见这些资源,就需要知道他们在哪里?在哪台服务器上(网络,IP),在什么路径底下(系统,路径)。

这就是为什么需要URL。值得辨别的是,在URL中,/不一定代表当前系统的根目录,它叫做web根目录,二者直接可能并不是同一个。

URL在端口号的帮助下,实现了进程间通信。但是URL中并没有包含端口号啊!!

首先,我们目前几乎所有情况用的都是 https(这个会在后面学习)。现在 URL 上默认不显示端口号。这是因为我们之前说过,1-1023的端口号都是固定分配给最重要、最广泛使用的系统服务。这些端口号是严格绑定的,不能用于其他目的。就好比说110,我们就知道是报警一样。另外,在@符号后跟着的,叫做域名,后续用通过比如 DNS 将其转化为ip地址而端口号后跟着的,就是文件路径!!!

所以,成熟的应用层协议,都是跟端口号强相关的,也就是说,http的端口号就是在1-1023之中,是固定的,所以我们不需要显式指定出来。

urlencode 和 urldecode

在这里插入图片描述

以百度搜索为例,我们随便搜索一个内容:

https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=c%20%3F%20%2F%20.%20%2B%20c&fenlei=256&oq=%253F%25E3%2580%2582%252B%2520-%2520%252F&rsv_pq=9409e2cc0070649e&rsv_t=57be6%2B2nWun06W5PRb%2FNOxiWr%2Bv6F4KOwATsCalCds%2Bt%2B2aGX73vvf14N2s&rqlang=cn&rsv_enter=1&rsv_dl=tb&rsv_btype=t&rsv_sug2=0&rsv_sug3=31&rsv_sug1=13&rsv_sug7=100

在这里插入图片描述

可以看见,我们搜索的内容中,两个c之间的各种符号已经变成了一堆莫名其妙的字符,这是什么原因呢?

像 / ? : 等这样的字符, 已经被 url 当做特殊意义理解了. 因此这些字符不能随意出现。比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。

转义的规则如下:

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

urldecode就是urlencode的逆过程。


HTTP 协议请求与响应格式

这是我们一个http请求的格式:
在这里插入图片描述

让我们先来为大家介绍一下首行的意思:

第一行是请求行,以\r\n结尾的字符串。

他的第一个字段是请求方法:GET/POST,之后用空格分割,第二个字段是URL,随后又是空格分割。第三个字段是所使用的http版本。

如何理解这个http的版本呢?

这就跟你平时使用微信一样,他的功能一开始可能只有聊天,后来的新版本加上了朋友圈。在你发送请求的时候,服务端会根据你的版本,来决定给你提供的功能,如果你的版本没有更新,那就只会跟你提供聊天的功能,不会提供朋友圈。

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

这里就是我们的Host到Hm_lpvt的所有行,都是Header,请求报头的内容。其中,都是以键值对的方式保存:Key:Value。

请求报头后面会有一个空行,来进行一个内容划分的分割。

之后是Body,请求正文。这里就是我们的username一行:

在这里插入图片描述

在进行网络通信时,我们会把这些内容全部序列化,随后通过我们之前所学的TCP来进行传输。以请求的序列化字段为例,本质上就是将一个个的字段拼接好后,连成一个长长的string再发送过来。中间用协议规定好的特殊标识符分开。


套接字的封装

基类与子类

为了方便我们后面写http协议的代码,我们这里选择先对我们的套接字进行封装。就是把绑定,连接这些操作全部封装成一个类,从此我们套接字的使用就变成对一个类对象的成员方法的各种调用了。

但是我们的套接字可不止一种类型,既有UDP套接字,也有TCP套接字。所以这里我们继续为大家带来一种方法模式:模板方法模式。

模板方法模式是一种行为设计模式。它的核心思想是:

在一个抽象类中定义一个操作算法的骨架(即“模板方法”),而将一些步骤的具体实现延迟到子类中。使得子类可以在不改变算法整体结构的情况下,重新定义算法中的某些特定步骤。

简单来说,就是 “父类搭架子,子类填里子”

所以我们就需要先定义一个基类:

#pragma once 

//既然是封装套接字,我们就先把要用到的头文件先写上去
#include<iostream>
#include<string>
#include<sys/socket.h>
#include<unistd.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>

//所谓模板方法模式,本质上就是C++多态继承特性的应用,所以我们就需要先定义一个基类
class Socket
{
public:
private:
};

随后,把子类可能要使用到的方法写进去:

// 所谓模板方法模式,本质上就是C++多态继承特性的应用,所以我们就需要先定义一个基类
// 用来规定创建Socket的方法
class Socket
{
public:
    virtual ~Socket()=default; // 防止不调用子类析构函数的情况出现
    
    virtual void SocketOrDie() = 0; // 创建套接字接口,这里的OrDie是一种常见的命名约定,特别是在C/C++和系统编程中。
    // 表示操作成功则继续,失败则直接终止程序,=0表示父类并没有实现,要求子类继承后必须实现
    //= default 表示显式要求编译器生成该函数的默认实现。

    virtual bool BindOrDie(int port) = 0;

    virtual void SetSocketOpt() = 0; // 这个我们之前没有接触到这个需求

    virtual bool ListenOrDie() = 0;

    virtual int Accepter() = 0;

    virtual void Close() = 0;

    // 模板方法:定义算法骨架
    void BuildTcpSocketMethod(int port)
    {
        SocketOrDie();  // 步骤1:创建socket
        SetSocketOpt(); // 步骤2:设置socket选项
        BindOrDie(port);    // 步骤3:绑定端口
        ListenOrDie();  // 步骤4:开始监听
    }

private:
};

这里的BuildTcpSocketMethod是我们的骨架,一般来说,我们使用Socket会直接用父类指针指向子类对象,我们调用父类的这个BuildTcpSocketMethod方法,他就会一键帮我们完成Socket的创建。同时,由于多态性,我们的父类并没有实现这些成员方法,从而只会调用子类自己实现的方法,不同的子类调用同一个骨架函数,实际调用的各种接口却是不一样的,从而实现多态性。

我们目前就写这点接口调用,后面需要新增什么函数就加在后面就行。

我们等会的http所用到的套接字是TCP,所以我们这里就写一个TCP子类,至于UDP,大家用到的时候可以自己加。

TCP的类成员变量,自然要有一个_socket来获取我们的套接字的文件描述符,此外,我们还应该用const定义一个默认的socket值。

const int gdefaultsockfd = -1;

class TcpSocket : public Socket
{
public:
    TcpSocket() : _sockfd(gdefaultsockfd)
    {
    }
    virtual ~TcpSocket()
    {
    }

    virtual void SocketOrDie() override
    {
    }

    virtual bool BindOrDie(int port) override
    {
    }
    virtual void SetSocketOpt() override
    {
    }
    virtual bool ListenOrDie() override
    {
    }
    virtual int Accepter() override
    {
    }
    virtual void Close() override
    {
    }

private:
    int _sockfd;
};

至此,当我们想使用我们的套接字时,大致所使用的方法就是:

int main()
{
    Socket *sk = new TcpSocket();
    sk->BuildTcpSocketMethod(8080);
}

子类方法的实现

我们之前已经多次用过TCP套接字了,对于他的使用过程我们也应该具备一定程度的了解了。

所以我们先创建我们的套接字(注意,这里我们引入了之前所封装的日志与锁等头文件):

class TcpSocket : public Socket
{
public:
    TcpSocket() : _sockfd(gdefaultsockfd)
    {
    }
    virtual ~TcpSocket()
    {
    }

    virtual void SocketOrDie() override
    {
        _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::ERROR) << "socket create error";
            exit(SOCKET_ERR);
        }

        LOG(LogLevel::DEBUG) << "socket create success";
    }

    virtual bool BindOrDie(int port) override
    {
        if (_sockfd == gdefaultsockfd) // 说明没有进行获取套接字
        {
            return false;
        }

        InetAddr addr(port);
        int n = ::bind(_sockfd, addr.Getsockaddr(), addr.GetSockaddrLen());
        if (n < 0)
        {
            LOG(LogLevel::ERROR) << "bind error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::DEBUG) << "bind create success: " << _sockfd;
        return true;
    }
    virtual void SetSocketOpt() override
    {
    }
    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 create success: " << _sockfd;
        return true;
    }
    virtual int Accepter() override//目前我们先不实现接受连接的功能,因为这个函数要跟我们之后的实现有关
    {
    }
    virtual void Close() override
    {
        if(_sockfd==gdefaultsockfd)
        {
            return ;
        }

        ::close(_sockfd);
    }

private:
    int _sockfd;
};

TCP套接字的创建,绑定,监听,我们之前都已经说过,这里就快速完成。大家可以看一下代码回顾一下。

另外,由于发送信息与接受信息我们之前使用的write,read,recv,send。我们这里也可以统一的进行封装:

virtual int Recv(std::string *out)
    {
        char buffer[4096];
        int n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);

        if (n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }

        return n;
    }

    virtual int Send(std::string &in) 
    {
        int n = ::send(_sockfd, in.c_str(), in.size(), 0);
        return n;
    }

所以我们目前就可以把套接字封装成这样:

#pragma once

// 既然是封装套接字,我们就先把要用到的头文件先写上去
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

// 所谓模板方法模式,本质上就是C++多态继承特性的应用,所以我们就需要先定义一个基类
// 用来规定创建Socket的方法
class Socket
{
public:
    virtual ~Socket() = default; // 防止不调用子类析构函数的情况出现

    virtual void SocketOrDie() = 0; // 创建套接字接口,这里的OrDie是一种常见的命名约定,特别是在C/C++和系统编程中。
    // 表示操作成功则继续,失败则直接终止程序,=0表示父类并没有实现,要求子类继承后必须实现
    //= default 表示显式要求编译器生成该函数的默认实现。

    virtual bool BindOrDie(int port) = 0;

    virtual void SetSocketOpt() = 0; // 这个我们之前没有接触到这个需求

    virtual bool ListenOrDie() = 0;

    virtual int Accepter() = 0;

    virtual void Close() = 0;

    virtual int Recv(std::string *out) = 0;

    virtual int Send(std::string &in) = 0;

    // 模板方法:定义算法骨架
    void BuildTcpSocketMethod(int port)
    {
        SocketOrDie();   // 步骤1:创建socket
        SetSocketOpt();  // 步骤2:设置socket选项
        BindOrDie(port); // 步骤3:绑定端口
        ListenOrDie();   // 步骤4:开始监听
    }

private:
};

class TcpSocket : public Socket
{
public:
    TcpSocket() : _sockfd(gdefaultsockfd)
    {
    }
    virtual ~TcpSocket()
    {
    }

    virtual void SocketOrDie() override
    {
        _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::ERROR) << "socket create error";
            exit(SOCKET_ERR);
        }

        LOG(LogLevel::DEBUG) << "socket create success";
    }

    virtual bool BindOrDie(int port) override
    {
        if (_sockfd == gdefaultsockfd) // 说明没有进行获取套接字
        {
            return false;
        }

        InetAddr addr(port);
        int n = ::bind(_sockfd, addr.Getsockaddr(), addr.GetSockaddrLen());
        if (n < 0)
        {
            LOG(LogLevel::ERROR) << "bind error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::DEBUG) << "bind create success: " << _sockfd;
        return true;
    }
    virtual void SetSocketOpt() override
    {
    }
    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 create success: " << _sockfd;
        return true;
    }
    virtual int Accepter() override // 目前我们先不实现接受连接的功能,因为这个函数要跟我们之后的实现有关
    {
    }
    virtual void Close() override
    {
        if (_sockfd == gdefaultsockfd)
        {
            return;
        }

        ::close(_sockfd);
    }

    virtual int Recv(std::string *out)
    {
        char buffer[4096];
        int n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);

        if (n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }

        return n;
    }

    virtual int Send(std::string &in) const
    {
        int n = ::send(_sockfd, in.c_str(), in.size(), 0);
        return n;
    }

private:
    int _sockfd;
};

为了防止大家遗忘,我这里把用到的之前写的头文件全部附上:
Common.hpp:主要是记录一下默认参数,共用的一些简单函数的头文件。

#pragma once

#include <iostream>
#include <string>

#define Die(code)   \
    do              \
    {               \
        exit(code); \
    } while (0)

#define CONV(v) (struct sockaddr *)(v)

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR
};

const int gdefaultsockfd = -1;
const int gbacklog = 8;

InetAddr.hpp:封装的sockaddr,我们在bind前经常创建,初始化它。

#pragma once
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>

class InetAddr
{
    private:
    void PortNet2Host()
    {
        _port=::ntohs(_addr.sin_port);
    }

    void IpNet2Host()
    {
        char ip[64];
        ::inet_ntop(AF_INET, &_addr.sin_addr, ip, sizeof(ip));
        _ip = std::string(ip);
    }
public:
    InetAddr()
    {
    }

    InetAddr(const struct sockaddr_in &addr)
    :_addr(addr)
    {
        PortNet2Host(); // 将网络字节序的端口转换为主机字节序
        IpNet2Host();   // 将网络字节序的IP地址转换为主机字节序
    }

    InetAddr(const uint16_t port)
    :_port(port),
    _ip("")
    {
        _addr.sin_family=AF_INET; // 设置地址族为IPv4
        _addr.sin_port=htons(port); // 将端口转换为网络字节序
        _addr.sin_addr.s_addr=INADDR_ANY; // 设置IP地址为任意
    }

    ~InetAddr()
    {
    }

    bool operator== (const InetAddr &other)const
    {
        return _ip==other._ip && _port==other._port;
    }

    std::string Addr() const
    {
        return _ip + ":" + std::to_string(_port);
    }

    struct sockaddr* Getsockaddr()
    {
        return (struct sockaddr*)&_addr;
    }

    size_t GetSockaddrLen()
    {
        return sizeof(_addr);
    }

    std::string GetIp()
    {
        return _ip;
    }

    uint16_t GetPort()
    {
        return _port;
    }


private:
    struct sockaddr_in _addr; // 用于存储IP地址和端口号的结构体
    std::string _ip;
    uint16_t _port;
};

log.hpp:封装的日志功能

#pragma once
#include <iostream>

#include <cstdio>
#include <string>
#include <fstream>
#include <sstream>
#include <memory>
#include <filesystem> //C++17
#include <unistd.h>
#include <time.h>
#include "mutex.hpp"

namespace LogModule
{

    // 记录日志文件默认名字与默认路径
    const std::string defaultlogname = "log.txt";
    const std::string defaultlogpath = "./log/";

    // 定义枚举体日志等级
    enum class LogLevel
    {
        DEBUG = 1,
        INFO,
        WARN,
        ERROR,
        FATAL
    };

    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;                     // 进行虚析构
        virtual void Synclog(const std::string &message) = 0; // 定义一个统一的调用接口以便后续子类重新定义实现多态
    };
    using namespace MutexModule;
    class ConsoleLogStrategy : public LogStrategy // 控制台策略
    {
    public:
        ConsoleLogStrategy()
        {
        }
        ~ConsoleLogStrategy()
        {
        }

        void Synclog(const std::string &message) // 打印我们的信息到控制台
        {
            LockGuard lockguard(_lock); // 防止多个进程打印乱码
            std::cout << message << std::endl;
        }

    private:
        Mutex _lock; // 互斥量锁
    };

    class FileLogStrategy : public LogStrategy // 文件(硬盘级)策略
    {
    public:
        FileLogStrategy(const std::string &logpath = defaultlogpath, const std::string &logname = defaultlogname)
            : _logpath(logpath),
              _logname(logname)
        {
            if (std::filesystem::exists(_logpath))
            {
                return; // 存在我们就直接返回防止重复创建路径
            }
            else
            {
                LockGuard LockGuard(_mutex);
                try
                {
                    std::filesystem::create_directory(_logpath);
                }
                catch (const std::filesystem::filesystem_error &e)
                {
                    std::cerr << "创建日志目录失败: " << e.what() << std::endl;
                }
            }
        }
        ~FileLogStrategy()
        {
        }
        void Synclog(const std::string &message)
        {
            LockGuard LockGuard(_mutex);
            std::string log = _logpath + _logname; // 合并文件路径与文件名
            std::ofstream out(log, std::ios::app); // 追加写入使用app
            if (!out.is_open())
            {
                return; // 如果打开失败
            }
            out << message << "\n";
            out.close(); // 关闭文件流
        }

    private:
        std::string _logpath;
        std::string _logname;

        Mutex _mutex;
    };
    std::string GetCurrentTime()
    {
        time_t t = ::time(nullptr);
        struct tm ttm;
        ::localtime_r(&t, &ttm); // ttm是一个输出型参数
        // 定义缓冲区将数据格式化
        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",
                 ttm.tm_year + 1900, // 这里注意给的年份是减去了1900的所以真实年要加回来,月份也是如此
                 ttm.tm_mon + 1,
                 ttm.tm_mday,
                 ttm.tm_hour,
                 ttm.tm_min,
                 ttm.tm_sec);
        return buffer;
    }

    std::string Level_to_string(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARN:
            return "WARN";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "NONE";
        }
    }
    class Log
    {
    public:
        Log()
        {
            // 默认采用ConsoleLogStrategy策略
            _strategy = std::make_shared<ConsoleLogStrategy>();
        }
        void EnableConsoleLog()
        {
            LockGuard lockguard(_mutex);
            _strategy = std::make_shared<ConsoleLogStrategy>();
        }
        void EnableFileLog()
        {
            LockGuard lockguard(_mutex);
            _strategy = std::make_shared<FileLogStrategy>();
        }
        ~Log() {}

        class logmessage
        {
        public:
            logmessage(LogLevel level, const std::string &filename, const int &line, Log &log) // 这里新增了一个参数
                : _level(level),
                  _line(line),
                  _time(GetCurrentTime()),
                  pid(getpid()),
                  _filename(filename),
                  _log(log) // 负责给你的log进行初始化,方便析构根据这个调用
            {
                std::stringstream ssbuffer;
                ssbuffer << "[" << _time << "] "
                         << "[" << Level_to_string(_level) << "] "
                         << "[" << pid << "] "
                         << "[" << _filename << "] "
                         << "[" << _line << "] - ";
                _loginfo = ssbuffer.str();
            }

            template <typename T>
            logmessage &operator<<(const T &message)
            {
                std::stringstream ss;
                ss << message;
                _loginfo += ss.str();

                return *this;
            }

            ~logmessage()
            {
                if (_log._strategy) // 如果该智能指针存在
                {
                    _log._strategy->Synclog(_loginfo); // 调用该函数指针所指向策略的Synclog函数,实现多态
                }
            }

        private:
            std::string _time;     // 时间
            LogLevel _level;       // 日志等级
            pid_t pid;             // 进程pid
            std::string _filename; // 源文件名称
            int _line;             // 行号

            Log &_log;            // 引用外部的Log类,负责根据不同的策略进行刷新
            std::string _loginfo; // 一条完整的日志记录
        };

        // 这里的返回临时对象logmessage是我们故意设置的
        logmessage operator()(LogLevel level, const std::string &filename, int line)
        {
            return logmessage(level, filename, line, *this); // 新增加入*this指针,因为该Log的*this就代表该类型
        }

    private:
        std::shared_ptr<LogStrategy> _strategy;
        Mutex _mutex;
    };
    Log log;
#define LOG(Level) log(Level, __FILE__, __LINE__)
#define ENABLE_CONSOLE_LOG() log.EnableConsoleLog()
#define ENABLE_FILE_LOG() log.EnableFileLog()
}

mutex.hpp:封装的锁

#ifndef _MUTEX_HPP_
#define _MUTEX_HPP_
#pragma once
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex(const Mutex &) = delete;// 禁用拷贝构造函数
        const Mutex &operator=(const Mutex &) = delete;// 禁用拷贝赋值运算符
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }
        bool lock()
        {
            return pthread_mutex_lock(&_mutex) == 0;
        }
        bool unlock()
        {
            return pthread_mutex_unlock(&_mutex) == 0;
        }
        pthread_mutex_t *LockPtr()
        {
            return &_mutex;
        }

    private:
        pthread_mutex_t _mutex; // 互斥量锁
    };

    class LockGuard // 采⽤RAII⻛格,进⾏锁管理
    {
    public:
        LockGuard(Mutex &mtx) : _mtx(mtx) // 通过后续使用时定义一个LockGuard类型的局部变量,在局部变量的声明周期内,互斥量会被自动加锁与解锁
        {
            _mtx.lock();
        }
        ~LockGuard()
        {
            _mtx.unlock();
        }

    private:
        Mutex &_mtx;
    };
}

#endif

TCP服务端封装

我们今天的目的虽然是要给大家熟悉http协议,但是http本身就是在TCP传输信息的功能上实现的,所以自然也会有服务端客户端这些。

所以,我们还需要对我们的TCP服务端的代码进行封装。

#pragma once

#include "Socket.hpp"

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

private:
};

根据之前所学的TCP,我们可以知道,一个TCP的服务端的代码中,会出现两次文件描述符的获取。

第一次是在我们创建套接字Socket的时候,返回值会返回给我们一个文件描述符,这个文件描述符是专门用于接受新连接的文件描述符,服务器运行期间通常只会存在一个。另外一个是我们在接受连接ACCEPT时的返回值,这个文件描述符是用来专门给一个用户端建立连接进行通信的,每一个用户都有一个,我们通常使用临时变量来存储,并通过回调函数的方式,把这个文件描述符以参数的形式传递进去。

我们之前的TCP服务端的类成员变量中的listensockfd,就是存储的第一种文件描述符,所以我们这里也一样,但是又有稍微一点不同。因为我们这里封装了Socket,Socket类中已经有一个sockfd类成员变量了。如果我们这里想要获取它的类成员变量中的fd,最好的做法就是使用一个指针。

我们这里选择智能指针,并且,一个服务端的启动必定带有一个端口,我们这里选择让外界来传递端口,并记得给它设置为缺省参数端口号8080。用类成员变量_port来接收(注意,我们这里的初始化要带上我们的指针。并且make_unique时的参数类型要选择子类。这是多态的前提,父类指针指向子类对象):

#pragma once

#include "Socket.hpp"

class TcpServer
{
public:
    TcpServer(int port = 8080) : 
    _port(port),
    _listensockp(std::make_unique<TcpSocket>())
    {
    }
    ~TcpServer()
    {
    }

private:
    std::unique_ptr<Socket> _listensockp;
    int _port;
};

我们可以在默认构造函数中直接调用我们刚刚封装好的套接字,这样就在生成一个TcpServer类时直接顺便对套接字进行了创建。

那么我们怎么调用呢?

还记得我们在Socket基类中完成的算法骨架函数吗?还记得我们的成员变量的智能指针吗?

我们已经在初始化列表中完成了对这个变量的初始化,所以,我们只需要在构造函数正文中通过智能指针进行调用即可:

TcpServer(int port = 8080) : 
    _port(port),
    _listensockp(std::make_unique<TcpSocket>())
    {
        _listensockp->BuildTcpSocketMethod(_port);
    }

除此之外,一个应该和之前的服务器一样,我们需要一个启动函数,就跟之前的start一样,直接启动我们的服务器,开始循环,我们这里就命名为Loop。

既然开始了运行,所以我们也要把运行状态的参数搬上来,所以新增成员变量:_isrunning。

   void Loop()
    {
        _isrunning=true;
        while(_isrunning)
        {

        }
        _isrunning=false;
    }

private:
    std::unique_ptr<Socket> _listensockp;
    int _port;

    bool _isrunning;

接下来,我们继续来完成Socket类中的Accepter的定义。我们之前一直没有对该函数进行重写,主要还是想让大家思考一下,我们这个函数的返回值类型到底应该是什么。

我们之前是暂时使用的是int,这是因为我们知道accept函数调用接受连接会返回一个文件描述符,这个文件描述符就是int类型的。

我们这里可以返回一个Socket类类型。为了方便管理,可以使用智能指针,类型为Socket,Accepter函数返回一个类型为Socket的智能指针,我们甚至还可以用using关键字对这个指针进行重命名。这样,我们想调用recv,send这些函数,就可以通过指针操作轻松进行,而不像以前是手动传递文件文件描述符。(这里指的注意,我们在using重命名时,Socket类必然没实现完成,所以重命名前必须加一个Socket类的声明)

class Socket;
using SockPtr = std::shared_ptr<Socket>;

既然是对建立连接的操作进行封装,那么就必然会用到sockaddr_in等结构体。但是我们之前是不是已经对该过程进行了封装,所以我们这里可以让函数传递一个InetAddr类型进来,以及建立一个新的sockaddr_in结构体,我们通过accept函数给sockaddr_in结构体对象进行赋值,并最后让其给传进来到参数InetAddr赋值。

但是我们目前的InetAddr类还缺少带出用户端信息的能力,所以我们要给其新增一个SetAddr的功能。

    void SetAddr(const sockaddr_in &client, socklen_t &len)
    {
        _addr = client;
        IpNet2Host();
    }

我们的Acceptr的声明变为了:

virtual SockPtr Accepter(InetAddr * client);

我们规定外界调用时必须先给我们传进来一个client* 的参数,这样我们就能对该client对象进行改变。

  virtual SockPtr Accepter(InetAddr *client) override // 目前我们先不实现接受连接的功能,因为这个函数要跟我们之后的实现有关
    {
        if (!client)
        {
            return nullptr;
        }

        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);

        int newfd = ::accept(_sockfd, CONV(&peer), &len);

        if (newfd < 0)
        {
            LOG(LogLevel::WARN) << "accept error";
            return nullptr;
        }

        client->SetAddr(peer, len);
        return std::make_shared<TcpSocket>(newfd);
    }

我们这里先判断一下传进来的参数,没有问题就开始连接,最后把连接的结果传进至外界传进来的client,所以这里的client是一个输出型参数,并且我们会返回一个TcpServer的智能指针对象,方便进行接收与传递信息。

值得注意的是我们之前的构造函数没有带参的,所以要给TcpServer新增一个构造函数:

 TcpSocket(int fd) : _sockfd(fd)
    {
    }

那么继续回到我们的Loop无限循环的逻辑中,我们在死循环中的第一步就是调用Accepter建立连接,如果成功,就进行第二步IO处理,如果没超过,我们不用中断循环,而是直接continue开始尝试建立第二个连接:

while (_isrunning)
        {
            // 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();
        }

我们这里只是最基础的,用来实验的代码,实际上应该和之前的服务端一样的方式进行处理。

所以我们先来测试一下,新建一个cc文件,建立一个TcpSer的智能指针对象并调用loop,我们可以通过浏览器访问ip,并通过日志查看信息:

在这里插入图片描述

看来我们的目前的实现并没有报错。


目前为止我们的代码已经能够接收到请求方的访问申请了,但是我们TCP服务端的封装并没有到此结束。

我们约定,我们的TCP服务端只进行IO工作,流式的IO,不对信息进行任何的加工处理。

那么我们就需要在其他地方对信息进行加工处理,因为我们之前已经补充了理论知识,http信息是有固定的格式的,我们需要对这些信息进行加工处理使其能被我们解析出来每个信息所代表的含义。

所以我们Loop中的循环并没有写完。

之前我们写过一个TCP通信,当时我们是让服务端进行了一个多进程,或者多线程的处理。我们这里就简单点,不要太过麻烦。

所以我们直接利用孙子进程来调用我们的回调函数,于此使得进入另外的处理逻辑:

 void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 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)
                {
                    //大于0的时儿子进程
                    exit(0);
                }//此时儿子进程的儿子,也就是孙子并没进入if语句中
                //此时就只有孙子进程在执行了

                //进行回调
                

            }

            //外面是父进程
            //关闭连接返回的文件描述符,父进程不需要这个
            sockfd->Close();

            waitpid(id,nullptr,0);
        }
        _isrunning = false;
    }

也就是说我们需要新增一个成员变量,来进行我们的回调操作,所以这里我们就和之前一样,单独写一个初始化TCPServer的接口来对这个回调函数进行初始化,决定我们的处理方式。

我们给回调函数类型命名为:

using tcphandler_t = std::function<bool(SockPtr, InetAddr)>;

#pragma once

#include "Socket.hpp"

// 我们约定,我们的TCP服务端只进行IO工作,流式的IO,不对信息进行任何的加工处理
class TcpServer
{
    using tcphandler_t = std::function<bool(SockPtr, InetAddr)>;

public:
    TcpServer(int port = 8080) : _port(port),
                                 _listensockp(std::make_unique<TcpSocket>())
    {
        _listensockp->BuildTcpSocketMethod(_port);
    }
    ~TcpServer()
    {
        _listensockp->Close();
    }
    void InitServer(tcphandler_t handler)
    {
        _handler = handler;
        _listensockp->BuildTcpSocketMethod(_port);
    }

    void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 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)
                {
                    // 大于0的时儿子进程
                    exit(0);
                } // 此时儿子进程的儿子,也就是孙子并没进入if语句中
                // 此时就只有孙子进程在执行了

                // 进行回调
                _handler(sockfd,clientaddr);
                exit(0);
            }

            // 外面是父进程
            // 关闭连接返回的文件描述符,父进程不需要这个
            sockfd->Close();

            waitpid(id, nullptr, 0);
        }
        _isrunning = false;
    }

private:
    std::unique_ptr<Socket> _listensockp;
    int _port;

    bool _isrunning;
    tcphandler_t _handler;
};

这里有几点我们要注意的是,我们需要做到关闭不必要的文件描述符,尤其是析构函数也需要关闭。第二点就是我们要手动进行服务端的初始化,其目的是从外界传入回调函数的详细内容,在此之后我们再通过之前写的一个骨架来进行绑定,监听等操作。

那么目前为止我们的服务端内容就写到这里就行了,我们接下来封装一下我的http。

http的初步封装

根据之前所学我们可以知道,http是基于TCP所实现的,所以我们就需要来接着实现一下我们http的代码。

首先我们先把框架代码写出来:

#pragma once

#include "TcpServer.hpp"

class HttpServer
{
public:
    HttpServer(int port) 
    :_tsvr(std::make_unique<TcpServer>(port))
    {}
    ~HttpServer() {}

private:
    std::unique_ptr<TcpServer> _tsvr;//由于http是由TCP实现的,所以我们肯定内部一定会有一个TCP服务
};

我们之前封装了TcpServer,里面有一个回调函数需要从外部传入具体的方法,很明显,就是在这里进行传入的,所以我们需要接口来进行回调函数的初始化:

#pragma once

#include "TcpServer.hpp"

class HttpServer
{
public:
    HttpServer(int port)
        : _tsvr(std::make_unique<TcpServer>(port))
    {
    }
    ~HttpServer() {}

    void Start()
    {
        // 这里需要访问 _tsvr 和其他成员变量
        // 需要 this 指针!
        _tsvr->InitServer([this](SockPtr sockfd, InetAddr client) -> bool
                          { return this->HandlerHttpRequest(sockfd, client); });

        _tsvr->Loop();
    }

    bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
    {
        //我们处理http的入口,在这里我们就可以实现我们的处理方法了
    }

private:
    std::unique_ptr<TcpServer> _tsvr; // 由于http是由TCP实现的,所以我们肯定内部一定会有一个TCP服务
};

于是我们要实现的目标,就变成了具体的HandlerHttpRequest函数方法。

我们这里的代码还是比较简单的,值得一提的是在写lambda表达式时我们要传递this指针,因为我们在函数内部需要使用_tsvr变量。

为了方便我们知道逻辑进入了这个方法里,我们在开头可以先使用日志来记录:
,所以代码就变成这样:

 bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
    {
        //我们处理http的入口,在这里我们就可以实现我们的处理方法了
        LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();
        
        std::string http_request; // 用来存储接收请求信息

        sockfd->Recv(&http_request);

        std::cout << http_request;

        return true;
    }

注意我们之前并没有实现Fd接口,这个主要是为了方便我们打印才产生的需求,所以我们可以现在补上,只需要手动返回我们Socket套接字类中的文件描述符的值就行了。

在我们日志后面的代码也是用来做测试的,我们首先就是常规的接收请求信息,随后打印。之前一直都是理论的认识http请求的格式,现在就让我们具体的来看一下一个http请求的格式应该是什么样子的。

我们先简单写一个httpserver的.cc内容,并将Makefile内容更改:

#include "HttpServer.hpp"

int main(int argc, char *argv[])
{
    auto httpserver = std::make_unique<HttpServer>(8080);

    httpserver->Start();
    return 0;
}

Makefile:

.PHONY:all
all:HttpServer 
HttpServer:HttpServer.cc
	g++ -o $@ $^ -std=c++17 -pthread -ljsoncpp

.PHONY:clean
clean:
	rm -f HttpServer

随后运行可执行文件,并在浏览器上进行对应IP加端口的访问:

我们可以看到以下内容:

ubuntu@VM-8-3-ubuntu:~/dailycode/套接字封装$ ./HttpServer 
[2025-09-02 20:52:39] [DEBUG] [2117166] [Socket.hpp] [83] - socket create success
[2025-09-02 20:52:39] [DEBUG] [2117166] [Socket.hpp] [100] - bind create success: 3
[2025-09-02 20:52:39] [DEBUG] [2117166] [Socket.hpp] [118] - listen create success: 3
[2025-09-02 20:52:47] [DEBUG] [2117166] [TcpServer.hpp] [36] - get a new client, info is: 171.214.233.239:10656
[2025-09-02 20:52:47] [DEBUG] [2117216] [HttpServer.hpp] [27] - HttpServer: get a new client: 4 addr info: 171.214.233.239:10656
GET / HTTP/1.1
Host: 82.157.70.111:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.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

[2025-09-02 20:53:31] [DEBUG] [2117166] [TcpServer.hpp] [36] - get a new client, info is: 171.214.233.239:10656
[2025-09-02 20:53:31] [DEBUG] [2117416] [HttpServer.hpp] [27] - HttpServer: get a new client: 4 addr info: 171.214.233.239:10656
GET / HTTP/1.1
Host: 82.157.70.111:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.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

[2025-09-02 20:57:52] [DEBUG] [2117166] [TcpServer.hpp] [36] - get a new client, info is: 204.76.203.212:10656
[2025-09-02 20:57:52] [DEBUG] [2118343] [HttpServer.hpp] [27] - HttpServer: get a new client: 4 addr info: 204.76.203.212:10656
GET / HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36 Edg/90.0.818.46
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
X-Requested-With: XMLHttpRequest
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: en US,en;q=0.9,sv;q=0.8
Host: 82.157.70.111:8080

这就是我们进行访问的http请求。

如何来分析这个请求呢?
以这个为例:

GET / HTTP/1.1
Host: 82.157.70.111:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.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

根据我们的理论知识可以知道,第一行是我们的请求行(请求头):

  • GET: HTTP方法,表示请求获取资源
  • /: 请求的路径,这里是根路径(之前说过只是web根目录,不一定是系统的根目录)
  • HTTP/1.1: 使用的HTTP协议版本

从Host到Upgrade都是我们的请求报文部分,除此之外,请求报文后面一定会跟上一个空行,用来跟请求体内容分割。


  • 请求行(Request Line)
GET / HTTP/1.1

- 方法 + 路径 + 协议版本
  • 请求头(Request Headers) - 您提供的就是这部分

Host: 82.157.70.111:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...
Accept: text/html,application/xhtml+xml,application/xml;q=0.9...
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
  • 空行 - 分隔头部和body
(空行)
  • 请求体(Request Body) - GET请求通常没有

(对于GET请求,通常为空)

我们在封装HandlerHttpRequest函数时,通过Recv获取了用户端发送来的请求,那我们接下来就需要对获取的信息进行处理并给予反馈,为了先简单实验,我们这里就想先返回一个固定的内容。

首先我们一定需要返回一个状态行,但是响应报头我们可以省略,随后在随便给他返回一个html网页(内容我们可以直接ai生成一个简单的网页)。

二者都用字符串进行接受,并在最后组合为一个字符串,并通过Send传送回客户端。

状态行(Status Line) 是HTTP响应的第一行,格式有严格的规定:

HTTP/{版本} {状态码} {状态短语}\r\n

我们将在后面具体的学习有关内容,目前就先用:

HTTP/1.1 200 OK

表示成功响应的意思。

具体操作如下:

 bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
    {
        // 我们处理http的入口,在这里我们就可以实现我们的处理方法了
        LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();

        std::string http_request; // 用来存储接收请求信息

        sockfd->Recv(&http_request);

        std::cout << http_request;

        // 我们通过Recv获取了用户端发送来的请求,就需要解析它,并给予反馈,进行文本处理
        // 1、读取请求的完整性
        // 2、完整http反序列化,http response序列化

        std::string status_line = "HTTP/1.1 200 OK" + Sep + Blankline;
        // 我们这里做实验就不需要返回响应报头,直接接空行表示响应报头为空

        // 空行后面就是我们的响应正文:
        std::string body = "<!DOCTYPE html>\
        <html><head><title>\
         Hello World</title>\
         </head><body>\
         Hello,World !\
         </body> </html>";

        std::string httprespnse = status_line + body;
        sockfd->Send(httprespnse);
        return true;
    }

我们这里的代码,会让每一次对该ip该端口的访问结果都是收到这个html文本,最后我们在浏览器上就会获得对应的html效果:

在这里插入图片描述

这里要值得注意的是,我们这一步为了后面的方便,把\r\n,与空行这种会经常用到的东西都定义了一个const常量,之后就用这些常量来表示了。

状态行后面跟着的应该是响应报头,响应报头后面才是空行,但是我们这里直接省略掉了,所以直接加了一个Blankline在后面,空行后面就是响应正文了,我们这里是一个html页面。


未来我们的响应正文肯定不会都是固定的形式,我们的响应正文也不可能全写到这里,万一很多呢?所以我们需要对http协议做一下协议的定制。

http协议HttpRequest的初步定制

那么,如何进行协议的定制呢?其实这是跟我们之前讲的http协议的理论有关,我们都知道,http的协议通信通常分为http请求与http响应两大类。

所以我们需要对此进行分开的封装,封装成两个类,在其内部调用函数来进行对应的处理。

我们首先定义一个HttpProtocol.hpp文件。

#pragma once

#include <iostream>
#include <string>
#include <vector>

class HttpRequest
{
public:
private:
    std::string _req_line;                // 请求行
    std::vector<std::string> _req_header; // 请求报头
    std::string _blankline;               // 空行
    std::string _body;                    // 请求正文
};
class HttpResponse
{
public:
private:
};

这个头文件负责对协议的请求与响应信息进行加工处理,那么我们之前设定的各种const常量自然就可以定义到这里:

const std::string Sep = "\r\n";
const std::string Blankline = "\r\n"; // 空行其实就是一个\r\n

我们先来对http请求进行封装,由于我们通过Recv得到的http请求早就已经是被客户端序列化后的信息,所以我们需要自己进行反序列化:Deserialize。

在反序列化过程中,我们的第一步就是对请求的第一行进行判断检测,因为第一行一定是请求行,包含请求方法,请求的对象路径,http的版本信息,我们需要对这些信息进行检测。

这个检测呢?我们只需要检测他是否完整,是否包含默认的分割符\r\n:

bool ParseOneLine(std::string &req_str,std::string *out,const std::string& sep)
{
    auto pos=req_str.find(sep);
    //找不到?找到
    if(pos==std::string::npos)
    {
        //找不到
        return false;
    }

    //找到了,说明这个请求行完整,但是你无法保证后面的请求报头与正文的完整
    *out=req_str.substr(0,pos);
    req_str.erase(0,pos+sep.size());
    return true;
}

我们这个函数的目的就是把第一行从原来的字符串中移除,表示已经被处理,所以这是一个通用的方法,我们自然可以直接写在Common.hpp中表示常用的通用函数调用。

在这个对第一行请求行的处理函数逻辑中,我们把类成员变量_req_line成功进行了赋值,并根据传过来的请求行是否完整来进行剔除。

根据对应的情况,我们自然就可以进行接下来的处理。

我们的函数最后结果一共可能有三种:
1、传过来的是正常字符串,我们进行正常的处理。
2、传过来的只有请求行,其他地方为空串,但是返回值为真。
3、传过来的请求行不完全,且out输出为空串,返回值为假。

我们继续处理前两种情况。

处理完请求行,就剩下一个请求报头与请求正文,二者之间由一个空行隔开分离。

我们先对刚刚分离出来的请求行进行处理,请求行有三个参数,分别是请求方法,请求路径与http版本,我们可以多创建几个类成员变量来进行描述:

// 在反序列化的过程中,细化我们解析出来的字段

    std::string _method;  // 请求方法
    std::string _uri;     // 请求路径
    std::string _version; // http版本

这几个结果保存在类成员变量字符串_req_line中,由分隔符空格隔开,所以我们可以使用stringstream来进行快速的分开.

目前代码如下:

class HttpRequest
{
public:
    bool Deserialize(std::string &req_str)
    {
        if (ParseOneLine(req_str, &_req_line, Sep))
        {
            ParseReqLine(_req_line);//对请求行进行处理
            //其他处理.....
        }
    }

private:
	// 不想让外部调用这个函数,放到private中
    void ParseReqLine(std::string &req)
    {
        std::stringstream ss(req);
        ss >> _method >> _uri >> _version;
    }

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

    // 在反序列化的过程中,细化我们解析出来的字段

    std::string _method;  // 请求方法
    std::string _uri;     // 请求路径
    std::string _version; // http版本
};

我们在上面对请求行进行处理后,也需要对后面紧跟着的请求报头进行处理。

所以我们需要一个对请求报头进行处理的接口:

bool ParseHeader(std::string &req_ptr)
    {
        std::string line;

        while (true)
        {
            bool ret = ParseOneLine(req_ptr, &line, Sep); // 只会提取一行报头数据

            //我们之前说过,该函数接口调用返回的情况有三种:
            // 1、传过来的是正常字符串,我们进行正常的处理。
            // 2、传过来的只有请求行(或者此时只有一个空行\r\n),其他地方为空串,但是返回值为真。
            // 3、传过来的请求行不完全,且out输出为空串,返回值为假。

            if(ret&&!line.empty())//第一种情况
            {
                _req_header.push_back(line);//正常读取,把请求报头存储在vector中
            }
            else if(ret&&line.empty())//第二种情况
            {
                //在我们检测请求报头时出现空行,就说明我们的请求报头已经结束了,这个就是我们的空行
                _blankline=Sep;
                 break;//我们直接break,结束处理逻辑
            }
            else
            {
               //其他情况都是不正常的,我们直接返回false
               return false;
            }
        }

        //这里还缺少一个步骤

        return true;
    }

我们首先定义一个string类型的变量来获取每一行的数据。
利用之前定义好的只提取分离一行数据的接口:ParseOneLine,来获取一行数据,存储在line变量中。

随后在循环中将我们的报头插入报头数组。

但是我们要注意,我们的报头的格式,是key:value的,我们这里没有对key:value做拆分。所以我们还需要一个接口来专门做这个工作,当然,格式为key:value的数据我们想存储,肯定使用的哈希表,所以我们在类成员变量中得专门新建一个哈希表:

std::unordered_map<std::string, std::string> _headerkv;
bool ParseHeaderkv()//我们的报头数据都在类成员变量数组中,所以不需要传递参数了
    //我们只需要对这个数组进行处理就行了
    {
        std::string key,value;
        for(auto &header:_req_header)
        {
            //写到这里我们会发现,header提取出来的还是一个key:value的字符串,我们没有进行分离,所以还需要先分离字符串
            //
        }
    }

写到这里我们会发现,header提取出来的还是一个key:value的字符串,我们没有进行分离,所以还需要先分离字符串,这个分离字符串的操作,其实可以写成一个模板,因为是可以通用的接口,那我们就定义在Common.hpp中:

bool SplitString(const std::string &header,const std::string &sep,std::string*key,std::string *value)
{
    //我们这里传递sep参数的原因是想实现一个模板,我们这个函数会根据传进的符号不同来进行分离。
    //因为我们的key: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;
}

随后继续完成我们的ParseHeaderkv:

 bool ParseHeaderkv() // 我们的报头数据都在类成员变量数组中,所以不需要传递参数了
    // 我们只需要对这个数组进行处理就行了
    {
        std::string key, value;
        for (auto &header : _req_header)
        {
            // 写到这里我们会发现,header提取出来的还是一个key:value的字符串,我们没有进行分离,所以还需要先分离字符串
            if (SplitString(header,HeaderLineSep,&key,&value ))
            {
                _headerkv.insert(std::make_pair(key,value));
            }
        }
        return true;
    }

我们会发现,此时我们的请求行,请求报头,空行都已被处理,且在处理时就与req_str完成切割分离,所以我们此时剩下的req_str的内容就是请求正文部分了,我们直接赋值就行:

bool Deserialize(std::string &req_str)
    {
        if (ParseOneLine(req_str, &_req_line, Sep))
        {
            ParseReqLine(_req_line); // 对请求行进行处理
            // 其他处理.....
            ParseHeader(req_str); // 对请求报头进行处理

            //剩下的就是请求正文:
            _body=req_str;

            //...后面我们等会在完善
        }
        return true;
    }

我们的HttpRequest的定制就先写到这里,接下来我们紧接着回调函数的逻辑来到这里:

bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
    {
        // 我们处理http的入口,在这里我们就可以实现我们的处理方法了
        LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();

        std::string http_request; // 用来存储接收请求信息

        sockfd->Recv(&http_request);

        std::cout << http_request;

        // 我们通过Recv获取了用户端发送来的请求,就需要解析它,并给予反馈,进行文本处理
        // 1、读取请求的完整性
        // 2、完整http反序列化,http response序列化

        // std::string status_line = "HTTP/1.1 200 OK" + Sep + Blankline;
        // // 我们这里做实验就不需要返回响应报头,直接接空行表示响应报头为空

        // // 空行后面就是我们的响应正文:
        // std::string body = "<!DOCTYPE html>\
        // <html><head><title>\
        //  Hello World</title>\
        //  </head><body>\
        //  Hello,World !\
        //  </body> </html>";

        // std::string httprespnse = status_line + body;
        // sockfd->Send(httprespnse);
        return true;
    }


回调接口的继续完善

我们这里继续接着上上步http的初步封装的逻辑,进入我们的回调函数逻辑中去。

此时我们通过Socket获取到请求字符串之后,需要创建一个HttpRequest的对象,负责对我们的请求字符串进行具体的处理:

  bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
    {
        // 我们处理http的入口,在这里我们就可以实现我们的处理方法了
        LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();

        std::string http_request; // 用来存储接收请求信息

        sockfd->Recv(&http_request);

        std::cout << http_request;

        HttpRequest req;
        req.Deserialize(http_request);//进行反序列化
        
        
    }

我们接下来要做什么呢?

在反序列化时,我们已经在req中拿到了请求字符串中的请求行的信息,也就是说,我们拿到了请求路径。

也就是说,我们要通过这个路径,来进行下一步的响应功能。
所以我们要先读取这个路径下的文件,是否存在?内容是什么?这里又要涉及到我们之前提过的web根目录的概念了。

假如我们的浏览器的请求是这样的:GET /images/photo.jpg HTTP/1.1。在我们云服务器上我们设置web根目录是:/var/www,那么我们这个请求的URL实际上访问的就是云服务器上的:/var/www+ /images/photo.jpg = /var/www/images/photo.jpg。

那我们先设定好一个文件夹叫做wwwroot,这个就是我们设定的web根目录,我们记得使用const常量记录一下:

const std::string defaulthomepage = “wwwroot”;//web根目录

于是在创建了HttpRequest对请求信息进行管理之后,我们就需要对url信息进行判断并提取:

std::string GetContent(const std::string path)
    {
        std::string content;//创建一个空字符串,用于存储文件内容
        std::ifstream in(path, std::ios::binary);//以二进制模式打开文件,std::ios::binary是二进制模式标志,防止文本转换

        if (!in.is_open())
            return std::string();

        //将文件指针移动到文件末尾
        in.seekg(0, in.end);
        //获取当前文件指针位置(即文件大小)
        int filesize = in.tellg();
        //随后又将文件指针移动到开头,以上步骤是为了获取大小预定空间
        in.seekg(0, in.beg);

        //预分配足够的内存空间
        content.resize(filesize);
        //一次性读取整个文件内容到字符串中
        in.read((char *)content.c_str(), filesize);
        in.close();
        
        LOG(LogLevel::DEBUG) << "content length: " << content.size();
        return content;
    }

这样子,我们就能为响应结构体HttpResponse进行封装了。


响应结构体HttpResponse的初步封装

状态码

我们已经知道了目标想要申请的资源,所以我们需要根据这个资源,来创建响应结构体,返回给客户。

首先要给大家说http的状态码:
![[Pasted image 20250907154321.png]]

最常见的状态码, 比如 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
服务器维护或过载,暂时无法处理请求

这里的3XX重定向状态码比较特殊,我来给大家简单介绍一下这里的重定向含义

HTTP重定向

HTTP重定向是指服务器告诉客户端:“你要找的资源不在这个地址,请去另一个地址访问”。这是一种服务器端的URL转发机制。

301 Moved Permanently - 永久重定向

该重定向适用于网站域名更换,比如我们有新旧两个域名:

旧域名www.old-site.com
新域名www.new-site.com

我们在服务端检测时提取出了目标URL,就可以这样判断:

// 服务器端代码示例
if (request.host == "www.old-site.com") 
{
    response.status_code = 301;
    response.headers["Location"] = "https://www.new-site.com" + request.path;
    return;
}

在申请旧域名资源时,直接把新域名资源发送过去,并且状态码设为301表示我给你永久重定向了。

2. 302 Found - 临时重定向

该重定向多用于用户登录成功后跳转的场景。

当我们在网站上登录账号成功时,部分会出现临时重定向,自动将网页转化到首页。

3. 307 Temporary Redirect - 临时重定向

307与302同样是临时重定向,但是307的重定向,会严格保证原来的方法不会变,POST还会是POST,不会变成GET,反之亦然。

他们两个最核心的区别就是:是否保持HTTP方法和请求体


封装响应结构体

我们的响应报文通常由:响应行,响应报头,空行,响应正文组成,所以我们的响应结构体也会包含这些变量:

  // 最终要这4部分,构建应答
    std::string _resp_line;
    std::vector<std::string> _resp_header;
    std::string _blank_line;
    std::string _body;

但是我们的响应行是由HTTP版本,状态码,原因短语组成,并且按照之前写请求报头的经验,我们的响应报头也应该由哈希表存储,所以我们要用到的类成员变量又多了这几个:

// 组成响应行的变量:
    std::string _version;     // 版本
    int _status_code;         // 状态码
    std::string _status_desc; // 原因
    std::string _content;     // 响应行
    
    // 响应报头的kv值
    std::unordered_map<std::string, std::string> _header_kv;

接下来,我们需要一个build接口来对我们的响应结构体进行初始化,而我们的默认构造函数版本,以及空行这种参数在初始化列表进行初始化就行

class HttpResponse
{
public:
    HttpResponse()
        : _version(http_version),
          _blank_line(Sep)
    {
    }

    void Serialize(std::string *resp_str)
    {
        for (auto &header : _header_kv)
        {
            _resp_header.push_back(header.first + HeaderLineSep + header.second);
        }
        _resp_line = _version + LineSep + std::to_string(_status_code) + LineSep + _status_desc + Sep;

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


    void Build(HttpRequest &req_str)
    {
        // 先获取传过来的想要获取的资源的URL
        std::string URL = defaulthomepage + "/" + req_str.Uri();
        // 但注意,此时可能有坑,因为传过来的可能以/结尾,那么结合的URL为:
        //: wwwroot/
        // 此时这个URL无法指明具体的想要获取的资源,所以我们要先进行判断:
        if (URL.back() == '/')
        {
            // 那么我们就默认这个路径下有一个index.html,即该路径的首页(这个板块的首页)
            URL += firstpage;
        }

        // 尝试获取网页内容
        _content = req_str.GetContent(URL);
        if (_content.empty())
        {
            // 用户请求的资源并不存在
            _status_code = 404;
            _content = req_str.GetContent(page404);
        }
        else
        {
            _status_code = 200;
        }

        // 根据状态码设置文本信息
        _status_desc = Code2Desc(_status_code);

    }

private:
    std::string Code2Desc(int status_code)
    {
        switch (status_code)
        {
        case 200:
            return "OK";
        case 404:
            return "Not Found";
        case 301:
            return "Moved Permanently";
        case 302:
            return "Found";
        default:
            return std::string();
        }
    }

private:
    // 最终要这4部分,构建应答
    std::string _resp_line;
    std::vector<std::string> _resp_header;
    std::string _blank_line;
    std::string _body;

    // 组成响应行的变量:
    std::string _version;     // 版本
    int _status_code;         // 状态码
    std::string _status_desc; // 原因
    
    std::string _content;     // 响应正文,我们从目标URL中提取的数据

    // 响应报头的kv值
    std::unordered_map<std::string, std::string> _header_kv;
};

来给大家详细介绍一下我们的代码。

首先,我们的build与Serialize接口的目的其实都是对我们的类成员变量进行应该的赋值,初始化操作。

在build函数中,我们首先是把请求报头中分离的URL与我们的web根目录先组合的加起来,从而形成一个正确的路径。

但是这个新路径的最后结果可能是一个目录,也可能是一个目录下的某一个确定的文件。

每一个板块下其实都会有一个对应板块的首页,我们规定这个首页的名字就叫做index.html,所以我们判断,如果最后的路径是以/结尾,我们就认为它要去访问该目录模块下的首页index.html,所以手动给URL增加上这个名字。

这一步过后,我们的url路径访问的一定就是一个文件了,但是这个文件存不存在,就不知道了。

所以我们先尝试获取文件的内容,如果没获取到,那就是不存在。之前已经封装了GetContent,获取对应文件内容数据,返回的字符串如果为空就说明该文件不存在或者没内容,我们用_content接收,如果_content为空,就把状态码初始化为404表示要访问的文件不存在,如果不为空就填200表示内容已经找到。

最后调用一个接口获取到我们的对应状态码的原因。这就是build的逻辑。

那么序列化呢?

序列化目的也是对各个参数进行初始化,但是序列化的参数是一个输出型参数,我们给各个参数初始化后,再把组合进这个输出型参数中,就能把完整的响应字符串传递出去,从而在外界调用send发送给客户端了。

我们进行序列化的时候,vector数组里大概率是空的,但是我们的哈希表中大概率是存有一下我们在运行时设定的k:v值的,所以我们需要先给vector数组中插入。

最后再进行组合,让我们的输出型参数resp_str 依次+=响应行,报头,空行,与正文。

也就是我们在代码中所看到的那样:

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

此时我们就已经完成了最基本的雏形。那么我们一股脑顺便完善一下我们的主逻辑HandlerHttpRequest,我们之前已经写到了:

bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
    {
        // 我们处理http的入口,在这里我们就可以实现我们的处理方法了
        LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();

        std::string http_request; // 用来存储接收请求信息

        sockfd->Recv(&http_request);

        std::cout << http_request;

        HttpRequest req;
        req.Deserialize(http_request); // 进行反序列化

        LOG(LogLevel::DEBUG) << "用户想要:" << req.Uri(); // 我们这里封装了一个Uri接口,就是单纯的返回_uri类成员变量

        // 随后我们需要对用户想要的URL的内容进行处理,包括判断URL的文件是否存在,如果存在,就读取内容
        // 构建响应

        return true;
    }

我们刚刚构建好了响应结构体,所以现在就可以构建响应结构体,处理我们上面弄好的请求结构体的数据信息了。

bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
    {
        // 我们处理http的入口,在这里我们就可以实现我们的处理方法了
        LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();

        std::string http_request; // 用来存储接收请求信息

        sockfd->Recv(&http_request);

        std::cout << http_request;

        HttpRequest req;
        req.Deserialize(http_request); // 进行反序列化

        LOG(LogLevel::DEBUG) << "用户想要:" << req.Uri(); // 我们这里封装了一个Uri接口,就是单纯的返回_uri类成员变量

        // 随后我们需要对用户想要的URL的内容进行处理,包括判断URL的文件是否存在,如果存在,就读取内容
        // 构建响应
        HttpResponse resp;
        resp.Build(req); // 必须调用Build来设置内容和状态码
 
        std::string resp_str;//这是一个空字符串,我们需要通过响应结构体的序列化,进入响应结构体中带出我们的响应回复字符串
        
        //进行序列化
        resp.Serialize(&resp_str);
        
        LOG(LogLevel::DEBUG) <<"resp_str:"<< resp_str;
        sockfd->Send(resp_str);
        return true;
    }

常见的Header

我们在上面的序列化中提到,我们的vector数组 resp_header通常是空的,而哈希表里的通常是有数据的。可是,哈希表里的数据又是我们什么时候插进去的呢?

:当然是我们在其他地方进行初始化时,获取到了对应的数据信息,进行了对应的检索,于是就得到了我们的响应报文的信息,随后我们就把这些信息设定进去了。

所以我们需要给大家介绍一下HTTP 常见的一些 Header(我们这里先初步的进行理论的认识,后面我会带着把这些Header全部插入我们的哈希表中给大家看):

Content-Type:

Content-Type是一个非常重要的Header,它描述了我们的HTTP报文body正文部分的数据类型和编码格式,告诉接收方“我应该以什么方式解析这堆数据”。

常见值:

  • text/html:最常见的类型,表示 Body 是 HTML 文本,浏览器会将其渲染成网页。

  • text/css:表示 Body 是 CSS 样式表。

  • application/javascript:表示 Body 是 JavaScript 代码。

  • application/json:非常重要,表示 Body 是 JSON 格式的数据,常用于 API 接口的请求和响应。

  • multipart/form-data:常用于表单提交,特别是当表单中包含文件上传时。

  • application/x-www-form-urlencoded:另一种常见的表单提交格式,用于普通的键值对提交。

  • image/png, image/jpeg:表示图片数据。

示例:

响应头:Content-Type: text/html; charset=utf-8 (还指定了字符集为 UTF-8)
请求头:Content-Type: application/json (告诉服务器,我发过来的数据是 JSON 格式)

而为什么会有这个报头呢?

其实,网页的请求的数据类型可能不只是简单的html这种,还可能是html与png照片的混合组成的数据,而照片是通过01二进制数据存储的,所以为了让服务端知道申请的资源类型,我们就会手动设置这个报头。

设置的内容也有所对应,比如:
在这里插入图片描述

而我们应该怎么在我们的代码中设置这个报头参数呢?

答案是通过申请资源的后缀,也就是文件拓展名。

我们可以通过请求结构体中,不是存储了_url变量吗,我们只需要对其进行字符串裁剪,就可以获取文件拓展名了。

即:

class HttpRequest
{
public:

    std::string Suffix()//我们这个函数只获取了文件的拓展名,并未获取type,所以要查表进行转换
    {
        //   wwwroot/index.html   wwwroot/images/111.jpg  我们要返回申请资源的类型
        auto pos=_uri.rfind(".");//我们怎么知道数据的类型的呢?答:通过后缀
        if(pos==std::string::npos)
        {
            return std::string(".html");
        }
        else 
        {
            return _uri.substr(pos);
        }
    }


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

    // 在反序列化的过程中,细化我们解析出来的字段

    std::string _method;  // 请求方法
    std::string _uri;     // 请求路径
    std::string _version; // http版本

    std::unordered_map<std::string, std::string> _headerkv; // 存储请求报头数据
};

随后在响应结构体的内部,我们也要封装一个接口来进行文件拓展名到mime_type的转化工作。也就是在build最后,我们已经判断了__contentd是否为空,我们在后面再进行报头的设置工作:

class HttpResponse
{
public:
    HttpResponse()
        : _version(http_version),
          _blank_line(Sep)
    {
    }

    void SetHeader(const std::string &k, const std::string &v)
    {
        _header_kv[k] = v;
    }

    void Build(HttpRequest &req_str)
    {
        // 先获取传过来的想要获取的资源的URL
        std::string URL = defaulthomepage + "/" + req_str.Uri();
        // 但注意,此时可能有坑,因为传过来的可能以/结尾,那么结合的URL为:
        //: wwwroot/
        // 此时这个URL无法指明具体的想要获取的资源,所以我们要先进行判断:
        if (URL.back() == '/')
        {
            // 那么我们就默认这个路径下有一个index.html,即该路径的首页(这个板块的首页)
            URL += firstpage;
        }

        // 尝试获取网页内容
        _content = req_str.GetContent(URL);
        if (_content.empty())
        {
            // 用户请求的资源并不存在
            _status_code = 404;
            _content = req_str.GetContent(page404);
        }
        else
        {
            _status_code = 200;
        }

        // 根据状态码甚至文本信息
        _status_desc = Code2Desc(_status_code);


        //获取文件拓展名并转化为content-type
        std::string mime_type=Suffix2Desc(req_str.Suffix());
        //设置报头信息
        SetHeader("Content-Type",mime_type);

        _body = _content;
    }

private:


    std::string Suffix2Desc(const std::string& suffix)
    {
        //使得文件拓展名字符串转化为对应的content-type
        if(suffix==".html")
        {
            return "text/html";
        }
        else if(suffix==".jpg")
        {
            return "application/x-jpg";
        }
        else
        {
            return "text/html";
        }
    }


private:
    // 最终要这4部分,构建应答
    std::string _resp_line;                // 响应行
    std::vector<std::string> _resp_header; // 响应报头
    std::string _blank_line;               // 空行
    std::string _body;                     // 正文

    // 组成响应行的变量:
    std::string _version;     // 版本
    int _status_code;         // 状态码
    std::string _status_desc; // 原因

    std::string _content; // 响应正文,我们从目标URL中提取的数据

    // 响应报头的kv值
    std::unordered_map<std::string, std::string> _header_kv;
};

至此我们对于这个报头的设置就已经完成。


Content-Length

:Body 的长度

该报头告诉了接受方,正文body部分的长度。尤其是在http以tcp来实现的情况下,(TCP是以流的形式),帮助了接收方知道什么时候该停止读取数据,从而判断一个完整的报文应该在什么时候结束。

这个报头代表响应正文的长度,而我们的_content不就是响应正文吗?

所以我们只需要同样的地方获取长度直接设置就行:

void Build(HttpRequest &req_str)
    {
        // 先获取传过来的想要获取的资源的URL
        std::string URL = defaulthomepage + "/" + req_str.Uri();
        // 但注意,此时可能有坑,因为传过来的可能以/结尾,那么结合的URL为:
        //: wwwroot/
        // 此时这个URL无法指明具体的想要获取的资源,所以我们要先进行判断:
        if (URL.back() == '/')
        {
            // 那么我们就默认这个路径下有一个index.html,即该路径的首页(这个板块的首页)
            URL += firstpage;
        }

        // 尝试获取网页内容
        _content = req_str.GetContent(URL);
        if (_content.empty())
        {
            // 用户请求的资源并不存在
            _status_code = 404;
            _content = req_str.GetContent(page404);
        }
        else
        {
            _status_code = 200;
        }

        // 根据状态码甚至文本信息
        _status_desc = Code2Desc(_status_code);

        if (!_content.empty())
        {
            SetHeader("Content-Length", std::to_string(_content.size()));
        }

        //获取文件拓展名并转化为content-type
        std::string mime_type=Suffix2Desc(req_str.Suffix());
        //设置报头信息
        SetHeader("Content-Type",mime_type);

        _body = _content;
    }

Host

:客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上

含义:HTTP/1.1 规范中要求必须提供的头部。它告诉服务器,客户端本次请求的域名和端口号。为什么需要它呢?因为一台服务器可能托管了多个网站(虚拟主机),服务器需要通过 Host 字段来判断客户端到底想访问哪个网站。

示例:当你访问 http://www.example.com:8080/index.html 时,请求头中会包含:Host: www.example.com:8080

我们这里没必要进行Host设置,所以我这里就不进行代码的添加。


User-Agent

: 声明用户的操作系统和浏览器版本信息;

含义:简称 UA,它是一个字符串,描述了发出请求的客户端(通常是浏览器)的操作系统、浏览器类型和版本等信息。网站常用这个字段来做统计(了解用户使用什么设备访问)或做兼容性处理(为不同浏览器返回不同的页面代码)。不过,这个值可以被用户修改(伪装)。

示例:
一个典型的 UA 可能长这样:User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36

这表示:Windows 10 系统,使用的是 Chrome 91 版本的浏览器。

这个是客户端,也就是浏览器应该带的报头,我们也不进行任何的代码添加。


referer

:当前页面是从哪个页面跳转过来的。

我们在网站中浏览时,总会点击一下超链接,从而从一个页面跳转到另外一个页面。比如从A跳转到B,那么此时我们的referer报头就会记录A的信息,从而让你知道上一步是从哪个页面跳转过来的。这也是我们浏览器中有返回上次浏览的页面的功能。

这个我们也不需要在服务端的代码中手动增加,因为这个一般是客户端(通常是浏览器) 在发送请求时自动添加的。

比如服务器给我发送的请求字符串为:

:GET /login.html HTTP/1.1
Host: 82.157.70.111:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.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
Referer: http://82.157.70.111:8080/
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

可以看见,请求字符串会自带这个报头


Location

: 搭配 3xx 状态码使用, 告诉客户端接下来要去哪里访问。这个头部通常出现在响应中,并且几乎总是与 3xx 重定向状态码(如 301, 302)配合使用。它的值是一个 URL,告诉浏览器(客户端):“你要找的东西不在这里了,请自动跳转到这个新地址”。

示例:
状态码:302 Found
响应头:Location: https://www.new-example.com/login

效果:浏览器收到这个响应后,会自动跳转到 https://www.new-example.com/login。

Cookie

: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能。


所以,我们可以写一个新的接口,用来随时向我们的哈希表中插入的新的数据。

void SetHeader(const std::string &k, const std::string &v)//插入新的kv值
    {
        _header_kv[k] = v;
    }

HTTP的方法

在我们的http协议中,我们曾在请求报文中提到过,请求行是由方法+URL+HTTP版本构成的。

那么,这里的方法,具体是指的什么呢?

所谓的方法,其实就是想要告诉响应方,我这次http请求的目的是什么。如果没有方法,我们就只能依靠url路径下的文件内容,去猜。

但是当我们提前说明了我们的目的,比如说GET,它的意义就是告诉请求方,我们这次访问的目的是获取某个资源,我们后面的处理就会往给予资源上面靠。

以下是各种类型的方法:

在这里插入图片描述

其中,我们最常使用的方法是POST与GET,如何给大家演示一下呢?

我们找ai生成了几个html页面,分别是:

index.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>简易商城</title>
    <style>
        body{font-family:Arial,Helvetica,sans-serif;margin:0;padding:0;background:#f5f5f5}
        header{background:#ff6600;color:#fff;padding:15px;text-align:center}
        .search-bar{margin:10px 0}
        .search-bar input{padding:5px;width:200px}
        .search-bar button{padding:5px 10px;background:#333;color:#fff;border:none;cursor:pointer}
        nav{background:#333;color:#fff;padding:10px;text-align:center}
        nav a{color:#fff;margin:0 15px;text-decoration:none}
        .banner{background:#fff;padding:20px;text-align:center}
        .products{display:flex;justify-content:center;gap:20px;padding:20px;flex-wrap:wrap}
        .product{background:#fff;padding:15px;border-radius:5px;width:200px;text-align:center;box-shadow:0 0 5px rgba(0,0,0,0.1)}
        .product img{width:100%;height:150px;background:#ddd;margin-bottom:10px}
        .price{color:#ff6600;font-size:18px;font-weight:bold}
        button{background:#ff6600;color:#fff;border:none;padding:8px 15px;cursor:pointer;border-radius:3px}
        footer{background:#333;color:#fff;text-align:center;padding:10px;margin-top:20px}
        /* 新增样式 */
        .user-actions{float:right;margin-right:20px}
        .user-actions a{color:#fff;text-decoration:none;margin-left:15px}
    </style>
</head>
<body>
    <header>
        <h1>简易商城</h1>
        <div class="search-bar">
            <input type="text" placeholder="搜索商品...">
            <button>搜索</button>
        </div>
    </header>
    
    <nav>
        <a href="#">首页</a>
        <a href="#">数码</a>
        <a href="#">服装</a>
        <a href="#">食品</a>
        <a href="#">图书</a>
        <!-- 新增的注册和登录链接 -->
        <div class="user-actions">
            <a href="/register.html">注册</a>
            <a href="/login.html">登录</a>
        </div>
    </nav>
    
    <div class="banner">
        <h2>今日特惠</h2>
        <p>全场满199减50,满399减120!</p>
    </div>
    
    <div class="products">
        <div class="product">
            <img src="/images/111.jpg" alt="哈基米">
            <h3>哈基米</h3>
            <p class="price">¥99</p>
            <button>立即购买</button>
        </div>
        <div class="product">
            <img src="/images/mcz.jpg" alt="马昌治硅胶娃娃军爷版">
            <h3>马昌治硅胶娃娃军爷版</h3>
            <p class="price">¥99</p>
            <button>立即购买</button>
        </div>
        <div class="product">
            <img src="/images/xiaobaga.jpg" alt="姜晨佳哭泣套餐">
            <h3>姜晨佳哭泣套餐</h3>
            <p class="price">¥128</p>
            <button>立即购买</button>
        </div>
        <div class="product">
            <img src="/images/111.jpg" alt="盗版哈基米">
            <h3>盗版哈基米</h3>
            <p class="price">¥39</p>
            <button>立即购买</button>
        </div>
    </div>
    
    <footer>
        <p>&copy; 2024 简易商城 - 版权所有</p>
    </footer>
</body>
</html>

404.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>404 - 页面找不到了</title>
    <style>
        body{
            margin:0;
            padding:0;
            height:100vh;
            background:#f5f5f5;
            font-family:Arial,Helvetica,sans-serif;
            display:flex;
            justify-content:center;
            align-items:center;
            text-align:center;
            color:#333;
        }
        .box img{
            width:180px;
            height:180px;
            border-radius:50%;
            object-fit:cover;
            margin-bottom:20px;
            box-shadow:0 0 10px rgba(0,0,0,.15);
        }
        .box h1{
            font-size:72px;
            margin:0 0 10px;
            color:#ff6600;
        }
        .box p{
            font-size:18px;
            margin:10px 0 25px;
        }
        .box a{
            display:inline-block;
            padding:10px 25px;
            background:#ff6600;
            color:#fff;
            text-decoration:none;
            border-radius:4px;
            transition:background .3s;
        }
        .box a:hover{
            background:#e55a00;
        }
    </style>
</head>
<body>
    <div class="box">
        <!-- 可任意替换为本地/网络图片 -->
        <img src="images/111.jpg" alt="萌萌的404">
        <h1>404</h1>
        <p>哦豁,页面不小心走丢了~</p>
        <a href="/index.html">返回商城首页</a>
    </div>
</body>
</html>

login.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>简易商城 - 登录</title>
    <style>
        body{font-family:Arial,Helvetica,sans-serif;margin:0;padding:0;background:#f5f5f5;display:flex;justify-content:center;align-items:center;height:100vh}
        .box{background:#fff;padding:30px 40px;border-radius:6px;box-shadow:0 0 10px rgba(0,0,0,.1);width:300px;text-align:center}
        .box h2{margin-bottom:20px;color:#333}
        .box input{width:100%;padding:10px;margin:8px 0;border:1px solid #ccc;border-radius:4px;box-sizing:border-box}
        .box button{width:100%;padding:10px;margin-top:10px;background:#ff6600;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:16px}
        .box button:hover{background:#e55a00}
        .box a{color:#ff6600;text-decoration:none;font-size:14px}
    </style>
</head>
<body>
    <div class="box">
        <h2>用户登录</h2>
        <form action="#" method="post">
            <input type="text" name="username" placeholder="用户名/邮箱" required>
            <input type="password" name="pwd" placeholder="密码" required>
            <button type="submit">登 录</button>
        </form>
        <p style="margin-top:15px;font-size:14px">还没有账号?<a href="register.html">立即注册</a></p>
        <a href="/index.html">回到首页</a>
    </div>
</body>
</html>

register.html:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>简易商城 - 注册</title>
    <style>
        body{font-family:Arial,Helvetica,sans-serif;margin:0;padding:0;background:#f5f5f5;display:flex;justify-content:center;align-items:center;height:100vh}
        .box{background:#fff;padding:30px 40px;border-radius:6px;box-shadow:0 0 10px rgba(0,0,0,.1);width:300px;text-align:center}
        .box h2{margin-bottom:20px;color:#333}
        .box input{width:100%;padding:10px;margin:8px 0;border:1px solid #ccc;border-radius:4px;box-sizing:border-box}
        .box button{width:100%;padding:10px;margin-top:10px;background:#ff6600;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:16px}
        .box button:hover{background:#e55a00}
        .box a{color:#ff6600;text-decoration:none;font-size:14px}
    </style>
</head>
<body>
    <div class="box">
        <h2>用户注册</h2>
        <form action="#" method="post">
            <input type="text" name="username" placeholder="用户名" required>
            <input type="email" name="email" placeholder="邮箱" required>
            <input type="password" name="pwd" placeholder="密码(≥6位)" minlength="6" required>
            <input type="password" name="repwd" placeholder="确认密码" minlength="6" required>
            <button type="submit">立即注册</button>
        </form>
        <p style="margin-top:15px;font-size:14px">已有账号?<a href="login.html">去登录</a></p>
        <a href="/index.html">回到首页</a>
    </div>
</body>
</html>

通过之前的学习我们可以知道,我们在浏览器中访问这个IP地址与端口,就是发送了一个请求,随后我们运行的服务端会接收并分析请求,给服务器发送响应报文。浏览器通过对响应报文的解析,形成了我们看到的页面。

我们的请求中,URL代表着请求的资源的位置,我们可以通过文件拓展名来知道所获取的资源的类型。这些都是说过的。比如我们默认的就是给其首页的html内容,但是这个首页中有这样的代码:

 <img src="/images/111.jpg" alt="盗版哈基米">

这个前端代码代表这个html内容不只是html的文本,还有着jpg图片,所以浏览器还会紧接着去申请这个/images/111.jpg路径下的jpg图片资源。

所以我们在加载首页的时候,他是一连串的申请了多个资源的:

 http_request:GET /index.html HTTP/1.1
Host: 82.157.70.111:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.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
Referer: http://82.157.70.111:8080/register.html
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


[2025-09-11 16:50:53] [DEBUG] [613473] [HttpServer.hpp] [40] - 用户想要:/index.html
[2025-09-11 16:50:53] [DEBUG] [613473] [HttpProtocol.hpp] [114] - content length: 3182
[2025-09-11 16:50:53] [DEBUG] [602367] [TcpServer.hpp] [36] - get a new client, info is: 120.194.222.21:12144
[2025-09-11 16:50:53] [DEBUG] [613475] [HttpServer.hpp] [29] - HttpServer: get a new client: 4 addr info: 120.194.222.21:12144
[2025-09-11 16:50:54] [DEBUG] [613475] [HttpServer.hpp] [35] - http_request:GET /images/111.jpg HTTP/1.1
Host: 82.157.70.111:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://82.157.70.111:8080/index.html
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


[2025-09-11 16:50:54] [DEBUG] [613475] [HttpServer.hpp] [40] - 用户想要:/images/111.jpg
[2025-09-11 16:50:54] [DEBUG] [613475] [HttpProtocol.hpp] [114] - content length: 150324
[2025-09-11 16:50:54] [DEBUG] [602367] [TcpServer.hpp] [36] - get a new client, info is: 120.194.222.21:12144
[2025-09-11 16:50:54] [DEBUG] [613478] [HttpServer.hpp] [29] - HttpServer: get a new client: 4 addr info: 120.194.222.21:12144
[2025-09-11 16:50:54] [DEBUG] [613478] [HttpServer.hpp] [35] - http_request:GET /images/mcz.jpg HTTP/1.1
Host: 82.157.70.111:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://82.157.70.111:8080/index.html
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


[2025-09-11 16:50:54] [DEBUG] [613478] [HttpServer.hpp] [40] - 用户想要:/images/mcz.jpg
[2025-09-11 16:50:54] [DEBUG] [613478] [HttpProtocol.hpp] [114] - content length: 330884
[2025-09-11 16:50:54] [DEBUG] [602367] [TcpServer.hpp] [36] - get a new client, info is: 120.194.222.21:12144
[2025-09-11 16:50:54] [DEBUG] [613480] [HttpServer.hpp] [29] - HttpServer: get a new client: 4 addr info: 120.194.222.21:12144
[2025-09-11 16:50:54] [DEBUG] [613480] [HttpServer.hpp] [35] - http_request:GET /images/xiaobaga.jpg HTTP/1.1
Host: 82.157.70.111:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://82.157.70.111:8080/index.html
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


[2025-09-11 16:50:54] [DEBUG] [613480] [HttpServer.hpp] [40] - 用户想要:/images/xiaobaga.jpg
[2025-09-11 16:50:54] [DEBUG] [613480] [HttpProtocol.hpp] [114] - content length: 188233

以上这些内容就可以正式引入我们的请求方法了。

大家请看这个代码:

 <form action="#" method="post">
            <input type="text" name="username" placeholder="用户名/邮箱" required>
            <input type="password" name="pwd" placeholder="密码" required>
            <button type="submit">登 录</button>

这个是我们的login.html代码里的登录模块,让我为大家进行介绍:

这段代码定义了一个用户登录表单。它包含了用户名输入框、密码输入框和一个提交按钮。用户填写信息后点击按钮,数据就会被发送到服务器进行处理。

action="#"表示我们的服务器会把这个数据送到这个#URL地址进行处理。

而method="post"就是我们即将着重介绍内容,HTTP的请求方法。接下来我为大家做两个实验,请大家看一下这两种请求方法造成的结果有什么不一样。

首先,我们用POST方法,进行登录:
在这里插入图片描述

我们的密码是123456啊,接下来我们点击一下登录,再查看一下日志信息,我们会发现,浏览器发送过来的请求,正文部分多了这样的内容:

POST /login.html HTTP/1.1
Host: 82.157.70.111:8080
Connection: keep-alive
Content-Length: 28
Cache-Control: max-age=0
Origin: http://82.157.70.111:8080
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.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
Referer: http://82.157.70.111:8080/login.html
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

username=zhangsan&pwd=123456

接下来,我们把登录的html中的method改成GET,再试试:

在这里插入图片描述

注意,此时我们的方法已经变成了get。

我们再点击登录,查看日志:

GET /images/111.jpg HTTP/1.1
Host: 82.157.70.111:8080
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://82.157.70.111:8080/login.html?username=zhangsan&pwd=654321
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


正文部分就不再携带我们的密码与账号信息了,但是大家可以看我们此时所处页面的URL地址,你会发现我们的地址惊奇的变成了:

http://82.157.70.111:8080/login.html?username=zhangsan&pwd=654321#

所以我们可以大胆猜测总结了,POST传参通过body,get传参通过url。

为了防止我们的一些信息暴露在明面上,所以一般我们使用get来获取静态的网页资源,所以我们加载404页面的时候,日志发现请求方法为get。为了私密性,post通常用来上传数据。

所以,我们又要对我们的代码进行处理了,为什么呢?因为我们的URL在post方法下,可能并不是如我们所愿的格式,比如我们在进行bing搜索的时候,它的网页的URL并不是规整的:

https://cn.bing.com/#

“#”代表着一个文件。

而是这样的形式:
https://cn.bing.com/search?q=%E6%B8%85%E5%8D%8E%E5%A4%A7%E5%AD%A6&cvid=c2bc9bbcbbcd484492ecf59d7a6a102f&gs_lcrp=EgRlZGdlKgYIABBFGDkyBggAEEUYOTIGCAEQABhAMgYIAhAuGEAyBggDEAAYQDIGCAQQABhAMgYIBRAAGEAyBggGEAAYQDIGCAcQABhAMgYICBAAGEDSAQgxMTc0ajBqNKgCCLACAQ&FORM=ANAB01&adppc=EDGEINJP&PC=EDGEINJP

后面加了许多的参数,我们进行的bing的搜索功能,也就是search,?隔开的后面部分的内容都是必要的参数。

也就是我们需要在自己的代码中进行方法的判断,以及对其URL进行正确的裁剪工作,否则在build的时候尝试获取正文内容时,我们通过URL判断会一直出错,因为我们的url就是错的!!!

而带有参数的请求,一般都是交互式的,什么是交互式呢?就是他并不是主要申请一个页面资源,而是会把运行逻辑移到我们的后端的处理中去。怎么说呢?就是我们这里的登录功能,就是一个交互的请求。

因为他会提供用户名与密码,随后在数据库中检测是否为正确的用户数据,最后完成登录的这个功能。而这个登录是我自己实现的一个函数,并不是html。

于此类似的操作还有很多,比如注册等。

功能路由

所以,我们需要一个操作集,就是用一个哈希表存储分钟具体的操作方法:

1、规定操作类型:

//创建操作类型(比如注册,登录操作,会传递参数)
using http_handler_t=std::function<void(HttpRequest&,HttpResponse&)>;

2、创建操作集,一个哈希表,充当一个功能路由的功能:

#pragma once

#include "TcpServer.hpp"
#include "Common.hpp"
#include "HttpProtocol.hpp"


//创建操作类型(比如注册,登录操作,会传递参数)
using http_handler_t=std::function<void(HttpRequest&,HttpResponse&)>;

class HttpServer
{
public:
    HttpServer(int port)
        : _tsvr(std::make_unique<TcpServer>(port))
    {
    }
    ~HttpServer() {}
	.......
private:
	......

    std::unordered_map<std::string,http_handler_t> _route;//充当一个功能路由
};

3、给请求结构体提供接口,方便获取

要实现这一步,首先我们需要完善之前没有处理好的反序列化操作:

 bool Deserialize(std::string &req_str)
    {
        if (ParseOneLine(req_str, &_req_line, Sep))
        {
            ParseReqLine(_req_line); // 对请求行进行处理
            // 其他处理.....
            ParseHeader(req_str); // 对请求报头进行处理

            // 剩下的就是请求正文:
            _body = req_str;

            //...后面我们等会在完善
        }
        return true;
    }

在反序列化中,我们初步给正文提供初始化后,后面还需要进行处理,包括判断请求方法的类型。(因为我们的反序列化是在创建响应之前的,所以这一步才是我们真正应该处理字符串的战场)

这里我们需要给请求结构体提供两个新的参数_args与_path,存储分离好的路径与参数:

// 当url格式为http://82.157.70.111:8080/login.html?username=zhangsan&pwd=654321#时,将其划分为_path与_args
    std::string _args; //?username=zhangsan&pwd=654321  或者用来存储body里的参数
    std::string _path; // 82.157.70.111:8080/login.html

首先就是进行if判断,根据情况不同执行对应的处理方法:

bool Deserialize(std::string &req_str)
    {
        if (ParseOneLine(req_str, &_req_line, Sep))
        {
            ParseReqLine(_req_line); // 对请求行进行处理
            // 其他处理.....
            ParseHeader(req_str); // 对请求报头进行处理

            // 剩下的就是请求正文:
            _body = req_str;

            if (_method == "POST")
            {
                _isexec = true;
                // POST方法,我们的参数都在body中
                _args = _body;
                _path = _uri;
                LOG(LogLevel::DEBUG) << "_path:" << _path;
                LOG(LogLevel::DEBUG) << "_args:" << _args;
            }
            else if (_method == "GET") // GET
            {
                // 我们要进行分离,通过?
                auto pos = _uri.find("?");
                if (pos != std::string::npos)
                {
                    _isexec = true;
                    // 说明参数在Uri上,分离:82.157.70.111:8080/login.html?username=zhangsan&pwd=654321
                    _path = _uri.substr(0, pos);
                    _args = _uri.substr(pos + 1);
                    LOG(LogLevel::DEBUG) << "_path:" << _path;
                    LOG(LogLevel::DEBUG) << "_args:" << _args;
                }
            }
        }
        return true;
    }

在请求结构体中设置返回path与args的接口:

	std::string Path()
    {
        return _path;
    }

    std::string Args()
    {
        return _args;
    }

4、完善我们HandlerHttpRequest函数的判断

	 if(req.IsHasArgs())//是否有参数
        {
            LOG(LogLevel::DEBUG)<<"处理带参URL";
            std::string service=req.Path();

            _route[service](req,resp);
        }
        else//没有就是正常的申请静态资源直接进入build逻辑就行了
        {
            LOG(LogLevel::DEBUG)<<"申请静态资源";
            resp.Build(req); // 必须调用Build来设置内容和状态码
        }

这里的IsHasArgs的实现很简单,我们只需要根据反序列化的时候对isexec参数的设置,就可以知道是否带参,所以返回_isexec就行。

bool IsHasArgs()
    {
        return _isexec;
    }

5、进行安全检查

但是我们的service内容不一定存在,所以我们要先检查一下:

	bool SafeCheck(const std::string&service)
    {
        auto it=_route.find(service);

        return it != _route.end();
    }
    bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
    {
        // 我们处理http的入口,在这里我们就可以实现我们的处理方法了
        LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();

        std::string http_request; // 用来存储接收请求信息
        sockfd->Recv(&http_request);
        LOG(LogLevel::DEBUG) << "http_request:" << http_request;
        HttpRequest req;
        req.Deserialize(http_request); // 进行反序列化

        LOG(LogLevel::DEBUG) << "用户想要:" << req.Uri(); // 我们这里封装了一个Uri接口,就是单纯的返回_uri类成员变量

        // 随后我们需要对用户想要的URL的内容进行处理,包括判断URL的文件是否存在,如果存在,就读取内容
        // 构建响应
        HttpResponse resp;

        // 这里要注意了,由于我们的http方法不同,请求会被分为两类:
        //  1、 请求一般的静态资源(比如get)  2、 提交参数,携带参数的url申请,需要我们进行交互设置
        if (req.IsHasArgs()) // 是否有参数
        {
            LOG(LogLevel::DEBUG) << "处理带参URL";
            std::string service = req.Path();
            if (SafeCheck(service))//进行一个安全检查
            {
                _route[service](req, resp);
            }
            else
            {
                resp.Build(req);
            }
        }
        else // 没有就是正常的申请静态资源直接进入build逻辑就行了
        {
            LOG(LogLevel::DEBUG) << "申请静态资源";
            resp.Build(req); // 必须调用Build来设置内容和状态码
        }

        std::string resp_str; // 这是一个空字符串,我们需要通过响应结构体的序列化,进入响应结构体中带出我们的响应回复字符串

        // 进行序列化
        resp.Serialize(&resp_str);

        // LOG(LogLevel::DEBUG) << "resp_str:" << resp_str;
        sockfd->Send(resp_str);
        return true;
    }

6、理解功能路由

我们这里的哈希表里存储的是什么呢?其实不是之前的什么login.html这种前端代码,而是我们再点击登录之后,浏览器会给服务端发送一个请求:


<form action="/login" method="get">
        <input type="text" name="username" placeholder="用户名/邮箱" required>
        <input type="password" name="pwd" placeholder="密码" required>
        <button type="submit">登 录</button>
</form>

当我们点击登录,请求里是这样的:

/login?username=zhangsan&pwd=123456

这个请求的目标资源就是这里记录的/login,也就是这里写的action的内容。随后使用?分隔开,username=zhangsan&pwd=123456部分就是携带的参数。

根据我们刚刚写的代码可以知道,这种请求会让我们的逻辑进入功能路由中执行名字为login的函数调用。

我们的安全检查的作用就是来检测功能路由中是否有对应名字的功能。
比如我这里需要login,但是此时哈希表里什么都没有,所以我们需要一个register函数(在http结构体中),用来给我们的功能路由的哈希表进行插入:

void Resgiter(std::string funcname,http_handler_t func)
    {
        _route[funcname]=func;
    }

其实功能的各种实现我们可以新开一个头文件进行编写,但是我们这里为了简便,就直接在.cc进行简单的函数实现

登录逻辑的补充

为了方便,我们直接在main函数旁边初始化我们的login函数。

那么我们要写出什么样的效果呢?

注意,这个函数的功能是对我们build的一个替代,所以我们的肯定是要完成build的效果,并根据传进来的参数来达成效果,并返回一个响应。

以我们这里的登录操作来看,我们主要实现有三步:
1、解析参数格式,得到我们想要的参数
2、访问数据库,验证对应的用户是否是合法的用户…
3、登录成功,创建响应字符串有关内容

void login(HttpRequest&req,HttpResponse&resp)
{
    LOG(LogLevel::DEBUG)<<"进入登录功能逻辑";
    //1、解析参数格式

    //2、fangwenshujuku1

    //3、登陆成功

    //第一种,跳转到专门的登录成功页面

    std::string body=req.GetContent("wwwroot/success.html");
    resp.SetCode(200);
    resp.SetHeader("Content_Length",std::to_string(body.size()));
    resp.SetHeader("Content_Type","text/html");
    resp.SetBody(body);

    //第二种,通过重定向跳转到首页,浏览器会解析响应报头作出对应的处理。
    //resp.SetCode(302);
    //resp.SetHeader("Location","/");
}

至此,我们继续随便输入一个账号和密码,点击登录,就会执行我们的登录功能的代码。

这就是我们的一个http的实现。

附主要代码(套接字封装与应用层协议http · 时光拾缀思念/小项目总集 - 码云 - 开源中国)