[C++]TinyWebServer

TinyWebServer

文章目录

  • TinyWebServer
  • 1 主体框架
  • 2 Buffer
    • 2.1 向Buffer写入数据
    • 2.2 从Buffer读取数据
    • 2.3 动态扩容
    • 2.4 从socket中读取数据
    • 2.5 具体实现
  • 3 日志系统
    • 3.1 生产者-消费者模型
    • 3.2 数据一致
    • 3.3 代码
  • 4 定时器
    • 4.1 调整堆中元素操作
    • 4.2 堆的操作
      • 4.2.1 增
      • 4.2.2 删
      • 4.2.3 改
      • 4.2.4 查
    • 4.3 代码
  • 5 线程池
    • 5.1 代码
  • 6 数据库连接池
    • 6.1 RAII
    • 6.2 代码
  • 7 HTTP层处理
    • 7.1 HTTP解析
      • 7.1.1 代码
    • 7.2 HTTP响应
      • 7.2.1 代码
    • 7.3 HTTP处理
      • 7.3.1 代码
  • 8 Server层处理
    • 8.1 代码
  • 9 压力测试
    • 9.1 ET模式
    • 9.2 LT模式
    • 9.3 测试环境
  • 10 运行说明
    • 10.1 数据库初始化
    • 10.2 导入mysql.h
    • 10.3 编译运行
  • 11 致谢

1 主体框架

客户端如果想和服务器通信,首先要和服务器建立TCP连接,然后发送HTTP请求。服务端接收并处理HTTP请求,然后发送HTTP响应。

在这里插入图片描述

服务器采用单Reactor多线程模式,主线程使用IO多路复用接口监听事件,收到事件后将其分发。连接事件直接处理,而读写事件由线程池负责处理。

Reactor模式介绍:https://xiaolincoding.com/os/8_network_system/reactor.html#单-reactor-多线程-多进程

在这里插入图片描述

项目主要包含以下模块:

  • Server层-基于EPOLL的I/O多路复用和Reactor网络模式
  • HTTP处理层-解析HTTP请求并处理,生成返回的HTTP响应
  • 日志系统
  • 线程池
  • 数据库连接池
  • 定时器
  • 缓冲区-暂存缓冲数据,读写socket

TinyWebServer
├── build
│   ├── bin
│   │   └── server
│   └── Makefile
├── CMakeLists.txt
├── config.ini
├── log
├── Makefile
├── resources
├── webbench-1.5
│   ├── Makefile
│   ├── socket.c
│   ├── webbench
│   ├── webbench.c
│   └── webbench.o
└── webserver├── buffer│   ├── Buffer.cpp│   └── Buffer.h├── http│   ├── HttpResponse.cpp│   ├── HttpResponse.h│   ├── HttpWork.cpp│   ├── HttpWork.h│   ├── ParseHttpRequest.cpp│   └── ParseHttpRequest.h├── lib│   └── inih-r58│       ├── cpp│       │   ├── INIReader.cpp│       │   └── INIReader.h│       ├── ini.c│       └── ini.h├── log│   ├── Log.cpp│   ├── Log.h│   ├── LogLevel.h│   └── LogQueue.h├── main.cpp├── pool│   ├── sqlconnpool.cpp│   ├── sqlconnpool.h│   ├── sqlconnRAII.h│   ├── ThreadPool.cpp│   └── ThreadPool.h├── server│   ├── Epoll.cpp│   ├── Epoll.h│   ├── Server.cpp│   └── Server.h├── timer│   ├── Timer.cpp│   └── Timer.h└── utils└── Utils.h

2 Buffer

为了高效便捷的实现数据的存取,我们自定义了一个缓冲区数据结构,其主要功能是实现数据的读取、写入和空间自增。下图是缓冲区的结构图,我们使用vector<char>作为最基本的数据结构,实现了一个队列结构的缓冲区,并定义了三个指针,分别是头指针、读指针和写指针(这里的指针都使用下标代替,并非真正的指针)。

在这里插入图片描述

2.1 向Buffer写入数据

写指针被初始化为0,指向vector的首元素。写入数据时,需要提供待写入字符串的首地址和长度。待写入字符串被拷贝到写指针指向的位置,此时可能会出现空间不够的情况。

  • 待写入数据长度小于等于预留区域和空闲区域长度之和:

    将数据区域搬运到头指针指向的位置,并更新读指针和写指针

  • 待写入数据长度大于预留区域和空闲区域长度之和:

    利用vector的动态扩容机制,增大缓冲区长度。

2.2 从Buffer读取数据

读指针被初始化为0,从读指针开始读取字符串,直到写指针为止。

2.3 动态扩容

当预留区域和空闲区域加在一起也不够写下新的数据时,需要对缓冲区进行扩容。vector.resize()函数将被调用,从而实现了扩充容量。

2.4 从socket中读取数据

由于从socket读取的数据长度未知,直接向buffer中写入的数据的长度可能会超过vector的最大容量而引发错误,而增大buffer的初始容量又会浪费资源。因此,可以借用一个大容量的栈区作为缓冲。buffer和栈区同时接收数据,之后再将栈区中的数据写入buffer中,这样便可巧妙地解决问题。

2.5 具体实现

