Webserver代码部分
1.线程同步机制封装类locker.h
#ifndef LOCKER_H #define LOCKER_H #include <pthread.h> #include <exception> #include <semaphore.h> // 线程同步机制封装类 // 互斥锁类 class locker { private: pthread_mutex_t m_mutex; // 互斥量类型 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; } }; // 条件变量类 class cond{ private: pthread_cond_t m_cond; // 条件变量类型 public: cond(){ if(pthread_cond_init(&m_cond, NULL) != 0){ // 初始化 throw std::exception(); } } ~cond(){ pthread_cond_destroy(&m_cond); // 释放 } bool wait(pthread_mutex_t* mutex){ return pthread_cond_wait(&m_cond, mutex) == 0; // 等待 } bool timedwait(pthread_mutex_t* mutex, struct timespec t){ // 在一定时间等待 return pthread_cond_timedwait(&m_cond, mutex, &t) == 0; } bool signal(){ return pthread_cond_signal(&m_cond) == 0; // 唤醒一个/多个线程 } bool broadcast(){ return pthread_cond_broadcast(&m_cond) == 0; // 唤醒所有线程 } }; // 信号量类 class sem { private: sem_t m_sem; public: sem(){ if(sem_init(&m_sem, 0, 0) != 0){ // 默认构造 throw std::exception(); } } sem(int num){ // 传参构造 if(sem_init(&m_sem, 0, num) != 0){ throw std::exception(); } } ~sem(){ // 释放 sem_destroy(&m_sem); } bool wait(){ // 等待(减少)信号量 return sem_wait(&m_sem) == 0; } bool post(){ // 增加信号量 return sem_post(&m_sem) == 0; } }; #endif
2.线性池类threadpool.h
#ifndef THREADPOOL_H #define THREADPOOL_H #include <pthread.h> #include <list> #include "locker.h" #include <cstdio> // 线程池类,定义成模板类,为了代码的复用,模板参数T是任务类 template<typename T> class threadpool { private: int m_thread_num; // 线程数量 pthread_t * m_threads; // 线程池数组,大小为m_thread_num,声明为指针,后面动态创建数组 int m_max_requests; // 请求队列中的最大等待数量 std::list<T*> m_workqueue; // 请求队列,由threadpool类型的示例进行管理 locker m_queue_locker; // 互斥锁 sem m_queue_stat; // 信号量 bool m_stop; // 是否结束线程,线程根据该值判断是否要停止 static void* worker(void* arg); // 静态函数,线程调用,不能访问非静态成员 void run(); // 线程池已启动,执行函数 public: threadpool(int thread_num = 8, int max_requests = 10000); ~threadpool(); bool append(T* request); // 添加任务的函数 }; template<typename T> threadpool<T>::threadpool(int thread_num, int max_requests) : // 构造函数,初始化 m_thread_num(thread_num), m_max_requests(max_requests), m_stop(false), m_threads(NULL) { if(thread_num <= 0 || max_requests <= 0){ throw std::exception(); } m_threads = new pthread_t[m_thread_num]; // 动态分配,创建线程池数组 if(!m_threads){ throw std::exception(); } for(int i = 0; i < thread_num; ++i){ printf("creating the N0.%d thread.\n", i); // 创建线程, worker(线程函数) 必须是静态的函数 if(pthread_create(m_threads + i, NULL, worker, this) != 0){ // 通过最后一个参数向 worker 传递 this 指针,来解决静态函数无法访问非静态成员的问题 //静态函数不依赖于任何特定的类实例,而是属于整个类本身。因此,它不能访问实例变量,因为这些变量只有在类的实例化过程中才会被创建。 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; // 释放线程数组空间 m_stop = true; // 标记线程结束 } template<typename T> bool threadpool<T>::append(T* request){ // 添加请求队列 m_queue_locker.lock(); // 队列为共享队列,上锁 if(m_workqueue.size() > m_max_requests){ m_queue_locker.unlock(); // 队列元素已满 return false; // 添加失败 } m_workqueue.push_back(request); // 将任务加入队列 m_queue_locker.unlock(); // 解锁 m_queue_stat.post(); // 增加信号量,线程根据信号量判断阻塞还是继续往下执行 return true; } template<typename T> void* threadpool<T>::worker(void* arg){ // arg 为线程创建时传递的threadpool类的 this 指针参数 threadpool* pool = (threadpool*) arg; // 参数是一个实例,创建一个实例去运行 pool->run(); // 线程实际执行函数 return pool; // 无意义 } template<typename T> void threadpool<T>::run(){ // 线程实际执行函数 while(!m_stop){ // 判断停止标记 m_queue_stat.wait(); // 等待信号量有数值(减一) m_queue_locker.lock(); // 上锁 if(m_workqueue.empty()){ // 空队列 m_queue_locker.unlock(); // 解锁 continue; } T* request = m_workqueue.front(); // 取出任务 m_workqueue.pop_front(); // 移出队列 m_queue_locker.unlock(); // 解锁 if(!request){ continue; } request->process(); // 任务类 T 的执行函数 } } #endif
3.主函数main
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <error.h> #include <fcntl.h> #include <sys/epoll.h> #include <signal.h> #include <assert.h> #include "locker.h" #include "threadpool.h" #include "http_conn.h" #include "lst_timer.h" #include "log.h" #define MAX_FD 65535 // 最大文件描述符(客户端)数量 #define MAX_EVENT_SIZE 10000 // 监听的最大的事件数量 static int pipefd[2]; // 管道文件描述符 0为读,1为写 // static sort_timer_lst timer_lst;// 定时器链表 // 信号处理,添加信号捕捉 void a**sig(int sig, void(handler)(int)){ struct sigaction sigact; // sig 指定信号, void handler(int) 为处理函数 memset(&sigact, '\0', sizeof(sigact)); // bezero 清空 sigact.sa_flags = 0; // 调用sa_handler // sigact.sa_flags |= SA_RESTART; // 指定收到某个信号时是否可以自动恢复函数执行,不需要中断后自己判断EINTR错误信号 sigact.sa_handler = handler; // 指定回调函数 sigfillset(&sigact.sa_mask); // 将临时阻塞信号集中的所有的标志位置为1,即都阻塞 //sa_mask:信号屏蔽字,用于设置在处理该信号时要屏蔽的信号集。我们不希望在处理信号中断时被其他的信号打断 sigaction(sig, &sigact, NULL); // 设置信号捕捉sig信号值 } // 向管道写数据的信号捕捉回调函数 void sig_to_pipe(int sig){ int save_errno = errno; int msg = sig; send( pipefd[1], ( char* )&msg, 1, 0 ); errno = save_errno; //写之前保存了errno的值,以便在写操作之后恢复errno的值,防止被信号处理函数修改。 //SIGALRM 和 SIGTERM 是定义为常量的宏,他们的值分别为14和15 } // 添加文件描述符到epoll中 (声明成外部函数) extern void a**fd(int epoll_fd, int fd, bool one_shot, bool et); // 从epoll中删除文件描述符 extern void rmfd(int epoll_fd, int fd); // 在epoll中修改文件描述符 extern void modfd(int epoll_fd, int fd, int ev); // 文件描述符设置非阻塞操作 extern void set_nonblocking(int fd); int main(int argc, char* argv[]){ if(argc <= 1){ // 形参个数,第一个为执行命令的名称 EMlog(LOGLEVEL_ERROR,"run as: %s port_number\n", basename(argv[0])); // argv[0] 可能是带路径的,用basename转换 exit(-1); } // 获取端口号 int port = atoi(argv[1]); // 字符串转整数 // 对SIGPIE信号进行处理(捕捉忽略,默认退出) // 当往一个写端关闭的管道或socket连接中连续写入数据时会引发SIGPIPE信号,引发SIGPIPE信号的写操作将设置errno为EPIPE // 因为SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为写操作的错误而导致程序退出,所以我们捕捉并忽略 a**sig(SIGPIPE, SIG_IGN); // https://blog.csdn.net/chengcheng1024/article/details/108104507 int listen_fd = socket(AF_INET, SOCK_STREAM, 0); // 监听套接字 assert( listen_fd >= 0 ); // ...判断是否创建成功 //assert宏是一个调试宏,用于在程序运行时进行断言检查,如果断言条件为假(即listen_fd小于0),则会触发断言失败,导致程序终止,并在标准错误流中输出错误信息。 // 设置端口复用 int reuse = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse)); // 绑定 struct socka**r_in a**r; a**r.sin_family = AF_INET; a**r.sin_a**r.s_a**r = INA**R_ANY; a**r.sin_port = htons(port); int ret = bind(listen_fd, (struct socka**r*)&a**r, sizeof(a**r)); assert( ret != -1 ); // ...判断是否成功 // 监听 ret = listen(listen_fd, 8); assert( ret != -1 ); // ...判断是否成功 // 创建epoll对象,事件数组(IO多路复用,同时检测多个事件) epoll_event events[MAX_EVENT_SIZE]; // 结构体数组,接收检测后的数据 int epoll_fd = epoll_create(5); // 参数 5 无意义, > 0 即可,返回一个int类型的epoll文件描述符 assert( epoll_fd != -1 ); // 将监听的文件描述符添加到epoll对象中 a**fd(epoll_fd, listen_fd, false, false); // 监听文件描述符不需要 ONESHOT & ET // 创建管道 ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd); assert( ret != -1 ); set_nonblocking( pipefd[1] ); // 写管道非阻塞 a**fd(epoll_fd, pipefd[0], false, false ); // epoll检测读管道 // 设置信号处理函数 a**sig(SIGALRM, sig_to_pipe); // 定时器信号 a**sig(SIGTERM, sig_to_pipe); // SIGTERM 关闭服务器 bool stop_server = false; // 关闭服务器标志位 // 创建一个保存所有客户端信息的数组 http_conn* users = new http_conn[MAX_FD]; http_conn::m_epoll_fd = epoll_fd; // 静态成员,类共享 // 创建线程池,初始化线程池 threadpool<http_conn> * pool = NULL; // 模板类 指定任务类类型为 http_conn try{ pool = new threadpool<http_conn>; }catch(...){//...表示捕获所有类型的异常 exit(-1); } bool timeout = false; // 定时器周期已到 alarm(TIMESLOT); // 定时产生SIGALRM信号,TIMESLOT的秒数过后产生SIGALRM信号 while(!stop_server){ // 检测事件 int num = epoll_wait(epoll_fd, events, MAX_EVENT_SIZE, -1); // 阻塞,返回事件数量,第三个参数是第二个参数的size if(num < 0 && errno != EINTR){ //EINTR表示在等待期间收到了中断信号,可以忽略该错误。否则,根据具体情况进行错误处理。 EMlog(LOGLEVEL_ERROR,"EPOLL failed.\n"); break; } // 循环遍历事件数组 for(int i = 0; i < num; ++i){ int sock_fd = events[i].data.fd; if(sock_fd == listen_fd){ // 监听文件描述符的事件响应 // 有客户端连接进来 struct socka**r_in client_a**r; socklen_t client_a**r_len = sizeof(client_a**r); int conn_fd = accept(listen_fd,(struct socka**r*)&client_a**r, &client_a**r_len); // ...判断是否连接成功 if(http_conn::m_user_cnt >= MAX_FD){ // 目前连接数满了 // ...给客户端写一个信息:服务器内部正忙 close(conn_fd); continue; } // 将新客户端数据初始化,放到数组中 users[conn_fd].init(conn_fd, client_a**r); // conn_fd 作为索引 // 当listen_fd也注册了ONESHOT事件时(a**fd), // 接受了新的连接后需要重置socket上EPOLLONESHOT事件,确保下次可读时,EPOLLIN 事件被触发,因此不应该将listen注册为oneshot // modfd(epoll_fd, listen_fd, EPOLLIN); } // 读管道有数据,SIGALRM 或 SIGTERM信号触发 else if(sock_fd == pipefd[0] && (events[i].events & EPOLLIN)){ int sig; char signals[1024]; ret = recv(pipefd[0], signals, sizeof(signals), 0); if(ret == -1){ continue; }else if(ret == 0){ continue; }else{ for(int i = 0; i < ret; ++i){ switch (signals[i]) // 字符ASCII码 { case SIGALRM: // 用timeout变量标记有定时任务需要处理,但不立即处理定时任务 // 这是因为定时任务的优先级不是很高,我们优先处理其他更重要的任务。 timeout = true; break; case SIGTERM: //程收到SIGTERM信号时,它会尝试优雅地终止自己的运行,也就是说,它会完成当前正在执行的任务,清理资源并退出 stop_server = true; } } } } else if(events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)){ // 对方异常断开 或 错误 等事件 EMlog(LOGLEVEL_DEBUG,"-------EPOLLRDHUP | EPOLLHUP | EPOLLERR--------\n"); users[sock_fd].conn_close(); http_conn::m_timer_lst.del_timer(users[sock_fd].timer); // 移除其对应的定时器 } else if(events[i].events & EPOLLIN){ EMlog(LOGLEVEL_DEBUG,"-------EPOLLIN-------\n\n"); if (users[sock_fd].read()){ // 主进程一次性读取缓冲区的所有数据 pool->append(users + sock_fd); // 加入到线程池队列中,数组指针 + 偏移 &users[sock_fd] }else{ users[sock_fd].conn_close(); http_conn::m_timer_lst.del_timer(users[sock_fd].timer); // 移除其对应的定时器 } } else if(events[i].events & EPOLLOUT){ EMlog(LOGLEVEL_DEBUG, "-------EPOLLOUT--------\n\n"); if (!users[sock_fd].write()){ // 主进程一次性写完所有数据 users[sock_fd].conn_close(); // 写入失败 http_conn::m_timer_lst.del_timer(users[sock_fd].timer); // 移除其对应的定时器 } } } // 最后处理定时事件,因为I/O事件有更高的优先级。当然,这样做将导致定时任务不能精准的按照预定的时间执行。 if(timeout) { // 定时处理任务,实际上就是调用tick()函数 http_conn::m_timer_lst.tick(); // 因为一次 alarm 调用只会引起一次SIGALARM 信号,所以我们要重新定时,以不断触发 SIGALARM信号。 alarm(TIMESLOT); timeout = false; // 重置timeout } } close(epoll_fd); close(listen_fd); close(pipefd[1]); close(pipefd[0]); delete[] users; delete pool; return 0; } /*basename用于从路径名中提取文件名部分,返回字符串中最后一个斜杠字符之后的所有字符 atoi函数,将字符串转换为int sigfillset函数用于将信号集合中所有信号都设置为“被阻塞”状态 SIG_IGN是一个指向空函数的指针,执行“什么都不做” catch(...)表示处理任何类型的异常 extern关键字,用于声明一个变量或者函数是在其他文件中定义的 */
4.HTTP连接的用户类http_conn.h和.cpp
http_conn.h
#ifndef HTTPCONNECTION_H #define HTTPCONNECTION_H #include <sys/epoll.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <fcntl.h> #include <arpa/inet.h> #include <sys/stat.h> // 文件状态 #include <sys/mman.h> // 内存映射 #include <stdarg.h> #include <errno.h> #include <sys/uio.h> #include <string.h> #include <time.h> #include <assert.h> #include "locker.h" #include "lst_timer.h" #include "log.h" class sort_timer_lst; class util_timer; #define COUT_OPEN 1 const bool ET = true; #define TIMESLOT 5 // 定时器周期:秒 // http 连接的用户数据类 class http_conn { public: // 共享对象,没有线程竞争资源,所以不需要互斥 static int m_epoll_fd; // 所有的socket上的事件都被注册到同一个epoll对象中 static int m_user_cnt; // 统计用户的数量 static int m_request_cnt; // 接收到的请求次数 static sort_timer_lst m_timer_lst;// 定时器链表 // static locker m_timer_lst_locker; // 定时器链表互斥锁 static const int RD_BUF_SIZE = 2048; // 读缓冲区的大小 static const int WD_BUF_SIZE = 2048; // 写缓冲区的大小 static const int FILENAME_LEN = 200; //文件名的最大长度 util_timer* timer; // 定时器 public: // HTTP请求方法,这里只支持GET enum METHOD {GET = 0, POST, HEAD, PUT, DELETE, TRACE, OPTIONS, CONNECT}; /* 解析客户端请求时,主状态机的状态 CHECK_STATE_REQUESTLINE:当前正在解析请求行 CHECK_STATE_HEADER: 当前正在解析头部字段 CHECK_STATE_CONTENT: 当前正在解析请求体 */ enum CHECK_STATE { CHECK_STATE_REQUESTLINE = 0, CHECK_STATE_HEADER, CHECK_STATE_CONTENT }; /* 服务器处理HTTP请求的可能结果,报文解析的结果 NO_REQUEST : 请求不完整,需要继续读取客户数据 GET_REQUEST : 表示获得了一个完成的客户请求 BAD_REQUEST : 表示客户请求语法错误 NO_RESOURCE : 表示服务器没有资源 FORBI**EN_REQUEST : 表示客户对资源没有足够的访问权限 FILE_REQUEST : 文件请求,获取文件成功 INTERNAL_ERROR : 表示服务器内部错误 CLOSED_CONNECTION : 表示客户端已经关闭连接了 */ enum HTTP_CODE { NO_REQUEST, GET_REQUEST, BAD_REQUEST, NO_RESOURCE, FORBI**EN_REQUEST, FILE_REQUEST, INTERNAL_ERROR, CLOSED_CONNECTION }; // 从状态机的三种可能状态,即行的读取状态,分别表示 // 1.读取到一个完整的行 2.行出错 3.行数据尚且不完整 enum LINE_STATUS { LINE_OK = 0, LINE_BAD, LINE_OPEN }; public: http_conn(); ~http_conn(); void process(); // 处理客户端的请求、对客户端的响应 void init(int sock_fd, const socka**r_in& a**r); // 初始化新的连接 void conn_close(); // 关闭连接 bool read(); // 非阻塞的读 bool write(); // 非阻塞的写 void del_fd(); // 定时器回调函数,被tick()调用 private: int m_sock_fd; // 该http连接的socket socka**r_in m_a**r; // 通信的socket地址 char m_rd_buf[RD_BUF_SIZE]; // 读缓冲区 int m_rd_idx; // 标识读缓冲区中已经读入的客户端数据的最后一个字节的下一个位置 int m_checked_idx; // 当前正在分析的字符在读缓冲区的位置 int m_line_start; // 当前正在解析的行的起始位置 char* m_url; // 请求目标文件的文件名 char* m_version; // 协议版本,HTPP1.1 METHOD m_method; // 请求方法 char* m_host; // 主机名 long m_content_len; // HTTP请求体的消息总长度 bool m_linger; // HTTP 请求是否要保持连接 keep-alive char m_real_file[FILENAME_LEN]; // 客户请求的目标文件的完整路径,其内容等于 doc_root + m_url, doc_root是网站根目录 CHECK_STATE m_check_stat; // 主状态机当前所处的状态 struct stat m_file_stat; // 目标文件的状态。通过它我们可以判断文件是否存在、是否为目录、是否可读,并获取文件大小等信息 char* m_file_a**ress; // 客户请求的目标文件被mmap到内存中的起始位置 char m_write_buf[WD_BUF_SIZE]; // 写缓冲区 int m_write_idx; // 写缓冲区中待发送的字节数 struct iovec m_iv[2]; // writev来执行写操作,表示分散写两个不连续内存块的内容 /* struct iovec { void *iov_base; // 缓冲区的起始地址 size_t iov_len; // 缓冲区的长度 }; iov_base:表示缓冲区的起始地址,通常是一个指向内存区域的指针。 iov_len:表示缓冲区的长度,即缓冲区中可用数据的字节数。 iovec 结构体通常用于 I/O 操作中,如在 readv()、writev() 等函数中,可以通过组合多个 iovec 结构体来一次性读取或写入多个缓冲区的数据,从而提高效率。 */ int m_iv_count; // 被写内存块的数量 int bytes_to_send; // 将要发送的字节 int bytes_have_send; // 已经发送的字节 private: void init(); // 私有函数,初始化连接以外的信息 HTTP_CODE process_read(); // 解析HTTP请求 bool process_write(HTTP_CODE ret); // 填充HTTP应答 // 下面这一组函数被process_read调用以分析HTTP请求 HTTP_CODE parse_request_line(char* text); // 解析请求首行 HTTP_CODE parse_request_headers(char* text); // 解析请求头部 HTTP_CODE parse_request_content(char* text); // 解析请求体 LINE_STATUS parse_one_line(); // 从状态机解析一行数据 char* get_line(){return m_rd_buf + m_line_start;} // 获取一行数据 return m_rd_buf + m_line_start; HTTP_CODE do_request(); // 处理具体请求 // 这一组函数被process_write调用以填充HTTP应答。 void unmap(); bool a**_response( const char* format, ... ); bool a**_content( const char* content ); bool a**_content_type(); bool a**_status_line( int status, const char* title ); void a**_headers( int content_length ); bool a**_content_length( int content_length ); bool a**_linger(); bool a**_blank_line(); }; #endif
http_conn.cpp
#include "http_conn.h" http_conn::http_conn(){} http_conn::~http_conn(){} int http_conn::m_epoll_fd = -1; // 类中静态成员需要外部定义 int http_conn::m_user_cnt = 0; int http_conn::m_request_cnt = 0; sort_timer_lst http_conn::m_timer_lst; // locker http_conn::m_timer_lst_locker; // 网站的根目录 const char* doc_root = "/home/nowcoder/tinywebserver/resources"; // 定义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 satisfy.\n"; const char* error_403_title = "Forbi**en"; const char* error_403_form = "You do not have permission to get file from 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 requested file.\n"; // 设置文件描述符为非阻塞 void set_nonblocking(int fd){ int flag = fcntl(fd, F_GETFL); flag |= O_NONBLOCK; fcntl(fd, F_SETFL, flag); } // 添加需要监听的文件描述符到epoll中 void a**fd(int epoll_fd, int fd, bool one_shot, bool et){ epoll_event event; event.data.fd = fd; if(et){ //EPOLLRDHUP是半关闭连接事件,如果一个连接的对端关闭了写端(即半关闭连接),内核会向应用程序发送EPOLLRDHUP事件通知。 event.events = EPOLLIN | EPOLLRDHUP | EPOLLET; // 对所有fd设置边沿触发,但是listen_fd不需要,可以另行判断处理 }else{ event.events = EPOLLIN | EPOLLRDHUP; // 默认水平触发 对端连接断开触发的epoll 事件包含 EPOLLIN | EPOLLRDHUP挂起,不用根据返回值判断,直接通过事件判断异常断开 } if(one_shot){ event.events |= EPOLLONESHOT; // 注册为 EPOLLONESHOT事件,防止同一个通信被不同的线程处理 } epoll_ctl(epoll_fd, EPOLL_CTL_A**, fd, &event);//加入红黑树中 // 设置文件描述符为非阻塞(epoll ET模式) set_nonblocking(fd); } // 从epoll中删除文件描述符 void rmfd(int epoll_fd, int fd){ epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, 0); close(fd); } // 在epoll中修改文件描述符,重置socket上EPOLLONESHOT事件,确保下次可读时,EPOLLIN 事件被触发 void modfd(int epoll_fd, int fd, int ev){ epoll_event event; //重新创建一个epoll_event类型的变量用来绑定fd并设置它的event event.data.fd = fd; event.events = ev | EPOLLONESHOT | EPOLLRDHUP; epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &event); } // 初始化新的连接 void http_conn::init(int sock_fd, const socka**r_in& a**r){ m_sock_fd = sock_fd; // 套接字 m_a**r = a**r; // 客户端地址 // 设置端口复用 int reuse = 1; setsockopt(sock_fd, SOL_SOCKET, SO_REUSEPORT, &reuse, sizeof(reuse)); // 添加sock_fd到epoll对象中,设置oneshot和ET边沿触发 a**fd(m_epoll_fd, sock_fd, true, ET); ++m_user_cnt; // 写日志 char ip[16] = ""; const char* str = inet_ntop(AF_INET, &a**r.sin_a**r.s_a**r, ip, sizeof(ip)); EMlog(LOGLEVEL_INFO, "The No.%d user. sock_fd = %d, ip = %s.\n", m_user_cnt, sock_fd, str); init(); // 初始化其他信息,私有 // 创建定时器,设置其回调函数与超时时间,然后绑定定时器与用户数据,最后将定时器添加到链表timer_lst中 util_timer* new_timer = new util_timer; new_timer->user_data = this; time_t curr_time = time(NULL); // time函数接收一个指向time_t 类型的指针作为参数timer,用于接收当前时间的时间戳。传递NULL,则time函数会将当前时间的时间戳返回 new_timer->expire = curr_time + 3 * TIMESLOT; this->timer = new_timer; m_timer_lst.a**_timer(new_timer); } // 初始化连接之外的其他信息 void http_conn::init(){ m_method = GET; m_url = 0; m_version = 0; m_linger = false; // 默认不保持连接 m_content_len = 0; // HTTP请求体的消息总长度 m_host = 0; m_check_stat = CHECK_STATE_REQUESTLINE; // 初始化状态为正在解析请求首行 m_checked_idx = 0; // 初始化解析字符索引 m_line_start = 0; // 行的起始位置 m_rd_idx = 0; // 读取字符的位置 m_write_idx = 0; bytes_have_send = 0; bytes_to_send = 0; bzero(m_rd_buf, RD_BUF_SIZE); // 清空读缓存 bzero(m_write_buf, WD_BUF_SIZE); // 清空写缓存 bzero(m_real_file, FILENAME_LEN); // 清空文件路径 } // 关闭连接 void http_conn::conn_close(){ if(m_sock_fd != -1){ --m_user_cnt; // 客户端数量减一 //写日志 EMlog(LOGLEVEL_INFO, "closing fd: %d, rest user num :%d\n", m_sock_fd, m_user_cnt); rmfd(m_epoll_fd, m_sock_fd); // 移除epoll检测,关闭套接字 m_sock_fd = -1; } } // 循环读取客户数据,直到无数据可读 或 关闭连接 bool http_conn::read(){ if(timer) { // 更新超时时间,否则可能执行中关闭 time_t curr_time = time( NULL ); timer->expire = curr_time + 3 * TIMESLOT; m_timer_lst.adjust_timer( timer ); } if(m_rd_idx >= RD_BUF_SIZE) return false; // 超过缓冲区大小 int bytes_rd = 0; while(true){ // m_sock_fd已设置非阻塞,因此recv函数的第四个参数flags不用再设置为MSG_DONTWAIT表示非阻塞,设置为0即可 bytes_rd = recv(m_sock_fd, m_rd_buf + m_rd_idx, RD_BUF_SIZE - m_rd_idx, 0); // 第二个参数传递的是缓冲区中开始读入的地址偏移 if(bytes_rd == -1){ if(errno == EAGAIN || errno == EWOULDBLOCK){ break; // 非阻塞读取,没有数据了,recv函数非阻塞返回-1时,并且错误码是 EAGAIN 或 EWOULDBLOCK,表示缓冲区中没有数据可读,可以继续尝试或者退出 } return false; // 读取错误,main中调用conn_close() }else if(bytes_rd == 0){ return false; // 对方关闭连接,调用conn_close() //在使用套接字进行网络通信时,当对方关闭了连接,recv 函数会返回 0,表示已经没有数据可读了。 //这是因为在 TCP 协议中,对方关闭连接会发送一个 FIN 信号,表示不再发送数据,这时本地套接字收到 FIN 信号后,recv 函数会返回 0,表示对方已经关闭了连接。 } m_rd_idx += bytes_rd; // 更新下一次读取位置 } //写日志 ++m_request_cnt; EMlog(LOGLEVEL_INFO, "sock_fd = %d read done. request cnt = %d\n", m_sock_fd, m_request_cnt); // 全部读取完毕 return true; } // 主状态机 解析HTTP请求 http_conn::HTTP_CODE http_conn::process_read(){ LINE_STATUS line_stat = LINE_OK; HTTP_CODE ret = NO_REQUEST; char* text = 0; while((m_check_stat == CHECK_STATE_CONTENT && line_stat == LINE_OK) // 解析到了头部和请求行一行完整的数据,或者解析到了请求体一行完整的数据 || (line_stat = parse_one_line()) == LINE_OK){ // 获取一行数据 text = get_line();//char* get_line(){return m_rd_buf + m_line_start;}前面的parse_one_line给我们设置了'\0',或者头部和请求行给我们设置好了换行 m_line_start = m_checked_idx; // 更新下一行的起始位置 EMlog(LOGLEVEL_DEBUG, ">>>>>> %s\n", text); switch(m_check_stat){ case CHECK_STATE_REQUESTLINE: { ret = parse_request_line(text); if(ret == BAD_REQUEST){ return BAD_REQUEST; } break; } case CHECK_STATE_HEADER: { ret = parse_request_headers(text); if(ret == BAD_REQUEST){ return BAD_REQUEST; }else if(ret == GET_REQUEST){ return do_request(); // 解析具体的请求信息 } break; } case CHECK_STATE_CONTENT: { ret = parse_request_content(text); if(ret == GET_REQUEST){ return do_request(); // 解析具体的请求信息 } line_stat = LINE_OPEN; // != GET_REQUEST break; } default: { return INTERNAL_ERROR; // 内部错误 } } } return NO_REQUEST; // 数据不完整 } // 解析请求首行,获得请求方法,目标URL,HTTP版本 http_conn::HTTP_CODE http_conn::parse_request_line(char* text){ // GET /index.html HTTP/1.1 m_url = strpbrk(text, " \t"); // 找到第一次出现空格或者\t的下标 // char* strpbrk(const char* str1,const char* str2) // 在str1中查找第一个出现在str2中的字符,返回该字符在str1的位置指针 if(!m_url) return BAD_REQUEST; *m_url = '\0'; // GET\0/index.html HTTP/1.1,此时text到\0结束,表示 GET\0 m_url++; // 现在m_url指向index前面的单斜线 /index.html HTTP/1.1 char* method = text; // GET\0,因此method就是"GET" if(strcasecmp(method, "GET") == 0){ m_method = GET; }else{ return BAD_REQUEST; // 非GET请求方法 } // /index.html HTTP/1.1 m_version = strpbrk(m_url, " \t"); if(!m_version) return BAD_REQUEST; *m_version = '\0'; // /index.html\0HTTP/1.1,此时m_url到\0结束,表示 /index.html\0 m_version++; // HTTP/1.1 // if(strcasecmp(m_version, "HTTP/1.1") != 0) return BAD_REQUEST; // 非HTTP1.1版本,压力测试时为1.0版本,忽略该行 // 可能出现带地址的格式 http://192.168.15.128.1:9999/index.html if(strncasecmp(m_url, "http://", 7) == 0){ m_url += 7; // 192.168.15.128.1:9999/index.html m_url = strchr(m_url, '/'); // /index.html // char* strchr(const char* str,int c) // 在str中查找字符c第一次出现的位置,并返回该位置的指针,未出现返回NULL } // int strncasecmp(const char* str1,const char* str2,size_t n) // 比较str1和str2的前n个字符是否相等,忽略大小写差异,相等返回0 if(!m_url || m_url[0] != '/'){ return BAD_REQUEST; } m_check_stat = CHECK_STATE_HEADER; // 主状态机状态改变为检查请求头部 return NO_REQUEST; // 请求尚未解析完成 } // 解析请求头部 http_conn::HTTP_CODE http_conn::parse_request_headers(char* text){ // 在枚举类型前加上 `http_conn::` 来指出它的所属作用域 // 遇到空行,表示头部字段解析完毕 if( text[0] == '\0' ) { // 如果HTTP请求有消息体,则还需要读取m_content_length字节的消息体, // 状态机转移到CHECK_STATE_CONTENT状态 if ( m_content_len != 0 ) { // 请求体有内容 m_check_stat = CHECK_STATE_CONTENT; return NO_REQUEST; } // 否则说明HTTP请求没有请求体,读完请求行后我们已经得到了一个完整的HTTP请求 return GET_REQUEST; } else if ( strncasecmp( text, "Connection:", 11 ) == 0 ) { // 处理Connection 头部字段 Connection: keep-alive text += 11; text += strspn( text, " \t" ); // 检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标。 // size_t strspn(const char* str1,const char* str2) // 返回值是str1连续包含str2中字符的个数 if ( strcasecmp( text, "keep-alive" ) == 0 ) { m_linger = true; } } else if ( strncasecmp( text, "Content-Length:", 15 ) == 0 ) { // 处理Content-Length头部字段 text += 15; text += strspn( text, " \t" ); m_content_len = atol(text); /* * long int atol(const char *nptr); atol 函数会从字符串 nptr 开始解析整数,直到遇到非数字字符为止,然后将解析到的整数值作为结果返回。如果字符串中没有有效的整数,atol 函数将返回 0。 */ } else if ( strncasecmp( text, "Host:", 5 ) == 0 ) { // 处理Host头部字段 text += 5; text += strspn( text, " \t" ); m_host = text; } else { #ifdef COUT_OPEN EMlog(LOGLEVEL_DEBUG,"oop! unknow header: %s\n", text ); #endif } return NO_REQUEST; } // 解析请求体 // 我们并没有真的解析请求体,只是判断它是够被完整读入 http_conn::HTTP_CODE http_conn::parse_request_content(char* text){ if ( m_rd_idx >= ( m_content_len + m_checked_idx ) ) // 读到的数据长度 大于 已解析长度(请求行+头部+空行)+请求体长度 { // 数据被完整读取 text[ m_content_len ] = '\0'; // 标志结束 return GET_REQUEST; } return NO_REQUEST; } // 从状态机解析一行数据,判断\r\n http_conn::LINE_STATUS http_conn::parse_one_line(){ char temp; for( ; m_checked_idx < m_rd_idx; ++m_checked_idx){ // 检查的索引 小于 读到的索引 temp = m_rd_buf[m_checked_idx]; // 遍历缓冲区字符 if(temp == '\r'){//检查后面的一个字符是否是'\n' if(m_checked_idx + 1 == m_rd_idx){ // 回车符是已经读到的最后一个字符,表示行数据尚不完整 return LINE_OPEN; }else if(m_rd_buf[m_checked_idx+1] == '\n'){// 当前检查到 \r\n m_rd_buf[m_checked_idx++] = '\0'; // \r 变 \0 idx++ m_rd_buf[m_checked_idx++] = '\0'; // \n 变 \0 idx++,到下一行的起始位置 return LINE_OK; } return LINE_BAD; // 语法有问题 }else if(temp == '\n'){//检查前面的一个字符是否是'\r' if(m_checked_idx > 1 && m_rd_buf[m_checked_idx-1] == '\r'){ // 上一次读取的数据行不完整,刚好\r \n 在不同数据的结尾和开头的情况 m_rd_buf[m_checked_idx-1] = '\0'; // \r 变 \0 m_rd_buf[m_checked_idx++] = '\0'; // \n 变 \0 idx++,到下一行的起始位置 return LINE_OK; } return LINE_BAD; } } return LINE_OPEN; // 没有到结束符,数据尚不完整 } // 当得到一个完整、正确的HTTP请求时,我们就分析目标文件的属性, // 如果目标文件存在、对所有用户可读,且不是目录,则使用mmap将其 // 映射到内存地址m_file_a**ress处,并告诉调用者获取文件成功 http_conn::HTTP_CODE http_conn::do_request(){ // "/home/cyf/Linux/webserver/resources" strcpy( m_real_file, doc_root ); int len = strlen( doc_root ); strncpy( m_real_file + len, m_url, FILENAME_LEN - len - 1 ); // 拼接目录 "/home/cyf/Linux/webserver/resources/index.html" // 获取m_real_file文件的相关的状态信息,-1失败,0成功 if ( stat( m_real_file, &m_file_stat ) < 0 ) { // int stat(const char *path, struct stat *buf);其中,path 是要获取状态的文件或目录的路径,buf 是用于保存状态信息的结构体指针。buf是传出参数 return NO_RESOURCE; } // 判断访问权限 if ( ! ( m_file_stat.st_mode & S_IROTH ) ) {// S_IROTH 是一个用于表示其他用户(除了文件所有者和文件所属组)对文件的读权限的宏定义 return FORBI**EN_REQUEST; } // 判断是否是目录 if ( S_ISDIR( m_file_stat.st_mode ) ) { // S_ISDIR 宏定义的值是一个函数,用于检查指定的文件模式是否表示一个目录。 //如果文件模式表示一个目录,则 S_ISDIR 返回非零值(真),否则返回零值(假) return BAD_REQUEST; } // 以只读方式打开文件 int fd = open( m_real_file, O_RDONLY ); // 创建内存映射,m_file_a**ress是客户请求的目标文件被mmap到内存中的起始位置 m_file_a**ress = ( char* )mmap( 0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0 ); // a**r:指定映射的起始地址,通常设置为 NULL,表示由系统自动选择合适的地址,0和null等效 // void *mmap(void *a**r, size_t length, int prot, int flags, int fd, off_t offset); // MAP_PRIVATE:映射区域与文件不共享,对映射区域的修改不会影响到文件 // 只读打开,PROT也是读 close( fd ); return FILE_REQUEST; } // 对内存映射区执行munmap操作 void http_conn::unmap(){ if(m_file_a**ress){ munmap(m_file_a**ress, m_file_stat.st_size); // int munmap(void* a**r, size_t length);第一个参数是映射区域的起始地址 // 第二个参数是映射区域的长度,与mmap的第二个参数需要保持一致 m_file_a**ress = 0; } } // 写HTTP响应数据 bool http_conn::write(){ int temp = 0; if(timer) { // 更新超时时间,防止在执行操作时超时 time_t curr_time = time( NULL ); timer->expire = curr_time + 3 * TIMESLOT; m_timer_lst.adjust_timer( timer ); } EMlog(LOGLEVEL_INFO, "sock_fd = %d writing %d bytes. request cnt = %d\n", m_sock_fd, bytes_to_send, m_request_cnt); if ( bytes_to_send == 0 ) { // 将要发送的字节为0,这一次响应结束。重新入队,设置oneshot modfd( m_epoll_fd, m_sock_fd, EPOLLIN ); init(); return true; } while(1) { // 分散写 m_write_buf + m_file_a**ress temp = writev(m_sock_fd, m_iv, m_iv_count); /* ssize_t writev(int fd, const struct iovec *iov, int iovcnt); fd 是文件描述符,iov 是一个指向 iovec 结构体数组的指针,iovcnt 是 iov 数组中结构体的数量。 writev() 函数将 iov 数组中的多个缓冲区的数据一并写入到文件描述符 fd 指向的文件中,实现了多个缓冲区的批量写入操作。返回值为写入的字节数, 如果执行失败,返回值为 -1,且 errno 的值为 EAGAIN 或 EWOULDBLOCK,通常表示 TCP 写缓冲区没有空间,即发送缓冲区已满。 */ if ( temp <= -1 ) { // 如果TCP写缓冲没有空间,则等待下一轮EPOLLOUT事件,将其modfd重新加入epoll队列。虽然在此期间, // 服务器无法立即接收到同一客户的下一个请求,但可以保证连接的完整性。 if( errno == EAGAIN ) { modfd( m_epoll_fd, m_sock_fd, EPOLLOUT ); return true; } unmap(); // 释放内存映射m_file_a**ress空间 return false; } bytes_to_send -= temp; bytes_have_send += temp; if (bytes_have_send >= m_iv[0].iov_len){ // 发完头部了 m_iv[0].iov_len = 0; // 更新两个发送内存块的信息 m_iv[1].iov_base = m_file_a**ress + (bytes_have_send - m_write_idx); // 已经发了部分的响应体数据 m_iv[1].iov_len = bytes_to_send; }else{ // 还没发完头部,更新头部m_iv[0]中下一轮发送的起始下标和长度 m_iv[0].iov_base = m_write_buf + bytes_have_send; m_iv[0].iov_len = m_iv[0].iov_len - temp; } if (bytes_to_send <= 0){ // 没有数据要发送了 unmap(); modfd(m_epoll_fd, m_sock_fd, EPOLLIN);// 重新加入epoll队列,设置读事件监听 if (m_linger){// 如果m_linger为true,保持连接,我们重新初始化 init(); return true; }else{ return false;// 返回值为false时,main函数中就帮我们销毁了 } } } // printf("write done.\n"); // return true; } // 往写缓冲中写入待发送的数据 bool http_conn::a**_response( const char* format, ... ) { if( m_write_idx >= WD_BUF_SIZE ) { // 写缓冲区满了 return false; } va_list arg_list; // 可变参数,格式化文本 va_start( arg_list, format ); // 添加文本到到写缓冲区m_write_buf中 int len = vsnprintf( m_write_buf + m_write_idx, WD_BUF_SIZE - 1 - m_write_idx, format, arg_list ); if( len >= ( WD_BUF_SIZE - 1 - m_write_idx ) ) { return false; // 没写完,已经满了 } m_write_idx += len; // 更新下次写数据的起始位置 va_end( arg_list ); return true; /* va_list是一个类型,用于存储传递给可变参数函数的参数列表 va_start()函数需要两个参数,第一个参数是我们定义的va_list类型的变量,第二个参数是可变参数列表中最后一个已知的参数 va_end()传入start中传入的va_list类型的变量 int vsnprintf(char* str,size_t len,const char* format,va_list ap) str是一个指向字符数组的指针,用于存储生成的字符串 size是字符数组的大小,限制生成的字符串的最大长度 format是一个格式化字符串,用于指定生成字符串的格式 ap是一个va_list类型的可变参数列表,用于填充格式化字符串中的控制符 */ } // 添加状态码(响应行) bool http_conn::a**_status_line( int status, const char* title ) { EMlog(LOGLEVEL_DEBUG,"<<<<<<< %s %d %s\r\n", "HTTP/1.1", status, title); return a**_response( "%s %d %s\r\n", "HTTP/1.1", status, title ); } // 添加了一些必要的响应头部 void http_conn::a**_headers(int content_len) { a**_content_length(content_len); a**_content_type(); a**_linger(); a**_blank_line(); } bool http_conn::a**_content_length(int content_len) { EMlog(LOGLEVEL_DEBUG,"<<<<<<< Content-Length: %d\r\n", content_len); return a**_response( "Content-Length: %d\r\n", content_len ); } bool http_conn::a**_content_type() { // 响应体类型,当前文本形式,这里我们写死了类型是text/html EMlog(LOGLEVEL_DEBUG,"<<<<<<< Content-Type:%s\r\n", "text/html"); return a**_response("Content-Type:%s\r\n", "text/html"); } bool http_conn::a**_linger(){ EMlog(LOGLEVEL_DEBUG,"<<<<<<< Connection: %s\r\n", ( m_linger == true ) ? "keep-alive" : "close" ); return a**_response( "Connection: %s\r\n", ( m_linger == true ) ? "keep-alive" : "close" ); } bool http_conn::a**_blank_line(){ EMlog(LOGLEVEL_DEBUG,"<<<<<<< %s", "\r\n" ); return a**_response( "%s", "\r\n" ); } bool http_conn::a**_content( const char* content ){ EMlog(LOGLEVEL_DEBUG,"<<<<<<< %s\n", content ); return a**_response( "%s", content ); } // 根据服务器处理HTTP请求的结果,决定返回给客户端的内容 bool http_conn::process_write(HTTP_CODE ret){ switch (ret) { case INTERNAL_ERROR: a**_status_line( 500, error_500_title ); a**_headers( strlen( error_500_form ) ); if ( ! a**_content( error_500_form ) ) { return false; } break; case BAD_REQUEST: a**_status_line( 400, error_400_title ); a**_headers( strlen( error_400_form ) ); if ( ! a**_content( error_400_form ) ) { return false; } break; case NO_RESOURCE: a**_status_line( 404, error_404_title ); a**_headers( strlen( error_404_form ) ); if ( ! a**_content( error_404_form ) ) { return false; } break; case FORBI**EN_REQUEST: a**_status_line( 403, error_403_title ); a**_headers(strlen( error_403_form)); if ( ! a**_content( error_403_form ) ) { return false; } break; case FILE_REQUEST: // 请求文件 a**_status_line(200, ok_200_title ); a**_headers(m_file_stat.st_size); EMlog(LOGLEVEL_DEBUG, "<<<<<<< %s", m_file_a**ress); // 封装m_iv m_iv[ 0 ].iov_base = m_write_buf; // 起始地址 m_iv[ 0 ].iov_len = m_write_idx; // 长度 m_iv[ 1 ].iov_base = m_file_a**ress; 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; default: return false; } m_iv[ 0 ].iov_base = m_write_buf; m_iv[ 0 ].iov_len = m_write_idx; m_iv_count = 1; return true; } // 由线程池中的工作线程调用,处理HTTP请求的入口函数 void http_conn::process(){ // 线程池中线程的业务处理 EMlog(LOGLEVEL_DEBUG, "=======parse request, create response.=======\n"); // 解析HTTP请求 EMlog(LOGLEVEL_DEBUG,"=============process_reading=============\n"); HTTP_CODE read_ret = process_read(); EMlog(LOGLEVEL_INFO,"========PROCESS_READ HTTP_CODE : %d========\n", read_ret); if(read_ret == NO_REQUEST){ modfd(m_epoll_fd, m_sock_fd, EPOLLIN); // 继续监听EPOLLIN (| EPOLLONESHOT) return; // 返回,线程空闲 } // 生成响应 bool write_ret = process_write(read_ret); if(!write_ret){ conn_close(); if(timer) m_timer_lst.del_timer(timer); // 失败,移除其对应的定时器 } modfd(m_epoll_fd, m_sock_fd, EPOLLOUT); // 重置EPOLLONESHOT }
5.定时器类和定时器链表
使用定时器可以更有效的对吞吐量进行控制,断开长时间占用但没有通信的连接
lst_timer.h
#ifndef LST_TIMER #define LST_TIMER #include <stdio.h> #include <time.h> #include <arpa/inet.h> #include "http_conn.h" #include "locker.h" class http_conn; // 前向声明 // 定时器类 class util_timer { public: util_timer() : prev(NULL), next(NULL){} public: time_t expire; // 任务超时时间,这里使用绝对时间 http_conn* user_data; util_timer* prev; // 指向前一个定时器 util_timer* next; // 指向后一个定时器 }; // 定时器链表,它是一个升序、双向链表,且带有头节点和尾节点。小的排前面 class sort_timer_lst { public: sort_timer_lst() : head( NULL ), tail( NULL ) {} // 链表被销毁时,删除其中所有的定时器 ~sort_timer_lst() { util_timer* tmp = head; while( tmp ) { head = tmp->next; delete tmp; tmp = head; } } // 将目标定时器timer添加到链表中 void a**_timer( util_timer* timer ); /* 当某个定时任务发生变化时,调整对应的定时器在链表中的位置。这个函数只考虑被调整的定时器的 超时时间延长的情况,即该定时器需要往链表的尾部移动。*/ void adjust_timer(util_timer* timer); // 将目标定时器 timer 从链表中删除 void del_timer( util_timer* timer ); /* SIGALARM 信号每次被触发就在其信号处理函数中执行一次 tick() 函数,以处理链表上到期任务。*/ void tick(); private: /* 一个重载的辅助函数,它被公有的 a**_timer 函数和 adjust_timer 函数调用 该函数表示将目标定时器 timer 添加到节点 lst_head 之后的部分链表中 */ void a**_timer(util_timer* timer, util_timer* lst_head); private: util_timer* head; // 头结点 util_timer* tail; // 尾结点 }; #endif
lst_timer.cpp
#include "lst_timer.h" // 将目标定时器timer添加到链表中 void sort_timer_lst::a**_timer( util_timer* timer ) { EMlog(LOGLEVEL_DEBUG, "===========a**ing timer.=============\n"); if( !timer ) { EMlog(LOGLEVEL_WARN ,"===========timer null.=========\n"); return; } if( !head ) { // 添加的为第一个节点,头结点(尾节点) head = tail = timer; } // 目标定时器的超时时间最小,则把该定时器插入链表头部,作为链表新的头节点 else if( timer->expire < head->expire ) { timer->next = head; head->prev = timer; head = timer; } // 否则调用重载函数,把它插入head节点之后合适的位置,以保证链表的升序特性 else{ a**_timer(timer, head); } // // http_conn::m_timer_lst_locker.unlock(); EMlog(LOGLEVEL_DEBUG,"===========a**ed timer.==========\n"); } /* 当某个定时任务发生变化时,调整对应的定时器在链表中的位置。 这个函数只考虑被调整的定时器的超时时间延长的情况,即该定时器需要往链表的尾部移动。*/ void sort_timer_lst::adjust_timer(util_timer* timer) { EMlog(LOGLEVEL_DEBUG,"===========adjusting timer.=========\n"); if( !timer ) { EMlog(LOGLEVEL_WARN, "===========timer null.==========\n"); return; } util_timer* tmp = timer->next; // 如果被调整的目标定时器处在链表的尾部,或者该定时器新的超时时间值仍然小于其下一个定时器的超时时间则不用调整 if( !tmp || ( timer->expire < tmp->expire ) ) { // return; } // 如果目标定时器是链表的头节点,则将该定时器从链表中取出并重新插入链表 else if( timer == head ) { head = head->next; // 取出头结点 head->prev = NULL; timer->next = NULL; a**_timer( timer, head ); // 重新加入 } else { // 如果目标定时器不是链表的头节点,则将该定时器从链表中取出,然后插入其原来所在位置后的部分链表中 timer->prev->next = timer->next; timer->next->prev = timer->prev; a**_timer( timer, timer->next ); } EMlog(LOGLEVEL_DEBUG,"===========adjusted timer.==========\n"); } /* 一个重载的辅助函数,它被公有的 a**_timer 函数和 adjust_timer 函数调用 该函数表示将目标定时器 timer 添加到节点 lst_head 之后的部分链表中 */ void sort_timer_lst::a**_timer(util_timer* timer, util_timer* lst_head) { util_timer* prev = lst_head; util_timer* tmp = prev->next; /* 遍历 list_head 节点之后的部分链表,直到找到一个超时时间大于目标定时器的超时时间节点 并将目标定时器插入该节点之前 */ while(tmp) { if( timer->expire < tmp->expire ) { prev->next = timer; timer->next = tmp; tmp->prev = timer; timer->prev = prev; break; } prev = tmp; tmp = tmp->next; } /* 如果遍历完 lst_head 节点之后的部分链表,仍未找到超时时间大于目标定时器的超时时间的节点, 则将目标定时器插入链表尾部,并把它设置为链表新的尾节点。*/ if( !tmp ) { prev->next = timer; timer->prev = prev; timer->next = NULL; tail = timer; } } // 将目标定时器 timer 从链表中删除 void sort_timer_lst::del_timer( util_timer* timer ) { EMlog(LOGLEVEL_DEBUG,"===========deleting timer.===========\n"); if( !timer ) { // http_conn::m_timer_lst_locker.unlock(); return; } // 下面这个条件成立表示链表中只有一个定时器,即目标定时器 if( ( timer == head ) && ( timer == tail ) ) { delete timer; head = NULL; tail = NULL; } /* 如果链表中至少有两个定时器,且目标定时器是链表的头节点, 则将链表的头节点重置为原头节点的下一个节点,然后删除目标定时器。 */ else if( timer == head ) { head = head->next; head->prev = NULL; delete timer; } /* 如果链表中至少有两个定时器,且目标定时器是链表的尾节点, 则将链表的尾节点重置为原尾节点的前一个节点,然后删除目标定时器。*/ else if( timer == tail ) { tail = tail->prev; tail->next = NULL; delete timer; } // 如果目标定时器位于链表的中间,则把它前后的定时器串联起来,然后删除目标定时器 else{ timer->prev->next = timer->next; timer->next->prev = timer->prev; delete timer; } EMlog(LOGLEVEL_DEBUG,"===========deleted timer.===========\n"); } /* SIGALARM 信号每次被触发就在其信号处理函数中执行一次 tick() 函数,以处理链表上到期任务。*/ void sort_timer_lst::tick() { if( !head ) { return; } EMlog(LOGLEVEL_DEBUG, "timer tick.\n" ); time_t curr_time = time(NULL); // 获取当前系统时间 util_timer* tmp = head; // 从头节点开始依次处理每个定时器,直到遇到一个尚未到期的定时器 while( tmp ) { /* 因为每个定时器都使用绝对时间作为超时值,所以可以把定时器的超时值和系统当前时间, 比较以判断定时器是否到期*/ if( curr_time < tmp->expire ) { // 当前未超时,则后面节点也不超时 break; } // 调用定时器的回调函数,以执行定时任务,关闭连接 tmp->user_data->conn_close(); // 删除定时器 del_timer(tmp); tmp = head; } }
6.日志系统
在项目中引入日志系统可以带来多种好处,包括但不限于以下几点:
(1)调试和排查问题:日志系统可以记录项目运行时的各种信息,包括程序的状态、执行路径、输入输出数据等。这对于项目的调试和排查问题非常有帮助,可以帮助开发人员快速定位和解决问题。
(2)性能优化:日志系统可以记录项目的性能指标,例如处理时间、资源占用情况等。通过分析日志,可以了解项目的性能瓶颈,进行优化和改进,提高项目的性能和效率。
(3)监控和运维:日志系统可以记录项目的运行状态和行为,包括错误日志、警告日志、操作日志等。这对于项目的监控和运维非常有帮助,可以帮助运维人员实时了解项目的运行情况,及时发现并解决潜在问题。
(4)安全和合规性:日志系统可以记录项目的安全事件、访问日志等,对于安全审计和合规性要求非常重要。通过日志系统,可以对项目的安全性进行监控和审计,发现并防范安全威胁,确保项目的合规性。
(5)数据分析和业务洞察:日志系统可以记录项目的业务日志、用户行为等,通过对日志进行分析,可以获取有价值的业务洞察和数据分析结果,帮助项目的业务决策和发展方向。
总的来说,引入日志系统可以帮助项目进行调试、性能优化、监控和运维、安全审计、数据分析等方面的工作,提高项目的可维护性、可靠性、安全性和业务洞察力。
log.h
#ifndef _EM_LOG_H_ // 多个文件引用时,不能重复定义 #define _EM_LOG_H_ #include <stdarg.h> #include <stdio.h> #include "locker.h" #include "lst_timer.h" #include "http_conn.h" #define OPEN_LOG 1 // 声明是否打开日志输出 #define LOG_LEVEL LOGLEVEL_INFO // 声明当前程序的日志等级状态,只输出等级等于或高于该值的内容 #define LOG_SAVE 0 // 可补充日志保存功能 typedef enum{ // 日志等级,越往下等级越高 LOGLEVEL_DEBUG = 0, LOGLEVEL_INFO, LOGLEVEL_WARN, LOGLEVEL_ERROR, }E_LOGLEVEL; void EM_log(const int level, const char* fun, const int line, const char *fmt, ...); #define EMlog(level, fmt...) EM_log(level, __FUNCTION__, __LINE__, fmt) // 宏定义,隐藏形参 /* 宏定义的语法格式是 #define 宏名称 替代文本 其中宏名称是定义的宏的名称,替代文本是宏在调用时会被替代的内容。 EMlog 是宏的名称,level 和 fmt... 是宏的参数,在调用时需要传递相应的参数值。 _FUNCTION__ 是C语言的内置宏,表示当前函数的名称,__LINE__ 也是C语言的内置宏,表示当前代码行号。fmt... 表示宏接受可变参数。 使用 EMlog 宏进行日志输出时,宏会自动将当前的日志等级、函数名、行号等作为参数传递给 EM_log 函数,从而实现了简化日志输出的目的。 */ #endif /* __FUNCTION__:它是一个字符串常量,表示当前代码所在的函数的名称。在使用时,__FUNCTION__ 会被替换成当前函数的名称 __LINE__ 宏会被替换为当前代码所在的行号,编译器会在编译时根据代码的实际行号来替换它的值 需要注意的是,行号的计数从1开始,而不是从0开始。 */ /* 这两个相当于参数有默认初始值,我们不用在传入的时候传 __FUNCTION__,和__LINE__ 调用EMlog(LOGLEVEL_ERROR,"EPOLL failed.\n")和EMlog(LOGLEVEL_ERROR,__FUNCTION__, __LINE__,"EPOLL failed.\n")是一样的 输出:[ERROR] [函数名 函数所在行号]: EPOLL failed. */
log.cpp
#include "log.h" char *EM_logLevelGet(const int level){ // 得到当前输入等级level的字符串 if(level == LOGLEVEL_DEBUG){ return (char*)"DEBUG"; }else if (level == LOGLEVEL_INFO ){ return (char*)"INFO"; }else if (level == LOGLEVEL_WARN ){ return (char*)"WARN"; }else if (level == LOGLEVEL_ERROR ){ return (char*)"ERROR"; }else{ return (char*)"UNKNOWN"; } } void EM_log(const int level, const char* fun, const int line, const char *fmt, ...){ // 日志输出函数 #ifdef OPEN_LOG // 判断开关 va_list arg; va_start(arg, fmt); char buf[1024]; // 创建缓存字符数组 vsnprintf(buf, sizeof(buf), fmt, arg); // 赋值 ftm 格式的 arg 到 buf //vsnprintf的第一个参数是一个传出参数,我们把格式化字符串 fmt 和可变参数 ... 格式化成一个字符数组 buf va_end(arg); if(level >= LOG_LEVEL){ // 判断当前日志等级,与程序日志等级状态对比 printf("[%s]\t[%s %d]: %s \n", EM_logLevelGet(level), fun, line, buf); } #endif }