[HTTP协议]应用层协议HTTP从入门到深刻理解并落地部署自己的云服务(2)实操部署

发布于:2025-03-09 ⋅ 阅读:(17) ⋅ 点赞:(0)

标题:[HTTP协议]应用层协议HTTP从入门到深刻理解并落地部署自己的云服务(2)实操部署
@水墨不写bug

在这里插入图片描述



本文将解释并实现一个HTTP服务器,从实现原理到细节分析都有讲解。
分模块化设计让代码逻辑清晰,易维护;

一、无法拷贝类(class uncopyable)的设计

为了驳回编译器暗自提供的功能,我们需要把相应的成员函数声明为private,并且不予实现。如果直接调用,报出编译错误;如果在其他成员函数/友元函数中调用,报出链接错误。

[援引自《Effective C++》by Scott Meyers 条款6:若不想使用编译器自动生成的函数,就该明确拒绝]。

于是,我们就需要设计出如下的不可拷贝类,来拒绝编译器的某些机能:

#pragma once

// 继承这个类的任何类都无法实现拷贝操作
class uncopyable
{
protected:
    uncopyable()
    {
    }
    ~uncopyable()
    {
    }

private:
    uncopyable(const uncopyable &) = delete;
    uncopyable &operator=(const uncopyable &) = delete;
};

这个代码设计实现了一个不可拷贝(uncopyable)类。继承这个类的任何类都无法进行拷贝操作。

解释:

  1. 类声明:

    • class uncopyable 是一个基类,设计用于防止派生类对象进行拷贝操作。
  2. 构造函数和析构函数:

    • protected 访问控制符使得构造函数和析构函数只能在派生类中访问。这意味着这个类不能被直接实例化,只能被继承。
    • uncopyable() 是默认构造函数。
    • ~uncopyable() 是析构函数。
  3. 删除拷贝构造函数和拷贝赋值运算符:

    • uncopyable(const uncopyable &) = delete; 删除了拷贝构造函数,防止对象通过拷贝构造函数进行拷贝。
    • uncopyable &operator=(const uncopyable &) = delete; 删除了拷贝赋值运算符,防止对象通过赋值操作进行拷贝。

重要思想:

  • 防止拷贝: 通过删除拷贝构造函数和拷贝赋值运算符,任何派生自 uncopyable 类的对象都无法进行拷贝。

  • 继承机制: 使用 protected 访问控制符,使得 uncopyable 类不能直接实例化,但可以被其他类继承。这种设计模式经常被称为“不可拷贝基类”(Non-Copyable Base Class)模式。

使用示例

class exClass : private uncopyable
{
public:
    exClass () {}
    // 其他成员函数
};

int main()
{
    exClass obj1;
    // exClass obj2 = obj1; // 错误:拷贝构造函数被删除
    // exClass obj3;
    // obj3 = obj1; // 错误:拷贝赋值运算符被删除
    return 0;
}

在这个示例中,exClass 继承自 uncopyable,因此 exClass 的对象不能被拷贝或赋值。这确保了 exClass 对象的独占性和唯一性。

我们设计这个类的最终目的就是防止服务器对象被拷贝。

二、锁的RAII设计

#pragma once
#include <pthread.h>

// 使用锁需要频繁调用系统调用,十分麻烦
// 于是实现锁的RAII设计
class LockGuard
{
private:
    pthread_mutex_t *GetMutex() { return _mutex; }

public:
    // 构造函数,接收一个pthread_mutex_t类型的指针,并在构造函数中加锁
    LockGuard(pthread_mutex_t* mutex)
        : _mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }

    // 析构函数,在对象销毁时解锁
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }

private:
    // 指向pthread_mutex_t类型的指针
    pthread_mutex_t *_mutex;
};

/*
 *之前的理解错误了:不需要init和destroy,因为锁本身就是存在的!
 *锁在一般情况下会内置在类的内部,需要使用(加锁)的时候,把锁的地址传进来就行了
 *在构造函数内加锁,析构函数内解锁。
 *e.g.
 while(ture)
 {
    LockGuard lockguard(td->_mutex);
    if(tickets > 0)
    {
        // 抢票
    }
    else
    {
        break;
    }
 }
 *while内每一次进行循环,都需要创建一个新的锁,创建即加锁,if代码块结束即为解锁
 */

这段代码实现了一个基于 RAII(Resource Acquisition Is Initialization)模式的锁保护类 LockGuard,用于简化多线程编程中的锁操作。

解释

  1. 类声明:

    • class LockGuard 是一个封装了 pthread_mutex_t 锁操作的类,使用 RAII 模式来管理锁的生命周期。
  2. 构造函数:

    • LockGuard(pthread_mutex_t* mutex) 是构造函数,接收一个 pthread_mutex_t* 类型的指针,并在构造函数中调用 pthread_mutex_lock 来加锁。这确保了在创建 LockGuard 对象时,锁自动被加上。
  3. 析构函数:

    • ~LockGuard() 是析构函数,在对象销毁时调用 pthread_mutex_unlock 来解锁。这确保了当 LockGuard 对象的生命周期结束时,锁自动被释放。
  4. 私有成员变量:

    • pthread_mutex_t *_mutex 是一个指向 pthread_mutex_t 类型的指针,用于存储传入的锁对象。

重要考虑

  • RAII 模式: RAII(Resource Acquisition Is Initialization)是一种管理资源的编程技巧,它将资源的获取和释放绑定到对象的生命周期中。在 LockGuard 中,构造函数获取锁(加锁),析构函数释放锁(解锁),这确保了锁的正确管理和避免忘记解锁的错误。

  • 简化锁操作: 传统的pthread库的锁操作需要显式调用 pthread_mutex_lockpthread_mutex_unlock,这不仅繁琐而且容易出错。通过 LockGuard 的封装,用户只需在需要加锁的代码块中创建一个 LockGuard 对象,锁操作将自动管理。