#ifndef TINYWEBSERVER_BUFFER_H
#define TINYWEBSERVER_BUFFER_H
#include <vector>
#include <atomic> // atomic
#include <sys/uio.h> // iovec readv
#include <cstring> // errno
#include <iostream>
#include <cassert> // assert
#include <unistd.h> // writeclass Buffer {
private:std::vector<char> buffer_;std::atomic<size_t> readIdx_;std::atomic<size_t> writeIdx_;int STACK_LEN;public:explicit Buffer(int init_size=1024, int stack_len=4096);~Buffer()=default;size_t getContentLen();size_t getBufferLen();size_t getLeftLen();size_t getRealLeftLen();char* getReadPtr();const char *getConstReadPtr();char* getWritePtr();ssize_t readFd(int fd, int* Errno);ssize_t writeFd(int fd, int* Errno);void append(const char* str, size_t len);void append(const std::string& str);void addWriteIdx(size_t len);void addReadIdx(size_t len);void addReadIdxUntil(const char* ed);// memset缓冲区void resetBuffer();std::string getStringAndReset();
private:char* getBeginPtr();bool confirmSpace(size_t len);
};#endif //TINYWEBSERVER_BUFFER_H
#include "Buffer.h"Buffer::Buffer(int init_size, int stack_len):buffer_(init_size), readIdx_(0), writeIdx_(0), STACK_LEN(stack_len){}// buffer中数据长度
size_t Buffer:: getContentLen() {return writeIdx_ - readIdx_;
}
// buffer的实际长度
size_t Buffer::getBufferLen() {return buffer_.size();
}
// 返回buffer的剩余空间
size_t Buffer::getLeftLen() {return getBufferLen() - writeIdx_;
}
// 返回buffer包含预留空间真正剩下的空间
size_t Buffer::getRealLeftLen() {return getBufferLen() - (writeIdx_ - readIdx_);
}
// 返回读指针
char *Buffer::getReadPtr() {return &buffer_[readIdx_];
}
//返回写指针
char *Buffer::getWritePtr() {return &buffer_[writeIdx_];
}// 返回buffer首元素的指针
char* Buffer::getBeginPtr() {return &buffer_[0];
}// 移动写指针
void Buffer::addWriteIdx(size_t len) {writeIdx_ += len;
}
// 移动读指针
void Buffer::addReadIdx(size_t len) {assert(len <= getContentLen());readIdx_ += len;
}// 增加读指针移到ed位置
void Buffer::addReadIdxUntil(const char *ed) {assert(getReadPtr() <= ed && ed <= getWritePtr());addReadIdx(ed - getReadPtr());
}ssize_t Buffer::readFd(int fd, int* Errno) {char stack_buf[STACK_LEN];iovec iv[2];size_t leftLen = getLeftLen();iv[0].iov_base = getWritePtr();iv[0].iov_len = leftLen;iv[1].iov_base = stack_buf;iv[1].iov_len = STACK_LEN;ssize_t len = readv(fd, iv, 2);if (len < 0) {// 记录报错信息*Errno = errno;return len;}// 刚好将buffer填满else if (static_cast<size_t>(len) <= leftLen) {addWriteIdx(len);}// 读入的数据超过bufferelse {writeIdx_ = getBufferLen();// 将栈区的数据复制到buffer中append(stack_buf, len - static_cast<ssize_t>(leftLen));}return len;
}ssize_t Buffer::writeFd(int fd, int* Errno) {ssize_t len = write(fd, getReadPtr(), getContentLen());if (len < 0) {*Errno = errno;return len;}addReadIdx(len);return len;
}// 添加char[]到buffer中
void Buffer::append(const char* str, size_t len) {assert(str);confirmSpace(len);std::copy(str, str+len, getWritePtr());addWriteIdx(len);
}
// 添加string到buffer中
void Buffer::append(const std::string &str) {append(str.c_str(), str.length());
}// 将buffer中内容转换成string,并清空buffer
std::string Buffer::getStringAndReset() {std::string str(getReadPtr(), getWritePtr());resetBuffer();return str;
}
// 重置buffer
void Buffer::resetBuffer() {writeIdx_ = 0;readIdx_ = 0;memset(&buffer_[0], 0, buffer_.size());
}// 分配空间,扩容
bool Buffer::confirmSpace(size_t len) {// 剩余空间能够满足写入lenif (getLeftLen() >= len) {return false;}// 不够Len,但是能够借用预留空间满足要求else if (getRealLeftLen() >= len) {auto contentLen = getContentLen();std::copy(getBeginPtr() + readIdx_, getBeginPtr() + writeIdx_, getBeginPtr());readIdx_ = 0;writeIdx_ = contentLen;assert(contentLen == getContentLen());}// 即使挪动也不够空间,需要对vector扩容else {buffer_.resize(writeIdx_ + len + 1);}assert(getLeftLen() >= len);return true;
}const char *Buffer::getConstReadPtr() {return &buffer_[readIdx_];
}

3 日志系统

日志系统是实现整个webserver项目的首要前提,利用日志可以方便地调试代码,记录输出。

为了使输出的日志清晰明了,日志信息被划分成了不同的等级:

  • DEBUG
  • INFO
  • WARN
  • ERROR
  • FATAL

服务器初始化时提供了一个日志等级的参数,只有大于等于该等级的日志条目才会输出。

该日志系统主要使用异步方式写入日志信息,由一个队列负责维护要输出的日志信息。其他线程要打印日志时,调用函数将内容插入到队列中;日志输出线程负责从队列中取出日志信息,并将其写入日志文件中。

3.1 生产者-消费者模型

在上述工作流程中,其他线程和输出线程构成一个生产者-消费者模型。其他线程在队列不满的情况下,插入日志信息并通知日志输出线程取出日志信息,否则挂起等待;而日志输出线程在队列不空的情况,从队列中取出日志信息并通知其他线程插入日志信息,否则挂起等待。

为了同步其他线程和日志输出线程,可以使用条件变量。

3.2 数据一致

由于该日志系统会被其他不同的线程调用,需要保证同一时间只有一个线程访问日志队列。可能会出现以下竞态情况:

  • 日志队列中插入日志和取出日志时,对日志队列的访问
  • 其他线程要插入日志时,会在buffer内构造日志信息

在这里插入图片描述

3.3 代码

#ifndef TINYWEBSERVER_LOG_H
#define TINYWEBSERVER_LOG_H#include <string>
#include <thread>
#include <mutex>
#include <cstdarg>
#include <sys/time.h>
#include "../buffer/Buffer.h"
#include "LogQueue.h"
#include "../utils/Utils.h"
#include "LogLevel.h"// 日志输出位置
enum LogTarget {LOG_TARGET_NONE = 0,LOG_TARGET_CONSOLE = 1,LOG_TARGET_FILE = 2
};class Log {
private:const char* saveDir_; // 日志存储路径char* filename_; // 初始化提供的文件名const char* suffix_; // 日志文件名后缀std::unique_ptr<LogQueue<std::string>> log_Queue_; // 日志队列std::unique_ptr<std::thread> workThread_; // 处理写日志的线程FILE *fp_; // 日志文件描述符LogTarget target_; // 日志文件输出位置LogLevel::value logLevel_; // 日志级别std::mutex mtx_;Buffer buf_; // 缓冲区bool isRun_;unsigned long long logCnt;bool isAsync_;static const int MAX_LINES = 50000;static const size_t MAX_FILENAME_LEN = 50; // 最大文件名限制
public:void init(LogTarget target, const char* save_dir, const char* suffix, LogLevel::value logLevel,size_t maxQueueSize =  1024); // 初始化日志系统static void asyncWriteLogThread(); // 工作线程将日志异步写入文件的函数bool initLogFile(); // 初始化日志文件// 外部调用接口,输出不同类型的日志信息void addLog(LogLevel::value type, const char *format, ...);bool isRun() const { return isRun_; }// 外部获取实例的接口static Log* getInstance();LogLevel::value getLevel() { return logLevel_; };void flush();void AsyncWrite_();LogTarget getTarget() {return  target_;}private:Log();~Log();void setEntryTime(); // 设置日志条目时间头void setEntryType(LogLevel::value t); // 设置日志条目类型void setEntryMsg(const std::string &msg);void appendEntry(const std::string& entry);
};#define LOG_BASE(level, format, ...) \do {\Log* l = Log::getInstance();\if (l->isRun() && l->getLevel() <= level && l->getTarget() != LOG_TARGET_NONE) {\l->addLog(level, format, ##__VA_ARGS__); \l->flush();\}\} while(0);#define LOG_DEBUG(format, ...) do {LOG_BASE(LogLevel::value::DEBUG, format, ##__VA_ARGS__)} while(0);
#define LOG_INFO(format, ...) do {LOG_BASE(LogLevel::value::INFO, format, ##__VA_ARGS__)} while(0);
#define LOG_WARN(format, ...) do {LOG_BASE(LogLevel::value::WARN, format, ##__VA_ARGS__)} while(0);
#define LOG_ERROR(format, ...) do {LOG_BASE(LogLevel::value::ERROR, format, ##__VA_ARGS__)} while(0);
#define LOG_FATAL(format, ...) do {LOG_BASE(LogLevel::value::FATAL, format, ##__VA_ARGS__)} while(0);
#endif //TINYWEBSERVER_LOG_H
#include "Log.h"Log::Log() {saveDir_ = nullptr;filename_ = nullptr;suffix_ = nullptr;log_Queue_ = nullptr;workThread_ = nullptr;isRun_ = true;fp_ = nullptr;target_ = LOG_TARGET_CONSOLE;logCnt = 0;
}Log::~Log() {printf("close logging...\n");if (log_Queue_->size()) {sleep(2);}isRun_ = false;fflush(fp_);if (fp_ != nullptr) {fclose(fp_);fp_ = nullptr;}delete[] filename_;
}Log* Log::getInstance() {static Log log_;return &log_;
}void
Log::init(LogTarget target, const char *save_dir, const char *suffix, LogLevel::value logLevel,size_t maxQueueSize) {saveDir_ = save_dir;suffix_ = suffix;logLevel_ = logLevel;filename_ = new char(MAX_FILENAME_LEN);target_ = target;if (maxQueueSize > 0) {isAsync_ = true;if (!log_Queue_) {std::unique_ptr<LogQueue<std::string>> q(new LogQueue<std::string>(maxQueueSize));log_Queue_ = std::move(q);std::unique_ptr<std::thread> t(new std::thread(asyncWriteLogThread));workThread_ = std::move(t);}} else {isAsync_ = false;}if (!initLogFile()) {printf("start loging failed...\n");return ;}
}void Log::asyncWriteLogThread() {Log::getInstance()->AsyncWrite_();
}bool Log::initLogFile() {if (target_ == LOG_TARGET_CONSOLE) {fp_ = stdout;} else if (target_ == LOG_TARGET_FILE){char time_str[25];util::Date::getDateTimeByFormat(time_str, 25, "%Y_%m_%d_%H_%M_%S");snprintf(filename_, MAX_FILENAME_LEN, "%s%s", time_str, suffix_);fp_ = util::File::createFile(saveDir_, filename_);if (fp_ == nullptr) {return false;}} else {fp_ = nullptr;return true;}printf("start logging...\n");return true;
}void Log::setEntryTime() {char time_str[25];util::Date::getDateTime(time_str, 25);size_t str_len = strlen(time_str);time_str[str_len] = ' ';buf_.append(time_str, str_len + 1); // 追加空格
}void Log::setEntryType(LogLevel::value t) {buf_.append(LogLevel::toString(t) + std::string(" "));
}void Log::setEntryMsg(const std::string &msg) {buf_.append(msg + std::string("\n"));
}void Log::appendEntry(const std::string &entry) {log_Queue_->push(entry);
}void Log::flush() {if (isAsync_) {log_Queue_->flush();}fflush(fp_);
}void Log::addLog(LogLevel::value type, const char *format, ...) {struct timeval now = {0, 0};gettimeofday(&now, nullptr);time_t tSec = now.tv_sec;struct tm *sysTime = localtime(&tSec);struct tm t = *sysTime;va_list vaList;// 向buf_中添加数据,如果多线程访问需要确保只有一个线程访问buf_std::unique_lock<std::mutex> locker(mtx_);int n = snprintf(buf_.getWritePtr(), 128, "%d-%02d-%02d %02d:%02d:%02d.%06ld ",t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,t.tm_hour, t.tm_min, t.tm_sec, now.tv_usec);buf_.addWriteIdx(n);setEntryType(type);va_start(vaList, format);int m = vsnprintf(buf_.getWritePtr(), buf_.getLeftLen(), format, vaList);va_end(vaList);buf_.addWriteIdx(m);buf_.append("\n\0", 2);if (isAsync_ && log_Queue_ && !log_Queue_->full()) {log_Queue_->push(buf_.getStringAndReset());} else {fputs(buf_.getReadPtr(), fp_); // 这一部分不确定作用}
}void Log::AsyncWrite_() {std::string str;while (log_Queue_->pop(str)) {std::lock_guard<std::mutex> locker(mtx_);fputs(str.c_str(), fp_);}
}
#ifndef TINYWEBSERVER_LOGQUEUE_H
#define TINYWEBSERVER_LOGQUEUE_H#include <queue>
#include <cassert>
#include <mutex>
#include <condition_variable>
template <typename T>
class LogQueue {
private:std::deque<T> log;size_t capacity;bool deleted;std::mutex mtx_;std::condition_variable consumer_cv;std::condition_variable producer_cv;
public:explicit LogQueue(size_t c);~LogQueue();void push(const T &data);bool pop(T &item);size_t size();bool empty();void onDelete();bool full();void flush();
};template<typename T>
void LogQueue<T>::flush() {consumer_cv.notify_one();
}template<typename T>
bool LogQueue<T>::full() {std::lock_guard<std::mutex> locker(mtx_);return log.size() >= capacity;
}template<typename T>
LogQueue<T>::LogQueue(size_t c): capacity(c), deleted(false) {assert(c > 0);
}template<typename T>
LogQueue<T>::~LogQueue() {onDelete();
}template<typename T>
void LogQueue<T>::onDelete() {{std::lock_guard<std::mutex> locker(mtx_);deleted = true;log.clear();}consumer_cv.notify_one();producer_cv.notify_one();
}template<typename T>
bool LogQueue<T>::empty() {std::lock_guard<std::mutex> locker(mtx_);return log.empty();
}template<typename T>
size_t LogQueue<T>::size() {std::lock_guard<std::mutex> locker(mtx_);return log.size();
}
// 消费者读取日志
template<typename T>
bool LogQueue<T>::pop(T &item) {std::unique_lock<std::mutex> locker(mtx_);while(log.empty()) {consumer_cv.wait(locker);if (deleted) {return false;}}item = log.front();log.pop_front();producer_cv.notify_one();return true;
}// 生产者插入日志
template<typename T>
void LogQueue<T>::push(const T &data) {std::unique_lock<std::mutex> locker(mtx_);// 直至log.size() <= capacity  缓冲区未满while(log.size() >= capacity) {producer_cv.wait(locker);}log.push_back(data);consumer_cv.notify_one();
}#endif //TINYWEBSERVER_LOGQUEUE_H
#ifndef TINYWEBSERVER_LOGLEVEL_H
#define TINYWEBSERVER_LOGLEVEL_H
class LogLevel
{
public:enum class value{UNKNOWN =0,DEBUG,INFO,WARN,ERROR,FATAL};static const char *toString(value level){switch (level){case LogLevel::value::DEBUG: return "[DEBUG]:";case LogLevel::value::INFO: return  "[INFO] :";case LogLevel::value::WARN: return  "[WARN] :";case LogLevel::value::ERROR: return "[ERROR]:";case LogLevel::value::FATAL: return "[FATAL]:";case LogLevel::value::OFF: return   "[OFF]  :";default: return "UNKNOW";}}
};
#endif //TINYWEBSERVER_LOGLEVEL_H

