TinyWebSever源码逐行注释(一)_webserver.cpp

2024-09-05 20:44

本文主要是介绍TinyWebSever源码逐行注释(一)_webserver.cpp,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

项目源码地址
项目详细介绍

项目简介:
Linux下C++轻量级Web服务器,助力初学者快速实践网络编程,搭建属于自己的服务器.

  1. 使用 线程池 + 非阻塞socket + epoll(ET和LT均实现) + 事件处理(Reactor和模拟Proactor均实现) 的并发模型
  2. 使用状态机解析HTTP请求报文,支持解析GET和POST请求
  3. 访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件
  4. 实现同步/异步日志系统,记录服务器运行状态
  5. 经Webbench压力测试可以实现上万的并发连接数据交换

webserver.cpp用于实现web服务器的核心配置和运行,包括但不限于监听套接字、数据库连接池、线程池、信号与管道配置、epoll(ET和LT模式)的配置,原项目地址的注释较少不适合初学者,于是我将每行都加上了注释帮助大家更好的理解:

源码注释

#include "webserver.h"
//构造函数
WebServer::WebServer()
{//创建客户端连接数组users = new http_conn[MAX_FD];//创建root文件夹路径char server_path[200];getcwd(server_path, 200);char root[6] = "/root";m_root = (char *)malloc(strlen(server_path) + strlen(root) + 1);strcpy(m_root, server_path);strcat(m_root, root);//创建定时器users_timer = new client_data[MAX_FD];
}//析构函数
WebServer::~WebServer()
{//释放epoll文件描述符close(m_epollfd);//释放监听文件描述符close(m_listenfd);//释放管道读端close(m_pipefd[1]);//释放管道写端close(m_pipefd[0]);//删除客户端连接及定时器数组delete[] users;delete[] users_timer;//删除线程池delete m_pool;
}//初始化函数(服务器相关参数)
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;//日志写入的方式 同步or异步m_log_write = log_write;//用于控制连接关闭的变量m_OPT_LINGER = opt_linger;//控制监听和连接采用ET or LT触发模式m_TRIGMode = trigmode;//日志的开关 0为开m_close_log = close_log;//控制事件处理的模型为Reactor还是Proactorm_actormodel = actor_model;
}//通过m_TRIGMode选择监听和连接的触发模式 LT: Level Triggered ET:Edge Triggered
void WebServer::trig_mode()
{//LT + LTif (0 == m_TRIGMode){m_LISTENTrigmode = 0;m_CONNTrigmode = 0;}//LT + ETelse if (1 == m_TRIGMode){m_LISTENTrigmode = 0;m_CONNTrigmode = 1;}//ET + LTelse if (2 == m_TRIGMode){m_LISTENTrigmode = 1;m_CONNTrigmode = 0;}//ET + ETelse if (3 == m_TRIGMode){m_LISTENTrigmode = 1;m_CONNTrigmode = 1;}
}//初始化日志
void WebServer::log_write()
{if (0 == m_close_log){//m_log_write控制异步还是同步if (1 == m_log_write)Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 800);else//每写一次刷新一次logLog::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 0);}
}//初始化数据库池
void WebServer::sql_pool()
{//初始化数据库连接池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);
}//初始化线程池
void WebServer::thread_pool()
{//(事件相应模型,数据库连接池,线程池中的线程数量)m_pool = new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num);
}//配置监听套接字和管道
void WebServer::eventListen()
{//创建套接字 PF_INET=IPV4 SOCK_STREAM=TCPm_listenfd = socket(PF_INET, SOCK_STREAM, 0);//检查是否创建成功assert(m_listenfd >= 0);//优雅关闭连接if (0 == m_OPT_LINGER){struct linger tmp = {0, 1};//setsockopt函数设置套接字的相关属性setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));}else if (1 == m_OPT_LINGER){struct linger tmp = {1, 1};setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));}//用于捕获和检查绑定和监听是否成功int ret = 0;//该结构体定义在头文件 <netinet/in.h> 中 用于存放IP地址和端口号struct sockaddr_in address;//清除内存中的垃圾,即结构体中的非0随机值bzero(&address, sizeof(address));//表示使用的是IPV4地址address.sin_family = AF_INET;//存放IPV4地址具体的值 这里的htonl(INADDR_ANY)绑定了所有本地IP地址address.sin_addr.s_addr = htonl(INADDR_ANY);//存放端口号 htons(m_port) 即Host TO Network Short 设置监听的端口号的过程中将主机字节序的端口号转换为网络字节序。address.sin_port = htons(m_port);//1即启用SO_REUSEEADDR允许重用本地地址  使得在重启服务器后快速重新绑定原来的端口int flag = 1;setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));//绑定套接字到本地IP地址ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));//是否绑定成功assert(ret >= 0);//设置套接字为监听模式,最多5个连接请求排队ret = listen(m_listenfd, 5);//检测是否成功assert(ret >= 0);//初始化定时器 设置最小超时单位utils.init(TIMESLOT);//存储事件信息结构体的数组epoll_event events[MAX_EVENT_NUMBER];//epoll创建内核事件表 管理着所有的文件描述符及其事件m_epollfd = epoll_create(5);//检查是否创建成功assert(m_epollfd != -1);//添加监听描述符到epoll事件表//这里的这个布尔值决定是否启用 EPOLLONESHOT 模式。//如果为 true,则在这个文件描述符上触发一个事件后,epoll 不会再次监视这个文件描述符,直到你手动重置它。这在多线程环境中非常有用,避免同一个文件描述符的事件被多个线程处理。//如果为 false,则 epoll 在每次该文件描述符上有事件发生时都会通知你。utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);//设置客户端连接的epollhttp_conn::m_epollfd = m_epollfd;//创建用于信号处理的套接字(管道) 这两个套接字 sv[0] 和 sv[1] 之间可以进行双向通信。socketpair 通常用于进程间通信ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);assert(ret != -1);//将管道的写段设置为非阻塞 避免写操作阻塞进程:如果 m_pipefd[1] 是阻塞的,当你尝试写入数据时,如果管道已满(即 m_pipefd[0] 没有被读取),写操作将阻塞进程,直到有足够的空间可用。这可能导致程序挂起,等待管道缓冲区变得可写。通过将 m_pipefd[1] 设置为非阻塞模式,如果管道已满,写操作将立即返回失败(通常返回 EAGAIN 或 EWOULDBLOCK),程序可以处理这个情况而不被阻塞。utils.setnonblocking(m_pipefd[1]);//将管道的读端添加到epoll 使其可以通过信号驱动utils.addfd(m_epollfd, m_pipefd[0], false, 0);//设置对关闭信号的处理(ignore)utils.addsig(SIGPIPE, SIG_IGN);//设置对定时器触发信号的处理()utils.addsig(SIGALRM, utils.sig_handler, false);utils.addsig(SIGTERM, utils.sig_handler, false);//设置定时器 每隔TIMESLOT秒发送一次SIGALRM用于检查超时alarm(TIMESLOT);//工具类,信号和描述符基础操作Utils::u_pipefd = m_pipefd;Utils::u_epollfd = m_epollfd;
}//定时器相关函数 参数为(连接描述符,客户端的网络信息结构体)
void WebServer::timer(int connfd, struct sockaddr_in client_address)
{//初始客户端相关信息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;//绑定用户数据timer->user_data = &users_timer[connfd];//设置回调函数,即定时器超时用cb_func来处理timer->cb_func = cb_func;//获取当前时间time_t cur = time(NULL);//超时时间为当前时间+3*TIMESLOTtimer->expire = cur + 3 * TIMESLOT;//将定时器与用户数据关联users_timer[connfd].timer = timer;//将定时器添加到定时器链表中,以便定时器可以参与时间轮机制或其他定时器管理机制。utils.m_timer_lst.add_timer(timer);
}//调整定时器
void WebServer::adjust_timer(util_timer *timer)
{//若有数据传输,则将定时器往后延迟3个单位time_t cur = time(NULL);timer->expire = cur + 3 * TIMESLOT;//并对新的定时器在链表上的位置进行调整utils.m_timer_lst.adjust_timer(timer);//写入logLOG_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);}//写入logLOG_INFO("close fd %d", users_timer[sockfd].sockfd);
}//根据监听模式处理客户端连接
bool WebServer::dealclientdata()
{struct sockaddr_in client_address;//知道客户端地址结构体大小,便于使用accept()socklen_t client_addrlength = sizeof(client_address);//LT触发模式下的acceptif (0 == m_LISTENTrigmode){int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);//记录出错的logif (connfd < 0){LOG_ERROR("%s:errno is:%d", "accept error", errno);return false;}//当前排队的客户端连接已到峰值if (http_conn::m_user_count >= MAX_FD){//向客户端发送busy信息utils.show_error(connfd, "Internal server busy");LOG_ERROR("%s", "Internal server busy");return false;}//为这个新连接设置一个定时器timer(connfd, client_address);}//ET模式处理监听事件else{//一次性接收所有客户端连接while (1){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;
}//信号处理函数(服务器操作系统层面上的通知)
bool WebServer::dealwithsignal(bool &timeout, bool &stop_server)
{int ret = 0;int sig;char signals[1024];//从管道中读取信号 将其存储在singals数组中ret = recv(m_pipefd[0], signals, sizeof(signals), 0);//出错if (ret == -1){return false;}//读完else if (ret == 0){return false;}//else{for (int i = 0; i < ret; ++i){//逐个处理信号switch (signals[i]){case SIGALRM:{timeout = true;break;}case SIGTERM:{stop_server = true;break;}}}}return true;
}//处理客户端读事件
void WebServer::dealwithread(int sockfd)
{//获取当前客户端的定时器util_timer *timer = users_timer[sockfd].timer;//reactor模式 以事件驱动if (1 == m_actormodel){//调整定时器时间if (timer){adjust_timer(timer);}//若监测到读事件,将该事件放入请求队列m_pool->append(users + sockfd, 0);//不断循环询问客户端该事件是否结束while (true){//improv为1代表事件处理完成if (1 == users[sockfd].improv){//timer_fkag代表定时器事件触发 需要deal_timer处理如超时或客户端关闭连接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].read_once()){LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));//将该事件放入请求队列m_pool->append_p(users + sockfd);//同Reactorif (timer){adjust_timer(timer);}}else{//服务器读不出来就调用回调函数结束该事件deal_timer(timer, sockfd);}}
}//处理客户断写事件 同上
void WebServer::dealwithwrite(int sockfd)
{util_timer *timer = users_timer[sockfd].timer;//reactorif (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{//proactorif (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);}}
}//服务器主事件循环
void WebServer::eventLoop()
{//控制超时事件bool timeout = false;//主循环的开关bool stop_server = false;while (!stop_server){//epoll_wait阻塞(监听)已注册的套接字 (epoll套接字 epoll内核表,最大事件数量,-1代表无限等待)int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);//检测epoll_wait是否出错{LOG_ERROR("%s", "epoll failure");break;}for (int i = 0; i < number; i++){//哪个套接字发生了事件int sockfd = events[i].data.fd;//如果是监听套接字,则处理新的客户端连接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);//处理失败时的logif (false == flag)LOG_ERROR("%s", "dealclientdata failure");}//当前事件是否为读事件else if (events[i].events & EPOLLIN){dealwithread(sockfd);}//当前事件是否为写事件else if (events[i].events & EPOLLOUT){dealwithwrite(sockfd);}}//超时事件if (timeout){utils.timer_handler();LOG_INFO("%s", "timer tick");timeout = false;}}
}

这篇关于TinyWebSever源码逐行注释(一)_webserver.cpp的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1140016

相关文章

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

vscode中文乱码问题,注释,终端,调试乱码一劳永逸版

忘记咋回事突然出现了乱码问题,很多方法都试了,注释乱码解决了,终端又乱码,调试窗口也乱码,最后经过本人不懈努力,终于全部解决了,现在分享给大家我的方法。 乱码的原因是各个地方用的编码格式不统一,所以把他们设成统一的utf8. 1.电脑的编码格式 开始-设置-时间和语言-语言和区域 管理语言设置-更改系统区域设置-勾选Bata版:使用utf8-确定-然后按指示重启 2.vscode

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

kubelet组件的启动流程源码分析

概述 摘要: 本文将总结kubelet的作用以及原理,在有一定基础认识的前提下,通过阅读kubelet源码,对kubelet组件的启动流程进行分析。 正文 kubelet的作用 这里对kubelet的作用做一个简单总结。 节点管理 节点的注册 节点状态更新 容器管理(pod生命周期管理) 监听apiserver的容器事件 容器的创建、删除(CRI) 容器的网络的创建与删除

red5-server源码

red5-server源码:https://github.com/Red5/red5-server

TL-Tomcat中长连接的底层源码原理实现

长连接:浏览器告诉tomcat不要将请求关掉。  如果不是长连接,tomcat响应后会告诉浏览器把这个连接关掉。    tomcat中有一个缓冲区  如果发送大批量数据后 又不处理  那么会堆积缓冲区 后面的请求会越来越慢。