深入解析C++轻量级WebServer实现

目录

写在前面

概述:这是基于bs模型并且使用epoll实现的高并发服务器,使用线程池+连接池+使用同步或异步的日志系统+定时器等;

线程池使用整体架构图

线程池详细工作流程图

数据库连接池和数据库服务器与线程池的关系

数据库与线程池共同使用流程图

Main.cpp

Webserver.h+Webserver.cpp

Webserver类的init的函数参数讲解:

Webserver.cpp

WebServer类的WebServer实现:

WebServer::~WebServer()的实现

WebServer::init()的实现:

WebServer::trig_mode()的实现:

WebServer::log_write()

WebServer::sql_pool()

WebServer::thread_pool()

Timer

lst_timer.h

lst_timer.cpp

Threadpool

CGImysql

sql_connection_pool.h

Sql_connection_pool.cpp

Log

block_queue.h

log.h

log.cpp

Lock

Http

http_conn.h

http_conn.cpp

Config

写在前面

事先声明:这是一篇对于github找的tinywebserver的个人理解,旨在帮助想学c++后端并且对该项目不知如何入手的同学,如有错误,请及时指出,本人不胜感激!!!

源码:qinguoyi/TinyWebServer: :fire: Linux下C++轻量级WebServer服务器

针对c++初学者需要学习这个项目需要掌握以下知识:

linux基础命令、linux系统编程、linux网络编程、c++基础命令 、MySQL基础命令以及c++实现连接数据库和实现数据库连接池、数据结构链表

如果想具体知道该server是如何一步一步运行的话,可以去打断点调试;

概述:这是基于bs模型并且使用epoll实现的高并发服务器,使用线程池+连接池+使用同步或异步的日志系统+定时器等;

线程池:线程池实现主要是为了高效处理大量短期任务,特别是在高并发服务器中管理连接和请求。

数据库连接池:是一个负责分配、管理和释放数据库连接的技术组件。它充当了应用程序和数据库之间的一个“缓冲层”,通过复用已经建立的数据库连接,来避免频繁创建和关闭连接所带来的巨大性能开销。

日志系统:是指一套用于生成、收集、存储、分析和展示应用程序运行时产生的事件、状态和错误信息的软件组件、库和工具的集合。它的核心目的是:为开发、测试和运维人员提供洞察应用内部行为的能力,以便于调试、监控和审计

定时器:是一种编程组件或系统服务,它允许你调度一个任务(一段代码/函数)在未来的某个特定时间点执行一次,或者以固定的时间间隔重复执行。它的核心思想是:将任务的执行与时间的流逝解耦,由系统来负责时间管理,并在恰当时机触发回调。

线程池使用整体架构图

线程池详细工作流程图

数据库连接池和数据库服务器与线程池的关系

数据库与线程池共同使用流程图

下面开始我们的讲解:

Main.cpp

#include "config.h" int main(int argc, char *argv[]) { //需要修改的数据库信息,登录名,密码,库名 string user = "root"; string passwd = "123456"; string databasename = "yourdb"; //命令行解析 Config config; config.parse_arg(argc, argv); WebServer server; //初始化 server.init(config.PORT, user, passwd, databasename, config.LOGWrite, config.OPT_LINGER, config.TRIGMode, config.sql_num, config.thread_num, config.close_log, config.actor_model); //日志 server.log_write(); //数据库 server.sql_pool(); //线程池 server.thread_pool(); //触发模式 server.trig_mode(); //监听 server.eventListen(); //运行 server.eventLoop(); return 0; }

Webserver.h+Webserver.cpp

WebServer类封装了服务器主要逻辑包括初始化、事件循环和事件处理,比较核心,所以选择看完main.cpp后看WebServer类

Webserver类的init的函数参数讲解:

 void init(int port , string user, string passWord, string databaseName, int log_write , int opt_linger, int trigmode, int sql_num, int thread_num, int close_log, int actor_model); 
其中log_write参数是日志写入方式:分同步写入和异步写入

同步写入:在应用程序执行日志记录语句时,必须等待日志数据被完全、安全地写入到持久化存储(如硬盘)后,才能执行后续代码

异步写入:应用程序执行日志记录语句时,将日志消息放入到一个内存缓冲区后便立即返回,不等待数据落盘。由一个后台线程负责将缓冲区中的日志批量、定期地写入磁盘
opt_linger参数是优雅关闭选项:

优雅关闭指的是当一个应用程序需要关闭时,它不是强制立即终止,而是通过一个预设的流程,有序的完成正在进行的任务、释放资源、并通知上下游组件,然后才安全退出的过程。

相反的是暴力关闭:即立即向进程发送SIGKILL(-9)信号,进程会立即停止,无论它在做什么。
trigmode参数指的是触发模式:

LT(水平触发):
只要条件满足,就会一直通知你

ET(边缘触发):只有当条件发生变化时,它才会通知你一次。

下面举个例子:

假设有一个 Socket,其接收缓冲区里收到了 2KB 的数据。

1. LT(水平触发) 的工作方式epoll_wait 函数返回,通知你这个 socket 有数据可读(读就绪)。你决定只从缓冲区读取 1KB 的数据。然后你再次调用 epoll_wait关键点:因为缓冲区里还有 1KB 数据(仍然处于“可读”状态),所以 epoll_wait 会立即再次返回,通知你这个 socket 仍然可读。你再去读取剩下的 1KB 数据。此时缓冲区为空,epoll_wait 才会休眠,直到下次有新数据到来。

LT 的优点:编程更安全。即使你一次没有处理完所有数据,下次还会得到通知,不会丢失数据。

LT 的缺点:可能会导致效率略低。因为如果对方发送数据很快,你每次只读一部分,会导致 epoll_wait 被频繁唤醒,即使还有旧数据没读完。

2. ET(边缘触发)的工作方式当数据包到达,缓冲区从空变为非空时,epoll_wait 返回,通知你这个 socket 有数据可读。你必须用一个循环,一直读这个 socket,直到 read 系统调用返回 -1,并且错误码 errno 为 EAGAIN 或 EWOULDBLOCK(表示本次通知下的所有数据已读完)。如果你只读了 1KB 就停止了,然后再次调用 epoll_wait关键点epoll_wait 会阻塞,即使缓冲区里还有 1KB 数据!因为它只在乎变化的那一瞬间。只要缓冲区里还有数据,状态就没有从“空”变为“非空”的变化(空到非空的意思是没有新数据的加入),所以它不会通知你。只有当对端再次发送数据,导致缓冲区从空(或未满)变为有数据(或更多数据) 时,epoll_wait 才会再次返回。

ET 的优点:减少了事件被触发的次数,理论上性能更高,尤其是在高并发、大流量的场景下。

ET 的缺点必须使用非阻塞 I/O:因为你必须一次性读完所有数据,如果使用阻塞 I/O,在读完所有数据后,最后一次 read 会阻塞住线程,导致程序卡死。必须保证一次性处理完:如果一次没有读完所有数据,剩下的数据将永远无法被读取,直到下一个数据包到来触发新的通知。
actor_model参数指的是并发模型:

Reactor 模式(非阻塞I/O+就绪通知):应用程序向内核询问“I/O是否就绪”当就绪时,由应用程序自己执行实际的I/O操作(将数据从内核缓冲区读到用户空间)。

Proactor 模式(异步I/O):应用程序发起一个异步I/O操作,内核(或底层框架)负责完成整个I/O操作(包括将数据读到用户空间),操作完成后,再通知应用程序。
#ifndef WEBSERVER_H #define WEBSERVER_H #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <fcntl.h> #include <stdlib.h> #include <cassert> #include <sys/epoll.h> //线程池和http处理模块 #include "./threadpool/threadpool.h" #include "./http/http_conn.h" const int MAX_FD = 65536; //最大文件描述符数量 const int MAX_EVENT_NUMBER = 10000; // epoll最大监听事件数 const int TIMESLOT = 5; //定时器超时时间(秒) class WebServer { public: WebServer(); ~WebServer(); void init(int port , string user, string passWord, string databaseName, int log_write , int opt_linger, int trigmode, int sql_num, int thread_num, int close_log, int actor_model); //端口、用户名、密码、数据库名、日志写入方式、优雅关闭连接选项、触发模式,数据库连接数量、线程数量、日志开关、并发模型 void thread_pool();//初始化线程池 void sql_pool();//初始化数据库连接池 void log_write();//初始化日志系统 void trig_mode();//设置触发模式 //核心网络方法 void eventListen();//创建监听socket和epoll void eventLoop();//主事件循环 //事件处理方法 bool dealclientdata();//处理新客户端连接 bool dealwithsignal(bool& timeout, bool& stop_server);//处理信号 void dealwithread(int sockfd);//处理读事件 void dealwithwrite(int sockfd);//处理写事件 //定时器管理 void timer(int connfd, struct sockaddr_in client_address);//创建定时器 void adjust_timer(util_timer* timer);//调整定时器时间 void deal_timer(util_timer *timer, int sockfd);//处理定时器连接 public: int m_port;//服务器监听的端口号 char *m_root;//网站根目录 int m_log_write;//日志写入方式 int m_close_log;//日志开关 int m_actormodel;//使用的反应堆模型类型 int m_pipefd[2];//父子进程通信管道描述符 int m_epollfd;//epoll句柄 http_conn* users;//http链接数组 //数据库相关 connection_pool *m_connPool;//连接池 string m_user; //登陆数据库用户名 string m_passWord; //登陆数据库密码 string m_databaseName; //使用数据库名 int m_sql_num;//执行的最大sql语句数 //线程池相关 threadpool<http_conn>* m_pool;//线程池对象 int m_thread_num;//线程池中的线程数 //epoll_event相关 epoll_event events[MAX_EVENT_NUMBER];//用于存储epoll_wait返回的事件的数组 int m_listenfd; int m_OPT_LINGER;// 优雅关闭连接选项 int m_TRIGMode;// 触发模式 int m_LISTENTrigmode;// 监听socket的触发模式 int m_CONNTrigmode;// 连接socket的触发模式 //定时器相关 client_data *users_timer; //客户连接的定时器信息 Utils utils;//工具集 }; #endif

Webserver.cpp

