云计算百科
云计算领域专业知识百科平台

TingWebServer服务器代码解读01

本解读文章对完整代码进行解读,从main.cpp着手主看代码,然后一步步剖解代码文件,但不会插入所有代码,所以需要搭配打开TingWebServer的源代码一起学习

源代码地址:TinyWebServer/webserver.cpp at master · qinguoyi/TinyWebServer

作者的原解读:最新版Web服务器项目详解 – 01 线程同步机制封装类

Tingwebserver 的配置与使用:c++ 经典服务器开源项目 Tinywebserver的使用与配置(百度智能云服务器安装ubuntu18.04可用公网ip访问)-CSDN博客

我们将按照这个关系图逐级解读代码

先看主代码:main.cpp

#include "config.h"

int main(int argc, char *argv[])
{
//需要修改的数据库信息,登录名,密码,库名
string user = "root";
string passwd = "root";
string databasename = "qgydb";

//命令行解析
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;
}

 这三行是在程序编译运行前就需要修改的,确保user,password和database是自己的数据库        

string user = "root";
string passwd = "root";
string databasename = "qgydb";

 这是config解析命令行的运行代码的,一遍的shell脚本代码格式为:

./server [-p port] [-l LOGWrite] [-m TRIGMode] [-o OPT_LINGER] [-s sql_num] [-t thread_num] [-c close_log] [-a actor_model]

而config是解析后面的-p,-l之类的指定参数,从而实现个性化运行服务器 

//命令行解析
Config config;
config.parse_arg(argc, argv);

