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

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

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

在这里插入图片描述


文章目录

  • 一、无法拷贝类(class uncopyable)的设计
      • 解释:
      • 重要思想:
      • 使用示例
  • 二、锁的RAII设计
      • 解释
      • 重要考虑
      • 使用示例
  • 三、基于RAII模式和互斥锁的的日志系统设计
      • 解释
      • 使用示例
  • 四、网络地址信息的封装管理
      • 解释
      • 使用示例
  • 五、基于OOP和RAII模式管理网络套接字
      • 解释
      • 使用示例
  • 六、下层TCP服务器类的设计
      • 解释
      • 使用示例
  • 七、服务器main函数设计
      • 主要步骤和解释
  • 八、应用层HTTP服务器的设计
      • 解释
      • 使用示例

本文将解释并实现一个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 // 使用锁需要频繁调用系统调用,十分麻烦// 于是实现锁的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 #include #include #include #include #include #include #include #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 #include #include #include #include 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 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 #include #include #include #include #include #include #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()的返回值 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 #include #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 定义了一个回调函数类型 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 utsp = std::make_unique( // 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 tsvr = std::make_unique(...) 创建一个 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 #include #include #include #include #include #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 += \"

hello linux!

\"
; 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报文,并响应返回对应请求的超文本资源。


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