使用示例

class GetTicket
{
public:
    GetTicket() : tickets(100)
    {
        pthread_mutex_init(&mutex, nullptr);
    }

    ~GetTicket()
    {
        pthread_mutex_destroy(&mutex);//传入要管理的锁的地址
    }

    void Tickets()
    {
        while (true)
        {
            LockGuard lockguard(&mutex);
            if (tickets > 0)
            {
                // 抢票
                --tickets;
            }
            else
                break;
        }
    }
private:
    int tickets;
    pthread_mutex_t mutex;
};

在这个示例中,GetTicket 类管理票的分发。LockGuard 确保在 Tickets 方法中,tickets 自减的操作是线程安全的。每次循环迭代都会创建一个新的 LockGuard 对象,自动加锁和解锁,确保代码块内的操作是互斥的。

这个模块的设计是为了简化pthread库的对锁的管理操作,从而确保线程间的互斥关系。

三、基于RAII模式和互斥锁的的日志系统设计

#pragma once
#include <string>
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include "LockGuard.hpp"

namespace log_ddsm
{
    // 日志信息管理
    enum LEVEL
    {
        DEBUG = 1,   // 调试信息
        INFO = 2,    // 提示信息
        WARNING = 3, // 警告
        ERROR = 4,   // 错误,但是不影响服务正常运行
        FATAL = 5,   // 致命错误,服务无法正常运行
    };
    enum 
    {
        TIMESIZE = 128,
        LOGSIZE = 1024,
        FILE_TYPE_LOG_SIZE = 2048
    };
    enum 
    {
        SCREEN_TYPE = 8,
        FILE_TYPE = 16
    };

    // 默认日志文件名称
    const char *DEFAULT_LOG_NAME = "./log.txt";
    // 全局锁,保护打印日志
    pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;

    // 结构体,用于存储日志信息
    struct logMessage
    {
        std::string _level; // 日志等级
        int _level_num;     // 日志等级的int格式

        pid_t _pid;             // 这条日志的进程id
        std::string _file_name; // 文件名
        int _file_number;       // 行号
        std::string _cur_time;  // 当时的时间
        std::string _log_info;  // 日志正文
    };

