标题:[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)类。继承这个类的任何类都无法进行拷贝操作。
解释:
类声明:
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代码块结束即为解锁
*/
这段代码实现了一个基于 RAII(Resource 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 模式:
RAII(Resource 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;
}
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)模式和互斥锁保护日志打印过程。
解释
日志级别管理:
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.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
类,用于封装和管理网络地址信息。
解释
封装网络地址信息:
Inet
类封装了 IP 地址和端口号的信息,并提供了一些便捷的方法来获取这些信息。通过这种封装,可以更加方便地管理和操作网络地址。
地址转换:
ConvertToHost
方法将sockaddr_in
结构体中的网络字节序 IP 地址和端口号转换为主机字节序,并将 IP 地址转换为字符串形式。因为网络传输使用网络字节序,而主机通常使用主机字节序。
唯一名称:
SetUname
方法将 IP 地址和端口号组合成一个唯一名称_uname
,用于标识一个唯一的网络地址。
操作符重载:
- 重载了
operator==
操作符,用于比较两个Inet
对象是否相等。两个Inet
对象相等的条件是它们的 IP 地址和端口号都相等。
- 重载了
获取方法:
- 提供了多个获取方法,如
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)
模式来管理网络套接字的创建、绑定、监听、连接等操作。
解释
抽象基类
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.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 服务器,能够接收客户端发送的信息,并发回响应。为了实现并发处理多个客户端的请求,代码使用了多线程。
解释
继承
uncopyable
类:TcpServer
继承自uncopyable
类,确保TcpServer
对象不能被拷贝,避免了拷贝可能带来的资源管理问题。
回调函数类型:
- 使用
std::function<std::string(std::string &)>
定义了一个回调函数类型service_t
,用于处理客户端请求并生成响应。通过将回调函数传递给TcpServer
构造函数,实现了逻辑的高度解耦。
- 使用
多线程处理客户端请求:
- 使用
ThreadData
结构体封装了需要传递给新线程的参数。这包括TcpServer
对象的指针、客户端套接字和客户端地址。 - 在
Start
方法中,服务器循环接受新连接,每接受一个新连接就创建一个新线程来处理该连接,避免了阻塞其他客户端的请求。
- 使用
线程执行函数
Excute
:Excute
是一个静态成员函数,用于作为线程的执行函数。它接受一个ThreadData
对象,调用回调函数处理请求并发送响应。- 静态成员函数不依赖于具体对象,因此不会受到
this
指针的干扰。通过将ThreadData
对象传递给Excute
函数,可以在函数内部访问TcpServer
对象及其成员。
资源管理:
- 使用智能指针
SockSPtr
管理套接字对象,确保资源在适当的时候自动释放,避免内存泄漏。 - 在
Excute
函数末尾关闭客户端套接字并释放ThreadData
对象。
- 使用智能指针
日志记录:
- 使用日志记录系统记录服务器操作和错误信息,方便调试和问题排查。
使用示例
#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 请求并进行处理。代码通过结合
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_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(...)
创建一个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.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 请求并返回响应。代码使用了面向对象编程的思想,定义了
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.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报文,并响应返回对应请求的超文本资源。
完~
转载请注明出处