WebServer类的WebServer实现:
WebServer::WebServer() { //在堆中初始化http_conn类对象数组,该数组最大长度为MAX_FD users = new http_conn[MAX_FD]; char server_path[200]; getcwd(server_path, 200);//获取当前工作目录,并将其存储在server_path字符数组中 //getcwd()会将当前工作目录的绝对路径复制到参数buffer所指的内存空间中,参数size为buf的空间大小 //将根目录的路径存储在root字符数组中 char root[6] = "/root";// “/root/0” //使用malloc函数在堆上分配一块内存,大小为server_path和root长度之和加1(/0) m_root = (char *)malloc(strlen(server_path) + strlen(root) + 1); // 将server_path和root连接成一个字符串并存储在m_root指针中 strcpy(m_root, server_path); strcat(m_root, root); //将源字符串src追加到目标字符串 dest 的末尾。 //如果当前工作目录是 /home/user/project,那么最终 m_root 指向的字符串将 是/home/user/project/root。 //client_data是自定义的定时器客户端数据结构 users_timer = new client_data[MAX_FD];//创建定时器数据数组 } 
char *getcwd(char *buf, size_t size); 参数说明: char *buf:一个指向字符数组的指针,用于存储获取到的当前工作目录路径。如果这个参数为 NULL,函数会根据 size 参数自动分配内存(需要调用者自己释放)。 size_t size:缓冲区 buf 的大小(以字节为单位)。这个大小必须足以容纳整个路径名加上终止的空字符\0。 返回值: 成功:返回一个指向包含当前工作目录路径的字符串的指针。这个指针通常与传入的 buf 参数值相同。如果 buf 是 NULL,则返回指向新分配内存的指针。 失败:返回 NULL,并设置全局变量 errno 以指示错误类型。 char *strcpy(char *dest, const char *src); 参数说明: char *dest:目标字符串指针,指向用于存放复制内容的内存地址。这块内存必须足够大,足以容纳源字符串,否则会导致缓冲区溢出。 const char *src:源字符串指针,指向要被复制的字符串。const 关键字表示函数内部不会修改源字符串。 返回值 返回 dest 指针本身。 char *strcat(char *dest, const char *src); 参数说明 char *dest:目标字符串指针。它必须是一个已经以\0 结尾的字符串,并且其所在缓冲区必须有足够的剩余空间来容纳 src 字符串的内容(包括其结尾的 \0)。 const char *src:源字符串指针,指向要被追加到目标字符串末尾的字符串。 返回值 返回 dest 指针本身。
WebServer::~WebServer()的实现
WebServer::~WebServer() { //关闭文件描述符,释放资源 close(m_epollfd);//关闭epoll文件描述符 close(m_listenfd);//关闭监听socket close(m_pipefd[1]);//关闭管道写端 close(m_pipefd[0]);//关闭管道读端 //释放动态分配的内存 delete[] users;//释放HTTP连接对象的数组(管理连接到服务器的客户端连接) delete[] users_timer;//释放定时器数据数组(每个元素都对应一个客户端连接,并存储了该连接的信息,如socket文件描述符、最后一次活跃时间等) delete m_pool;//释放线程池对象 } 
WebServer::init()的实现:
void WebServer::init(int port, string user, string passWord, string databaseName, int log_write, int opt_linger, int trigmode, int sql_num, int thread_num, int close_log, int actor_model) { //参数传递 m_port = port; m_user = user; m_passWord = passWord; m_databaseName = databaseName; m_sql_num = sql_num;// 每个线程处理的最大数据库任务数 m_thread_num = thread_num;//线程池中的线程数 m_log_write = log_write;//日志写入方式 m_OPT_LINGER = opt_linger;//优雅关闭连接的方式 m_TRIGMode = trigmode;//触发模式 m_close_log = close_log;//是否关闭日志 m_actormodel = actor_model;//并发模型选择 }
WebServer::trig_mode()的实现:
void WebServer::trig_mode() { //LT + LT if (0 == m_TRIGMode) { m_LISTENTrigmode = 0; m_CONNTrigmode = 0; } //LT + ET else if (1 == m_TRIGMode) { m_LISTENTrigmode = 0; m_CONNTrigmode = 1; } m_ else if (2 == m_TRIGMode) { m_LISTENTrigmode = 1; m_CONNTrigmode = 0; } //ET + ET else if (3 == m_TRIGMode) { m_LISTENTrigmode = 1; m_CONNTrigmode = 1; } }
WebServer::log_write()
void WebServer::log_write() { // 如果m_close_log为0(即不需要关闭),则调用Log类的get_instance静态方法获得唯一的Log实例,并使用其init方法初始化日志输出 if (0 == m_close_log) { //初始化日志,如果m_log_write为1,则采用异步写入方式 if (1 == m_log_write) Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 800); else //同步写入 Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 0); } }
WebServer::sql_pool()
//初始化数据库连接池 void WebServer::sql_pool() { //调用connection_pool类的GetInstance静态方法获得唯一的连接池实例 m_connPool = connection_pool::GetInstance(); m_connPool->init("localhost", m_user, m_passWord, m_databaseName, 3306, m_sql_num, m_close_log); //初始化数据库读取表 users->initmysql_result(m_connPool); /*初始化数据库读取表:使用users数组中的每个http_conn对象的initmysql_result方法, 将数据库连接池对象m_connPool作为参数传入。这样,在每个http_conn对象被创建时, 都会自动获取一个可用的数据库连接,并用该连接初始化mysql_result对象,以便后续进行数据库查询*/ } 
WebServer::thread_pool()
void WebServer::thread_pool() { /* new一个线程池对象 m_actormodel:并发模型选择 m_connPool:连接池对象,用于管理和复用客户端与服务器之间的连接。 m_thread_num:线程池中的线程数。 */ m_pool = new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num); }
#include <assert.h>

assert(expression);如果 expression 为真(非零),程序继续正常执行如果 expression 为假(零),assert 会:输出错误信息到 stderr调用 abort() 终止程序

struct linger {
    int l_onoff;   // 是否启用 linger 选项 (0=禁用, 非0=启用)
    int l_linger;  // 等待时间(秒)
};

l_onoff 决定 close() 函数的行为模式:l_onoff = 0close() 立即返回,系统后台处理关闭l_onoff = 1close() 可能阻塞,程序亲自等待关闭完成

l_linger只在 l_onoff = 1 时起作用!当 l_onoff = 0 时:l_linger 被完全忽略当 l_onoff = 1 时:l_linger 决定等待行为



#include <sys/socket.h>

int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);

//eventListen函数主要用于创建套接字、绑定端口、监听连接请求,并初始化epoll内核事件表 void WebServer::eventListen() { //网络编程基础步骤 m_listenfd = socket(PF_INET, SOCK_STREAM, 0); assert(m_listenfd >= 0); /* 断言,用来在程序运行时确保前面的 socket 调用成功。 如果 m_listenfd < 0,断言失败,程序在调试模式下会中止,并给出断言错误信息。 */ //优雅关闭连接选项 if (0 == m_OPT_LINGER)//后台优雅关闭(快速重启模式) { struct linger tmp = {0, 1}; setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp)); // close()立即返回,系统快速回收端口 // 服务器可以立即重启 } else if (1 == m_OPT_LINGER)//等待式优雅关闭(安全关闭模式) { struct linger tmp = {1, 1}; setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp)); // close()等待1秒完成优雅关闭 // 确保所有连接正确处理 } int ret = 0; struct sockaddr_in address; bzero(&address, sizeof(address));//将address清零 address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_ANY); address.sin_port = htons(m_port); //端口复用 int flag = 1; setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)); ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address)); assert(ret >= 0); ret = listen(m_listenfd, 5); assert(ret >= 0); //初始化utils工具类,通过调用utils工具类的init方法,对定时器参数进行初始化 utils.init(TIMESLOT); //这个数组是 epoll_wait() 函数的输出参数,用于存放所有已经就绪的事件。 epoll_event events[MAX_EVENT_NUMBER]; // 使用epoll_create函数创建一个epoll内核事件表,返回一个文件描述符 m_epollfd = epoll_create(5); assert(m_epollfd != -1); // 向epoll内核事件表中添加监听套接字(使用utils工具类的addfd方法) //并设置是否采用LT模式等触发方式,以及是否启用ET模式等触发方式 utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode); // 设置http_conn类的静态成员变量m_epollfd,将创建好的epoll内核事件表的文件描述符赋值给http_conn类的静态成员变量m_epollfd,以便后续http_conn对象的创建和管理。 http_conn::m_epollfd = m_epollfd; //使用socketpair函数创建一个双向管道,并将文件描述符存储在m_pipefd数组中 ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd); assert(ret != -1); utils.setnonblocking(m_pipefd[1]);//将管道写端设置为非阻塞 utils.addfd(m_epollfd, m_pipefd[0], false, 0); //向epoll内核事件表中添加管道读端,使用utils工具类的addfd方法,将管道读端添加到epoll内核事件表中,并设置是否采用LT模式等触发方式,以及是否启用ET模式等触发方式 //设置信号处理函数,使用utils工具类的addsig方法,将SIGPIPE、SIGALRM和SIGTERM三个信号的处理函数设置为SIG_IGN或utils的sig_handler函数 utils.addsig(SIGPIPE, SIG_IGN); utils.addsig(SIGALRM, utils.sig_handler, false); utils.addsig(SIGTERM, utils.sig_handler, false); // 定时器初始化,使用alarm函数定时器,定时时间为TIMESLOT秒 alarm(TIMESLOT); //将双向管道写端、epoll内核事件表文件描述符等变量存储在Utils的静态成员变量u_pipefd和u_epollfd中,以便后续工具类的操作 Utils::u_pipefd = m_pipefd; Utils::u_epollfd = m_epollfd; } /* 在每次有新连接时被调用,用于初始化用户数据和创建定时器:该函数的主要目的是为每个连接套接字创建一个定时器,以便在一定时间内未收到客户端请求时能够自动关闭连接,防止服务器资源浪费 */
void WebServer::timer(int connfd, struct sockaddr_in client_address) { // 初始化用户数据,使用连接套接字connfd和客户端地址client_address初始化一个User[connfd]对象,并将其保存在全局数组users中,以便后续使用 users[connfd].init(connfd, client_address, m_root, m_CONNTrigmode, m_close_log, m_user, m_passWord, m_databaseName); //初始化client_data数据 users_timer[connfd].address = client_address; users_timer[connfd].sockfd = connfd; //创建定时器,设置回调函数和超时时间,绑定用户数据,将定时器添加到链表中 util_timer *timer = new util_timer; // 绑定用户数据,将该连接套接字对应的User对象的指针保存在定时器的user_data成员变量中,以便在定时器回调函数中使用 timer->user_data = &users_timer[connfd]; timer->cb_func = cb_func;// 将回调函数设置为cb_func //将定时器的超时时间设置为当前时间加上3倍的TIMESLOT值 time_t cur = time(NULL); timer->expire = cur + 3 * TIMESLOT; users_timer[connfd].timer = timer; utils.m_timer_lst.add_timer(timer);// 将定时器添加到链表中 }
void WebServer::adjust_timesr(util_timer *timer) { time_t cur = time(NULL); // 将定时器的超时时间设置为当前时间+3个单位的时间片,这意味着定时器将在当前时间之后的3个时间片后过期,即等待数据传输完成后再执行回调函数 timer->expire = cur + 3 * TIMESLOT; utils.m_timer_lst.adjust_timer(timer);// 对定时器在链表上的位置进行调整 LOG_INFO("%s", "adjust timer once"); }
//用于处理定时器的函数。当定时器超时时,该函数被调用以执行定时器回调函数 void WebServer::deal_timer(util_timer *timer, int sockfd) { timer->cb_func(&users_timer[sockfd]);// 用定时器的回调函数 if (timer)// 如果定时器存在 { // 删除该定时器 utils.m_timer_lst.del_timer(timer); } LOG_INFO("close fd %d", users_timer[sockfd].sockfd); }
bool WebServer::dealclientdata() { struct sockaddr_in client_address; socklen_t client_addrlength = sizeof(client_address); // 根据服务器的触发模式(m_LISTENTrigmode),执行不同的操作 if (0 == m_LISTENTrigmode) // 如果触发模式为0,即LT模式 { // 则调用accept()函数接受客户端连接请求,并传入参数m_listenfd作为监听套接字 int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength); if (connfd < 0)// 如果连接失败,则记录错误信息并返回false { LOG_ERROR("%s:errno is:%d", "accept error", errno); return false; } if (http_conn::m_user_count >= MAX_FD)// 如果当前连接数超出最大限制(MAX_FD) { utils.show_error(connfd, "Internal server busy"); LOG_ERROR("%s", "Internal server busy"); return false; } timer(connfd, client_address); //创建一个定时器,并将套接字和客户端地址作为参数传入 } else //ET { while (1) { // 使用while循环接受所有连接请求 int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength); if (connfd < 0) { LOG_ERROR("%s:errno is:%d", "accept error", errno); break; } if (http_conn::m_user_count >= MAX_FD) { utils.show_error(connfd, "Internal server busy"); LOG_ERROR("%s", "Internal server busy"); break; } timer(connfd, client_address); } return false; } return true; }
函数原型

recv 函数在 <sys/socket.h> 头文件中声明,其标准原型如下:

参数详解int sockfd含义: 一个已连接的套接字描述符。这个套接字必须是:面向连接的(如 SOCK_STREAM,即 TCP 套接字)。或者是一个已经通过 connect 函数连接到对等方的无连接套接字(如 SOCK_DGRAM,即 UDP 套接字)。对于普通的无连接 UDP 套接字,通常使用 recvfrom 来同时获取数据来源的地址。void *buf含义: 指向一个内存缓冲区的指针,用于存放接收到的数据。size_t len含义: 缓冲区 buf 的最大长度(以字节为单位)。recv 函数最多会向缓冲区写入 len 个字节的数据。int flags含义: 用于修改函数行为的标志位。可以单独使用,也可以通过按位或 | 组合使用。常见的标志有:0: 默认行为,阻塞等待,直到有数据到达。MSG_PEEK: “窥探”数据。将数据从网络缓冲区复制到应用缓冲区 buf 中,但不从内核的网络接收缓冲区中移除这些数据。下一次调用 recv(不带 MSG_PEEK)时,还会收到同样的数据。MSG_WAITALL: 等待所有数据到达。函数会阻塞,直到接收到了恰好 len 个字节的数据,或者连接被关闭/发生错误。注意:即使设置了此标志,如果连接被对端正常关闭(收到 FIN),它也可能返回少于 len 字节的数据。MSG_DONTWAIT: 以非阻塞方式操作。如果没有数据立即可用,函数会立即返回失败,并设置错误码为 EAGAIN 或 EWOULDBLOCK,而不是阻塞等待。

返回值成功: 返回实际接收到的字节数。返回值可能小于参数 len,这很常见。对于 TCP,返回值 0 有特殊含义:表示对端已经关闭了连接(发送了 FIN 包)。这是判断连接是否关闭的重要依据。对于 UDP,返回值 0 是合法的,表示收到了一个长度为 0 的数据报。失败: 返回 -1,并设置全局变量 errno 以指示具体的错误。常见的错误有:EAGAIN / EWOULDBLOCK: 套接字被设置为非阻塞,并且没有数据可读。ECONNRESET: 连接被对端强制重置(如对端进程崩溃)。EINTR: 系统调用被信号中断。
bool WebServer::dealwithsignal(bool &timeout, bool &stop_server) { int ret = 0; int sig; char signals[1024]; // 使用recv()函数从管道读取信号,并将信号存储在名为“signals”的字符数组中 ret = recv(m_pipefd[0], signals, sizeof(signals), 0); if (ret == -1) {// 如果读取失败,则返回false return false; } else if (ret == 0) {// 如果没有收到任何信号,则返回false return false; } else { // 否则,遍历字符数组并根据不同的信号类型执行相应的操作 for (int i = 0; i < ret; ++i) { switch (signals[i]) { // 如果收到SIGALRM信号,则将timeout设置为true,表示定时器超时 // 这通常是由定时器到期引起的,可以让主函数检查此标志并执行相应的操作 case SIGALRM: { timeout = true; break; } case SIGTERM: { // 如果收到SIGTERM信号,则将stop_server设置为true,表示需要停止服务器。 // 这通常是由管理员或操作系统发出的信号,用于安全关闭服务器 stop_server = true; break; } } } } return true; } 
//用于处理读事件的函数。当服务器监测到套接字上有可读事件时,该函数将被调用以处理数据 void WebServer::dealwithread(int sockfd) { // 获取与套接字相关联的定时器 util_timer *timer = users_timer[sockfd].timer; // 根据服务器模型(m_actormodel)执行不同的操作 // 如果服务器模型为1,即采用Reactor模型 if (1 == m_actormodel) { if (timer)//调整定时器:给定时器续命 去处理业务 这个设计确保了在处理业务期间连接不会被意外断开。 { adjust_timer(timer); } //若监测到读事件,将该事件放入请求队列 m_pool->append(users + sockfd, 0);//users + sockfd 实际上就是 &users[sockfd],即获取对应客户端的数据结构指针 while (true) // 使用while循环等待其他线程处理请求并更新用户状态,直到发现用户状态已经被改变为“improv=1”才退出循环 { if (1 == users[sockfd].improv) { //如果定时器标志为1 if (1 == users[sockfd].timer_flag) { //处理定时器事件 deal_timer(timer, sockfd); users[sockfd].timer_flag = 0; } users[sockfd].improv = 0; break; } } } // 如果服务器模型为0,即采用Proactor模型 else { //主线程进行读取套接字上的数据 if (users[sockfd].read_once()) { LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr)); //若监测到读事件,将该事件放入请求队列 m_pool->append_p(users + sockfd);//append_p是Proactor模式,I/O已完成,工作只需要业务处理 if (timer) { adjust_timer(timer); } } // 如果读取失败,则调用“deal_timer(timer,sockfd)”函数处理定时器事件(关闭连接) else { deal_timer(timer, sockfd); } } } 
//同上 void WebServer::dealwithwrite(int sockfd) { // 获取与套接字相关联的定时器 util_timer *timer = users_timer[sockfd].timer; //reactor if (1 == m_actormodel) { if (timer) { adjust_timer(timer); } m_pool->append(users + sockfd, 1);// 将写事件放入请求队列 while (true) { if (1 == users[sockfd].improv) { if (1 == users[sockfd].timer_flag) { deal_timer(timer, sockfd); users[sockfd].timer_flag = 0; } users[sockfd].improv = 0; break; } } } else { //proactor if (users[sockfd].write())// 使用“users[sockfd].write()”函数向客户端发送数据 { LOG_INFO("send data to the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr)); if (timer) { adjust_timer(timer); } } else { deal_timer(timer, sockfd); } } }
函数原型

参数详解

1. epfd作用epoll 实例的文件描述符来源:由 epoll_create() 或 epoll_create1() 创建

2. events作用:输出参数,用于接收就绪事件的数组类型struct epoll_event*重要:这是就绪事件的集合,不是所有监控的事件

3. maxevents作用:指定 events 数组的大小要求:必须大于 0建议:通常设置为 events 数组的长度

4. timeout作用:指定超时时间(毫秒)取值-1:阻塞等待,直到有事件发生0:立即返回,即使没有事件(非阻塞轮询)>0:最多等待指定的毫秒数

返回值成功:返回就绪的文件描述符数量超时:返回 0(没有事件发生)错误:返回 -1,并设置 errno
void WebServer::eventLoop() { bool timeout = false;// 用于表示是否超时 bool stop_server = false;// 用于表示是否停止服务器 while (!stop_server) //当stop_server变量被设置为true时,表示服务器需要停止运行,则跳出循环,结束主函数的执行 { // 调用 epoll_wait 函数等待事件的发生,在有事件发生时返回相应的事件类型和文件描述符 int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1); //错误处理 if (number < 0 && errno != EINTR) { LOG_ERROR("%s", "epoll failure"); break; } for (int i = 0; i < number; i++) { int sockfd = events[i].data.fd; // 如果事件类型是新连接请求,则调用 dealclinetdata 函数进行处理 if (sockfd == m_listenfd) { bool flag = dealclientdata(); if (false == flag) continue; } //如果事件类型是客户端断开、服务端关闭连接或出现错误,则移除对应的定时器 else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) { //服务器端关闭连接,移除对应的定时器 util_timer *timer = users_timer[sockfd].timer; deal_timer(timer, sockfd); } //处理信号 else if ((sockfd == m_pipefd[0]) && (events[i].events & EPOLLIN)) { bool flag = dealwithsignal(timeout, stop_server); if (false == flag) LOG_ERROR("%s", "dealclientdata failure"); } //处理客户连接上接收到的数据 else if (events[i].events & EPOLLIN) { dealwithread(sockfd); } //写事件 else if (events[i].events & EPOLLOUT) { dealwithwrite(sockfd); } } //如果timeout变量为 true,则表明当前轮询已经超时,需要处理定时器事件 if (timeout) { // 提供timer_handler函数来处理链表中已经到期的定时器事件 utils.timer_handler(); LOG_INFO("%s", "timer tick"); timeout = false; } } }

Timer

lst_timer.h

函数指针:是C语言中的一个高级特性,它允许将函数作为参数传递给其他函数,或者保存函数的地址以便后续调用。函数指针的定义格式通常为:类型名 (* 函数名) (函数参数列表);

void func1(void) {
   printf("test for function pointer.\n");
}
void (*pFunc)(void); // 函数指针定义
pFunc = func1; // 函数指针赋值
(*pFunc)(); // 函数指针调用
// pFunc(); // 调用的第二种写法,效果和上面一样

函数指针的作用:

函数指针的主要作用是两个方面:一是作为回调函数,允许在运行时决定调用哪个函数;二是作为函数的参数,使得函数可以接受不同行为的函数作为参数,提高代码的复用性和灵活性。
#ifndef LST_TIMER #define LST_TIMER #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <sys/epoll.h> #include <fcntl.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <sys/stat.h> #include <string.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <stdarg.h> #include <errno.h> #include <sys/wait.h> #include <sys/uio.h> #include <time.h> #include "../log/log.h" class util_timer;//先说明,client_data要用到得先声明 struct client_data { sockaddr_in address;//cfd_address int sockfd;//cfd util_timer *timer;//client_data内部封装timer指针 }; class util_timer { public: util_timer() : prev(NULL), next(NULL) {}//双向链表 public: time_t expire; // 超时时间(绝对时间) void (*cb_func)(client_data*); // 超时回调函数指针 client_data* user_data; // 回调函数的用户数据 util_timer* prev; // 前向指针 util_timer* next; // 后向指针 }; //有两个类:sort_timer_lst(排序定时器链表)和(工具定时器)以及结构体client_data(客户端数据) class sort_timer_lst { public: sort_timer_lst(); ~sort_timer_lst(); void add_timer(util_timer* timer); // 添加定时器 void adjust_timer(util_timer* timer); // 调整定时器位置 void del_timer(util_timer* timer); // 删除定时器 void tick(); // 处理超时定时器 private: void add_timer(util_timer *timer, util_timer *lst_head); util_timer *head; util_timer *tail; }; //开始定义 class Utils //工具类 { public: Utils() {} ~Utils() {} void init(int timeslot); //对文件描述符设置非阻塞(ET) int setnonblocking(int fd); //将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT void addfd(int epollfd, int fd, bool one_shot, int TRIGMode); //信号处理函数 static void sig_handler(int sig); //设置信号函数 void addsig(int sig, void(handler)(int), bool restart = true); //定时处理任务,重新定时以不断触发SIGALRM信号 void timer_handler(); void show_error(int connfd, const char *info); public: static int* u_pipefd; // 管道文件描述符(用于信号通知) sort_timer_lst m_timer_lst; // 定时器链表 static int u_epollfd; // epoll实例 int m_TIMESLOT; // 超时时间间隔 }; void cb_func(client_data *user_data);//函数cb_func可以认为是默认的回调函数 #endif

lst_timer.cpp

#include "lst_timer.h" #include "../http/http_conn.h" //初始化链表 sort_timer_lst::sort_timer_lst() { head = NULL; tail = NULL; } sort_timer_lst::~sort_timer_lst() { util_timer *tmp = head; while (tmp) //一个一个清空 { head = tmp->next; delete tmp; tmp = head; } } void sort_timer_lst::add_timer(util_timer *timer) { if (!timer) { return; } if (!head) //空链表情况 { head = tail = timer; return; } // 插入到头部 都是些特殊情况 if (timer->expire < head->expire) { timer->next = head; head->prev = timer; head = timer; return; } add_timer(timer, head); } void sort_timer_lst::adjust_timer(util_timer *timer) //新加入的timer { //第一步:检查是否需要调整 if (!timer) { return; } util_timer *tmp = timer->next; /* !tmp:当前定时器是链表最后一个,不需要调整 timer->expire < tmp->expire:当前定时器的过期时间仍然比下一个早,位置正确 */ if (!tmp || (timer->expire < tmp->expire)) { return; } //第二步:处理头节点情况 if (timer == head) { head = head->next; // 头指针后移 head->prev = NULL; // 新头节点前驱置空 timer->next = NULL; // 分离当前定时器 add_timer(timer, head); // 重新插入到合适位置 } //第三步:处理中间节点情况 else { timer->prev->next = timer->next; // 前驱指向后继 timer->next->prev = timer->prev; // 后继指向前驱 add_timer(timer, timer->next); // 重新插入 } } void sort_timer_lst::del_timer(util_timer *timer) { if (!timer) { return; } if ((timer == head) && (timer == tail)) //空链表的情况 { delete timer; head = NULL; tail = NULL; return; } if (timer == head) { head = head->next; head->prev = NULL; delete timer; return; } if (timer == tail) { tail = tail->prev; tail->next = NULL; delete timer; return; } timer->prev->next = timer->next; timer->next->prev = timer->prev; delete timer; } void sort_timer_lst::tick() //处理超时的定时器 { if (!head) { return; } time_t cur = time(NULL); util_timer *tmp = head; while (tmp) //从头开始遍历 { if (cur < tmp->expire) //是没超时的情况 时间大于当前时间就是没超时 { break; } //超时的情况 tmp->cb_func(tmp->user_data);//执行回调函数 head = tmp->next;//下一位 if (head) { head->prev = NULL;//前面一位删了 } delete tmp; tmp = head;//继续下一位 } } void sort_timer_lst::add_timer(util_timer *timer, util_timer *lst_head) //刚才的是public现在是private函数 { util_timer *prev = lst_head; util_timer *tmp = prev->next;//刚才处理了异常的情况 while (tmp) //当tmp!=null的情况 { //现在就是在中间的情况 if (timer->expire < tmp->expire) //接起来就行 { prev->next = timer; timer->next = tmp; tmp->prev = timer; timer->prev = prev; break; } prev = tmp; tmp = tmp->next;//继续向后遍历 } if (!tmp) //tmp=null的时候 1是链表初始的时候为空 2是遍历到链表末尾 { prev->next = timer; timer->prev = prev; timer->next = NULL; tail = timer; } } //初始化Util工具类的类内参数 void Utils::init(int timeslot) { m_TIMESLOT = timeslot; } //对文件描述符设置非阻塞 int Utils::setnonblocking(int fd) { int old_option = fcntl(fd, F_GETFL);//使用fcntl(fd, F_GETFL)获取文件描述符fd的当前状态标志 //保存到old_option中,以便后续恢复使用 int new_option = old_option | O_NONBLOCK;//使用位或操作|在原有标志基础上添加O_NONBLOCK标志 fcntl(fd, F_SETFL, new_option);//将包含非阻塞标志的新标志设置回文件描述符 return old_option;//返回修改前的原始标志,方便调用者需要时恢复原状态 } //Utils添加addfd函数 向epoll实例中添加文件描述符并设置相应的事件模式 void Utils::addfd(int epollfd, int fd, bool one_shot, int TRIGMode) { epoll_event event; event.data.fd = fd; if (1 == TRIGMode) //ET event.events = EPOLLIN | EPOLLET | EPOLLRDHUP; //EPOLLET: 边缘触发模式 else //LT event.events = EPOLLIN | EPOLLRDHUP; //EPOLLRDHUP: 对端关闭连接或半关闭 if (one_shot) event.events |= EPOLLONESHOT; //EPOLLONESHOT: 保证事件只被触发一次,需要重新注册 epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);//注册到epoll实例(&event) setnonblocking(fd);//ET要用非阻塞I/O } //Utils信号处理函数 用于在信号发生时通过管道通知主程序 void Utils::sig_handler(int sig) { //为保证函数的可重入性,保留原来的errno int save_errno = errno; int msg = sig; send(u_pipefd[1], (char *)&msg, 1, 0); // u_pipefd[1]: 管道的写端文件描述符 // (char *)&msg: 将信号值转换为字符指针,1是字节大小,0是默认发送标志 errno = save_errno;//恢复errno } //设置信号函数 void Utils::addsig(int sig, void(handler)(int), bool restart) { struct sigaction sa; //创建sigaction结构体 memset(&sa, '\0', sizeof(sa));//清空结构体 sa.sa_handler = handler;//上面写的信号处理函数 if (restart) sa.sa_flags |= SA_RESTART; //SA_RESTART: 被信号中断的系统调用自动重启,避免系统调用被信号中断后需要手动重试 sigfillset(&sa.sa_mask);//在处理当前信号时,阻塞所有其他信号 assert(sigaction(sig, &sa, NULL) != -1); //使用sigaction系统调用注册信号处理,使用assert确保设置成功,失败时程序终止 } //定时处理任务,重新定时以不断触发SIGALRM信号 void Utils::timer_handler() { m_timer_lst.tick(); alarm(m_TIMESLOT); } void Utils::show_error(int connfd, const char *info) { send(connfd, info, strlen(info), 0); close(connfd); } int *Utils::u_pipefd = 0; int Utils::u_epollfd = 0; class Utils; void cb_func(client_data *user_data) //定时器回调函数 { assert(user_data); epoll_ctl(Utils::u_epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);//从epoll中移除文件描述符 close(user_data->sockfd);//关闭socket连接 http_conn::m_user_count--;//更新用户计数 } 

Threadpool

#ifndef THREADPOOL_H #define THREADPOOL_H #include <list> #include <cstdio> #include <exception> #include <pthread.h> #include "../lock/locker.h" #include "../CGImysql/sql_connection_pool.h" //这是一个模板类,T代表要处理的任务类型。 template <typename T> class threadpool //由任务队列和线程池工作线程完成 { public: //初始化线程 threadpool(int actor_model, connection_pool *connPool, int thread_number = 8, int max_request = 10000); ~threadpool(); //往请求队列中添加任务(任务入队) bool append(T *request, int state);//actor模式 bool append_p(T *request);//proactor模式 private: //工作线程运行的函数,它不断从工作队列中取出任务并执行之 static void *worker(void *arg); //必须是静态函数:pthread_create要求回调函数是普通C函数或静态成员函数 void run(); private: //线程池相关 int m_thread_number; //线程池中的线程数 pthread_t *m_threads; //描述线程池的数组,其大小为m_thread_number //请求队列相关 std::list<T *> m_workqueue; //请求队列 int m_max_requests; //请求队列中允许的最大请求数 locker m_queuelocker; //保护请求队列的互斥锁 sem m_queuestat; //是否有任务需要处理(信号量,用于线程同步) connection_pool *m_connPool;//数据库 /*数据库主要是跟线程池工作线程有关系,其中客户端发送请求过来,线程接收请求,然后线程进行业务逻辑,接着申请数据库连接,主要是线程要用到数据库的操作*/ int m_actor_model; //模型切换 }; template <typename T> threadpool<T>::threadpool( int actor_model, connection_pool *connPool, int thread_number, int max_requests) : m_actor_model(actor_model),m_thread_number(thread_number), m_max_requests(max_requests), m_threads(NULL),m_connPool(connPool) //构造函数 初始化操作 { if (thread_number <= 0 || max_requests <= 0) //如果线程数量和最大请求数不合理 抛出异常 throw std::exception(); m_threads = new pthread_t[m_thread_number]; //创建线程数组 if (!m_threads) throw std::exception(); for (int i = 0; i < thread_number; ++i) { //创建线程,指定线程执行的函数为worker,传入当前线程池对象作为参数,NULL表示使用默认的线程属性 if (pthread_create(m_threads + i, NULL, worker, this) != 0) { delete[] m_threads; throw std::exception(); } if (pthread_detach(m_threads[i])) //分离线程 { delete[] m_threads; throw std::exception(); } } } //线程池的析构函数 template <typename T> threadpool<T>::~threadpool() { delete[] m_threads; } //向队列添加任务 template <typename T> bool threadpool<T>::append(T *request, int state) { //1.加锁 保护请求队列的访问 m_queuelocker.lock(); //2.判断条件 判断请求队列是否已满 if (m_workqueue.size() >= m_max_requests) { m_queuelocker.unlock(); return false; } //3.将请求添加到请求队列中 request->m_state = state;//设置任务的状态 m_workqueue.push_back(request); //4.解锁 m_queuelocker.unlock(); //5.通知有任务需要处理 m_queuestat.post();//增加信号量的值,唤醒等待的工作线程 return true;//表示成功添加任务 } template <typename T> bool threadpool<T>::append_p(T *request) { m_queuelocker.lock(); if (m_workqueue.size() >= m_max_requests) { m_queuelocker.unlock(); return false; } m_workqueue.push_back(request); m_queuelocker.unlock(); m_queuestat.post(); return true; } //线程工作函数 template <typename T> void *threadpool<T>::worker(void *arg) { threadpool* pool = (threadpool*)arg;//1.将参数转化为线程池对象指针 pool->run(); // 2. 调用线程池的run方法 return pool;// 3. 返回线程池指针 } //pool->run run()的实现 template <typename T> void threadpool<T>::run() { while (true) // 无限循环,线程持续工作 { //等待任务信号 m_queuestat.wait(); //加锁 m_queuelocker.lock(); if (m_workqueue.empty()) { m_queuelocker.unlock(); continue; } T* request = m_workqueue.front();//取出请求队列中的第一个任务 m_workqueue.pop_front();//拿出后弹出 m_queuelocker.unlock(); if (!request) continue; if (1 == m_actor_model)//Reactor { if (0 == request->m_state)//读事件 { if (request->read_once())//成功读取 { request->improv = 1;//标记改进 connectionRAII mysqlcon(&request->mysql, m_connPool);//自动管理数据库连接 request->process();//处理业务逻辑 } else //读取失败 { request->improv = 1; request->timer_flag = 1; } } else { if (request->write()) //写事件 { request->improv = 1; } else { request->improv = 1; request->timer_flag = 1; } } } else //proactor模式 { connectionRAII mysqlcon(&request->mysql, m_connPool); request->process(); } } } #endif 

CGImysql

sql_connection_pool.h

#ifndef _CONNECTION_POOL_ #define _CONNECTION_POOL_ #include <stdio.h> #include <list> #include <mysql/mysql.h> #include <error.h> #include <string.h> #include <iostream> #include <string> #include "../lock/locker.h" #include "../log/log.h" class connection_pool { public: MYSQL *GetConnection(); //获取可用连接 bool ReleaseConnection(MYSQL *conn); //释放连接 int GetFreeConn(); //获取空闲连接 void DestroyPool(); //销毁所有连接 //单例模式 获取唯一实例 static connection_pool *GetInstance(); void init(string url, string User, string PassWord, string DataBaseName, int Port, int MaxConn, int close_log); private: connection_pool(); ~connection_pool(); int m_MaxConn; //最大连接数 int m_CurConn; //当前已使用的连接数 int m_FreeConn; //当前空闲的连接数 locker lock; list<MYSQL *> connList; //连接池 sem reserve;//// 信号量,控制连接获取 public: string m_url //主机地址 string m_Port; //数据库端口号 string m_User; //登陆数据库用户名 string m_PassWord; //登陆数据库密码 string m_DatabaseName; //使用数据库名 int m_close_log; //日志开关 }; //RAII:这是一种设计模式,能保证数据库安全关闭连接 class connectionRAII{ public: connectionRAII(MYSQL **con, connection_pool *connPool); ~connectionRAII(); private: MYSQL *conRAII; connection_pool *poolRAII; }; #endif

Sql_connection_pool.cpp

#include <mysql/mysql.h> #include <stdio.h> #include <string> #include <string.h> #include <stdlib.h> #include <list> #include <pthread.h> #include <iostream> #include "sql_connection_pool.h" //构造函数 connection_pool::connection_pool() { m_CurConn = 0; m_FreeConn = 0; } //获取唯一实例 connection_pool *connection_pool::GetInstance() { static connection_pool connPool; return &connPool; } //初始化 主要是实现参数初始化、在连接池中添加可用连接(为maxconn个) void connection_pool::init(string url, string User, string PassWord, string DBName, int Port, int MaxConn, int close_log) { m_url = url; m_Port = Port; m_User = User; m_PassWord = PassWord; m_DatabaseName = DBName; m_close_log = close_log; for (int i = 0; i < MaxConn; i++) { MYSQL *con = NULL; con = mysql_init(con);//数据库初始化函数 if (con == NULL) //初始化失败 { LOG_ERROR("MySQL Error");//在日志上记录 exit(1); } con = mysql_real_connect(con, url.c_str(), User.c_str(), PassWord.c_str(), DBName.c_str(), Port, NULL, 0);//连接数据库 if (con == NULL) { LOG_ERROR("MySQL Error"); exit(1); } connList.push_back(con);//连接池增加连接 ++m_FreeConn;//空闲数加1 } reserve = sem(m_FreeConn);// 创建信号量,初始值为空闲连接数,用于控制并发访问 m_MaxConn = m_FreeConn;//重新赋值 } //当有请求时,从数据库连接池中返回一个可用连接,更新使用和空闲连接数 MYSQL *connection_pool::GetConnection() { MYSQL *con = NULL; if (0 == connList.size()) return NULL; reserve.wait(); /*如果信号量值 > 0,立即获取并继续执行,如果信号量值 = 0,线程会阻塞等待,直到有连接被释放*/ lock.lock(); con = connList.front(); connList.pop_front(); --m_FreeConn; ++m_CurConn; lock.unlock(); return con; } /*线程A请求连接 → 信号量wait() → 获取锁 → 操作连接池 → 释放锁 → 返回连接 线程B请求连接 → 信号量wait() → 阻塞等待 → 线程A释放连接 → 信号量post() → 线程B继续*/ //释放当前使用的连接 bool connection_pool::ReleaseConnection(MYSQL *con) { if (NULL == con) return false; lock.lock(); connList.push_back(con); ++m_FreeConn; --m_CurConn; lock.unlock(); reserve.post();//释放后回到连接池,又可用了 return true; } //销毁数据库连接池 void connection_pool::DestroyPool() { lock.lock(); if (connList.size() > 0) { list<MYSQL *>::iterator it;//迭代器遍历 for (it = connList.begin(); it != connList.end(); ++it) { MYSQL *con = *it; mysql_close(con);//逐个释放 } m_CurConn = 0; m_FreeConn = 0; connList.clear();//清空容器 } lock.unlock(); } //返回当前空闲的连接数 int connection_pool::GetFreeConn() { return this->m_FreeConn; } //析构函数:摧毁连接池 connection_pool::~connection_pool() { DestroyPool(); } //设计模式的实现 connectionRAII::connectionRAII(MYSQL **SQL, connection_pool *connPool){ //MYSQL**SQL:二级指针,用于返回获取的数据库连接 connection_pool *connPool:连接池对象指针 *SQL = connPool->GetConnection();//从连接池获取连接 conRAII = *SQL;//保存连接指针 poolRAII = connPool;//保存连接池指针 }//功能:从连接池获取一个数据库连接,并保存相关指针 connectionRAII::~connectionRAII(){ poolRAII->ReleaseConnection(conRAII); }//功能:自动将数据库连接释放回连接池

Log

block_queue.h

#ifndef BLOCK_QUEUE_H #define BLOCK_QUEUE_H #include <iostream> #include <stdlib.h> #include <pthread.h> #include <sys/time.h> #include "../lock/locker.h" //使用队列实现异步模式 template <class T> class block_queue { private: locker m_mutex;//互斥锁 cond m_cond;//条件变量 T* m_array;//循环数组 int m_size;//当前元素数量 int m_max_size;//队列最大容量 int m_front;//队首索引 int m_back;//队尾索引 public: block_queue(int max_size = 1000) { if (max_size <= 0)//检查传入的队列大小是否合法 { exit(-1); } //成员变量初始化 m_max_size = max_size;//设置队列最大容量 m_array = new T[max_size];//动态分配数组内存 m_size = 0;//当前元素个数为0 m_front = -1; m_back = -1;//表示队列为空 还没有元素 } void clear() { m_mutex.lock(); m_size = 0; m_front = -1; m_back = -1; m_mutex.unlock(); } ~block_queue()//释放数组 { m_mutex.lock(); if (m_array != NULL) delete [] m_array; m_mutex.unlock(); } //判断队列是否满了 bool full() { m_mutex.lock(); if (m_size >= m_max_size) { m_mutex.unlock(); return true; } m_mutex.unlock(); return false; } //判断队列是否为空 bool empty() { m_mutex.lock(); if (0 == m_size) { m_mutex.unlock(); return true; } m_mutex.unlock(); return false; } //返回队首元素 bool front(T &value) { m_mutex.lock(); if (0 == m_size) { m_mutex.unlock(); return false; } value = m_array[m_front]; m_mutex.unlock(); return true; } //返回队尾元素 bool back(T &value) { m_mutex.lock(); if (0 == m_size) { m_mutex.unlock(); return false; } value = m_array[m_back]; m_mutex.unlock(); return true; } int size() { int tmp = 0; m_mutex.lock(); tmp = m_size; m_mutex.unlock(); return tmp; } int max_size() { int tmp = 0; m_mutex.lock(); tmp = m_max_size; m_mutex.unlock(); return tmp; } //往队列添加元素,需要将所有使用队列的线程先唤醒 //当有元素push进队列,相当于生产者生产了一个元素 //若当前没有线程等待条件变量,则唤醒无意义 bool push(const T &item) { m_mutex.lock(); if (m_size >= m_max_size)//队列满时 { m_cond.broadcast();//调用 m_cond.broadcast() 唤醒所有等待的消费者线程 让他们去消费 m_mutex.unlock(); return false; } m_back = (m_back + 1) % m_max_size;//移动队尾指针 m_array[m_back] = item; m_size++; m_cond.broadcast();//通知所有等待的消费者线程有新数据可用 m_mutex.unlock(); return true; } //pop时,如果当前队列没有元素,将会等待条件变量 //消费者:从队列取出日志 bool pop(T &item) //T&item:用于存储弹出的元素 { m_mutex.lock(); //队列空时等待 while (m_size <= 0)//使用 while 而不是 if 是为了防止虚假唤醒 { if (!m_cond.wait(m_mutex.get()))// 等待条件满足(其中wait等待前,会先释放锁,然后等待,最后等待成功会重新获取锁) { m_mutex.unlock();// 等待失败,显式释放锁(安全措施,确保锁在任何情况都能被释放) return false; } } m_front = (m_front + 1) % m_max_size;//循环队列,移动头指针 item = m_array[m_front]; m_size--; m_mutex.unlock(); return true; } //增加了超时处理(队列没有可用的元素,就等待,直到超时) bool pop(T &item, int ms_timeout) //T&item:用于存储弹出的元素 int ms_timeout:超时时间(毫秒) { struct timespec t = {0, 0};//秒 纳秒 struct timeval now = {0, 0};//秒 毫秒 gettimeofday(&now, NULL);//获取当前系统时间 m_mutex.lock(); if (m_size <= 0)//队列为空 { t.tv_sec = now.tv_sec + ms_timeout / 1000;//秒数 = 当前秒数 + 超时毫秒数 / 1000 t.tv_nsec = (ms_timeout % 1000) * 1000;//纳秒数 = (超时毫秒数 % 1000) * 1000 if (!m_cond.timewait(m_mutex.get(), t)) //带超时的条件等待 { m_mutex.unlock(); return false; } } if (m_size <= 0) //即使被唤醒,队列也可能为空 防止虚假唤醒 { m_mutex.unlock(); return false; } m_front = (m_front + 1) % m_max_size;//循环队列,移动头指针 item = m_array[m_front]; m_size--; m_mutex.unlock(); return true; } }; #endif 

log.h

#ifndef LOG_H #define LOG_H #include <stdio.h> #include <iostream> #include <string> #include <stdarg.h> #include <pthread.h> #include "block_queue.h" class Log { public: //C++11以后,使用局部变量懒汉不用加锁(懒汉模式:首次调用时创建实例) //单例模式实现 整个程序只有一个日志实例 static Log *get_instance()//单例模式实现 { static Log instance; return &instance; } static void *flush_log_thread(void *args)//异步内容 { Log::get_instance()->async_write_log(); } bool init(const char *file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0); void write_log(int level, const char *format, ...);//核心写入函数 void flush(void);//手动刷新缓冲区 private: Log(); virtual ~Log(); //异步日志机制 void *async_write_log() { string single_log; while (m_log_queue->pop(single_log))// 从阻塞队列消费日志 { m_mutex.lock(); fputs(single_log.c_str(), m_fp);//将日志字符串写入文件 m_mutex.unlock(); } } private: //文件操作相关 char dir_name[128]; //日志目录 char log_name[128]; //log文件名 int m_split_lines; //日志分隔行数 int m_log_buf_size; //日志缓冲区大小 long long m_count; //日志行数计数 int m_today; //因为按天分类,记录当前时间是那一天 //同步机制 locker m_mutex; FILE *m_fp; //打开log的文件指针 char *m_buf; //格式化缓冲区 //异步相关 block_queue<string> *m_log_queue; //阻塞队列 bool m_is_async; //是否同步标志位 int m_close_log; //是否关闭日志 }; #define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();} #define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();} #define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();} #define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();} /* * 宏定义结构分析 #define 宏名(参数, ...) 条件 { 操作; 操作; } 条件判断:0==close_log 开启日志 变参宏:(format, ...) ...表示可变参数 ##__VA_ARGS__用来处理可变参数 日志级别:0-调试 1-信息 2-警告 3-错误 */ #endif

log.cpp



参数说明thread (输出参数)类型pthread_t *作用: 指向 pthread_t 类型变量的指针。函数成功返回后,该变量会被填充为新创建线程的线程标识符(Thread ID)。这个 ID 在后面其他线程函数(如 pthread_joinpthread_cancel)中用于引用这个特定的线程。attr (输入参数)类型pthread_attr_t *作用: 用于设置新线程的属性,如栈大小、调度策略、分离状态等。特殊值:如果传入 NULL,线程将使用默认属性创建。在大多数情况下,我们使用默认属性就足够了。如果需要自定义属性,必须先使用 pthread_attr_init 初始化一个属性对象,设置好所需属性后传入。start_routine (输入参数)类型void *(*)(void *)作用: 这是一个函数指针,指向新线程将要执行的函数。这个函数必须具有特定的签名:返回类型是 void *接收一个 void * 类型的参数arg (输入参数)类型void *作用: 传递给 start_routine 函数的参数。它是一个泛型指针,你可以传递任何数据的地址(如一个变量的地址、一个结构体的地址等)。如果不需要传递参数,可以传入 NULL

返回值成功: 返回 0失败: 返回一个错误代码(非零值),而不是设置 errno。常见的错误代码有:EAGAIN: 系统资源不足,无法创建另一个线程,或线程数量超过了系统的限制。EINVAL: 参数 attr 无效。EPERM: 没有权限设置调度策略和参数。
#include <string.h> #include <time.h> #include <sys/time.h> #include <stdarg.h> #include "log.h" #include <pthread.h> Log::Log()//构造初始化 { m_count = 0;// 日志行数计数清零 m_is_async = false;// 默认同步模式 } Log::~Log() { if (m_fp != NULL) { fclose(m_fp); // 关闭日志文件 } } //init主要是:1.异步设置 2.智能为日记添加日期 3.初始化参数 然后开始追加文件 bool Log::init(const char *file_name, int close_log, int log_buf_size, int split_lines, int max_queue_size) { //如果设置了max_queue_size,则设置为异步模式 if (max_queue_size >= 1) { m_is_async = true; m_log_queue = new block_queue<string>(max_queue_size); pthread_t tid;//创建线程来消费日志 pthread_create(&tid, NULL, flush_log_thread, NULL);//参数3调用消费日志函数(pop)(将日志写入文件) } m_close_log = close_log; m_log_buf_size = log_buf_size; m_buf = new char[m_log_buf_size]; // 分配(格式化)缓冲区 memset(m_buf, '\0', m_log_buf_size);// 清空缓冲区 全变成0 m_split_lines = split_lines;// 设置文件分割行数 time_t t = time(NULL); struct tm *sys_tm = localtime(&t); struct tm my_tm = *sys_tm; //解引用*用以复制数据 // 构造日志文件名:前缀_年_月_日.log char log_full_name[256] = {0}; const char *p = strrchr(file_name, '/');// 在字符串 str 中从后向前查找字符/最后一次出现的位置(找不到返回null) if (p == NULL)// 没有目录路径,只有文件名 { //年份(上面的是按间隔算(1900)、月份(0-11变成1-12)、日期(1-31),例如:2024_11_25_server.log snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900,my_tm.tm_mon + 1, my_tm.tm_mday, file_name); } else // 包含目录路径 { //假设 file_name = "/var/log/server.log" strcpy(log_name, p + 1);//复制文件名部分(server.log) strncpy(dir_name, file_name, p - file_name + 1);//复制目录路径,其中+1是为了包含 '/' 本身 snprintf(log_full_name, 255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name); //"/var/log/2024_11_25_server.log" } m_today = my_tm.tm_mday;// 记录当前日期,用于按天分割 m_fp = fopen(log_full_name, "a"); // 以追加模式打开文件 if (m_fp == NULL)//打开失败 { return false; } return true; } //初始化追加了 直接写 void Log::write_log(int level, const char *format, ...) { struct timeval now = {0, 0};//秒和微秒 timeval(time_t、suseconds_t) gettimeofday(&now, NULL); time_t t = now.tv_sec;//提取秒数 struct tm *sys_tm = localtime(&t); struct tm my_tm = *sys_tm;//深拷贝时间结构 char s[16] = {0};//日志级别标识 switch (level) { case 0: strcpy(s, "[debug]:");//向字符数组加入相对应的标识 break; case 1: strcpy(s, "[info]:"); break; case 2: strcpy(s, "[warn]:"); break; case 3: strcpy(s, "[erro]:"); break; default: strcpy(s, "[info]:"); break; } m_mutex.lock(); m_count++;//增加行数计数 // 检查是否需要分割文件:如果日期变化或达到最大行数则需要分隔文件 //日志轮转机制 if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) { //需要写下一个日志了 char new_log[256] = {0}; fflush(m_fp); //将标准I/O库缓冲区中的数据强制写入到底层文件中。 fclose(m_fp);//关闭当前文件 char tail[16] = {0}; snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday); //分割文件的两种情况分别讨论 if (m_today != my_tm.tm_mday)//日期变化 { snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);//分别记录了不变的值(dir_name,log_name)只用修改要改的值(tail) m_today = my_tm.tm_mday;//更新日期 m_count = 0;//重置计数器 } else//行数达到限制 { snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines); } /*/var/log/myapp/2024_01_15_app.log.1 /var/log/myapp/2024_01_15_app.log.2*/ m_fp = fopen(new_log, "a");//创建新文件 } m_mutex.unlock(); //写入内容 va_list valst; va_start(valst, format);// 初始化可变参数列表 string log_str; m_mutex.lock(); // 格式化时间戳和日志级别 // "2024-12-25 14:30:45.123456 INFO " int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);//格式化用户内容 //写入核心内容 int m = vsnprintf(m_buf + n, m_log_buf_size - n - 1, format, valst);//-1是留给\0的 m_buf[n + m] = '\n'; // 添加换行 m_buf[n + m + 1] = '\0'; log_str = m_buf; // 转为string格式 m_mutex.unlock(); //写入策略选择 if (m_is_async && !m_log_queue->full())// 异步模式且队列未满 { m_log_queue->push(log_str); // 放入队列,立即返回 } else// 同步模式或队列已满 //确保日志不丢 { m_mutex.lock(); fputs(log_str.c_str(), m_fp);// 直接写入文件里 m_mutex.unlock(); } va_end(valst);// 清理可变参数 } void Log::flush(void) { m_mutex.lock(); //强制刷新写入流缓冲区 fflush(m_fp); //强制把数据从缓存推到磁盘(不然可能会丢失) m_mutex.unlock(); } 

Lock

#ifndef LOCKER_H #define LOCKER_H //多线程同步,确保任一时刻只能有一个线程能进入关键代码段 /* ​ =>信号量 ​ =>互斥锁 ​ =>条件变量 */ #include <exception> #include <pthread.h> #include <semaphore.h> //sem类是一个信号量封装类 /* 信号量是一种同步机制,用于控制多个线程之间的访问顺序。通常情况下, 当多个线程需要共享一个资源时, 需要使用信号量来确保每个线程按照一定的顺序访问该资源,从而避免出现竞争条件和数据不一致等问题 */ class sem { public: sem() { /*sem_init是一个用于初始化信号量的函数,它有三个参数,分别是: sem:指向要初始化的信号量的指针。 pshared:指定信号量是在进程间共享还是在线程间共享的标志。如果pshared为0,则表示信号量将在同一进程内的线程之间共享;如果pshared为非0值,则表示信号量可以在多个进程之间共享。 value:指定信号量的初始值 return 0表示成功 */ if (sem_init(&m_sem, 0, 0) != 0) { throw std::exception();//信号量初始化失败时,抛出一个std::exception对象,安全地中止当前操作并将错误状态通知给调用者 } } sem(int num) { if (sem_init(&m_sem, 0, num) != 0)//创建多个信号量 { throw std::exception(); } } ~sem() { // 毁信号量对象,释放资源 sem_destroy(&m_sem); } bool wait() //该方法会对信号量进行等待操作,如果信号量的值为0,则该方法会阻塞当前线程,直到有其他线程通过 post() 方法将信号量的值增加至大于0,才会唤醒该线程 { return sem_wait(&m_sem) == 0; } bool post() { //sem_post核心作用是增加(解锁)一个信号量的值。成功返回0,失败返回-1 return sem_post(&m_sem) == 0; } private: sem_t m_sem;// 初始化一个名为m_sem的信号量 }; /* 在多线程程序中,使用互斥锁可以避免多个线程同时访问共享资源而导致的数据竞争问题。 这里的 lock() 和 unlock() 方法就是为了控制对共享资源的访问, 只有获得了互斥锁的线程才能访问共享资源,其他线程则必须等待锁被释放才能获取它。 */ class locker { public: locker() { if (pthread_mutex_init(&m_mutex, NULL) != 0) { throw std::exception(); } } ~locker() { pthread_mutex_destroy(&m_mutex); } bool lock() { return pthread_mutex_lock(&m_mutex) == 0; } bool unlock() { return pthread_mutex_unlock(&m_mutex) == 0; } pthread_mutex_t *get() { return &m_mutex;// 返回互斥锁的指针 } private: pthread_mutex_t m_mutex; }; /* 多用于生产者消费者模型,只有符合条件变量才可以生产或者是进行消费 */ class cond { public: cond() { if (pthread_cond_init(&m_cond, NULL) != 0) { //pthread_mutex_destroy(&m_mutex); throw std::exception();//抛出异常 } } ~cond() { pthread_cond_destroy(&m_cond); } bool wait(pthread_mutex_t *m_mutex)//调用前必须已经获得 m_mutex 锁 { int ret = 0; //pthread_mutex_lock(&m_mutex); ret = pthread_cond_wait(&m_cond, m_mutex); //pthread_mutex_unlock(&m_mutex); /* 原子地释放锁并进入等待 被唤醒时重新获取锁 返回时线程持有锁 */ return ret == 0; } bool timewait(pthread_mutex_t *m_mutex, struct timespec t) { int ret = 0; //pthread_mutex_lock(&m_mutex); ret = pthread_cond_timedwait(&m_cond, m_mutex, &t); //pthread_mutex_unlock(&m_mutex); return ret == 0; } bool signal() { return pthread_cond_signal(&m_cond) == 0;//唤醒一个等待该条件变量的线程 } bool broadcast() { return pthread_cond_broadcast(&m_cond) == 0;//唤醒所有等待该条件变量的线程 } private: //static pthread_mutex_t m_mutex; pthread_cond_t m_cond; }; #endif 

Http

http_conn.h

#ifndef HTTPCONNECTION_H #define HTTPCONNECTION_H #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <sys/epoll.h> #include <fcntl.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <assert.h> #include <sys/stat.h> #include <string.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <stdarg.h> #include <errno.h> #include <sys/wait.h> #include <sys/uio.h> #include <map> #include "../lock/locker.h" #include "../CGImysql/sql_connection_pool.h" #include "../timer/lst_timer.h" #include "../log/log.h" class http_conn //存在于客户端和服务器相交流之间,客户端发起请求,服务器返回一个响应 { // 客户端:请求行、请求头部、空行、和请求体 //HTTP响应也由四个部分组成:分别是: 状态行、响应头、空行、响应体 public: //静态常量定义 static const int FILENAME_LEN = 200;//文件名的最大长度 static const int READ_BUFFER_SIZE = 2048;//读缓冲区的大小 static const int WRITE_BUFFER_SIZE = 1024;//写缓冲区的大小 //枚举类型 enum METHOD { GET = 0,//GET请求 POST,//POST请求 HEAD,//HEAD请求 PUT,//PUT DELETE,//DELETE TRACE,//TRACE OPTIONS,//OPTIONS CONNECT,//CONNECT PATH//PATH }; //解析状态枚举 服务器端 enum CHECK_STATE { CHECK_STATE_REQUESTLINE = 0,//正在解析请求行 CHECK_STATE_HEADER,//正在解析请求头 CHECK_STATE_CONTENT//正在解析请求体 }; // HTTP处理结果枚举 enum HTTP_CODE { NO_REQUEST,//请求不完整,需要继续读取客户数据 GET_REQUEST,//获得了完整的客户请求 BAD_REQUEST,//客户请求有语法错误 NO_RESOURCE,//没有资源 FORBIDDEN_REQUEST,//客户对资源没有足够的访问权限 FILE_REQUEST,//文件请求 INTERNAL_ERROR,//服务器内部错误 CLOSED_CONNECTION//客户端已经关闭连接 }; //行解析状态枚举(辅助解析内容) enum LINE_STATUS { LINE_OK = 0,//读取到完整行 LINE_BAD,//行格式出错 LINE_OPEN//行数据尚且不完整,需要继续读取 }; public: http_conn() {} ~http_conn() {} public: //初始化HTTP连接 void init(int sockfd, const sockaddr_in& addr, char*, int, int, string user, string passwd, string sqlname); void close_conn(bool real_close = true); void process();//处理HTTP请求 bool read_once();//一次性读取所有可用的数据到读缓冲区 bool write();//将响应数据写入套接字 sockaddr_in *get_address() { return &m_address;//获取客户端地址信息 } void initmysql_result(connection_pool *connPool);//从数据库连接池初始化用户数据 int timer_flag; int improv; private: void init();//初始化连接的各种状态变量 HTTP_CODE process_read();//http请求解析的主状态机 返回的是HTTP_CODE bool process_write(HTTP_CODE ret);//响应生成 HTTP_CODE parse_request_line(char *text);//请求行解析 HTTP_CODE parse_headers(char *text);//请求头解析 HTTP_CODE parse_content(char *text);//请求体解析 HTTP_CODE do_request();//业务逻辑处理 //辅助工具函数 //行处理相关 char *get_line() { return m_read_buf + m_start_line; };//返回当前要解析的行起始位置 LINE_STATUS parse_line();//在读取缓冲区中查找完整的行(以 \r\n 结尾) //内存管理 void unmap();//取消文件的内存映射 释放资源 //响应构建函数 //响应组件构建 bool add_response(const char *format, ...);//可变参数函数,格式化字符串到写缓冲区 bool add_status_line(int status, const char *title);//添加状态行,如 HTTP/1.1 200 OK bool add_headers(int content_length);//添加完整的响应头 bool add_content(const char *content);//添加响应体内容 //响应头字段 bool add_content_type(); bool add_content_length(int content_length); bool add_linger(); bool add_blank_line(); public: static int m_epollfd; //所有对象共享的epoll文件描述符 static int m_user_count;//当前连接的用户数(HTTP连接数量) MYSQL* mysql;//数据库连接指针 int m_state; //连接状态:读为0, 写为1 private: //包含了 HTTP 连接处理的所有状态和数据 //套接字和地址 int m_sockfd;//client sockaddr_in m_address; //读缓冲区管理 char m_read_buf[READ_BUFFER_SIZE];//读缓冲区(2048字节) long m_read_idx;// 当前已读取数据的末尾位置 long m_checked_idx;// 当前已解析数据的位置 int m_start_line; // 当前解析行的起始位置 //m_read_buf: [已解析数据 | 待解析数据 | 空闲空间] //↑ ↑ ↑ //m_start_line m_checked_idx m_read_idx //写缓冲区管理 char m_write_buf[WRITE_BUFFER_SIZE];// 写缓冲区(1024字节) int m_write_idx;// 写缓冲区当前写入位置 //解析状态机 CHECK_STATE m_check_state;// 当前解析状态(请求行/头部/内容) METHOD m_method;// HTTP 方法(GET/POST等) //请求信息 char *m_url;// 请求的URL char *m_version;// HTTP版本 char *m_host;// 主机名 long m_content_length;// 内容长度(POST请求) bool m_linger; // 是否保持连接 char *m_string; //存储请求头数据 int cgi; //是否启用的POST处理 //文件操作 char m_real_file[FILENAME_LEN];// 实际文件路径(200字节) char *m_file_address; // 文件内存映射地址 struct stat m_file_stat;//文件状态信息 struct iovec m_iv[2];//分散写结构数组 int m_iv_count;//分散写向量数量 /* m_iv[0]: 指向 HTTP 响应头(m_write_buf) m_iv[1]: 指向文件内容(m_file_address) */ int bytes_to_send;// 剩余要发送的字节数 int bytes_have_send;// 已发送的字节数 //服务器配置 char* doc_root; // 文档根目录 int m_TRIGMode; // 触发模式(ET/LT) int m_close_log; // 日志开关 map<string, string> m_users; // 用户缓存(用户名-密码) char sql_user[100]; // 数据库用户名 char sql_passwd[100]; // 数据库密码 char sql_name[100]; // 数据库名 }; #endif 

http_conn.cpp

strpbrk(char *str1, char *str2)在 str1 中查找 str2 中任意字符的第一次出现示例:strpbrk("GET /", " \t") 返回指向空格的指针

strspn(char *str1, char *str2)返回 str1 开头连续包含 str2 中字符的个数示例:strspn(" /index", " \t") 返回 3(3个空格)

strcasecmp / strncasecmp不区分大小写的字符串比较strncasecmp 只比较前 n 个字符
#include "http_conn.h" #include <mysql/mysql.h> #include <fstream> //定义http响应的一些状态信息 const char *ok_200_title = "OK"; const char *error_400_title = "Bad Request"; const char *error_400_form = "Your request has bad syntax or is inherently impossible to staisfy.\n"; const char *error_403_title = "Forbidden"; const char *error_403_form = "You do not have permission to get file form this server.\n"; const char *error_404_title = "Not Found"; const char *error_404_form = "The requested file was not found on this server.\n"; const char *error_500_title = "Internal Error"; const char *error_500_form = "There was an unusual problem serving the request file.\n"; locker m_lock; map<string, string> users;//存储user数据(姓名:密码) void http_conn::initmysql_result(connection_pool *connPool) { MYSQL *mysql = NULL; connectionRAII mysqlcon(&mysql, connPool);//使用connectionRAII函数获取连接同时自动管理释放 //在user表中检索username,passwd数据 if (mysql_query(mysql, "SELECT username,passwd FROM user"))//执行sql查询 { LOG_ERROR("SELECT error:%s\n", mysql_error(mysql)); } //从表中检索完整的结果集 MYSQL_RES *result = mysql_store_result(mysql); //返回结果集中的列数 int num_fields = mysql_num_fields(result); //返回所有字段结构的数组 MYSQL_FIELD *fields = mysql_fetch_fields(result); //从结果集中获取下一行,将对应的用户名和密码,存入map中 while (MYSQL_ROW row = mysql_fetch_row(result)) { string temp1(row[0]); string temp2(row[1]); users[temp1] = temp2; } } //对文件描述符设置非阻塞(lst_timer) int setnonblocking(int fd) { int old_option = fcntl(fd, F_GETFL);// 获取当前文件状态标志 int new_option = old_option | O_NONBLOCK; fcntl(fd, F_SETFL, new_option);// 设置新的文件状态标志 return old_option;// 返回原始状态(用于后续恢复) } //lst.timer已实现 void addfd(int epollfd, int fd, bool one_shot, int TRIGMode) { epoll_event event; event.data.fd = fd; if (1 == TRIGMode)//ET event.events = EPOLLIN | EPOLLET | EPOLLRDHUP; else //LT event.events = EPOLLIN | EPOLLRDHUP; if (one_shot) event.events |= EPOLLONESHOT; epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event); setnonblocking(fd); } //从内核时间表删除描述符 void removefd(int epollfd, int fd) { epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0); close(fd); } //将事件重置为ET/LT void modfd(int epollfd, int fd, int ev, int TRIGMode) { epoll_event event; event.data.fd = fd; if (1 == TRIGMode) event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP; else event.events = ev | EPOLLONESHOT | EPOLLRDHUP; epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);//修改 } int http_conn::m_user_count = 0; int http_conn::m_epollfd = -1; //关闭连接,关闭一个sockfdfd连接,客户总量减一 void http_conn::close_conn(bool real_close) { if (real_close && (m_sockfd != -1))//只有当需要真正关闭且 socket 有效时才执行 { printf("close %d\n", m_sockfd); removefd(m_epollfd, m_sockfd); // 从 epoll 中移除监控 m_sockfd = -1; // 标记 socket 为无效 m_user_count--; // 减少用户计数 } } //初始化连接,外部调用(接收配置参数,建立基础连接) void http_conn::init(int sockfd, const sockaddr_in &addr, char *root, int TRIGMode, int close_log, string user, string passwd, string sqlname) { m_sockfd = sockfd;//客户端文件描述符 m_address = addr;//客户端地址结构 addfd(m_epollfd, sockfd, true, m_TRIGMode);// 将 socket 添加到 epoll 监控(启用 EPOLLONESHOT) m_user_count++; doc_root = root; m_TRIGMode = TRIGMode; m_close_log = close_log; strcpy(sql_user, user.c_str()); strcpy(sql_passwd, passwd.c_str()); strcpy(sql_name, sqlname.c_str()); init();//内部调用 (重置会话状态,准备处理请求) } //初始化新接受的连接 //check_state默认为分析请求行状态 void http_conn::init() { mysql = NULL; // MySQL 连接指针,初始为 NULL //解析状态相关 m_check_state = CHECK_STATE_REQUESTLINE;//解析请求行 m_start_line = 0;//当前解析行的起始位置 m_checked_idx = 0;//当前已解析的数据位置 m_read_idx = 0;//当前已读取的末尾位置 //数据传输相关 bytes_to_send = 0; // 待发送字节数清零 bytes_have_send = 0; // 已发送字节数清零 m_write_idx = 0; // 写缓冲区位置重置 //HTTP请求相关 m_linger = false;//是否保持连接 m_method = GET;//http方法 m_url = 0;//请求的url m_version = 0;//http版本 m_host = 0;//主机名 m_content_length = 0;//内容长度 cgi = 0; //是否启用post处理 // m_state = 0;//连接状态 timer_flag = 0;//定时器标志 improv = 0;//是否improv //缓冲区管理 memset(m_read_buf, '\0', READ_BUFFER_SIZE); // 清空读缓冲区 memset(m_write_buf, '\0', WRITE_BUFFER_SIZE); // 清空写缓冲区 memset(m_real_file, '\0', FILENAME_LEN); // 清空文件路径缓冲区; } //行解析 查找\r\n分隔符 http_conn::LINE_STATUS http_conn::parse_line() { char temp; for (; m_checked_idx < m_read_idx; ++m_checked_idx) { temp = m_read_buf[m_checked_idx]; if (temp == '\r') { if ((m_checked_idx + 1) == m_read_idx) return LINE_OPEN;// 数据不完整:只有\r,没有\n,LINE_OPEN - 数据不完整,需要继续读取 else if (m_read_buf[m_checked_idx + 1] == '\n') { m_read_buf[m_checked_idx++] = '\0'; // 将\r替换为字符串结束符\0 m_read_buf[m_checked_idx++] = '\0'; // 将\n替换为字符串结束符\0 return LINE_OK;//表示成功解析一行 } return LINE_BAD;// 格式错误:\r后面不是\n } else if (temp == '\n') { if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == '\r') { m_read_buf[m_checked_idx - 1] = '\0'; m_read_buf[m_checked_idx++] = '\0'; return LINE_OK; } return LINE_BAD; // 格式错误:\n前面不是\r } } return LINE_OPEN;//否则返回数据不完整 } /* m_read_buf (缓冲区起始地址) │ ▼ [ 已处理数据 | 待处理数据 | 空闲空间 ] ↑ ↑ ↑ 0 m_start_line m_read_idx */ //读取客户数据 bool http_conn::read_once() { if (m_read_idx >= READ_BUFFER_SIZE) //防止缓冲区溢出 { return false; } int bytes_read = 0; //LT读取数据 if (0 == m_TRIGMode) { bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE -m_read_idx, 0);//m_read_buf读取缓冲区的起始地址,m_read_idx当前已存储数据的偏移量,组合起来指向缓冲区中下一个可写入位置,len:缓冲区剩余空间,返回值为实际接收到的字节数 m_read_idx += bytes_read;//更新缓冲区已读到位置指针 if (bytes_read <= 0) //<0读取出错,==0对端关闭连接 { return false; } return true; } //ET读数据 else { while (true)//得一次性读完 { bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0); if (bytes_read == -1) //读取错误 { if (errno == EAGAIN || errno == EWOULDBLOCK) break; return false; } else if (bytes_read == 0)//连接关闭 { return false; } m_read_idx += bytes_read } return true; } } //请求行解析:获得请求方法,目标url及http版本号 例如:GET /index.html HTTP/1.1 http_conn::HTTP_CODE http_conn::parse_request_line(char *text) { m_url = strpbrk(text, " \t");//查找第一个空行或制表符 if (!m_url) { return BAD_REQUEST; } *m_url++ = '\0';//空格替换为\0,*是解引用获取内存地址的值 char *method = text;//method指向text //text="GET\0" //m_url="/index.html HTTP/1.1" if (strcasecmp(method, "GET") == 0) m_method = GET; else if (strcasecmp(method, "POST") == 0) { m_method = POST; cgi = 1;// 标记需要CGI处理 } else return BAD_REQUEST;//不支持的方法 //分离url和版本号 m_url += strspn(m_url, " \t");//跳过空白字符 指向URL真正开始位置 m_version = strpbrk(m_url, " \t");//查找url后的的空格 if (!m_version) return BAD_REQUEST; *m_version++ = '\0'; m_version += strspn(m_version, " \t"); if (strcasecmp(m_version, "HTTP/1.1") != 0)//只支持HTTP/1.1 return BAD_REQUEST; if (strncasecmp(m_url, "http://", 7) == 0)//处理http://开头的URL { m_url += 7;//跳过"http://" m_url = strchr(m_url, '/');//找到主机名后的第一个'/' } if (strncasecmp(m_url, "https://", 8) == 0) { m_url += 8; m_url = strchr(m_url, '/'); } if (!m_url || m_url[0] != '/') return BAD_REQUEST; //默认页面处理 if (strlen(m_url) == 1)//如果只是'/',默认显示judge.html strcat(m_url, "judge.html"); m_check_state = CHECK_STATE_HEADER;//下一步解析请求头 return NO_REQUEST;//请求还未完整解析(还有别的要解析) } //解析http请求头,处理Connection、Content-length、Host等重要头部字段 http_conn::HTTP_CODE http_conn::parse_headers(char *text) { if (text[0] == '\0')//表示遇到空行,HTTP头部结束 { if (m_content_length != 0) //说明后面还有请求体,切换到内容解析状态 { m_check_state = CHECK_STATE_CONTENT return NO_REQUEST;//还有请求体需要解析 } return GET_REQUEST; //没有请求体,表示请求解析完成 } //connection头部 else if (strncasecmp(text, "Connection:", 11) == 0)//处理连接保持设置 { text += 11; text += strspn(text, " \t");// 跳过空白字符 if (strcasecmp(text, "keep-alive") == 0) { m_linger = true;//设置长连接标志 } } //connection-length头部 else if (strncasecmp(text, "Content-length:", 15) == 0) //获取请求头长度 { text += 15; text += strspn(text, " \t"); m_content_length = atol(text);//atol(text)将字符串转为长整型 } else if (strncasecmp(text, "Host:", 5) == 0)//获取目标主机名 { text += 5; text += strspn(text, " \t"); m_host = text;//直接保存指针 } else { LOG_INFO("oop!unknow header: %s", text);//对于不认识的头部字段,记录日志但继续解析 } return NO_REQUEST; } //这个函数负责检查POST请求的请求体受否被完全接收 http_conn::HTTP_CODE http_conn::parse_content(char *text) { // 检查是否已经读取了完整的消息体 if (m_read_idx >= (m_content_length + m_checked_idx))//已接收数据量 >= 期望的请求体长度 + 已解析位置(未解析的数据长度 = m_read_idx - m_checked_idx) { text[m_content_length] = '\0';// 在消息体末尾添加字符串结束符 m_string = text;//保存消息体内容 return GET_REQUEST;//返回完整请求 } return NO_REQUEST;//需要继续读取 } //这是一个HTTP请求处理的主状态机函数,负责协调整个HTTP请求的解析过程。 http_conn::HTTP_CODE http_conn::process_read() { LINE_STATUS line_status = LINE_OK;// 行解析状态 HTTP_CODE ret = NO_REQUEST;//HTTP解析结果 char *text = 0;// 当前正在处理的行数据 //m_check_state: 主解析状态(请求行、头部、内容) while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK))//正在解析请求体(直接进入内容处理)和按行解析模式(调用parse_line()先解析一行数据后继续处理) { text = get_line(); //get_line():返回当前解析行的指针 m_start_line = m_checked_idx; // 更新起始行位置,用于下一轮解析 LOG_INFO("%s", text); // 记录日志 switch (m_check_state) { case CHECK_STATE_REQUESTLINE: //解析请求行 { ret = parse_request_line(text); if (ret == BAD_REQUEST) return BAD_REQUEST; break; } case CHECK_STATE_HEADER://解析头 { ret = parse_headers(text); // 解析各个头部字段:Connection、Content-Length、Host等头部 if (ret == BAD_REQUEST) return BAD_REQUEST; else if (ret == GET_REQUEST) // 头部解析完成且无请求体 { return do_request(); // 处理请求 } break; } case CHECK_STATE_CONTENT://解析请求体 { ret = parse_content(text); // 检查消息体是否完整 if (ret == GET_REQUEST) // 消息体接收完整 return do_request(); // 处理请求 line_status = LINE_OPEN; // 否则设置为打开状态,继续读取 break; } default: return INTERNAL_ERROR; } } return NO_REQUEST; } //HTTP请求处理的核心业务逻辑 http_conn::HTTP_CODE http_conn::do_request() { //基础路径设置 strcpy(m_real_file, doc_root);// 设置网站根目录 int len = strlen(doc_root); const char *p = strrchr(m_url, '/'); // 找到最后一个斜杠 //处理cgi 登录或注册 if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3')) //其中/2登录 /3注册 { //根据标志判断是登录检测还是注册检测 char flag = m_url[1]; //构建真实文件路径 char *m_url_real = (char *)malloc(sizeof(char) * 200); strcpy(m_url_real, "/"); strcat(m_url_real, m_url + 2); strncpy(m_real_file + len, m_url_real, FILENAME_LEN - len - 1); free(m_url_real); //解析用户名和密码 //user=123&passwd=123 char name[100], password[100]; int i; for (i = 5; m_string[i] != '&'; ++i) name[i - 5] = m_string[i]; name[i - 5] = '\0'; int j = 0; for (i = i + 10; m_string[i] != '\0'; ++i, ++j) password[j] = m_string[i]; password[j] = '\0'; if (*(p + 1) == '3') { //如果是注册,先检测数据库中是否有重名的 //没有重名的,进行增加数据 char *sql_insert = (char *)malloc(sizeof(char) * 200); strcpy(sql_insert, "INSERT INTO user(username, passwd) VALUES("); strcat(sql_insert, "'"); strcat(sql_insert, name); strcat(sql_insert, "', '"); strcat(sql_insert, password); strcat(sql_insert, "')"); if (users.find(name) == users.end())// 检查用户名是否已存在 { m_lock.lock(); int res = mysql_query(mysql, sql_insert);//插入数据库 users.insert(pair<string, string>(name, password));/更新缓存 m_lock.unlock(); if (!res) strcpy(m_url, "/log.html"); // 注册成功 else strcpy(m_url, "/registerError.html");// 数据库错误 } else strcpy(m_url, "/registerError.html");// 用户名已存在 } //如果是登录,直接判断 //若浏览器端输入的用户名和密码在表中可以查找到,返回1,否则返回0 else if (*(p + 1) == '2') { if (users.find(name) != users.end() && users[name] == password) strcpy(m_url, "/welcome.html");// 登录成功 else strcpy(m_url, "/logError.html");//登录失败 } } // 根据URL数字路由到不同页面 if (*(p + 1) == '0') { char *m_url_real = (char *)malloc(sizeof(char) * 200); strcpy(m_url_real, "/register.html"); strncpy(m_real_file + len, m_url_real, strlen(m_url_real)); free(m_url_real); } else if (*(p + 1) == '1') { char *m_url_real = (char *)malloc(sizeof(char) * 200); strcpy(m_url_real, "/log.html"); strncpy(m_real_file + len, m_url_real, strlen(m_url_real)); free(m_url_real); } else if (*(p + 1) == '5') { char *m_url_real = (char *)malloc(sizeof(char) * 200); strcpy(m_url_real, "/picture.html"); strncpy(m_real_file + len, m_url_real, strlen(m_url_real)); free(m_url_real); } else if (*(p + 1) == '6') { char *m_url_real = (char *)malloc(sizeof(char) * 200); strcpy(m_url_real, "/video.html"); strncpy(m_real_file + len, m_url_real, strlen(m_url_real)); free(m_url_real); } else if (*(p + 1) == '7') { char *m_url_real = (char *)malloc(sizeof(char) * 200); strcpy(m_url_real, "/fans.html"); strncpy(m_real_file + len, m_url_real, strlen(m_url_real)); free(m_url_real); } else strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);//直接访问 // 检查文件状态 if (stat(m_real_file, &m_file_stat) < 0) return NO_RESOURCE; // 文件不存在 if (!(m_file_stat.st_mode & S_IROTH)) return FORBIDDEN_REQUEST; // 无读取权限 if (S_ISDIR(m_file_stat.st_mode)) return BAD_REQUEST; // 不能访问目录 // 使用内存映射提高文件读取性能 int fd = open(m_real_file, O_RDONLY); m_file_address = (char*)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0); close(fd); return FILE_REQUEST; } //这是一个内存映射清理函数,用于释放之前通过mmap创建的内存映射。 void http_conn::unmap() { if (m_file_address) { munmap(m_file_address, m_file_stat.st_size); m_file_address = 0; } } //HTTP响应数据发送函数,使用了writev系统调用来实现高性能的分散写操作。 bool http_conn::write() { int temp = 0; if (bytes_to_send == 0) { modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode); //重新监听读事件 //初始化连接状态 init(); return true; } while (1) { temp = writev(m_sockfd, m_iv, m_iv_count); //使用writev系统调用一次性发送多个缓冲区的数据。 if (temp < 0) { if (errno == EAGAIN)//写缓冲区满,注册写事件等待下次可写 { modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode); return true; } //清理资源并关闭连接 unmap(); return false; } //更新发送状态 bytes_have_send += temp; bytes_to_send -= temp; if (bytes_have_send >= m_iv[0].iov_len) { // 情况1:第一个缓冲区已发送完 m_iv[0].iov_len = 0; m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx); m_iv[1].iov_len = bytes_to_send; } else { // 情况2:第一个缓冲区未发送完 m_iv[0].iov_base = m_write_buf + bytes_have_send; m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send; } if (bytes_to_send <= 0) { unmap(); modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode); if (m_linger) { init(); return true; } else { return false; } } } } //这是一个可变参数响应构建函数,用于格式化并添加HTTP响应数据到写缓冲区。 bool http_conn::add_response(const char *format, ...) { if (m_write_idx >= WRITE_BUFFER_SIZE) return false; va_list arg_list; va_start(arg_list, format); int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list); if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx)) { va_end(arg_list); return false; } m_write_idx += len; va_end(arg_list); LOG_INFO("request:%s", m_write_buf); return true; } bool http_conn::add_status_line(int status, const char *title) { return add_response("%s %d %s\r\n", "HTTP/1.1", status, title); } bool http_conn::add_headers(int content_len) { return add_content_length(content_len) && add_linger() && add_blank_line(); } bool http_conn::add_content_length(int content_len) { return add_response("Content-Length:%d\r\n", content_len); } bool http_conn::add_content_type() { return add_response("Content-Type:%s\r\n", "text/html"); } bool http_conn::add_linger() { return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close"); } bool http_conn::add_blank_line() { return add_response("%s", "\r\n"); } bool http_conn::add_content(const char *content) { return add_response("%s", content); } bool http_conn::process_write(HTTP_CODE ret) { switch (ret) { case INTERNAL_ERROR://内部服务器错误 (500) { add_status_line(500, error_500_title); add_headers(strlen(error_500_form)); if (!add_content(error_500_form)) return false; break; } case BAD_REQUEST: { add_status_line(404, error_404_title); add_headers(strlen(error_404_form)); if (!add_content(error_404_form)) return false; break; } case FORBIDDEN_REQUEST: { add_status_line(403, error_403_title); add_headers(strlen(error_403_form)); if (!add_content(error_403_form)) return false; break; } case FILE_REQUEST: { add_status_line(200, ok_200_title); if (m_file_stat.st_size != 0) { add_headers(m_file_stat.st_size); m_iv[0].iov_base = m_write_buf; m_iv[0].iov_len = m_write_idx; m_iv[1].iov_base = m_file_address; m_iv[1].iov_len = m_file_stat.st_size; m_iv_count = 2; bytes_to_send = m_write_idx + m_file_stat.st_size; return true; } else { const char *ok_string = "<html><body></body></html>"; add_headers(strlen(ok_string)); if (!add_content(ok_string)) return false; } } default: return false; } m_iv[0].iov_base = m_write_buf; m_iv[0].iov_len = m_write_idx; m_iv_count = 1; bytes_to_send = m_write_idx; return true; } //这是一个HTTP连接处理的主流程函数,负责协调整个HTTP请求的处理过程。 void http_conn::process() { HTTP_CODE read_ret = process_read(); if (read_ret == NO_REQUEST) { modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode); return; } bool write_ret = process_write(read_ret); if (!write_ret) { close_conn(); } modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode); } 

Config

config.h和config.cpp主要是对一些参数赋初值以及解析参数,以此就不过多讲解

Read more

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

摘要:本文聚焦OpenClaw从测试环境走向生产环境的核心痛点,围绕“性能优化、安全加固、监控运维”三大维度展开实操讲解。先明确生产环境硬件/系统选型标准,再通过硬件层资源管控、模型调度策略、缓存优化等手段提升响应速度(实测响应效率提升50%+);接着从网络、权限、数据三层构建安全防护体系,集成火山引擎安全方案拦截高危操作;最后落地TenacitOS可视化监控与Prometheus告警体系,配套完整故障排查清单和虚拟实战案例。全文所有配置、代码均经实测验证,兼顾新手入门实操性和进阶读者的生产级部署需求,帮助开发者真正实现OpenClaw从“能用”到“放心用”的跨越。 优质专栏欢迎订阅! 【DeepSeek深度应用】【Python高阶开发:AI自动化与数据工程实战】【YOLOv11工业级实战】 【机器视觉:C# + HALCON】【大模型微调实战:平民级微调技术全解】 【人工智能之深度学习】【AI 赋能:Python 人工智能应用实战】【数字孪生与仿真技术实战指南】 【AI工程化落地与YOLOv8/v9实战】【C#工业上位机高级应用:高并发通信+性能优化】 【Java生产级避坑指南:

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言  一、实验基础说明 1.1、互斥体简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 互斥体 LED 驱动代码(mutex.c) 3.2.1、设备结构体定义(28-39

By Ne0inhk
Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 后端工程师扔给你一个 Swagger (OpenAPI) 文档地址,你会怎么做? 1. 对着文档,手写 Dart Model 类(容易写错字段类型)。 2. 手写 Retrofit/Dio 的 API 接口定义(容易拼错 URL)。 3. 当后端修改了字段名,你对着报错修半天。 这是重复劳动的地狱。 swagger_dart_code_generator 可以将 Swagger (JSON/YAML) 文件直接转换为高质量的 Dart 代码,包括: * Model 类:支持 json_serializable,带 fromJson/

By Ne0inhk
Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

文章目录 * 前言 * make/makefile * 文件的三个时间 * Linux第一个小程序-进度条 * 回车和换行 * 缓冲区 * 程序的代码展示 * git指令 * 关于gitee * Linux调试器-gdb使用 * 作业部分 前言 做 Linux 开发时,你是不是也遇到过这些 “卡脖子” 时刻?写 makefile 时,明明语法没错却报错,最后发现是依赖方法行没加 Tab;想提交代码到 gitee,记不清 git add/commit/push 的 “三板斧”,还得反复搜教程;用 gdb 调试程序,输了命令没反应,才想起编译时没加-g生成 debug 版本;甚至连写个进度条,都搞不懂\r和\n的区别,导致进度条乱跳…… 其实这些问题,

By Ne0inhk