    // 通过int获取日志等级
    std::string getLevel(int level)
    {
        switch (level)
        {
        case 1:
            return "DEBUG";
        case 2:
            return "INFO";
        case 3:
            return "WARNING";
        case 4:
            return "ERROR";
        case 5:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    // 获取当前时间的字符串表示
    std::string getCurTime()
    {
        time_t cur = time(nullptr);
        struct tm *curtime = localtime(&cur);
        char buf[TIMESIZE] = {0};
        snprintf(buf, sizeof(buf), "%d-%d-%d %d:%d:%d",
                 curtime->tm_year + 1900,
                 curtime->tm_mon + 1,
                 curtime->tm_mday,
                 curtime->tm_hour,
                 curtime->tm_min,
                 curtime->tm_sec);
        return buf;
    }

    // 日志类,用于管理日志的打印
    class Log
    {
    private:
        // 刷新日志信息,根据打印类型选择打印到屏幕或文件
        void FlushMessage(const logMessage &lg)
        {
            // 互斥锁,保护Print过程
            LockGuard lockguard(&gmutex);

            // 过滤逻辑
            // 致命错误到文件逻辑
            if (lg._level_num >= _ignore_level)
            {
                if (_print_type == SCREEN_TYPE)
                    PrintToScreen(lg);
                else if (_print_type == FILE_TYPE)
                    PrintToFile(lg);
                else
                    std::cerr << __FILE__ << " " << __LINE__ << ":" << " UNKNOWN_TYPE " << std::endl;
            }
        }

        // 打印日志信息到屏幕
        void PrintToScreen(const logMessage &lg)
        {
            printf("[%s][%d][%s][%d][%s] %s", lg._level.c_str(),
                   lg._pid,
                   lg._file_name.c_str(),
                   lg._file_number,
                   lg._cur_time.c_str(),
                   lg._log_info.c_str());
        }

        // 打印日志信息到文件
        void PrintToFile(const logMessage &lg)
        {
            std::ofstream out(_log_file_name, std::ios::app); // 追加打印
            if (!out.is_open())
            {
                std::cerr << __FILE__ << " " << __LINE__ << ":" << " LOG_FILE_OPEN fail " << std::endl;
                return;
            }
            char log_txt[FILE_TYPE_LOG_SIZE] = {0}; // 缓冲区
            snprintf(log_txt, sizeof(log_txt), "[%s][%d][%s][%d][%s] %s", lg._level.c_str(),
                     lg._pid,
                     lg._file_name.c_str(),
                     lg._file_number,
                     lg._cur_time.c_str(),
                     lg._log_info.c_str());

            out.write(log_txt, strlen(log_txt)); // 写入文件
        }

    public:
        // 构造函数,初始化打印方式和日志文件名称
        Log(int print_type = SCREEN_TYPE)
            : _print_type(print_type), _log_file_name(DEFAULT_LOG_NAME), _ignore_level(DEBUG)
        {
        }

        // 设置日志打印方式
        void Enable(int type)
        {
            _print_type = type;
        }

        /// @brief 加载日志信息,并根据打印方式进行打印
        /// @param format 格式化输出
        /// @param  可变参数
        /*区分了本层和上层之后,就容易设置参数了*/
        void load_message(int level, std::string filename, int filenumber, const char *format, ...)
        {
            logMessage lg;
            lg._level = getLevel(level);
            lg._level_num = level;

            lg._pid = getpid();
            lg._file_name = filename;
            lg._file_number = filenumber;
            lg._cur_time = getCurTime();

            // valist + vsnprintf 处理可变参数
            va_list ap;
            va_start(ap, format);
            char log_info[LOGSIZE] = {0};
            vsnprintf(log_info, sizeof(log_info), format, ap);
            va_end(ap);
            lg._log_info = log_info;

            // 打印逻辑
            FlushMessage(lg);
        }

        /// @brief 设置忽略的日志等级
        /// @param ignorelevel 忽略的日志等级
        /* DEBUG = 1,   调试信息
         *INFO = 2,     提示信息
         *WARNING = 3,  警告
         *ERROR = 4,    错误,但是不影响服务正常运行
         *FATAL = 5,    致命错误,服务无法正常运行 */
        void SetIgnoreLevel(int ignorelevel)
        {
            _ignore_level = ignorelevel;
        }

        /// @brief 设置日志文件名称
        /// @param newlogfilename 新的日志文件名称
        void SetLogFileName(const char *newlogfilename)
        {
            _log_file_name = newlogfilename;
        }

        ~Log() {}

    private:
        int _print_type;          // 打印类型
        std::string _log_file_name; // 日志文件名称
        int _ignore_level;        // 忽略的日志等级
    };

    // 全局的Log对象
    Log lg;

    // 使用日志的一般格式
#define LOG(Level, Format, ...)                                            \
    do                                                                     \
    {                                                                      \
        lg.load_message(Level, __FILE__, __LINE__, Format, ##__VA_ARGS__); \
    } while (0)

    /// 无法设计为inline——__VA_ARGS只能出现在宏替换中
    // inline void LOG(int level,const char* format, ...)
    // {
    //     lg.load_message(level,__FILE__,__LINE__,format,__VA_ARGS__);
    // }

    // 往文件打印
#define ENABLE_FILE()         \
    do                        \
    {                         \
        lg.Enable(FILE_TYPE); \
    } while (0)

    // 往显示器打印
#define ENABLE_SCREEN()         \
    do                          \
    {                           \
        lg.Enable(SCREEN_TYPE); \
    } while (0)

    // 设置日志忽略等级
#define SET_IGNORE_LEVEL(Level)  \
    do                           \
    {                            \
        lg.SetIgnoreLevel(Level); \
    } while (0)

    // 设置日志文件名称
#define SET_LOG_FILENAME(Name)  \
    do                          \
    {                           \
        lg.SetLogFileName(Name); \
    } while (0)

}; // namespace log_ddsm

以上逻辑设计了一个日志管理系统,提供了日志的多种输出方式(如屏幕输出和文件输出),并使用 RAII(Resource Acquisition Is Initialization)模式和互斥锁保护日志打印过程。

解释

  1. 日志级别管理:

    • enum LEVEL 定义了不同的日志级别,从 DEBUGFATAL,用于标识日志的重要性。在不同的场景下,需要输出的日志等级是不同的。比如在测试(Debug)阶段,最好输出全部日志信息;而在发行(Release)之后,只需要输出Warning以上级别的日志信息即可。
  2. 日志信息结构体:

    • struct logMessage 包含了日志的详细信息,包括日志级别、进程ID、文件名、行号、当前时间和日志内容。
  3. 获取日志级别字符串:

    • getLevel(int level) 函数根据日志级别的整数值返回相应的字符串表示。
  4. 获取当前时间:

    • getCurTime() 函数获取当前的系统时间,并格式化为字符串。
  5. 日志类 Log

    • Log 类负责管理日志的打印。它包含了打印到屏幕和打印到文件的功能,并通过 RAII 模式和互斥锁保护打印过程。
    • FlushMessage(const logMessage &lg) 函数根据日志级别和打印类型选择合适的打印方式。
    • PrintToScreen(const logMessage &lg) 函数将日志打印到屏幕。
    • PrintToFile(const logMessage &lg) 函数将日志打印到文件。
    • load_message(int level, std::string filename, int filenumber, const char *format, ...) 函数加载日志信息,并调用 FlushMessage 进行打印。
    • SetIgnoreLevel(int ignorelevel) 函数设置忽略的日志级别。
    • SetLogFileName(const char *newlogfilename) 函数设置日志文件名称。
  6. 宏定义:

    • LOG(Level, Format, ...) 宏用于简化日志的打印调用,自动获取文件名和行号,并调用 load_message 函数。
    • ENABLE_FILE() 宏用于设置日志打印到文件。
    • ENABLE_SCREEN() 宏用于设置日志打印到屏幕。
    • SET_IGNORE_LEVEL(Level) 宏用于设置忽略的日志级别。
    • SET_LOG_FILENAME(Name) 宏用于设置日志文件名称。

使用示例

#include "log_ddsm.hpp"

int main()
{
    // 设置日志打印到文件
    ENABLE_FILE();
    // 设置日志文件名称
    SET_LOG_FILENAME("my_log.txt");
    // 设置忽略等级为INFO,低于INFO的日志不会打印
    SET_IGNORE_LEVEL(log_ddsm::INFO);

    // 打印日志
    LOG(log_ddsm::DEBUG, "This is a debug message\n");
    LOG(log_ddsm::INFO, "This is an info message\n");
    LOG(log_ddsm::WARNING, "This is a warning message\n");
    LOG(log_ddsm::ERROR, "This is an error message\n");
    LOG(log_ddsm::FATAL, "This is a fatal message\n");

    return 0;
}

在这个示例中,我们设置日志打印到文件 my_log.txt,并设置忽略级别为 INFO。然后,我们打印不同级别的日志信息。
在my_log.txt中的打印结果:

[INFO][3089][test.cc][17][2025-3-8 13:51:14] This is an info message
[WARNING][3089][test.cc][18][2025-3-8 13:51:14] This is a warning message
[ERROR][3089][test.cc][19][2025-3-8 13:51:14] This is an error message
[FATAL][3089][test.cc][20][2025-3-8 13:51:14] This is a fatal message


四、网络地址信息的封装管理

#pragma once

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

class Inet
{
private:
    // 将 sockaddr_in 转换为主机序列并提取 IP 和端口
    void ConvertToHost(const sockaddr_in &addr)
    {
        // 使用 inet_ntop 将网络序列 IP 地址转换为点分十进制字符串形式
        char ip_buf[32] = {0};
        inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));
        _ip = ip_buf;

        // 将网络字节序的端口号转换为主机字节序
        _port = ntohs(addr.sin_port);
    }

    // 设置唯一名称
    void SetUname()
    {
        _uname = _ip;
        _uname += "  ";
        _uname += std::to_string(_port);
    }

public:
    // 默认构造函数
    Inet() {}

    // 通过 sockaddr_in 构造 Inet 类
    Inet(sockaddr_in addr)
        : _addr(addr)
    {
        ConvertToHost(_addr);
        SetUname();
    }

    // 重载等于操作符
    bool operator==(const Inet &inet)
    {
        return (this->_ip == inet._ip && this->_port == inet._port);
    }

    // 获取 sockaddr_in 结构体
    struct sockaddr_in ADDR()
    {
        return _addr;
    }

    // 获取字符串 IP 地址
    std::string IP()
    {
        return _ip;
    }

    // 获取端口号
    uint16_t PORT()
    {
        return _port;
    }

    // 获取唯一名称
    std::string UniqueName()
    {
        return _uname;
    }

    // 获取唯一名称(const 版本)
    const std::string UniqueName() const
    {
        return _uname;
    }

    // 析构函数
    ~Inet() {}

private:
    std::string _ip;    // 字符串 IP 地址
    uint16_t _port;     // 端口号
    sockaddr_in _addr;  // 地址结构体
    std::string _uname; // 唯一名称
};

以上代码实现了一个 Inet 类,用于封装和管理网络地址信息。

解释

