网络推广专员,宜春网站推广优化,seo优化培训多少钱,wordpress调用自定义分类标题标题#xff1a;[HTTP协议]应用层协议HTTP从入门到深刻理解并落地部署自己的云服务(2) 水墨不写bug 文章目录 一、无法拷贝类(class uncopyable)的设计解释#xff1a;重要思想#xff1a;使用示例 二、锁的RAII设计解释重要考虑使用示例 三、基于RAII模式和互斥锁的的日志…标题[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类。继承这个类的任何类都无法进行拷贝操作。 解释 类声明 class uncopyable 是一个基类设计用于防止派生类对象进行拷贝操作。 构造函数和析构函数 protected 访问控制符使得构造函数和析构函数只能在派生类中访问。这意味着这个类不能被直接实例化只能被继承。uncopyable() 是默认构造函数。~uncopyable() 是析构函数。 删除拷贝构造函数和拷贝赋值运算符 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代码块结束即为解锁*/这段代码实现了一个基于 RAIIResource Acquisition Is Initialization模式的锁保护类 LockGuard用于简化多线程编程中的锁操作。
解释 类声明 class LockGuard 是一个封装了 pthread_mutex_t 锁操作的类使用 RAII 模式来管理锁的生命周期。 构造函数 LockGuard(pthread_mutex_t* mutex) 是构造函数接收一个 pthread_mutex_t* 类型的指针并在构造函数中调用 pthread_mutex_lock 来加锁。这确保了在创建 LockGuard 对象时锁自动被加上。 析构函数 ~LockGuard() 是析构函数在对象销毁时调用 pthread_mutex_unlock 来解锁。这确保了当 LockGuard 对象的生命周期结束时锁自动被释放。 私有成员变量 pthread_mutex_t *_mutex 是一个指向 pthread_mutex_t 类型的指针用于存储传入的锁对象。
重要考虑 RAII 模式 RAIIResource Acquisition Is Initialization是一种管理资源的编程技巧它将资源的获取和释放绑定到对象的生命周期中。在 LockGuard 中构造函数获取锁加锁析构函数释放锁解锁这确保了锁的正确管理和避免忘记解锁的错误。 简化锁操作 传统的pthread库的锁操作需要显式调用 pthread_mutex_lock 和 pthread_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;}elsebreak;}}
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.hppnamespace 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; // 这条日志的进程idstd::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);elsestd::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以上逻辑设计了一个日志管理系统提供了日志的多种输出方式如屏幕输出和文件输出并使用 RAIIResource Acquisition Is Initialization模式和互斥锁保护日志打印过程。 解释 日志级别管理 enum LEVEL 定义了不同的日志级别从 DEBUG 到 FATAL用于标识日志的重要性。在不同的场景下需要输出的日志等级是不同的。比如在测试Debug阶段最好输出全部日志信息而在发行Release之后只需要输出Warning以上级别的日志信息即可。 日志信息结构体 struct logMessage 包含了日志的详细信息包括日志级别、进程ID、文件名、行号、当前时间和日志内容。 获取日志级别字符串 getLevel(int level) 函数根据日志级别的整数值返回相应的字符串表示。 获取当前时间 getCurTime() 函数获取当前的系统时间并格式化为字符串。 日志类 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) 函数设置日志文件名称。 宏定义 LOG(Level, Format, ...) 宏用于简化日志的打印调用自动获取文件名和行号并调用 load_message 函数。ENABLE_FILE() 宏用于设置日志打印到文件。ENABLE_SCREEN() 宏用于设置日志打印到屏幕。SET_IGNORE_LEVEL(Level) 宏用于设置忽略的日志级别。SET_LOG_FILENAME(Name) 宏用于设置日志文件名称。
使用示例
#include log_ddsm.hppint 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 stringclass 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 类用于封装和管理网络地址信息。 解释 封装网络地址信息 Inet 类封装了 IP 地址和端口号的信息并提供了一些便捷的方法来获取这些信息。通过这种封装可以更加方便地管理和操作网络地址。 地址转换 ConvertToHost 方法将 sockaddr_in 结构体中的网络字节序 IP 地址和端口号转换为主机字节序并将 IP 地址转换为字符串形式。因为网络传输使用网络字节序而主机通常使用主机字节序。 唯一名称 SetUname 方法将 IP 地址和端口号组合成一个唯一名称 _uname用于标识一个唯一的网络地址。 操作符重载 重载了 operator 操作符用于比较两个 Inet 对象是否相等。两个 Inet 对象相等的条件是它们的 IP 地址和端口号都相等。 获取方法 提供了多个获取方法如 ADDR()、IP()、PORT() 和 UniqueName()用于获取 Inet 对象的不同属性。这些方法使得 Inet 类的使用更加便捷。
使用示例
#include Inet.hpp
#include iostreamint 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.hppnamespace socket_ddsm
{// 展开日志命名空间using namespace log_ddsm;// 默认port和backlogconst 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_sharedTcpSocket()的返回值using SockSPtr std::shared_ptrSocket;// 模板方法类模式/*基类提供方法并组合具体实现在派生类的实现*/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_sharedTcpSocket(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和RAIIResource Acquisition Is Initialization模式来管理网络套接字的创建、绑定、监听、连接等操作。 解释 抽象基类 Socket Socket 类是一个抽象基类定义了创建、绑定、监听、接受连接、连接到服务器、发送和接收消息等虚函数。这些函数在派生类中需要实现。提供了 BuildListenSocket 和 BuildClientSocket 两个组合方法用于简化服务器和客户端套接字的创建和配置过程。 具体类 TcpSocket TcpSocket 类继承自 Socket 基类实现了基类中定义的所有虚函数具体负责TCP套接字的创建、绑定、监听、接受连接、连接到服务器、发送和接收消息等操作。使用日志记录LOG 宏来记录每一步的操作和错误信息方便调试和问题排查。 日志管理 使用 log_ddsm 命名空间中的日志功能记录各种操作和错误信息提供了详细的调试信息。 智能指针 使用 std::shared_ptr 来管理 TcpSocket 对象的生命周期确保资源的自动释放避免内存泄漏。 RAII 通过 RAII 模式管理套接字的生命周期在创建对象时初始化资源在对象销毁时释放资源。
使用示例
#include socket_ddsm.hpp
#include Log.hpp
#include Inet.hppint 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.hppusing namespace socket_ddsm;// Tcp服务器接受用户发送的信息发回消息
class TcpServer : public uncopyable
{// 回调函数的类型using service_t std::functionstd::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_sharedTcpSocket()), _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_castThreadData *(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);}// 面向对象,关闭sockfdptd-_sockfd-Close();delete ptd;return nullptr;}~TcpServer() {}private:uint16_t _port; // 端口SockSPtr _listensock; // 自定义实现的socket类,交给智能指针管理bool _isrunning;service_t _service;
};上述代码设计了一个 TcpServer 类用于创建和管理一个 TCP 服务器能够接收客户端发送的信息并发回响应。为了实现并发处理多个客户端的请求代码使用了多线程。 解释 继承 uncopyable 类 TcpServer 继承自 uncopyable 类确保 TcpServer 对象不能被拷贝避免了拷贝可能带来的资源管理问题。 回调函数类型 使用 std::functionstd::string(std::string ) 定义了一个回调函数类型 service_t用于处理客户端请求并生成响应。通过将回调函数传递给 TcpServer 构造函数实现了逻辑的高度解耦。 多线程处理客户端请求 使用 ThreadData 结构体封装了需要传递给新线程的参数。这包括 TcpServer 对象的指针、客户端套接字和客户端地址。在 Start 方法中服务器循环接受新连接每接受一个新连接就创建一个新线程来处理该连接避免了阻塞其他客户端的请求。 线程执行函数 Excute Excute 是一个静态成员函数用于作为线程的执行函数。它接受一个 ThreadData 对象调用回调函数处理请求并发送响应。静态成员函数不依赖于具体对象因此不会受到 this 指针的干扰。通过将 ThreadData 对象传递给 Excute 函数可以在函数内部访问 TcpServer 对象及其成员。 资源管理 使用智能指针 SockSPtr 管理套接字对象确保资源在适当的时候自动释放避免内存泄漏。在 Excute 函数末尾关闭客户端套接字并释放 ThreadData 对象。 日志记录 使用日志记录系统记录服务器操作和错误信息方便调试和问题排查。
使用示例
#include TcpServer.hppstd::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.hppint 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_ptrTcpServer tsvr std::make_uniqueTcpServer(std::bind(HttpServer::HanderHttpRequest, hserver,std::placeholders::_1),localport);// 启动 TcpServertsvr-Start();return 0;
}/*// NetCal cal;// IOService io_service(// std::bind(NetCal::Calculator, cal,// std::placeholders::_1));// // 耦合了io_service和tcpserver// std::unique_ptrTcpServer utsp std::make_uniqueTcpServer(// std::bind(IOService::IOExcute, io_service,// std::placeholders::_1,// std::placeholders::_2),// localport);// utsp-Start();*/上述代码实现了一个简单的 TCP 服务器能够接受客户端的 HTTP 请求并进行处理。代码通过结合 TcpServer 和 HttpServer 类实现了处理 HTTP 请求的功能。 主要步骤和解释 包含头文件 #include Socket.hpp#include TcpServer.hpp#include Http.hpp这些头文件包含了 Socket 类、TcpServer 类和 HttpServer 类的定义和实现。 main 函数 int main(int args, char *argv[]) 定义了程序的入口点。 检查命令行参数 if (args ! 2) 检查命令行参数的数量是否正确。如果参数数量不正确则输出使用提示并终止程序。LOG(INFO, Usage: %s localport\n, argv[0]); 使用日志系统输出使用提示信息。exit(0); 终止程序。 获取本地端口 uint16_t localport std::stoi(argv[1]); 将命令行参数转换为整数表示本地端口号。 创建 HttpServer 对象 HttpServer hserver; 创建一个 HttpServer 对象用于处理 HTTP 请求。 创建 TcpServer 对象 std::unique_ptrTcpServer tsvr std::make_uniqueTcpServer(...) 创建一个 TcpServer 对象使用智能指针管理其生命周期。std::bind(HttpServer::HanderHttpRequest, hserver, std::placeholders::_1) 将 HttpServer 的 HanderHttpRequest 方法绑定为 TcpServer 的回调函数用于处理客户端请求。localport 作为参数传递给 TcpServer 构造函数指定服务器监听的端口。 启动 TcpServer tsvr-Start(); 启动 TcpServer开始监听并处理客户端请求。 返回值 return 0; 表示程序成功结束。 这些头文件分别定义了 Socket 类、TcpServer 类和 HttpServer 类的接口和实现。完整的实现可以参考上述解释中的类设计。 八、应用层HTTP服务器的设计
#pragma once#include fstream
#include sstream
#include string
#include iostream
#include vector
#include unordered_map#include uncopyable.hppconst 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); // 如果找到空行则pos0截取出来的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::vectorstd::string _req_headers;std::string _blank_line;std::string _body_text;// 具体的属性字段需要进一步反序列化std::string _method;std::string _url;std::string _path; // 用户请求的资源的真实路径路径前需要拼上wwwrootstd::string _verson;std::unordered_mapstd::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_mapstd::string, std::string _headers_kv; // 属性KV// HTTP报文格式std::string _status_line;std::vectorstd::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 DEBUGstd::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 htmlh1hello linux!/h1/html;return responsestr;
#elseHttpRequest 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 请求并返回响应。代码使用了面向对象编程的思想定义了 HttpRequest、HttpReponse 和 HttpServer 三个类分别用于处理 HTTP 请求、构建 HTTP 响应和管理服务器逻辑。 解释 HttpRequest 类 用于解析 HTTP 请求提取请求行、请求头和请求正文。Deserialize 方法通过分割字符串的方式解析请求报文并调用 PraseReqLine 和 PraseReqHeader 方法进一步解析请求行和请求头。Getline 方法用于从请求字符串中读取一行内容。Print 方法用于打印请求的详细信息用于调试。GetUrl 和 GetPath 方法用于获取请求的 URL 和路径。 HttpReponse 类 用于构建 HTTP 响应包含状态行、响应头和响应正文。AddCode 方法用于添加响应状态码。AddHeader 方法用于添加响应头。AddBodyText 方法用于添加响应正文。Serialize 方法用于将响应对象序列化为字符串准备发送给客户端。 HttpServer 类 用于处理 HTTP 请求并生成响应。GetFileContent 方法用于读取请求的文件内容。HanderHttpRequest 方法用于处理 HTTP 请求解析请求并生成响应。 在调试模式下#ifdef DEBUG直接返回一个简单的 HTML 响应。在非调试模式下解析请求并读取请求文件内容构建 HTTP 响应对象并序列化为字符串返回。 继承自 uncopyable 类确保 HttpServer 对象不能被拷贝避免资源管理问题。
使用示例
#include Socket.hpp
#include TcpServer.hpp
#include Http.hppint 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_ptrTcpServer tsvr std::make_uniqueTcpServer(std::bind(HttpServer::HanderHttpRequest, hserver,std::placeholders::_1),localport);// 启动 TcpServertsvr-Start();return 0;
}在这个示例中我们创建了一个 TcpServer 对象传入了一个 HttpServer 对象的 HanderHttpRequest 方法作为回调函数用于处理客户端的 HTTP 请求。服务器在指定端口上监听并处理客户端请求返回响应。 通过以上的分模块的设计我们已经实现了一个简单的HTTP服务器它可以接受HTTP报文并响应返回对应请求的超文本资源。 完~ 转载请注明出处