Web服务器---TinyWebServer代码详细讲解(main与WebServer)

这里的参照的代码是https://github.com/qinguoyi/TinyWebServer

  • 对于原代码的不足之处,我会在之后的文章中给出改进代码
  • 在笔者fork的这版中,原代码作者对于代码作出了更细化的分类

细节问题可以参考《APUE》《Linux高性能服务器编程》或者我之前的博客

阅读任何源码一定要先从readme入手,如果没有readme,请从main入口入手。

config 独立参数模块

首先映入眼帘的是一个config的头文件,根据标识可以知道这是一个用户自定义头,所以我们先跳进去看看有什么东西。
我在每个每个条目中都给出了注释。

这里体现出整个项目的优点:模式切换
也就是说这是一个复合的ET/LT。作者在Listenfd上和cfd上都建立了两种模式,意味着我们有四种组合方式。

ET与LT模式

lfd的ET代表着一次性接受所有连接,笔者认为这里是考虑到网络接入量巨大,瞬间占到Max_size的情况。LT代表一次取一个,当然这是默认的方式也是最常见的方式。

cfd的两种方式也就是对应了epoll的方式,默认的LT和手动的ET

config.h代码解读

#ifndef CONFIG_H
#define CONFIG_H

#include "webserver.h" //懒得引用一堆头文件了

using namespace std;

class Config
{
   
public:
    Config();
    ~Config(){
   };

    void parse_arg(int argc, char*argv[]);

    //端口号
    int PORT;

    //日志写入方式
    int LOGWrite;

    //触发组合模式
    int TRIGMode;

    //listenfd触发模式
    int LISTENTrigmode;

    //connfd触发模式
    int CONNTrigmode;

    //优雅关闭链接
    int OPT_LINGER;

    //数据库连接池数量
    int sql_num;

    //线程池内的线程数量
    int thread_num;

    //是否关闭日志
    int close_log;

    //并发模型选择
    int actor_model;
};

#endif

config.cpp代码解读

其实很简单,在构造函数里作出了对于各种初始模式的设定
并且在这版代码中,对于并发模式的处理,作者给出了reactor和preactor两种方式。(后面会详细讲解)
原作者的测试环境为4核8线,所以这里给出了池内线程为8
TRIGMode默认为最低效模式,可以改为1,实现服务器的最高性能,大概实现10wQPS

#include "config.h"

Config::Config(){
   
    //端口号,默认9006
    PORT = 9006;

    //日志写入方式,默认同步
    LOGWrite = 0;

    //触发组合模式,默认listenfd LT + connfd LT
    TRIGMode = 0;

    //listenfd触发模式,默认LT
    LISTENTrigmode = 0;

    //connfd触发模式,默认LT
    CONNTrigmode = 0;

    //优雅关闭链接,默认不使用
    OPT_LINGER = 0;

    //数据库连接池数量,默认8
    sql_num = 8;

    //线程池内的线程数量,默认8
    thread_num = 8;

    //关闭日志,默认不关闭
    close_log = 0;

    //并发模型,默认是proactor
    actor_model = 0;
}

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;
        }
        case 'l':
        {
   
            LOGWrite = atoi(optarg);
            break;
        }
        case 'm':
        {
   
            TRIGMode = atoi(optarg);
            break;
        }
        case 'o':
        {
   
            OPT_LINGER = atoi(optarg);
            break;
        }
        case 's':
        {
   
            sql_num = atoi(optarg);
            break;
        }
        case 't':
        {
   
            thread_num = atoi(optarg);
            break;
        }
        case 'c':
        {
   
            close_log = atoi(optarg);
            break;
        }
        case 'a':
        {
   
            actor_model = atoi(optarg);
            break;
        }
        default:
            break;
        }
    }
}

总结: 简单的初始化形式的分割,改动参数的时候我只需要改动config.cpp就行了。

main 模块

main模块的主要功能是,驱动Sever。
WebServer被单独作为一个类实现,并且封装好了调度函数。
也就是说,main函数相当于一个开关,我打开了服务器的开关让他进入了listen状态,同时也转身打开了数据库的开关。
当然,这个开关的信息,来自于config

main.cpp代码解读

这里的命令行解析是给数据库的运行方式传参,当然你可以什么都不传。
定义,之后初始化了一个服务器对象。
首先打开线程池,然后设置运行模式,之后就是启动监听和进入工作循环(事务循环)

#include "config.h"

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

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

总结: 其实这一版的好处就是,给main瘦身。main本质上就是提供了入口,入口不需要太复杂。

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

在仔细阅读这种复杂功能的模块之前,一定要理解清除调用逻辑。
首先看main中分别调用了eventListen与eventLoop。根据见名知意的原则,我们可以推测出这是实现了listen部分与事务处理部分。

eventListen函数

如果你是初学者,不需要关注什么叫优雅的关闭连接这一部分,具体可以参考我的文章:Linux网络编程:知识点补充
简单的民工三连调用不需要解释,作者加入了assert,提升了健壮性。
之后就是调用epoll的三连了,将lfd上树,这里的上树封装为了addfd目的是为了可以更改模式。(cfd需要one_shot而lfd不需要)

之后就是创建了管道,这里牵扯到进程间通信的问题。这么做的好处就是统一事件源。因为正常情况下,信号处理与IO处理不走一条路。
这里的信号主要是超时问题
具体的做法是,信号处理函数使用管道将信号传递给主循环,信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出信号值,使用I/O复用系统调用来监听管道读端的可读事件,这样信号事件与其他文件描述符都可以通过epoll来监测,从而实现统一处理。

void WebServer::eventListen()
{
   
    //网络编程基础步骤
    m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
    //如果它的条件返回错误,则终止程序执行
    assert(m_listenfd >= 0);

    //TCP连接断开的时候调用closesocket函数,有优雅的断开和强制断开两种方式
    
    //优雅关闭连接
    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)
    {
   
        //这种方式下,在调用closesocket的时候不会立刻返回,内核会延迟一段时间,这个时间就由l_linger得值来决定。
        //如果超时时间到达之前,发送完未发送的数据(包括FIN包)并得到另一端的确认,closesocket会返回正确,socket描述符优雅性退出。
        //否则,closesocket会直接返回 错误值,未发送数据丢失,socket描述符被强制性退出。需要注意的时,如果socket描述符被设置为非堵塞型,则closesocket会直接返回值。
        struct linger tmp = {
   1, 1};
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }

    int ret = 0;
    struct sockaddr_in address;
    // bzero() 会将内存块(字符串)的前n个字节清零;
    // s为内存(字符串)指针,n 为需要清零的字节数。
    // 在网络编程中会经常用到。
    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));
    //传统绑定步骤
    ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address))
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值