  1. 封装网络地址信息:

    • Inet 类封装了 IP 地址和端口号的信息,并提供了一些便捷的方法来获取这些信息。通过这种封装,可以更加方便地管理和操作网络地址。
  2. 地址转换:

    • ConvertToHost 方法将 sockaddr_in 结构体中的网络字节序 IP 地址和端口号转换为主机字节序,并将 IP 地址转换为字符串形式。因为网络传输使用网络字节序,而主机通常使用主机字节序。
  3. 唯一名称:

    • SetUname 方法将 IP 地址和端口号组合成一个唯一名称 _uname,用于标识一个唯一的网络地址。
  4. 操作符重载:

    • 重载了 operator== 操作符,用于比较两个 Inet 对象是否相等。两个 Inet 对象相等的条件是它们的 IP 地址和端口号都相等。
  5. 获取方法:

    • 提供了多个获取方法,如 ADDR()IP()PORT()UniqueName(),用于获取 Inet 对象的不同属性。这些方法使得 Inet 类的使用更加便捷。

使用示例

#include "Inet.hpp"
#include <iostream>

int main()
{
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    addr.sin_port = htons(8080);

    Inet inet(addr);

    std::cout << "IP: " << inet.IP() << std::endl;
    std::cout << "Port: " << inet.PORT() << std::endl;
    std::cout << "Unique Name: " << inet.UniqueName() << std::endl;

    return 0;
}

在这个示例中,我们创建了一个 sockaddr_in 结构体并初始化它,然后使用它来构造一个 Inet 对象。接着,我们使用 Inet 对象的获取方法来打印 IP 地址、端口号和唯一名称。

五、基于OOP和RAII模式管理网络套接字

#pragma once

#include <iostream>
#include <memory>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "Log.hpp"
#include "Inet.hpp"

namespace socket_ddsm
{
    // 展开日志命名空间
    using namespace log_ddsm;

    // 默认port和backlog
    const static int gport = 8888;
    const static int gblcklog = 8;

    // 错误码和常量定义
    enum
    {
        SOCK_CREAT_FAIL = 1,
        BIND_FAIL,
        LISTEN_FAIL,
        MSSIZE = 4096 // 最大消息大小
    };

    // 对Socket的前置声明
    class Socket;
    // 是一个类型名,可用于接受std::make_shared<TcpSocket>()的返回值
    using SockSPtr = std::shared_ptr<Socket>;

    // 模板方法类模式
    /*基类提供方法,并组合,具体实现在派生类的实现*/
    class Socket
    {
    public:
        // 创建套接字
        virtual void CreateSocketOrDie() = 0;

        // 绑定端口
        virtual void BindOrDie(uint16_t port) = 0;

        // 监听端口
        virtual void ListenOrDie(int blcklog = gblcklog) = 0;

        // 接受连接
        virtual SockSPtr Accepter(Inet *addr) = 0;

        // 连接到服务器
        virtual bool Connector(const std::string &peerip, uint16_t peerport) = 0;

        // 获取套接字文件描述符
        virtual int GetSocket() = 0;

        // 关闭套接字
        virtual void Close() = 0;

        // 接收消息
        virtual ssize_t Recv(std::string *out) = 0;

        // 发送消息
        virtual ssize_t Send(const std::string &in) = 0;

    public:
        // 组合的创建TCP的方法集合
        void BuildListenSocket(int port = gport)
        {
            CreateSocketOrDie(); // 创建套接字
            BindOrDie(port);     // 绑定端口
            ListenOrDie();       // 监听端口
        }

        // 创建客户端套接字并连接到服务器
        bool BuildClientSocket(const std::string &peerip, uint16_t peerport)
        {
            CreateSocketOrDie(); // 创建套接字
            return Connector(peerip, peerport); // 连接到服务器
        }
    };

    // TCP套接字类,继承自Socket基类
    class TcpSocket : public Socket
    {
    public:
        TcpSocket()
            : _socket()
        {
        }

        TcpSocket(int socket)
            : _socket(socket)
        {
        }

        ~TcpSocket()
        {
            // 析构函数中不自动关闭套接字,避免意外关闭
        }