4 定时器

客户端和服务器建立TCP连接后,客户端可能会不再发送数据,此时需要断开连接释放资源。在服务器初始化时指定超时时间,当客户端和服务器之间未发生通信的时间超过超时时间后,便关闭二者的连接。

定时器的数据结构基于小跟堆实现,堆顶是距离过期最近的连接。当客户端连接服务器后,向定时器内插入超时关闭连接事件。每当服务器收到来自客户端的请求时,更新定时器内的超时时间。当某个连接超时时,将其从堆顶取出,执行关闭连接回调函数。

4.1 调整堆中元素操作

定时器的堆基于vector动态扩容数组实现。以下定义了两个调整堆中元素的操作:

  • 向上调整:将某个结点不断地与其父结点比较交换,直到不能交换为止
  • 向下调整:将某个结点不断地与其子结点比较交换,直到不能交换为止

4.2 堆的操作

4.2.1 增

向堆中插入元素,可以现将新插入的元素放入vector最后位置,并执行向上调整操作。

4.2.2 删

将堆顶元素与vector最后一个元素交换位置,并将记录元素个数的变量递减,然后执行向上调整操作。

4.2.3 改

利用元素与堆中下标的映射数组,找到在堆中的位置,然后分别执行向上调整和向下调整操作。

4.2.4 查

取出堆顶元素。

4.3 代码

#ifndef TINYWEBSERVER_TIMER_H
#define TINYWEBSERVER_TIMER_H
#include <vector>
#include <functional>
#include <cassert>
#include <chrono>
#include <unordered_map>
#include <algorithm>
#include <iostream>
#include <atomic>
#include "../log/Log.h"
typedef std::function<void()> TimerCallback;
typedef std::chrono::high_resolution_clock Clock;
typedef std::chrono::milliseconds MS;
typedef Clock::time_point TimeStamp;// 定时器结点
struct TimerNode {int id_; // 定时器idTimeStamp expires_; // 定时器过期时间点TimerCallback cb_; // 回调函数bool operator<(const TimerNode &tn) const {return expires_ < tn.expires_;}bool operator>(const TimerNode &tn) const {return expires_ > tn.expires_;}
};// 堆定时器,存储定时事件
class Timer {
private:// 定时器堆,采用vector数组方式存储std::vector<TimerNode> heap_;std::unordered_map<int, size_t> ref_;  // 从id到heap中的下标映射,方便直接操作某个nodestd::atomic<size_t> si_{};private:void up(size_t u); // 将某个结点向上调整的操作void down(size_t u); // 将某个结点向下调整的操作void pop(); // 删除堆顶的结点TimerNode &top(); // 获得堆顶的结点void del(size_t i); // 删除下标为i的结点 并执行回调函数void swap_(size_t t1, size_t t2); // 交换两个下标的位置public:explicit Timer(size_t cnt);~Timer();int getNextTick(); // 返回最近的定时器事件超时的间隔时间void reset(int id, int timeout); // 重新设置某个结点的过期时间void reset(int id, int timeout, TimerCallback &cb); // 重设某个结点的过期时间和回调函数void execCb(int id); // 执行id的回调函数void tick(); // 处理堆中过期的定时器void push(int id, int timeout, const TimerCallback &cb);bool empty();
};#endif //TINYWEBSERVER_TIMER_H
#include "Timer.h"#include <utility>void Timer::up(size_t u) {while(u != 1 && heap_[u] < heap_[u/2]) {swap_(u, u / 2);u /= 2;}
}
// 从1开始 1 2 3 4 ... 1是根结点
void Timer::down(size_t u) {size_t t = u;if (u * 2 <= si_ && heap_[u*2] < heap_[t]) t = u * 2;if (u * 2 + 1 <= si_ && heap_[u * 2 + 1] < heap_[t]) t = u * 2 + 1;if (t != u) {swap_(u, t);down(t);}
}void Timer::swap_(size_t t1, size_t t2) {assert(t1 >= 1 && t1 < heap_.size());assert(t2 >= 1 && t2 <= heap_.size());std::swap(heap_[t1], heap_[t2]);ref_[heap_[t1].id_] = t1;ref_[heap_[t2].id_] = t2;
}
// 删除顶部结点
void Timer::pop() {assert(si_ > 0);del(1);
}TimerNode& Timer::top() {return heap_[1];
}// 删除给定结点i,将其和最后一个结点交换,之后执行up和down操作
void Timer::del(size_t i) {assert(i > 0 && i <= si_);swap_(i, si_);-- si_;ref_.erase(heap_.back().id_);heap_.pop_back();up(i);down(i);
}
// 执行id的回调函数
void Timer::execCb(int id) {if (si_ == 0 || ref_.count(id) == 0) {return ;}auto idx = ref_[id];auto &node = heap_[idx];node.cb_();del(idx);
}void Timer::push(int id, int timeout, const TimerCallback &cb) {assert(id >= 0);if (ref_.count(id)) {auto idx = ref_[id];auto &node = heap_[idx];node.expires_ = Clock::now() + MS(timeout);node.cb_ = cb;up(idx);down(idx);} else {LOG_INFO("增加计时时间 id: %d timeout: %d", id, timeout);heap_.push_back({id, MS(timeout) + Clock::now(), cb});++ si_;ref_[id] = si_;up(si_);}
}Timer::Timer(size_t cnt) {
//    Log::INFO("%s", "Timer start...");heap_.reserve(cnt + 1);heap_.emplace_back();si_ = 0;
}void Timer::reset(int id, int timeout, TimerCallback &cb) {assert(id >= 0);auto idx = ref_[id];auto &node = heap_[idx];node.expires_ = Clock::now() + MS(timeout);node.cb_ = cb;down(idx);up(idx);
}void Timer::reset(int id, int timeout) {assert(id >= 0);auto idx = ref_[id];auto &node = heap_[idx];node.expires_ = Clock::now() + MS(timeout);down(idx);up(idx);
}void Timer::tick() {while(si_) {auto &node = top();if (std::chrono::duration_cast<MS>(node.expires_ - Clock::now()).count() > 0)break;LOG_INFO("timer %d is expired", node.id_);node.cb_();pop();}
}bool Timer::empty() {return si_ == 0;
}int Timer::getNextTick() {tick();size_t res = -1;if (si_) {res = std::chrono::duration_cast<MS>(top().expires_ - Clock::now()).count();if (res < 0) {res = 0;}}return res;
}Timer::~Timer() {heap_.clear();ref_.clear();
}

5 线程池

由于线程的创建和销毁需要开销,频繁创建和销毁线程会影响服务器的性能。因此,服务器维护一个预先创建好的线程池。每当任务队列中有任务时,某个线程将其从中取出并执行。执行完成后,继续等待任务。

在这里插入图片描述

在这里使用一个条件变量,当任务队列为空时阻塞线程,当有任务插入队列时,通知线程执行任务。

5.1 代码