后面则是webserver的初始化,传入参数是config的解析命令,之后便是服务器启动的规范标准流程:日志->数据库->线程池-> 触发模式-> 事件监听-> 事件循环,服务器开始运行

  • 资源初始化顺序:
    • 服务器启动时,首先需要确保日志系统可以记录后续的事件。
    • 然后初始化数据库连接池,确保后续操作(如请求处理)能够顺利与数据库交互。
    • 接下来初始化线程池,以确保服务器能够并发处理客户端请求。
    • 随后配置触发模式,确保事件处理方式符合需求。
  • 逐步初始化依赖关系:
    • 事件监听是服务器开始接收请求的步骤,它依赖于前面所有资源的配置,包括日志、数据库连接、线程池和触发模式。
    • 事件循环是服务器的主循环,它依赖于所有初始化过程(如监听端口、线程池等)完成才能启动。
  • 这个顺序确保了每个系统组件的初始化都在其依赖的组件之前进行,从而避免出现资源不可用或配置错误的问题。

    因为main.cpp中只是include了config .h这个头文件,我们就跟着引用头文件入手 

     命令行解析config.h+config.cpp

    config.h非常简单并且参数都给了注解,提一嘴宏定义头文件,为了防止重复编译报错,所以采用这种方式

    // config.h #ifndef CONFIG_H #define CONFIG_H

    // 头文件的内容

    #endif // CONFIG_H  

    现代编译器通常还支持一种更简洁的方式来避免头文件多次包含,那就是使用 #pragma once。它的效果与 #ifndef / #define 配合使用的头文件保护机制相同,但写法更简洁,且不容易出错

    在config.cpp中,构造函数Config()定义了默认参数,服务器的初始定义状态,默认端口号为9006。

    void Config::parse_arg(int argc, char*argv[])
    {
    int opt;
    const char *str = "p:l:m:o:s:t:c:a:";
    while ((opt = getopt(argc, argv, str)) != -1)
    {
    switch (opt)
    {
    case 'p':
    {
    PORT = atoi(optarg);
    break;
    }

    default:
    break;
    }
    }

    使用解析命令行函数getopt(),返回的opt指向合法命令行选项,例如-p 返回 'p',而optarg 是 getopt 解析到的当前选项的参数值。它是一个 char* 类型的全局变量,用于存储当前选项对应的参数。

    getopt 是一个标准库函数,用于解析命令行参数。它通过一个选项字符串(比如 str)定义程序支持的命令行参数,并自动处理参数解析。

    • 参数说明:
      • argc: 参数的数量(来自命令行)。
      • argv: 参数的值(命令行中输入的具体内容)。
      • str: 一个字符串,指定程序支持的命令行选项。每个字符代表一个选项,字符后面可能跟着一个冒号(:),表示该选项后面需要一个参数

    主体文件:webserver.h+webserver.cpp

     webserver .h

    因为是主体文件,include了很多需要使用到的头文件和库,除了main中用到的运行函数和初始化之外,public里还有定时器函数和epoll的相关配置

    void timer(int connfd, struct sockaddr_in client_address); // 创建定时器
    void adjust_timer(util_timer *timer); // 调整定时器
    void deal_timer(util_timer *timer, int sockfd); // 处理定时器
    bool dealclientdata(); // 处理客户端数据

    // 定时器配置
    client_data *users_timer; // 定时器相关数据结构,管理连接的超时
    Utils utils; // 工具类,用于处理定时器等
    //定义在timer的lst_timer中
    //epoll事件
    bool dealwithsignal(bool& timeout, bool& stop_server); // 处理信号
    void dealwithread(int sockfd); // 处理读操作
    void dealwithwrite(int sockfd); // 处理写操作

    epoll_event events[MAX_EVENT_NUMBER]; // 存放epoll事件的数组

    int m_listenfd; // 监听套接字文件描述符
    int m_OPT_LINGER; // SO_LINGER设置标志,是否延迟关闭套接字
    int m_TRIGMode; // 总的触发模式(水平触发/边缘触发)
    int m_LISTENTrigmode; // 监听套接字的触发模式(水平触发/边缘触发)
    int m_CONNTrigmode; // 客户端连接的触发模式(水平触发/边缘触发)

    webserver .cpp

    WebServer::WebServer()

    构造函数: 

    //http_conn类对象
    users = new http_conn[MAX_FD];

     http_conn 类是 Web 服务器项目中与每个客户端连接相关的核心类,承担着管理连接、处理 HTTP 请求和响应、以及与事件驱动机制(如 epoll)协同工作等任务。通过该类,服务器能够高效地处理大量的并发连接和 I/O 操作。

    //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);

    这段代码的目的是在程序启动时动态构建一个路径,该路径是当前工作目录路径(通过 getcwd 获取)加上一个固定的 /root 路径。例如,如果当前工作目录是 /home/user/project,那么构建出来的路径会是 /home/user/project/root。

    • getcwd 获取当前工作目录。
    • strcat 和 strcpy 拼接得到一个新的路径,存储在 m_root 中。

    WebServer::~WebServer()       

    析构函数,关闭epoll,listen,和管道pipe,delete释放users http_conn和users_timer定时器和池空间m_pool

    WebServer::init()

    main()中的初始化server对象

    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;//并发模型选择
    }

    trig_mode()

    根据传入的m_TRIGMode值来设定服务器运行的模式,默认:LT+LT,通过改变 m_LISTENTrigmode 和m_CONNTrigmode 的值来控制。

    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;
    }
    //ET + LT
    else if (2 == m_TRIGMode)
    {
    m_LISTENTrigmode = 1;
    m_CONNTrigmode = 0;
    }
    //ET + ET
    else if (3 == m_TRIGMode)
    {
    m_LISTENTrigmode = 1;
    m_CONNTrigmode = 1;
    }
    }

    日志函数 

    log_write() 

    void WebServer::log_write()
    {
    if (0 == m_close_log) //日志没有被关闭
    {
    //初始化日志
    if (1 == m_log_write) //日志可写
    Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 800);
    else //日志不可写,最后的0表示日志的条目
    Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 0);
    }
    }

    sql_pool()

    数据库初始化函数 

    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);
    }

    thread_pool()

    线程池初始化函数 

    void WebServer::thread_pool()
    {
    //初始化线程池
    m_pool = new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num);
    }

    eventLoop()

    Server的逻辑中心,loop函数,处理

    • 新客户端连接
    • 客户端连接的关闭或错误
    • 信号事件
    • 客户端数据的读取和响应数据的写入
    • 定时器任务

    重点

    因为webserver.cpp在构建的时候涉及函数间使用依赖的问题,eventLoop写在源代码的最下面,而笔者把本函数调度到最上面讲解是以为loop涉及大体流程和其他函数,在看loop函数的同时找到对应的使用函数能更好理清思路理解代码。 

    void WebServer::eventLoop()
    {
    bool timeout = false;
    bool stop_server = false;//标记是否有定时器超时事件和服务器是否停止运行

    while (!stop_server)//循环处理事件
    {
    //阻塞模式,等待事件发生(io复用)
    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;

    //处理新到的客户连接
    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);
    }
    //处理信号,在listen中创建了pipe管道并挂上监听,这里是监听到管道可读数据时的处理
    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);
    }
    }
    if (timeout)//计时器关闭
    {
    utils.timer_handler();

    LOG_INFO("%s", "timer tick");日志记录

    timeout = false;
    }
    }
    }

    eventListen()

    网络编程步骤,设立监听器,socket listen,判断是否优雅关闭连接,设置 setsockopt

    优雅关闭选项:setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));

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

    • sockfd:要设置选项的套接字的文件描述符。
    • level:选项所在的协议层,通常是 SOL_SOCKET(套接字层)或协议层(如 IPPROTO_TCP)。
    • optname:要设置的选项的名称,例如 SO_REUSEADDR,SO_LINGER,SO_RCVBUF 等。
    • optval:指向包含选项值的缓冲区的指针。
    • optlen:选项值的大小。

    SO_LINGER:设置套接字关闭时的延迟时间。

    SO_LINGER 选项定义了在套接字被关闭时,应该如何处理剩余数据。其结构体定义如下:

    struct linger { int l_onoff; // 是否启用延迟关闭(0为不启用,1为启用)

            int l_linger; // 延迟关闭的时间(单位为秒) };

    当 l_onoff 设置为 1 时,表示启用延迟关闭,l_linger 设置为指定的时间长度(单位是秒)。在此期间,套接字不会立即关闭,而是等待所有未发送完的数据发送完毕后再关闭连接。

    当 l_onoff 设置为 0 时,表示禁用延迟关闭。此时,调用 close() 函数会立即关闭套接字,并且任何未发送的数据将会被丢弃。

    利用setsockopt完成优雅关闭操作

    注:uilts类为timer文件夹lst_timer.cpp中定义的类文件,这是一个封装的工具函数,webserver.cpp和其他文件中会大量用到,看到时可以去lst_timer.cpp查阅具体函数定义

    void WebServer::eventListen()
    {
    //网络编程基础步骤
    m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
    assert(m_listenfd >= 0);//断言设置,如果失败退出

    //优雅关闭连接
    if (0 == m_OPT_LINGER)
    {
    struct linger tmp = {0, 1};
    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;
    struct sockaddr_in address;
    bzero(&address, sizeof(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));
    //设置端口复用
    //SO_REUSEADDR:允许重用本地地址。常用于允许多个服务器进程绑定到同一端口,或者在程序异常退出 后快速绑定到同一端口。
    ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
    assert(ret >= 0);
    ret = listen(m_listenfd, 5);
    assert(ret >= 0);
    //初始化定时器
    utils.init(TIMESLOT);

    //epoll创建内核事件表
    epoll_event events[MAX_EVENT_NUMBER];//创建epoll数组
    m_epollfd = epoll_create(5);//创建epoll实例用于监听
    assert(m_epollfd != -1);

    //将m_listenfd挂在监听m_epollfd的epoll实例中,m_LISTENTrigmode为触发模式
    utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);
    //m_epollfd赋值给http请求类
    http_conn::m_epollfd = m_epollfd;

    //创建了管道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);
    //忽略 SIGPIPE 信号,有助于避免进程因管道错误而意外退出
    utils.addsig(SIGPIPE, SIG_IGN);
    //设置 SIGALRM 信号(定时器信号),定时器到期时发送
    utils.addsig(SIGALRM, utils.sig_handler, false);
    //设置 SIGTERM 信号(终止信号),外部请求终止程序时发出
    utils.addsig(SIGTERM, utils.sig_handler, false);
    //信号设定后触发调用handler函数,send进管道的写端
    //定时器设置
    alarm(TIMESLOT);

    //工具类,信号和描述符基础操作,存储全局资源
    Utils::u_pipefd = m_pipefd;
    Utils::u_epollfd = m_epollfd;
    }

    timer() 

    为fd设置计时器,计时器函数都在timer目录的lst_ timer下,相关函数定义可以跳转翻看

    //为新连接设置计时器
    void WebServer::timer(int connfd, struct sockaddr_in client_address)
    {
    //users是webserver.h里http_conn *users;初始化http请求数据
    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];
    timer->cb_func = cb_func;//回调函数,处理函数
    time_t cur = time(NULL);//获取当前的时间戳
    timer->expire = cur + 3 * TIMESLOT;//设置定时器的到期时间
    users_timer[connfd].timer = timer;//定时器关联
    utils.m_timer_lst.add_timer(timer);//定时器挂载列表
    }

    adjust_timer()

    调整计时器,延长连接时间 

    //若有数据传输,则将定时器往后延迟3个单位
    //并对新的定时器在链表上的位置进行调整
    void WebServer::adjust_timer(util_timer *timer)
    {
    time_t cur = time(NULL);
    timer->expire = cur + 3 * TIMESLOT;
    utils.m_timer_lst.adjust_timer(timer);
    //日志记录调整
    LOG_INFO("%s", "adjust timer once");
    }

     deal_timer()

    关闭计时器

    //关闭计时器
    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);
    }

    dealclientdata()

    处理连接事件,根据不同的触发模式,accept创建新的套接字处理后续操作

    //处理连接事件
    bool WebServer::dealclientdata()
    {
    struct sockaddr_in client_address;
    socklen_t client_addrlength = sizeof(client_address);
    if (0 == m_LISTENTrigmode) //如果设置LT触发模式
    {
    int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
    if (connfd < 0)
    {
    LOG_ERROR("%s:errno is:%d", "accept error", errno);//日志
    return false;
    }
    if (http_conn::m_user_count >= 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)
    {
    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;
    }

    dealwithsignal()

    处理信号 

    //处理信号,判断条件:是否停止服务器,是否计时器结束,否->处理
    bool WebServer::dealwithsignal(bool &timeout, bool &stop_server)
    {
    int ret = 0;
    int sig;
    char signals[1024];
    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;//信号处理完毕
    }

    dealwithread()

    读事件处理函数,根据设定模式进行不同操作,逻辑相同,都是把读事件加入请求处理队列 

    //读事件处理函数
    void WebServer::dealwithread(int sockfd)
    {
    //每个客户端都有一个计时器,确保连接超时进行释放资源操作
    util_timer *timer = users_timer[sockfd].timer;

    //reactor 如果设定为reactor模式下
    if (1 == m_actormodel)
    {
    if (timer)
    {
    adjust_timer(timer);//调整timer,以便客户端在有活动期间不断开连接
    }

    //若监测到读事件,将该事件放入请求队列
    m_pool->append(users + sockfd, 0);

    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].read_once())//服务器主动从客户端读取数据并进行处理
    {
    LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));//打印日志,记录客户端IP

    //若监测到读事件,将该事件放入请求队列
    m_pool->append_p(users + sockfd);

    if (timer)
    {
    adjust_timer(timer);//延迟连接时间
    }
    }
    else
    {
    deal_timer(timer, sockfd);//处理失败,关闭连接
    }
    }
    }

    dealwithwrite()

    写事件处理,和上述读事件处理相同 

    //处理写事件,和读事件代码逻辑相同,先判断何种触发模式,然后交给处理队列处理,关闭或者延长计时器
    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())
    {
    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);
    }
    }
    }

    总结webserver:

    在看完webserver的代码后,我们知道了服务器的大体模型框架为:

    先初始化日志,数据库,线程池,触发模式,然后eventlisten创建了socket套接字,进行服务器的常规化构建操作,通过socket,bind,listen。

    然后setsocketopt设置是否优雅关闭连接,接着epoll创建内核事件表,将m_listenfd挂上监听树,与此同时,创建m_pipe管道,将读端一起挂上监听树,信号传输就是在管道间进行的,然后设置事件信号 ,如:计时器结束信号,进程结束信号,并忽略pipe间通信的常见错误信号以免干扰接受信号的准确度。

    接着,在服务器接收到客户端发来的请求时,loop判断处理请求为请求连接,调用dealclient函数处理,创建clientfd建立客户端连接,然后dealclient函数调用timer函数设立定时器,timer函数中又将client数据给http_conn初始化,挂上监听树。

    loop循环到pipe上的信号时进行信号处理,收到服务端关闭连接时,关闭计时器。

    loop循环到来自客户端的请求时,判断事件为读或写事件,进行dealwithread/dealwithwrite处理,根据不同的设定模式进行reactor/proactor处理,将事件防放入请求队列中,让线程池进行处理。

    最后如计时器结束调用utils.timer_handler处理计时器,while循环由服务器关闭连接,stop_server=true时跳出,服务器停止运行。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » TingWebServer服务器代码解读01
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!