        // 创建TCP套接字
        void CreateSocketOrDie() override
        {
            _socket = socket(AF_INET, SOCK_STREAM, 0);
            if (_socket < 0)
            {
                LOG(FATAL, "socket create fail!\n");
                exit(SOCK_CREAT_FAIL);
            }
            LOG(DEBUG, "create sockfd success socket: %d\n", _socket);
        }

        // 绑定端口
        void BindOrDie(uint16_t port) override
        {
            struct sockaddr_in local;
            memset(&local, 0, sizeof(local));

            local.sin_family = AF_INET;
            local.sin_port = htons(port);
            local.sin_addr.s_addr = INADDR_ANY;

            int n = bind(_socket, (struct sockaddr *)&local, sizeof(local));
            if (n < 0)
            {
                LOG(FATAL, "bind fail!\n");
                exit(BIND_FAIL);
            }
            LOG(DEBUG, "bind success\n");
        }

        // 监听端口
        void ListenOrDie(int blcklog) override
        {
            int n = listen(_socket, blcklog);
            if (n < 0)
            {
                LOG(FATAL, "listen fail");
                exit(LISTEN_FAIL);
            }
            LOG(DEBUG, "listen success\n");
        }

        // 接受连接
        SockSPtr Accepter(Inet *peer) override
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int rwfd = accept(_socket, (struct sockaddr *)&client, &len);
            if (rwfd < 0)
            {
                LOG(WARNING, "accept error\n");
                return nullptr;
            }
            *peer = Inet(client);
            LOG(INFO, "accept success, client info: %s\n", peer->UniqueName().c_str());

            return std::make_shared<TcpSocket>(rwfd);
        }

        // 连接到服务器
        bool Connector(const std::string &peerip, uint16_t peerport) override
        {
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(peerport);
            inet_pton(AF_INET, peerip.c_str(), &server.sin_addr);

            int n = ::connect(_socket, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                return false;
            }
            return true;
        }

        // 获取套接字文件描述符
        int GetSocket() override
        {
            return _socket;
        }

        // 关闭套接字
        void Close() override
        {
            if (_socket > 0)
            {
                int reval = ::close(_socket);
                if (reval < 0)
                {
                    LOG(ERROR, "_socket close error\n");
                }
            }
        }

        // 接收消息
        ssize_t Recv(std::string *out) override
        {
            char buf[MSSIZE] = {0};
            int n = ::recv(_socket, buf, sizeof(buf), 0);
            if (n > 0)
            {
                buf[n] = 0;
                *out += buf;
            }
            return n;
        }

        // 发送消息
        ssize_t Send(const std::string &in) override
        {
            return ::send(_socket, in.c_str(), in.size(), 0);
        }

    private:
        int _socket; // 套接字文件描述符
    };

} // namespace socket_ddsm

以上代码设计了一个基于TCP的网络通信类库,使用了面向对象编程(OOP)RAII(Resource Acquisition Is Initialization)模式来管理网络套接字的创建、绑定、监听、连接等操作。

解释

  1. 抽象基类 Socket

    • Socket 类是一个抽象基类,定义了创建、绑定、监听、接受连接、连接到服务器、发送和接收消息等虚函数。这些函数在派生类中需要实现。
    • 提供了 BuildListenSocketBuildClientSocket 两个组合方法,用于简化服务器和客户端套接字的创建和配置过程。
  2. 具体类 TcpSocket

    • TcpSocket 类继承自 Socket 基类,实现了基类中定义的所有虚函数,具体负责TCP套接字的创建、绑定、监听、接受连接、连接到服务器、发送和接收消息等操作。
    • 使用日志记录(LOG 宏)来记录每一步的操作和错误信息,方便调试和问题排查。
  3. 日志管理:

    • 使用 log_ddsm 命名空间中的日志功能记录各种操作和错误信息,提供了详细的调试信息。
  4. 智能指针:

    • 使用 std::shared_ptr 来管理 TcpSocket 对象的生命周期,确保资源的自动释放,避免内存泄漏。
  5. RAII:

    • 通过 RAII 模式管理套接字的生命周期,在创建对象时初始化资源,在对象销毁时释放资源。

使用示例

#include "socket_ddsm.hpp"
#include "Log.hpp"
#include "Inet.hpp"

int main()
{
    // 初始化日志系统
    ENABLE_SCREEN();
    SET_IGNORE_LEVEL(log_ddsm::DEBUG);

    // 创建服务器套接字
    socket_ddsm::TcpSocket server;
    server.BuildListenSocket(8888);

    // 等待客户端连接
    socket_ddsm::Inet client_addr;
    auto client = server.Accepter(&client_addr);
    if (client)
    {
        // 接收客户端消息
        std::string msg;
        client->Recv(&msg);
        std::cout << "Received message: " << msg << std::endl;

        // 发送回复
        client->Send("Hello, client!");
    }

    // 关闭服务器套接字
    server.Close();

    return 0;
}

在这个示例中,我们初始化了日志系统,创建了服务器套接字并进行监听,等待客户端连接。接受到客户端连接后,接收客户端消息并发送回复,最后关闭服务器套接字。

六、下层TCP服务器类的设计

#pragma once

#include <functional>
#include <pthread.h>

#include "uncopyable.hpp"
#include "Socket.hpp"

using namespace socket_ddsm;

// Tcp服务器,接受用户发送的信息,发回消息
class TcpServer : public uncopyable
{
    // 回调函数的类型
    using service_t = std::function<std::string(std::string &)>;

    // 使用多线程解决服务器无法同时服务多个客户端的问题----原生线程的使用
    // 创建的目的是便于传递参数
    struct ThreadData
    {
        TcpServer *_self;
        SockSPtr _sockfd;
        Inet _addr;