#ifndef TINYWEBSERVER_THREADPOOL_H
#define TINYWEBSERVER_THREADPOOL_H
#include <queue>
#include <functional>
#include <mutex>
#include <cassert>
#include <thread>
#include <condition_variable>
#include <unistd.h>
#include "../log/Log.h"
class ThreadPool {
private:struct Pool {std::mutex mtx_;bool isRun = true;std::condition_variable cv;std::queue<std::function<void()>>taskQueue_;};std::shared_ptr<Pool> pool_;//private:
//    static void work(); // 工作函数,从任务队列中取出任务并执行
public:explicit ThreadPool(int max_thread_cnt);ThreadPool() = default;// 定义移动构造函数ThreadPool(ThreadPool&&) = default;~ThreadPool();bool addTask(std::function<void()> &&f); // 外部调用接口void resetTaskQueue();
};#endif //TINYWEBSERVER_THREADPOOL_H
#include "ThreadPool.h"ThreadPool::ThreadPool(int max_thread_cnt): pool_(std::make_shared<Pool>()) {assert(max_thread_cnt > 0);for (int i = 0; i < max_thread_cnt; ++ i) {printf("init thread %d\n", i);std::thread([pool = pool_, i]{std::unique_lock<std::mutex> locker(pool->mtx_);while(true) {if (!pool->taskQueue_.empty()) {
//                    LOG_INFO("thread pool: thread %d process task", i);// 有任务,开始干活// 这个地方使用move,提高效率auto task = std::move(pool->taskQueue_.front());pool->taskQueue_.pop();locker.unlock();task(); // 处理任务locker.lock();} else if (!pool->isRun) {break;} else {pool->cv.wait(locker);}}}).detach();}
}ThreadPool::~ThreadPool() {if (static_cast<bool>(pool_)){{std::lock_guard<std::mutex> locker(pool_->mtx_);pool_->isRun = false;}pool_->cv.notify_all();}
}void ThreadPool::resetTaskQueue() {std::queue<std::function<void()>> q;swap(q, pool_->taskQueue_);
}bool ThreadPool::addTask(std::function<void()> &&f) {{std::lock_guard<std::mutex> locker(pool_->mtx_);pool_->taskQueue_.emplace(std::forward<std::function<void()>>(f));}
//    LOG_INFO("thead pool: %s", "add task");pool_->cv.notify_one();return true;
}

6 数据库连接池

和线程池类似,数据库的连接池由一个队列来维护与数据库的多个连接。初始化时,创建n个数据库连接并将其插入到队列中。在需要访问数据库时,从队列中取出一个连接进行数据库读写操作。在读写完数据后重新将连接插入到队列中。

6.1 RAII

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种C++编程惯用法,用于管理资源(如内存、文件句柄、网络连接等),确保它们在对象的生命周期内得到正确的管理和释放。

  • 资源绑定到对象的生命周期: 资源在对象创建时被获取,并在对象销毁时被释放。构造函数负责获取资源,析构函数负责释放资源。
  • 自动管理:通过栈上对象的自动创建和销毁,避免手动管理资源的复杂性和潜在错误(如资源泄漏)。

数据库连接的管理采用RAII机制,可以简化资源管理。

6.2 代码

#ifndef SQLCONNPOOL_H
#define SQLCONNPOOL_H#include <mysql/mysql.h>
#include <string>
#include <queue>
#include <mutex>
#include <semaphore.h>
#include <thread>
#include <cassert>
#include "../log/Log.h"class SqlConnPool {
public:static SqlConnPool *Instance();MYSQL *GetConn();void FreeConn(MYSQL * conn);int GetFreeConnCount();void Init(const char* host, int port,const char* user,const char* pwd, const char* dbName, int connSize);void ClosePool();private:SqlConnPool();~SqlConnPool();int MAX_CONN_;int useCount_;int freeCount_;std::queue<MYSQL *> connQue_;std::mutex mtx_;sem_t semId_;
};#endif // SQLCONNPOOL_H
#include "sqlconnpool.h"
using namespace std;SqlConnPool::SqlConnPool() {useCount_ = 0;freeCount_ = 0;
}SqlConnPool* SqlConnPool::Instance() {static SqlConnPool connPool;return &connPool;
}void SqlConnPool::Init(const char* host, int port,const char* user,const char* pwd, const char* dbName,int connSize = 10) {assert(connSize > 0);for (int i = 0; i < connSize; i++) {MYSQL *sql = nullptr;sql = mysql_init(sql);if (!sql) {LOG_ERROR("MySql init error!");assert(sql);}sql = mysql_real_connect(sql, host,user, pwd,dbName, port, nullptr, 0);if (!sql) {LOG_ERROR("MySql Connect error!");}connQue_.push(sql);}MAX_CONN_ = connSize;sem_init(&semId_, 0, MAX_CONN_);
}MYSQL* SqlConnPool::GetConn() {MYSQL *sql = nullptr;if(connQue_.empty()){LOG_WARN("SqlConnPool busy!");return nullptr;}sem_wait(&semId_);{lock_guard<mutex> locker(mtx_);sql = connQue_.front();connQue_.pop();}return sql;
}void SqlConnPool::FreeConn(MYSQL* sql) {assert(sql);lock_guard<mutex> locker(mtx_);connQue_.push(sql);sem_post(&semId_);
}void SqlConnPool::ClosePool() {lock_guard<mutex> locker(mtx_);while(!connQue_.empty()) {auto item = connQue_.front();connQue_.pop();mysql_close(item);}mysql_library_end();        
}int SqlConnPool::GetFreeConnCount() {lock_guard<mutex> locker(mtx_);return connQue_.size();
}SqlConnPool::~SqlConnPool() {ClosePool();
}
#ifndef SQLCONNRAII_H
#define SQLCONNRAII_H
#include "sqlconnpool.h"/* 资源在对象构造初始化 资源在对象析构时释放*/
class SqlConnRAII {
public:SqlConnRAII(MYSQL** sql, SqlConnPool *connpool) {assert(connpool);*sql = connpool->GetConn();sql_ = *sql;connpool_ = connpool;}~SqlConnRAII() {if(sql_) { connpool_->FreeConn(sql_); }}private:MYSQL *sql_;SqlConnPool* connpool_;
};#endif //SQLCONNRAII_H

7 HTTP层处理

7.1 HTTP解析

HTTP请求的格式如下图所示:

在这里插入图片描述

HTTP请求报文遵循着规定的格式,我们只需要按要求即可准确解析。

报文分为三个部分:

  • 请求行:包含请求方法(GET,POST,…),请求url和HTTP版本。中间由空格分隔,最后有个\r\n
  • 请求头:包含若干个请求体,由key: value组成,末尾有\r\n。最后一个请求头最后有两个\r\n
  • 请求体(可有可无)
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

由于HTTP报文每一行最后都有一个\r\n,我们可以从缓冲区中搜索该字符串,将每一行截取出来再进行解析。整个过程由于分三个阶段进行,因此可使用状态机解决。为此,我们规定4个状态,分别是解析请求行、解析请求头、解析请求体和结束。在不同的状态执行不同的操作,来解析不同的内容。

7.1.1 代码