        ThreadData(TcpServer *self, SockSPtr sockfd, const Inet &addr)
            : _self(self), _sockfd(sockfd), _addr(addr)
        {
        }
        ~ThreadData()
        {
        }
    };

public:
    // 在构造的时候,传入回调函数即可,实现高度解耦
    TcpServer(service_t service, uint16_t port = gport)
        : _port(port), _listensock(std::make_shared<TcpSocket>()), _isrunning(false), _service(service)
    {
        // 面向对象式的创建tcp socket
        _listensock->BuildListenSocket(port);
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            Inet client;
            SockSPtr newsock = _listensock->Accepter(&client);
            if (newsock == nullptr)
                continue;
            LOG(DEBUG, "get a new link,client info: %s,sockfd is %d\n", client.UniqueName().c_str(), newsock->GetSocket());

            // 5.提供服务(服务器需要能够同时为多个客户端提供服务)
            /* 使用多线程解决服务器无法同时服务多个客户端的问题----原生线程的使用
                创建一个新线程来提供服务
            */
            pthread_t tid;
            ThreadData *td = new ThreadData(this, newsock, client);
            pthread_create(&tid, nullptr, Excute, (void *)td);
        }
        _isrunning = false;
    }

    static void *Excute(void *args) // static 防止this指针干扰函数类型
    {
        // 新线程解除与主线程的等待关系,主线程不再需要等待新线程
        pthread_detach(pthread_self());
        // 目的是调用Service---static内部无法获取对象,需要传递进来--所以通过一个设计的ThreadData类把需要的参数传递进来
        ThreadData *ptd = static_cast<ThreadData *>(args);
        
        std::string requeststr;
        ssize_t n = ptd->_sockfd->Recv(&requeststr);
        if (n > 0)
        {
            // 回调
            std::string reponsestr = ptd->_self->_service(requeststr);
            ptd->_sockfd->Send(reponsestr);
        }

        // 面向对象,关闭sockfd
        ptd->_sockfd->Close();
        delete ptd;
        return nullptr;
    }

    ~TcpServer() {}

private:
    uint16_t _port;       // 端口
    SockSPtr _listensock; // 自定义实现的socket类,交给智能指针管理
    bool _isrunning;
    service_t _service;
};

上述代码设计了一个 TcpServer 类,用于创建和管理一个 TCP 服务器,能够接收客户端发送的信息,并发回响应。为了实现并发处理多个客户端的请求,代码使用了多线程。

解释

  1. 继承 uncopyable 类:

    • TcpServer 继承自 uncopyable 类,确保 TcpServer 对象不能被拷贝,避免了拷贝可能带来的资源管理问题。
  2. 回调函数类型:

    • 使用 std::function<std::string(std::string &)> 定义了一个回调函数类型 service_t,用于处理客户端请求并生成响应。通过将回调函数传递给 TcpServer 构造函数,实现了逻辑的高度解耦。
  3. 多线程处理客户端请求:

    • 使用 ThreadData 结构体封装了需要传递给新线程的参数。这包括 TcpServer 对象的指针、客户端套接字和客户端地址。
    • Start 方法中,服务器循环接受新连接,每接受一个新连接就创建一个新线程来处理该连接,避免了阻塞其他客户端的请求。
  4. 线程执行函数 Excute

    • Excute 是一个静态成员函数,用于作为线程的执行函数。它接受一个 ThreadData 对象,调用回调函数处理请求并发送响应。
    • 静态成员函数不依赖于具体对象,因此不会受到 this 指针的干扰。通过将 ThreadData 对象传递给 Excute 函数,可以在函数内部访问 TcpServer 对象及其成员。
  5. 资源管理:

    • 使用智能指针 SockSPtr 管理套接字对象,确保资源在适当的时候自动释放,避免内存泄漏。
    • Excute 函数末尾关闭客户端套接字并释放 ThreadData 对象。
  6. 日志记录:

    • 使用日志记录系统记录服务器操作和错误信息,方便调试和问题排查。

使用示例

#include "TcpServer.hpp"

std::string echo_service(std::string &request)
{
    return "Echo: " + request;
}

int main()
{
    // 设置日志系统
    ENABLE_SCREEN();
    SET_IGNORE_LEVEL(log_ddsm::DEBUG);

    // 创建TCP服务器
    TcpServer server(echo_service, 8888);

    // 启动服务器
    server.Start();

    return 0;
}

在这个示例中,我们创建了一个 TcpServer 对象,传入了一个简单的回显服务 echo_service。服务器在端口 8888 上监听,并处理客户端请求,将客户端发送的消息回显给客户端。

七、服务器main函数设计

#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Http.hpp"

int main(int args, char *argv[])
{
    // 检查使用格式
    if (args != 2)
    {
        LOG(INFO, "Usage: %s localport\n", argv[0]);
        exit(0);
    }
    // 获取localport并传递给TcpServer构造
    uint16_t localport = std::stoi(argv[1]);

    // 创建 HttpServer 对象
    HttpServer hserver;
    // 创建 TcpServer 对象,绑定 HttpServer::HanderHttpRequest 方法作为处理回调
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
        std::bind(&HttpServer::HanderHttpRequest, &hserver,
                  std::placeholders::_1),
        localport);
    
    // 启动 TcpServer
    tsvr->Start();

    return 0;
}

/*// NetCal cal;

    // IOService io_service(
    //     std::bind(&NetCal::Calculator, &cal,
    //               std::placeholders::_1));

    // // 耦合了io_service和tcpserver
    // std::unique_ptr<TcpServer> utsp = std::make_unique<TcpServer>(
    //     std::bind(&IOService::IOExcute, &io_service,
    //               std::placeholders::_1,
    //               std::placeholders::_2),
    //     localport);

    // utsp->Start();*/

上述代码实现了一个简单的 TCP 服务器,能够接受客户端的 HTTP 请求并进行处理。代码通过结合 TcpServerHttpServer 类,实现了处理 HTTP 请求的功能。

主要步骤和解释

  1. 包含头文件:

    • #include "Socket.hpp"
    • #include "TcpServer.hpp"
    • #include "Http.hpp"
    • 这些头文件包含了 Socket 类、TcpServer 类和 HttpServer 类的定义和实现。
  2. main 函数:

    • int main(int args, char *argv[]) 定义了程序的入口点。
  3. 检查命令行参数:

    • if (args != 2) 检查命令行参数的数量是否正确。如果参数数量不正确,则输出使用提示并终止程序。
    • LOG(INFO, "Usage: %s localport\n", argv[0]); 使用日志系统输出使用提示信息。
    • exit(0); 终止程序。
  4. 获取本地端口:

    • uint16_t localport = std::stoi(argv[1]); 将命令行参数转换为整数,表示本地端口号。
  5. 创建 HttpServer 对象:

    • HttpServer hserver; 创建一个 HttpServer 对象,用于处理 HTTP 请求。
  6. 创建 TcpServer 对象:

    • std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(...) 创建一个 TcpServer 对象,使用智能指针管理其生命周期。
    • std::bind(&HttpServer::HanderHttpRequest, &hserver, std::placeholders::_1)HttpServerHanderHttpRequest 方法绑定为 TcpServer 的回调函数,用于处理客户端请求。
    • localport 作为参数传递给 TcpServer 构造函数,指定服务器监听的端口。
  7. 启动 TcpServer

    • tsvr->Start(); 启动 TcpServer,开始监听并处理客户端请求。
  8. 返回值:

    • return 0; 表示程序成功结束。

这些头文件分别定义了 Socket 类、TcpServer 类和 HttpServer 类的接口和实现。完整的实现可以参考上述解释中的类设计。

八、应用层HTTP服务器的设计

#pragma once

#include <fstream>
#include <sstream>
#include <string>
#include <iostream>
#include <vector>
#include <unordered_map>

#include "uncopyable.hpp"

const static std::string com_sep = "\r\n";
const static std::string req_sep = ": ";
const static std::string prefixpath = "wwwroot"; // web根目录
const static std::string homepage = "index.html";
const static std::string httpversion = "HTTP/1.0";
const static std::string spacesep = " ";

class HttpRequest // 根据Http请求的结构确定
{
private:
    /// @brief 获得正文内容之前的信息
    /// @param reqstr
    /// @return 当前调用获取的一行字符串
    std::string Getline(std::string &reqstr)
    {
        auto pos = reqstr.find(com_sep);
        if (pos == std::string::npos)
            return std::string();

        std::string line = reqstr.substr(0, pos);      // 如果找到空行,则pos==0,截取出来的line是一个空串
        reqstr.erase(0, line.size() + com_sep.size()); // 若是,同样删掉空串
        if (line.empty())
            return com_sep; // 以这种方式返回,表示已经读取到空行

        return line;
    }

    // 解析请求行
    void PraseReqLine()
    {
        // 创建一个字符串流,类似于cin,可以以空格为分隔符分别输入给多个变量
        std::stringstream ss(_req_line);
        ss >> _method >> _url >> _verson;

        // 构建真正的资源路径
        _path += _url;

        if (_path[_path.size() - 1] == '/')
        {
            _path += homepage;
        }
    }

    // 解析请求头
    void PraseReqHeader()
    {
        for (auto &header : _req_headers)
        {
            auto pos = header.find(req_sep);
            if (pos == std::string::npos)
                continue;
            std::string k = header.substr(0, pos);
            std::string v = header.substr(pos + req_sep.size());
            if (k.empty() || v.empty())
                continue;
            _headers_kv.insert(std::make_pair(k, v));
        }
    }

public:
    HttpRequest()
        : _blank_line(com_sep), _path(prefixpath)
    {
    }

    /// @brief 反序列化,解析HTTP请求内容
    /// @param reqstr 请求以字符串形式发送
    void Deserialize(std::string &reqstr)
    {
        // 基础反序列化
        _req_line = Getline(reqstr); // 获取请求行
        std::string header;
        while (true)
        {
            header = Getline(reqstr);
            if (header.empty())
                break;
            if (header == com_sep)
                break;

            _req_headers.push_back(header);
        }
        // 到这里,请求行和报头,空行被获取完
        if (!reqstr.empty())
            _body_text = reqstr;

        // 进一步反序列化(填写属性字段)
        PraseReqLine();
        PraseReqHeader();
    }

    void Print()
    {
        std::cout << "请求行:" << _req_line << std::endl;

        for (auto &header : _req_headers)
        {
            std::cout << "报头:" << header << std::endl;
        }
        std::cout << "空行:" << _blank_line;
        std::cout << "正文:" << _body_text << std::endl;

        std::cout << "method:" << _method << std::endl;
        std::cout << "url:" << _url << std::endl;
        std::cout << "verson:" << _verson << std::endl;

        for (auto header : _headers_kv)
        {
            std::cout << header.first << " " << header.second << std::endl;
        }
    }

    std::string GetUrl()
    {
        LOG(DEBUG, "client want url %s\n", _url.c_str());
        return _url;
    }

    std::string GetPath()
    {
        LOG(DEBUG, "client want path %s\n", _path.c_str());
        return _path;
    }

private:
    // 基本的HTTP请求格式
    std::string _req_line; // 请求行
    std::vector<std::string> _req_headers;
    std::string _blank_line;
    std::string _body_text;

    // 具体的属性字段,需要进一步反序列化
    std::string _method;
    std::string _url;
    std::string _path; // 用户请求的资源的真实路径,路径前需要拼上wwwroot
    std::string _verson;
    std::unordered_map<std::string, std::string> _headers_kv; // 存储属性字段KV结构
};

class HttpReponse // 根据HTTP响应的结构确定
{
public:
    HttpReponse() : _version(httpversion), _blank_line(com_sep)
    {
    }

    // 添加状态码
    void AddCode(int code)
    {
        _status_code = code;
        _desc = "OK";
    }