#ifndef TINYWEBSERVER_PARSEHTTPREQUEST_H
#define TINYWEBSERVER_PARSEHTTPREQUEST_H#include <unordered_map>
#include <regex>
#include <unordered_set>
#include <mysql/mysql.h>
#include "../buffer/Buffer.h"
#include "../utils/Utils.h"
#include "../log/Log.h"
#include "../pool/sqlconnpool.h"
#include "../pool/sqlconnRAII.h"class ParseHttpRequest {
public:// 当前的处理状态enum Status {PARSE_LINE,PARSE_HEADERS,PARSE_BODY,FINISH};
private:std::string version_; // HTTP版本std::unordered_map<std::string, std::string> headers_; // 头部字段Status state_ = PARSE_LINE;std::string url_;std::string method_;std::string body_;static const std::unordered_set<std::string> DEFAULT_HTML;static const std::unordered_map<std::string, int>DEFAULT_HTML_TAG;std::unordered_map<std::string, std::string> post_;
public:ParseHttpRequest();~ParseHttpRequest();void init();bool parse(Buffer &buf);void parse_url();bool parseRequestLine(const std::string &request_line);void parseRequestHeader(const std::string &header_line);void parseRequestBody(const std::string &body);std::string &method();std::string &version();std::string &path();bool keepAlive();void parsePost();void parseFromUrlencoded();static bool userVerify(const std::string &name, const std::string &pwd, bool isLogin);static int convertHex(char ch);
};#endif //TINYWEBSERVER_PARSEHTTPREQUEST_H
#include "ParseHttpRequest.h"
using namespace std;const unordered_set<string> ParseHttpRequest::DEFAULT_HTML{"/index", "/register", "/login","/welcome", "/video", "/picture", };const unordered_map<string, int> ParseHttpRequest::DEFAULT_HTML_TAG {{"/register.html", 0}, {"/login.html", 1},  };void ParseHttpRequest::init() {method_ = url_ = version_ = body_ = "";state_ = PARSE_LINE;headers_.clear();post_.clear();
}bool ParseHttpRequest::keepAlive() {if(headers_.count("Connection") == 1) {return headers_.find("Connection")->second == "keep-alive" && version_ == "1.1";}return false;
}bool ParseHttpRequest::parse(Buffer& buff) {const char CRLF[] = "\r\n";if(buff.getContentLen() <= 0) {return false;}while(buff.getContentLen() && state_ != FINISH) {const char* lineEnd = search(buff.getReadPtr(), buff.getWritePtr(), CRLF, CRLF + 2);std::string line(buff.getConstReadPtr(), lineEnd);switch(state_){case PARSE_LINE:if(!parseRequestLine(line)) {return false;}parse_url();break;case PARSE_HEADERS:parseRequestHeader(line);if(buff.getContentLen() <= 2) {state_ = FINISH;}break;case PARSE_BODY:parseRequestBody(line);break;default:break;}if(lineEnd == buff.getWritePtr()) { break; }buff.addReadIdxUntil(lineEnd + 2);}LOG_DEBUG("[%s], [%s], [%s]", method_.c_str(), url_.c_str(), version_.c_str());return true;
}void ParseHttpRequest::parse_url() {if(url_ == "/") {url_ = "/index.html";}else {for(auto &item: DEFAULT_HTML) {if(item == url_) {url_ += ".html";break;}}}
}bool ParseHttpRequest::parseRequestLine(const string& line) {regex patten("^([^ ]*) ([^ ]*) HTTP/([^ ]*)$");smatch subMatch;if(regex_match(line, subMatch, patten)) {method_ = subMatch[1];url_ = subMatch[2];version_ = subMatch[3];state_ = PARSE_HEADERS;return true;}LOG_ERROR("RequestLine Error");return false;
}void ParseHttpRequest::parseRequestHeader(const string& line) {regex patten("^([^:]*): ?(.*)$");smatch subMatch;if(regex_match(line, subMatch, patten)) {headers_[subMatch[1]] = subMatch[2];}else {state_ = PARSE_BODY;}
}void ParseHttpRequest::parseRequestBody(const string& line) {body_ = line;parsePost();state_ = FINISH;LOG_DEBUG("Body:%s, len:%d", line.c_str(), line.size());
}int ParseHttpRequest::convertHex(char ch) {if(ch >= 'A' && ch <= 'F') return ch -'A' + 10;if(ch >= 'a' && ch <= 'f') return ch -'a' + 10;return ch;
}void ParseHttpRequest::parsePost() {if(method_ == "POST" && headers_["Content-Type"] == "application/x-www-form-urlencoded") {parseFromUrlencoded();if(DEFAULT_HTML_TAG.count(url_)) {int tag = DEFAULT_HTML_TAG.find(url_)->second;LOG_DEBUG("Tag:%d", tag);if(tag == 0 || tag == 1) {bool isLogin = (tag == 1);if(userVerify(post_["username"], post_["password"], isLogin)) {url_ = "/welcome.html";}else {url_ = "/error.html";}}}}
}void ParseHttpRequest::parseFromUrlencoded() {if(body_.size() == 0) { return; }string key, value;int num = 0;int n = body_.size();int i = 0, j = 0;for(; i < n; i++) {char ch = body_[i];switch (ch) {case '=':key = body_.substr(j, i - j);j = i + 1;break;case '+':body_[i] = ' ';break;case '%':num = convertHex(body_[i + 1]) * 16 + convertHex(body_[i + 2]);body_[i + 2] = num % 10 + '0';body_[i + 1] = num / 10 + '0';i += 2;break;case '&':value = body_.substr(j, i - j);j = i + 1;post_[key] = value;LOG_DEBUG("%s = %s", key.c_str(), value.c_str());break;default:break;}}assert(j <= i);if(post_.count(key) == 0 && j < i) {value = body_.substr(j, i - j);post_[key] = value;}
}bool ParseHttpRequest::userVerify(const string &name, const string &pwd, bool isLogin) {if(name.empty() || pwd.empty()) { return false; }LOG_INFO("Verify name:%s pwd:%s", name.c_str(), pwd.c_str());MYSQL* sql;SqlConnRAII give_me_a_name(&sql,  SqlConnPool::Instance());assert(sql);bool flag = false;char order[256] = { 0 };MYSQL_RES *res = nullptr;if(!isLogin) { flag = true; }/* 查询用户及密码 */snprintf(order, 256, "SELECT username, password FROM user WHERE username='%s' LIMIT 1", name.c_str());LOG_DEBUG("%s", order);if(mysql_query(sql, order)) {mysql_free_result(res);return false;}res = mysql_store_result(sql);mysql_num_fields(res);mysql_fetch_fields(res);while(MYSQL_ROW row = mysql_fetch_row(res)) {LOG_DEBUG("MYSQL ROW: %s %s", row[0], row[1]);string password(row[1]);/* 注册行为 且 用户名未被使用*/if(isLogin) {if(pwd == password) { flag = true; }else {flag = false;LOG_DEBUG("pwd error!");}}else {flag = false;LOG_DEBUG("user used!");}}mysql_free_result(res);/* 注册行为 且 用户名未被使用*/if(!isLogin && flag) {LOG_DEBUG("regirster!");bzero(order, 256);snprintf(order, 256,"INSERT INTO user(username, password) VALUES('%s','%s')", name.c_str(), pwd.c_str());LOG_DEBUG( "%s", order)if(mysql_query(sql, order)) {LOG_DEBUG( "Insert error!");flag = false;}flag = true;}SqlConnPool::Instance()->FreeConn(sql);LOG_DEBUG( "UserVerify success!!");return flag;
}std::string &ParseHttpRequest::path(){return url_;
}
std::string &ParseHttpRequest::method() {return method_;
}std::string &ParseHttpRequest::version() {return version_;
}ParseHttpRequest::ParseHttpRequest() {init();
}ParseHttpRequest::~ParseHttpRequest() {}

7.2 HTTP响应

以下是HTTP响应报文的一个例子,主要包含响应行、响应头和响应体。

HTTP/1.1 200 OK
Date: Fri, 19 Jul 2024 10:00:00 GMT
Server: Apache/2.4.41 (Ubuntu)
Last-Modified: Mon, 28 Jun 2024 14:30:00 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 305
Connection: close<!DOCTYPE html>
<html>
<head><title>Example Page</title>
</head>
<body><h1>Welcome to Example Page</h1><p>This is a sample HTML page.</p>
</body>
</html>

根据对HTTP请求的处理结果,生成相应的HTTP响应结果。

响应行中包含HTTP版本、响应状态码和摘要。

响应头中包含连接状态、返回的文件类型和长度。

响应体中包含返回的资源文件。

7.2.1 代码

#ifndef TINYWEBSERVER_HTTPRESPONSE_H
#define TINYWEBSERVER_HTTPRESPONSE_H
#include <unordered_map>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include "../utils/Utils.h"
#include "../buffer/Buffer.h"
#include "../log/Log.h"// 构造HTTP响应报文
class HttpResponse {
private:static const std::unordered_map<std::string, std::string> SUFFIX_TYPE;static const std::unordered_map<int, std::string> CODE_PATH;static std::unordered_map<int, std::string> CODE;std::string srcDir_;std::string path_;bool keepAlive_{};int code_{};char* mmFile_{};struct stat mmFileStat_{};
public:HttpResponse() = default;~HttpResponse() = default;static void addCors(Buffer &buf);static void ErrorContent(Buffer &buff, std::string &&message);void unmapFile();void makeResponse(Buffer &buf);void init(const std::string &srcDir, const std::string &path, bool isKeepAlive, int code);void ErrorHtml_();void AddStateLine_(Buffer &buf);void AddHeader_(Buffer &buf);void AddContent_(Buffer &buff);std::string GetFileType_();size_t fileLen() const;char* file();
};#endif //TINYWEBSERVER_HTTPRESPONSE_H
#include "HttpResponse.h"const std::unordered_map<std::string, std::string> HttpResponse::SUFFIX_TYPE = {{ ".html",  "text/html" },{ ".xml",   "text/xml" },{ ".xhtml", "application/xhtml+xml" },{ ".txt",   "text/plain" },{ ".rtf",   "application/rtf" },{ ".pdf",   "application/pdf" },{ ".word",  "application/nsword" },{ ".png",   "image/png" },{ ".gif",   "image/gif" },{ ".jpg",   "image/jpeg" },{ ".jpeg",  "image/jpeg" },{ ".au",    "audio/basic" },{ ".mpeg",  "video/mpeg" },{ ".mpg",   "video/mpeg" },{ ".avi",   "video/x-msvideo" },{ ".gz",    "application/x-gzip" },{ ".tar",   "application/x-tar" },{ ".css",   "text/css "},{ ".js",    "text/javascript "},
};
std::unordered_map<int, std::string> HttpResponse::CODE = {{ 200, "OK" },{ 400, "Bad Request" },{ 403, "Forbidden" },{ 404, "Not Found" },
};
const std::unordered_map<int, std::string> HttpResponse::CODE_PATH = {{ 400, "/400.html" },{ 403, "/403.html" },{ 404, "/404.html" },
};void HttpResponse::init(const std::string& srcDir, const std::string& path, bool isKeepAlive, int code) {if (mmFile_) {unmapFile();}keepAlive_ = isKeepAlive;srcDir_ = srcDir;mmFile_ = nullptr;mmFileStat_ = { 0 };code_ = code;path_ = path;
}//void HttpResponse::addHeaders(bool keepAlive, Buffer &buf, int type) {
//    buf.append("Connection: ");
//    if (keepAlive) {
//        buf.append("keep-alive\r\n");
//        buf.append("keep-alive: max=6, timeout=120\r\n");
//    } else {
//        buf.append("close\r\n");
//    }
//    if (type) {
//        buf.append("Content-Type:application/json\r\n");
//    } else {
//        buf.append("Content-Type:text/html\r\n");
//    }
//    buf.append("Access-Control-Allow-Origin:*\r\n");
//}void HttpResponse::addCors(Buffer &buf) {buf.append("Access-Control-Allow-Methods:POST, OPTIONS, GET, PUT, DELETE\r\n");buf.append("Access-Control-Allow-Headers:Content-Type, Connection, Content-Length, Keep-Alive, \r\n");buf.append("Access-Control-Max-Age:3600\r\n");buf.append("Cache-Control:no-cache, no-store, must-revalidate\r\n");
}//void HttpResponse::addBody(const std::string &&data, Buffer &buf) {
//    buf.append("Content-Length: " + std::to_string(data.length()) + "\r\n\r\n");
//    buf.append(data + "\r\n");
//}void HttpResponse::AddStateLine_(Buffer& buf) {std::string status;if(CODE.count(code_) == 1) {status = CODE.find(code_)->second;}else {code_ = 400;status = CODE.find(400)->second;}buf.append("HTTP/1.1 " + std::to_string(code_) + " " + status + "\r\n");
}void HttpResponse::AddHeader_(Buffer& buf) {buf.append("Connection: ");if(keepAlive_) {buf.append("keep-alive\r\n");buf.append("keep-alive: max=6, timeout=120\r\n");} else{buf.append("close\r\n");}buf.append("Content-type: " + GetFileType_() + "\r\n");
}void HttpResponse::AddContent_(Buffer& buf) {int srcFd = open((srcDir_ + path_).data(), O_RDONLY);if(srcFd < 0) {ErrorContent(buf, "File NotFound!");return;}/* 将文件映射到内存提高文件的访问速度MAP_PRIVATE 建立一个写入时拷贝的私有映射*/
//    LOG_DEBUG("file path %s, size: %d", (srcDir_ + path_).data(), mmFileStat_.st_size);int* mmRet = (int*)mmap(nullptr, mmFileStat_.st_size, PROT_READ, MAP_PRIVATE, srcFd, 0);if(*mmRet == -1) {LOG_ERROR("map file failed");ErrorContent(buf, "File NotFound!");return;}mmFile_ = (char*)mmRet;close(srcFd);buf.append("Content-length: " + std::to_string(mmFileStat_.st_size) + "\r\n\r\n");
}void HttpResponse::makeResponse(Buffer &buf) {/* 判断请求的资源文件 */if(stat((srcDir_ + path_).data(), &mmFileStat_) < 0 || S_ISDIR(mmFileStat_.st_mode)) {code_ = 404;}else if(!(mmFileStat_.st_mode & S_IROTH)) {code_ = 403;}else if(code_ == -1) {code_ = 200;}ErrorHtml_();AddStateLine_(buf);AddHeader_(buf);AddContent_(buf);
}void HttpResponse::ErrorContent(Buffer& buff, std::string &&message)
{std::string body;body += "<html><title>Error</title>";body += "<body bgcolor=\"ffffff\">";body += "<p>" + message + "</p>";body += "<hr><em>TinyWebServer</em></body></html>";buff.append("Content-length: " + std::to_string(body.size()) + "\r\n\r\n");buff.append(body);
}void HttpResponse::unmapFile() {if(mmFile_) {munmap(mmFile_, mmFileStat_.st_size);mmFile_ = nullptr;}
}
void HttpResponse::ErrorHtml_() {if(CODE_PATH.count(code_) == 1) {path_ = CODE_PATH.find(code_)->second;stat((srcDir_ + path_).data(), &mmFileStat_);}
}
std::string HttpResponse::GetFileType_() {/* 判断文件类型 */std::string::size_type idx = path_.find_last_of('.');if(idx == std::string::npos) {return "text/plain";}std::string suffix = path_.substr(idx);if(SUFFIX_TYPE.count(suffix) == 1) {return SUFFIX_TYPE.find(suffix)->second;}return "text/plain";
}char *HttpResponse::file() {return mmFile_;
}size_t HttpResponse::fileLen() const {return mmFileStat_.st_size;
}