    // 添加响应头
    void AddHeader(const std::string &k, const std::string &v)
    {
        _headers_kv[k] = v;
    }

    // 添加响应正文
    void AddBodyText(const std::string &body_text)
    {
        _resp_body_text = body_text;
    }

    // 序列化响应内容
    std::string Serialize()
    {
        // 构建状态行
        _status_line = _version + spacesep + std::to_string(_status_code) + spacesep + _desc + com_sep;
        // 构建应答报头
        for (auto &header : _headers_kv)
        {
            std::string header_line = header.first + req_sep + header.second + com_sep;
            _resp_headers.push_back(header_line);
        }
        // 空行和正文
        // 正式序列化---构建HTTP应答报文
        std::string responsestr = _status_line;
        for (auto &line_kv : _resp_headers)
        {
            responsestr += line_kv;
        }
        responsestr += _blank_line;
        responsestr += _resp_body_text;

        return responsestr;
    }

    ~HttpReponse()
    {
    }

private:
    // HttpReponse 基本属性
    std::string _version;                                     // 版本
    int _status_code;                                         // 状态码
    std::string _desc;                                        // 状态描述
    std::unordered_map<std::string, std::string> _headers_kv; // 属性KV

    // HTTP报文格式
    std::string _status_line;
    std::vector<std::string> _resp_headers;
    std::string _blank_line;
    std::string _resp_body_text;
};

class HttpServer : public uncopyable
{
private:
    // 获取文件内容
    std::string GetFileContent(const std::string &path)
    {
        std::ifstream in(path, std::ios::binary);
        if (!in.is_open())
            return std::string();

        // 通过获得偏移量的方法计算文件大小
        in.seekg(0, in.end);
        int f_size = in.tellg();
        in.seekg(0, in.beg);

        std::string content;
        content.resize(f_size);
        in.read((char *)content.c_str(), f_size);
        in.close();

        return content;
    }

public:
    HttpServer() {}

    // 处理HTTP请求
    std::string HanderHttpRequest(std::string &reqstr)
    {
#ifdef DEBUG
        std::cout << "---------------------------------------------" << std::endl;
        std::cout << reqstr;
        std::string responsestr = "HTTP/1.1 200 OK\r\n";
        responsestr += "Content-Type: text/html\r\n";
        responsestr += "\r\n";
        responsestr += "<html><h1>hello linux!</h1></html>";

        return responsestr;
#else
        HttpRequest req;
        req.Deserialize(reqstr);
        std::string content = GetFileContent(req.GetPath()); // 获取文件内容
        if (content.empty())
        {
            return std::string(); // 读取失败,不考虑
        }
        // req.Print();
        // std::string url = req.GetUrl();
        // std::string path = req.GetPath();

        // 到这里,读取一定成功
        HttpReponse rsp;
        rsp.AddCode(200);
        rsp.AddHeader("Content-Length", std::to_string(content.size()));
        rsp.AddBodyText(content);

        return rsp.Serialize();
#endif
    }

    ~HttpServer() {}

private:
};

上述代码实现了一个简单的 HTTP 服务器,能够接收 HTTP 请求并返回响应。代码使用了面向对象编程的思想,定义了 HttpRequestHttpReponseHttpServer 三个类,分别用于处理 HTTP 请求、构建 HTTP 响应和管理服务器逻辑。

解释

  1. HttpRequest 类:

    • 用于解析 HTTP 请求,提取请求行、请求头和请求正文。
    • Deserialize 方法通过分割字符串的方式解析请求报文,并调用 PraseReqLinePraseReqHeader 方法进一步解析请求行和请求头。
    • Getline 方法用于从请求字符串中读取一行内容。
    • Print 方法用于打印请求的详细信息(用于调试)。
    • GetUrlGetPath 方法用于获取请求的 URL 和路径。
  2. HttpReponse 类:

    • 用于构建 HTTP 响应,包含状态行、响应头和响应正文。
    • AddCode 方法用于添加响应状态码。
    • AddHeader 方法用于添加响应头。
    • AddBodyText 方法用于添加响应正文。
    • Serialize 方法用于将响应对象序列化为字符串,准备发送给客户端。
  3. HttpServer 类:

    • 用于处理 HTTP 请求并生成响应。
    • GetFileContent 方法用于读取请求的文件内容。
    • HanderHttpRequest 方法用于处理 HTTP 请求,解析请求并生成响应。
      • 在调试模式下(#ifdef DEBUG),直接返回一个简单的 HTML 响应。
      • 在非调试模式下,解析请求并读取请求文件内容,构建 HTTP 响应对象并序列化为字符串返回。
    • 继承自 uncopyable 类,确保 HttpServer 对象不能被拷贝,避免资源管理问题。

使用示例

#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Http.hpp"

int main(int args, char *argv[])
{
    // 检查使用格式
    if (args != 2)
    {
        LOG(INFO, "Usage: %s localport\n", argv[0]);
        exit(0);
    }
    // 获取localport并传递给TcpServer构造
    uint16_t localport = std::stoi(argv[1]);

    // 创建 HttpServer 对象
    HttpServer hserver;
    // 创建 TcpServer 对象,绑定 HttpServer::HanderHttpRequest 方法作为处理回调
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(
        std::bind(&HttpServer::HanderHttpRequest, &hserver,
                  std::placeholders::_1),
        localport);
    
    // 启动 TcpServer
    tsvr->Start();

    return 0;
}

在这个示例中,我们创建了一个 TcpServer 对象,传入了一个 HttpServer 对象的 HanderHttpRequest 方法作为回调函数,用于处理客户端的 HTTP 请求。服务器在指定端口上监听,并处理客户端请求返回响应。


通过以上的分模块的设计,我们已经实现了一个简单的HTTP服务器,它可以接受HTTP报文,并响应返回对应请求的超文本资源。


在这里插入图片描述
完~
转载请注明出处