7.3 HTTP处理

HTTP处理模块是整个服务器的核心模块,负责管理客户端的连接、读写数据、HTTP处理逻辑。

  • 管理连接:

    当有客户端连接时,初始化相关数据,存储fd和客户端地址。

    当由于某种原因,需要断开连接时,该模块关闭fd,重置相关数据

  • 读写数据:

    • 根据不同的模式读取数据,调用buffer中的readFd函数。
    • 将缓冲区的数据写入socket中。此时需要注意一次可能不能将全部数据写出,需要循环写出,并更新指针。
  • 负责整个HTTP的处理流程:

    首先调用ParseHttpRequest解析读缓冲区的数据,然后再调用HttpResponse生成响应报文,并放入写缓冲区中,最后将写缓冲和请求文件地址赋值给iovec结点,等待写出。

7.3.1 代码

#ifndef TINYWEBSERVER_HTTPWORK_H
#define TINYWEBSERVER_HTTPWORK_H#include <sys/socket.h>
#include <netinet/in.h>
#include "HttpResponse.h"
#include <mutex>
#include "ParseHttpRequest.h"
#include "../buffer/Buffer.h"// 每个工作线程操纵的类接口,负责读写数据,处理Http请求,每个用户持有一个类
class HttpWork {
private:Buffer writeBuf_;Buffer readBuf_;int fd_{};bool isRun_;struct sockaddr_in addr_{};iovec iv[2]{};int io_cnt = 2;std::mutex mtx_;public:ParseHttpRequest request_;HttpResponse response_;static std::string srcDir_;static bool et_;static std::atomic<int> userCount;
public:HttpWork();~HttpWork();void init(int fd, const sockaddr_in &addr);ssize_t writeFd(int *Errno);ssize_t readFd(int *Errno);bool processHttp();size_t getWriteLen();void closeConn();int getFd();bool isKeepAlive();void resetBuffer();bool getIsRun();
};#endif //TINYWEBSERVER_HTTPWORK_H
#include "HttpWork.h"
bool HttpWork::et_;
std::string HttpWork::srcDir_;
std::atomic<int> HttpWork::userCount;void HttpWork::init(int fd, const sockaddr_in &addr) {assert(fd > 0);std::lock_guard<std::mutex> locker(mtx_);isRun_ = true;fd_ = fd;addr_ = addr;writeBuf_.resetBuffer();readBuf_.resetBuffer();request_.init();userCount ++;
}HttpWork::HttpWork() {isRun_ = false;addr_ = {0};
}ssize_t HttpWork::readFd(int *Errno) {assert(fd_ >= 0);ssize_t len = 0;do {auto t_len = readBuf_.readFd(fd_, Errno);// 返回0代表此次读取数据为0if (t_len <= 0) {break;}len += t_len;} while(et_);// len是此次总计读取的数据return len;
}ssize_t HttpWork::writeFd(int *Errno) {assert(fd_ >= 0);ssize_t len = 0;do {len = writev(fd_, iv, io_cnt);if (len <= 0) {// 写错误*Errno = errno;break;}// 处理第一个缓冲区if (iv[0].iov_len > 0) {// 此时第一个iovec没有写完// 我们将更新iovec的base和buffer中的指针位置auto iv_len1 = writeBuf_.getContentLen(); // 获取待写入数据的长度if (iv_len1 <= static_cast<size_t>(len)) { // buf中全部写完// iv1已经全部写完,后续不再处理iv[0].iov_base = nullptr;iv[0].iov_len = 0;writeBuf_.resetBuffer();len = static_cast<ssize_t>(static_cast<size_t>(len) - iv_len1); // 获取第二个iv结点写入的数据} else {// iv1写了一部分writeBuf_.addReadIdx(len); // 更新// 指针iv[0].iov_base = writeBuf_.getReadPtr();iv[0].iov_len = writeBuf_.getContentLen();len = 0;}}// 处理第二个缓冲区if (iv[0].iov_len == 0){iv[1].iov_base = (uint8_t*)iv[1].iov_base + len;iv[1].iov_len -= len;}if (0 == getWriteLen()) {iv[1].iov_base = nullptr;iv[1].iov_len = 0;break; // 写成功}} while(et_);return len;
}HttpWork::~HttpWork() {writeBuf_.resetBuffer();readBuf_.resetBuffer();fd_ = -1;close(fd_);
}size_t HttpWork::getWriteLen() {return iv[0].iov_len + iv[1].iov_len;
}void HttpWork::closeConn() {std::lock_guard<std::mutex> locker(mtx_);if (isRun_) {close(fd_);fd_ = -1;isRun_ = false;userCount --;LOG_DEBUG("client %d is closed", fd_);}
}bool HttpWork::getIsRun() {std::lock_guard<std::mutex> locker(mtx_);return isRun_;
}int HttpWork::getFd() {std::lock_guard<std::mutex> locker(mtx_);return fd_;
}bool HttpWork::isKeepAlive() {return request_.keepAlive();
}void HttpWork::resetBuffer() {readBuf_.resetBuffer();writeBuf_.resetBuffer();
}bool HttpWork::processHttp() {// 读缓冲中没有数据,接下来继续等待读if (readBuf_.getContentLen() <= 0) {return false;}
//    LOG_DEBUG("readBuf: %s", std::string(readBuf_.getConstReadPtr(), readBuf_.getContentLen()).c_str());request_.init(); // 清空上一次的数据// 请求成功解析if (request_.parse(readBuf_)) {// 解析成功,正式进入业务逻辑处理流程response_.init(srcDir_, request_.path(), request_.keepAlive(), 200);} else {response_.init(srcDir_, request_.path(), false, 400);}LOG_INFO("%s %s", request_.method().c_str(), request_.path().c_str());response_.makeResponse(writeBuf_);// 输出报文11iv[0].iov_base = writeBuf_.getReadPtr();iv[0].iov_len = writeBuf_.getContentLen();io_cnt = 1;if (response_.file() && response_.fileLen() > 0) {iv[1].iov_base = response_.file();iv[1].iov_len = response_.fileLen();io_cnt = 2;}
//    LOG_DEBUG("wait for write data: %d", getWriteLen());// 返回true表示等待写return true;
}

8 Server层处理

上层的基础API已经实现,Server层主要负责监听事件,等待客户端连接、接收请求和发送响应。

在这里使用EPOLL来监听各种事件,之后分类处理。但是主线程并不是真正的处理,而是将读写事件插入到任务队列中,由线程池负责处理。

此外当某个fd有事件发生时,要延长定时器的超时时间。

服务器所需的参数使用配置文件的形式传入程序中。

8.1 代码

#ifndef TINYWEBSERVER_SERVER_H
#define TINYWEBSERVER_SERVER_H
#include "../http/HttpWork.h"
#include "../http/HttpResponse.h"
#include "../http/ParseHttpRequest.h"
#include "../log/Log.h"
#include "../pool/ThreadPool.h"
#include "../timer/Timer.h"
#include "Epoll.h"
#include <unordered_map>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <functional>class Server {
private:const char *ip_;int port_;int trigMod_;int timeoutMs_;int MAXFD_;std::unique_ptr<ThreadPool> threadPool_;std::unique_ptr<Timer> timer_;std::unique_ptr<Epoll> epoll_;std::unordered_map<int, HttpWork> users_; // 负责处理HTTP请求uint32_t httpConnEvents_{};uint32_t listenEvents_{};int listenFd_{};bool isRun_;std::string log_dir_;std::string srcDir_;public:// 提供服务器运行参数Server(const char* ip, int port, int trigMod, int timeout, LogTarget target, LogLevel::value logLevel,int max_thread_cnt, int max_timer_cnt, int max_fd, int max_epoll_events, int sqlPort, const char * sqlUser,const char * sqlPwd, const char * dbName, int connPoolNum);~Server();void initTrigMode();bool startListen();static int setNonBlocking(int fd);void dealListen();void addClient(int fd, sockaddr_in &addr);void dealWrite(HttpWork &client);void dealRead(HttpWork &client);static void sendError(int fd, const char *msg);void closeConn(HttpWork &client);void extendTime(int fd);void readCb(HttpWork &client);void writeCb(HttpWork &client);void run();
};#endif //TINYWEBSERVER_SERVER_
#include "Server.h"Server::Server(const char *ip, int port, int trigMod, int timeout, LogTarget target, LogLevel::value logLevel, int max_thread_cnt,int max_timer_cnt, int max_fd, int max_epoll_events, int sqlPort, const char *sqlUser,const char * sqlPwd, const char * dbName, int connPoolNum):ip_(ip), port_(port),trigMod_(trigMod), timeoutMs_(timeout), MAXFD_(max_fd),threadPool_(new ThreadPool(max_thread_cnt)), timer_(new Timer(max_timer_cnt)),epoll_(new Epoll(max_epoll_events)) {isRun_ = false;srcDir_ = getcwd(nullptr, 256);auto l = Log::getInstance();
//     初始化日志系统l->init(target, (srcDir_ + "/log").c_str(), ".log", logLevel);HttpWork::srcDir_ = srcDir_ + "/resources";SqlConnPool::Instance()->Init("localhost", sqlPort, sqlUser, sqlPwd, dbName, connPoolNum);
//     初始化监听事件initTrigMode();
//     启动listenFdif (startListen()) {isRun_ = true;}
}void Server::initTrigMode() {listenEvents_ = EPOLLRDHUP;    // 检测socket关闭httpConnEvents_ = EPOLLONESHOT | EPOLLRDHUP;     // EPOLLONESHOT由一个线程处理switch (trigMod_) {case 0:break;case 1:httpConnEvents_ |= EPOLLET;break;case 2:listenEvents_ |= EPOLLET;break;case 3:listenEvents_ |= EPOLLET;httpConnEvents_ |= EPOLLET;break;default:listenEvents_ |= EPOLLET;httpConnEvents_ |= EPOLLET;}HttpWork::et_ = (httpConnEvents_ & EPOLLET);
}bool Server::startListen() {struct sockaddr_in address = {0};address.sin_port = htons(port_);address.sin_family = AF_INET;inet_pton(AF_INET, ip_, &address.sin_addr);listenFd_ = socket(PF_INET, SOCK_STREAM, 0);if (listenFd_ < 0) {LOG_FATAL("create socket failed");return false;}setNonBlocking(listenFd_);int res;int optVal = 1;res = setsockopt(listenFd_, SOL_SOCKET, SO_REUSEADDR, &optVal, sizeof(int));if(res == -1) {LOG_FATAL("set socket setsockopt error !");close(listenFd_);return false;}res = bind(listenFd_, (struct sockaddr*)&address, sizeof address);if (res == -1) {LOG_FATAL("bind socket failed");return false;}res = listen(listenFd_, 8);if (res < 0) {LOG_FATAL("%s %d", "listen failed", res);return false;}epoll_->addFd(listenFd_, EPOLLIN|listenEvents_);LOG_INFO("listening on %s:%d", ip_, port_);return true;
}int Server::setNonBlocking(int fd) {int old = fcntl(fd, F_GETFL);int newOp = old | O_NONBLOCK;fcntl(fd, F_SETFL, newOp);return old;
}void Server::run() {if (!isRun_) {LOG_ERROR("Server start failed");return;}int timeout = -1;LOG_INFO("Server start running");while(isRun_) {if (timeoutMs_ > 0) {// 清理过期时间timeout = timer_->getNextTick();}// 等待直到下一个定时事件超时,如果timeout为-1代表队列中已经没有定时任务,阻塞等待int cnt = epoll_->wait(timeout);for (int i = 0; i < cnt; ++ i) {// 以此处理每个事件int fd = epoll_->getEventFd(i);uint32_t events = epoll_->getEvents(i);if (fd == listenFd_) {// 处理服务器连接请求dealListen();} else if (events & (EPOLLRDHUP & EPOLLERR & EPOLLHUP)) {LOG_WARN("(main): close event: fd(%d)", fd);closeConn(users_[fd]); // 关闭连接} else if (events & EPOLLIN) {dealRead(users_[fd]);} else if (events & EPOLLOUT) {dealWrite(users_[fd]);} else {LOG_ERROR("(main): unexpected event");}}}
}void Server::dealListen() {sockaddr_in address{0};socklen_t addr_len = sizeof address;do {int fd = accept(listenFd_, (struct sockaddr*)&address, &addr_len);if (fd <= 0) {return;} else if (HttpWork::userCount >= MAXFD_) {sendError(fd, "Server busy");LOG_ERROR("server is full");return;}addClient(fd, address);} while(listenEvents_ & EPOLLET);
}
void Server::addClient(int fd, sockaddr_in &addr) {// 初始化连接users_[fd].init(fd, addr);HttpWork &client = users_[fd];
//    Log::DEBUG("(main): user %d isRun: %s", fd, std::to_string(client.getIsRun()).c_str());setNonBlocking(fd);// 假如监听列表epoll_->addFd(fd, EPOLLIN|httpConnEvents_);// 超时后断开连接if (timeoutMs_ > 0) {// 添加定时事件utimer_->push(fd, timeoutMs_, [this, &client] { closeConn(client); }); // 这里报错了,原因是closeConn的client参数应为指针}LOG_INFO("(main): user[%d] in, ip: %s, port: %d", fd, inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
}void Server::dealWrite(HttpWork &client) {assert(client.getIsRun());extendTime(client.getFd());threadPool_->addTask([this, &client] { writeCb(client); });
}void Server::dealRead(HttpWork &client) {
//    LOG_INFO("(main): dealRead client: %d", client.getFd());assert(client.getIsRun());extendTime(client.getFd());threadPool_->addTask([this, &client] { readCb(client); });
}void Server::sendError(int fd, const char *msg) {assert(fd >= 0);auto len = write(fd, msg, sizeof msg);if (len <= 0) {LOG_WARN("(main): send error to client %d error", fd);}close(fd);
}void Server::extendTime(int fd) {assert(fd >= 0);timer_->reset(fd, timeoutMs_);
}void Server::readCb(HttpWork &client) {assert(client.getIsRun());int Errno = 0;auto len = client.readFd(&Errno);if (len <= 0 && !(Errno == EAGAIN || Errno == 0)) {// 出现了其他错误,关闭连接LOG_ERROR("(thread):read error: %d, client %d is closing", Errno, client.getFd());closeConn(client);return;}if (client.processHttp()) {// 成功处理了http读请求,response已生成,等待写出epoll_->modFd(client.getFd(), EPOLLOUT | httpConnEvents_);} else {// http请求未处理,读缓冲为空,重新等待请求epoll_->delFd(client.getFd());LOG_ERROR("(thread): readBuf is none, client: %d", client.getFd());closeConn(client);}
}void Server::writeCb(HttpWork &client) {assert(client.getIsRun()); // 连接未关闭int Errno = 0;auto len = client.writeFd(&Errno);
//    LOG_DEBUG("Error: %d", Errno);if (client.getWriteLen() == 0) {LOG_INFO("(thread): write successfully from user %d", client.getFd());// 传输成功if (client.isKeepAlive()) {epoll_->modFd(client.getFd(), EPOLLIN | httpConnEvents_);client.resetBuffer();return;}} else if (len <= 0 && Errno == EAGAIN) {// 写缓冲满了,继续传输
//        LOG_WARN("EAGAIN, continue write, client %d", client.getFd());epoll_->modFd(client.getFd(), EPOLLOUT | httpConnEvents_);return;}LOG_INFO("(thread): client %d is closing", client.getFd());closeConn(client);
}void Server::closeConn(HttpWork &client) {if (!client.getIsRun())return;LOG_INFO("(main): client %d is closing", client.getFd());epoll_->delFd(client.getFd());client.closeConn();
}Server::~Server() {close(listenFd_);isRun_ = false;
}

9 压力测试

9.1 ET模式

./webbench-1.5/webbench -c 5000 -t 10 http://127.0.0.1:20001/

在这里插入图片描述

./webbench-1.5/webbench -c 8000 -t 10 http://127.0.0.1:20001/

在这里插入图片描述

./webbench-1.5/webbench -c 10000 -t 10 http://127.0.0.1:20001/

在这里插入图片描述

9.2 LT模式

./webbench-1.5/webbench -c 10000 -t 10 http://127.0.0.1:20001/

在这里插入图片描述

9.3 测试环境

  • Ubuntu: 20.04
  • cpu: i5-1035G1
  • 内存: 16G

10 运行说明

10.1 数据库初始化

CREATE DATABASE webserver;
USE webserver;CREATE TABLE user (username VARCHAR(50) NOT NULL,password VARCHAR(50) NOT NULL,PRIMARY KEY (username)
);INSERT INTO user (username, password) VALUES ('root', '123456');

10.2 导入mysql.h

安装mysql驱动

sudo apt-get install libmysqlclient-dev

10.3 编译运行

进入项目根目录

make
./build/bin/server

11 致谢

https://github.com/markparticle/WebServer
Linux高性能服务器编程,游双著.

完整项目链接:https://github.com/Joker0x00/TinyWebServer

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/48912.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微信小程序-应用,页面和组件生命周期总结

情景1&#xff1a;小程序冷启动时候的顺序 情景2: 使用navigator&#xff08;保留并打开另一个页面&#xff09;和redirect&#xff08;关闭并打开另一个页面&#xff09;的执行顺序 情景3&#xff1a;切后台和切前台

Linux——组管理和权限管理

目录 组管理 Linux 组基本介绍 文件/目录所有者 组的创建 查看&修改文件/目录所在组 改变用户所在组 权限管理 基本介绍 rwx 文件/目录权限详解 chmod 修改文件或目录权限 chown 修改文件所有者 组管理 Linux 组基本介绍 关于第二张图中问题&#xff0c;答案…

【Qt】Qt的坐标转换(mapToGlobal)

1、QPoint QWidget::mapToGlobal(const QPoint &pos) const 将小部件坐标转换为全局坐标。mapToGlobal(QPoint(0,0))可以得到小部件左上角像素的全局坐标。2、QPoint QWidget::mapToParent(const QPoint &pos) const 将小部件坐标转换为父部件坐标。如果小部件没有父部…

Jmeter之count函数

counter函数 1、功能解释 count函数--计数器&#xff0c;每调用这个函数一次&#xff0c;它就会自动加1。它有两个参数&#xff0c;第一个参数是布尔型的&#xff0c;只能设置成 “TRUE”或者“FALSE”&#xff0c;如果是TRUE&#xff0c;那么每个用户有自己的计数器&#xf…

常用的网络爬虫工具推荐

在推荐常用的网络爬虫工具时&#xff0c;我们可以根据工具的易用性、功能强大性、用户口碑以及是否支持多种操作系统等多个维度进行考量。以下是一些常用的网络爬虫工具推荐&#xff1a; 1. 八爪鱼 简介&#xff1a;八爪鱼是一款免费且功能强大的网站爬虫&#xff0c;能够满足…

vxe-table——实现切换页码时排序状态的回显问题(ant-design+elementUi中table排序不同时回显的bug)——js技能提升

之前写的后台管理系统&#xff0c;都是用的antdelement&#xff0c;table组件中的【排序】问题是有一定的缺陷的。 想要实现的效果&#xff1a; antv——table组件一次只支持一个参数的排序 如下图&#xff1a; 就算是可以自行将排序字段拼接到列表接口的入参中&#xff0c…

环信+亚马逊云科技服务:助力出海AI社交应用扬帆起航

随着大模型技术的飞速发展&#xff0c;AI智能体的社交体验得到了显著提升&#xff0c;AI社交类应用在全球范围内持续火热。尤其是年轻一代对新技术和新体验的热情&#xff0c;使得AI社交产品在海外市场迅速崛起。作为领先的即时通讯解决方案提供商&#xff0c;环信与亚马逊云科…

计算机体系结构|| 再定序缓冲(ROB)原理(6)

实验6 再定序缓冲&#xff08;ROB&#xff09;原理 6.1实验目的 &#xff08;1&#xff09;加深对指令级并行性及其开发的理解。 &#xff08;2&#xff09;加深对基于硬件的前瞻执行的理解。 &#xff08;3&#xff09;掌握 ROB 在流出、执行、写结果确认4 个阶段所进行的…

vue3 -layui项目-左侧导航菜单栏

1.创建目录结构 进入cmd,先cd到项目目录&#xff08;项目vue3-project&#xff09; cd vue3-project mkdir -p src\\views\\home\\components\\menubar 2.创建组件文件 3.编辑menu-item-content.vue <template><template v-if"item.icon"><lay-ic…

SQL injection UNION attacks SQL注入联合查询攻击

通过使用UNION关键字&#xff0c;拼接新的SQL语句从而获得额外的内容&#xff0c;例如 select a,b FROM table1 UNION select c,d FROM table2&#xff0c;可以一次性查询 2行数据&#xff0c;一行是a&#xff0c;b&#xff0c;一行是c&#xff0c;d。 UNION查询必须满足2个条…

java面试题,有synchronized锁,threadlocal、数据可以设置默认值、把redis中的json转为对象

有面试题&#xff0c;有synchronized锁&#xff0c;threadlocal 一、面试题小记二、加锁synchronized1. 先看代码2. synchronized 讲解2.1. 同步代码块2.2. 同步方法2.3. 锁的选择和影响2.4. 注意事项2.5 锁的操作&#xff0c;手动释放锁&#xff0c;显式地获取锁&#xff08;属…

开源XDR-SIEM一体化平台 Wazuh (1)基础架构

简介 Wazuh平台提供了XDR和SIEM功能&#xff0c;保护云、容器和服务器工作负载。这些功能包括日志数据分析、入侵和恶意软件检测、文件完整性监控、配置评估、漏洞检测以及对法规遵从性的支持。详细信息可以参考Wazuh - Open Source XDR. Open Source SIEM.官方网站 Wazuh解决…

AV1技术学习:Transform Coding

对预测残差进行变换编码&#xff0c;去除潜在的空间相关性。VP9 采用统一的变换块大小设计&#xff0c;编码块中的所有的块共享相同的变换大小。VP9 支持 4 4、8 8、16 16、32 32 四种正方形变换大小。根据预测模式选择由一维离散余弦变换 (DCT) 和非对称离散正弦变换 (ADS…

免费分享一套微信小程序图书馆座位预约管理系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】,帅呆了~~

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的微信小程序图书馆座位预约管理系统(SpringBoot后端Vue管理端)&#xff0c;分享下哈。 项目介绍 随着移动互联网技术的飞速发展和智能设备的普及&#xff0c;图书馆服务模式正在经历深刻的变革。本论文旨在…

从PyTorch官方的一篇教程说开去(3.3 - 贪心法)

您的进步和反馈是我最大的动力&#xff0c;小伙伴来个三连呗&#xff01;共勉。 贪心法&#xff0c;可能是大家在处理陌生问题时候&#xff0c;最容易想到的办法了吧&#xff1f; 还记得小时候&#xff0c;国足请了位洋教练发表了一句到现在还被当成段子的话&#xff1a;“如…

第2章-数学建模

目录 一、数据类型 【函数】&#xff1a; &#xff08;1&#xff09;find()、rfind()、index()、rindex()、count() &#xff08;2&#xff09;split()、rsplit() &#xff08;3&#xff09;join() &#xff08;4&#xff09;strip()、rstrip()、lstrip() &#xff08;5&…

【Python】sqlite加密库pysqlcipher3编译安装步骤

目录 说明准备工作openssl编译sqlite tclsetup.py修改quote_argumentopenssl路径 安装加密示例代码测试附录参考 说明 pysqlcipher3是针对Python 3使用的pysqlcipher的一个分支&#xff0c; 尽管仍然维护对Python 2的支持。它仍然处于测试阶段&#xff0c; 尽管这个库包含的最…

请你谈谈:spring bean的生命周期 - 阶段5:BeanPostProcessor前置处理-自定义初始化逻辑-BeanPostProcess后置处理

BeanPostProcessor的postProcessBeforeInitialization方法是在bean的依赖注入&#xff08;即属性填充&#xff09;完成后&#xff0c;但在bean的初始化回调&#xff08;如PostConstruct注解的方法或InitializingBean接口的afterPropertiesSet方法&#xff09;之前被调用的。 具…

sql_exporter通过sql收集业务数据并通过prometheus+grafana展示

下载并解压安装sql_exporter wget https://github.com/free/sql_exporter/releases/download/0.5/sql_exporter-0.5.linux-amd64.tar.gz #解压 tar xvf sql_exporter-0.5.linux-amd64.tar.gz -C /usr/local/修改主配置文件 cd /usr/local/ mv sql_exporter-0.5.linux-amd64 s…

google 浏览器插件开发简单学习案例:TodoList

参考&#xff1a; google插件支持&#xff1a; https://blog.csdn.net/weixin_42357472/article/details/140412993 这里是把前面做的TodoList做成google插件&#xff0c;具体网页可以参考下面链接 TodoList网页&#xff1a; https://blog.csdn.net/weixin_42357472/article/de…