C++Web服务器(一):服务器整体运行流程

服务器介绍

本项目大部分参考社长的TinyWebServer。首先认识一下什么是服务器。

服务器就是一个服务器软件(程序),其主要功能是通过HTTP协议与客户端(通常是浏览器browser)进行通信,并对其请求做出HTTP响应,返回客户端所请求的内容(文件,网页等)。

通常用户使用Web浏览器与相应服务器进行通信。在浏览器中键入 “域名”“IP地址:端口号”,浏览器则先将你的域名解析成相应的IP地址或者直接根据你的IP地址向对应的Web服务器发送一个HTTP请求。这一过程首先要通过TCP协议的三次握手建立与目标Web服务器的连接,然后HTTP协议生成针对目标Web服务器的HTTP请求报文,通过TCP、IP等协议发送到目标Web服务器上。

服务器端整体运行流程

webserver的初始化

首先我们在服务器端通过mysql建立一个名叫“yourdb”的数据库,登录数据库的用户名和密码默认为root,123456。并且在“yourdb”中建立一张包含userpassword两个字段的名叫user的表。

当我们在命令行运行服务器程序,我们可以自定义设置端口号,线程池数量,日志写入方式,反应堆模型等等。
因为我们程序将运行config.parse_arg()进行命令行解析,获取我们的自定义设置,当然也可以使用默认设置。

void Config::parse_arg(int argc, char*argv[]){
    int opt;
    //getopt()方法是用来分析命令行参数
    //argc:通常由 main 函数直接传入,表示参数的数量
    //argv:通常也由 main 函数直接传入,表示参数的字符串变量数组
    //*str用于参数的解析。例如 “abc:”,其中 -a,-b 就表示两个普通选项,
    //-c 表示一个必须有参数的选项,因为它后面有一个冒号。
    //全局变量optarg:如果某个选项有参数,这包含当前选项的参数字符串
    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;
        }
    }
}

接着调用默认构造函数初始化webserver对象,即创建HTTP对象数组和资源文件夹路径初始化。最后就可以运行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;
    //关闭日志,默认0不关闭
    m_close_log = close_log;
    //初始化并发模式,默认proactor
    m_actormodel = actor_model;
}

日志、数据库、线程池和触发模式

日志和数据库均只有一个实例对象,均通过局部静态成员变量的懒汉模式实现单例模式。优点:延迟实例化,节约资源;线程安全;性能提高;不存在内存泄漏。

日志的初始化需要设置日志文件名,日志缓冲区大小,日志最大行数。如果是异步方式写入日志,我们还需要设置阻塞队列的大小,本项目借鉴生产者消费者模型,采用循环数组结构实现阻塞队列。

为什么要创建连接池?

从一般流程中可以看出,若系统需要频繁访问数据库,则需要频繁创建和断开数据库连接,而创建数据库连接是一个很耗时的操作,也容易对数据库造成安全隐患。

在程序初始化的时候,集中创建多个数据库连接,并把他们集中管理,供程序使用,可以保证较快的数据库读写速度,更加安全可靠。将数据库连接的获取与释放通过RAII机制封装(也称为**“资源获取就是初始化”**,是c++等编程语言常用的管理资源、避免内存泄露的方法),避免手动释放。

数据库连接池内部通过链表list存储mysql连接。并且使用信号量机制进行同步。

整个项目采用半同步\半反应堆的并发模式工作,线程池的内部构造类似数据库连接池,同样采样链表将HTTP对象存储起来形成请求队列,同时使用信号量机制和互斥锁进行同步。初始化线程池需要设置线程池数量(默认为8)、并发模式和能接受的最大请求队列长度

需要设置用于监听的套接字(listenfd)和用于通信的套接字(connfd)的触发模式,默认是同步I/O模拟的proactor模式。

主线程监听连接

使用socket()创建一个用于监听连接的套接字,并绑定默认的网卡和默认的端口(9006),我设置了端口复用,能复用处于TIME_WAIT的socket。

创建epoll实例,将listenfd加入epoll树并注册其读事件。

创建一对套接字进行管道通信,管道写端将定时信号发送给管道读端。管道读端的可读事件加入epoll实例中,即统一事件源

统一事件源,是指将信号事件与其他事件一样被处理。

最后设置时钟信号终止信号的的处理动作(即管道写端将信号发送给管道读端),至此就完成了准备工作。

主线程处理监控文件描述符上的事件

void WebServer::eventLoop()
{
    bool timeout = false;
    bool stop_server = false;

    while (!stop_server)
    {
        //等待所监控文件描述符上有事件的产生
        //检测发生事件的文件描述符
        int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
        if (number < 0 && errno != EINTR) //被中断的系统调用返回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 = dealclinetdata();
                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);
            }
        }
        if (timeout)
        {
        	//处理定时任务,并重新定时以不断触发SIGALRM信号
            utils.timer_handler();

            LOG_INFO("%s", "timer tick");

            timeout = false;
        }
    }
}

从以上**dealwithread()dealwithwrite()**内部实现可以看出两种并发模式( reactorproactor )的区别。简单来说:

reactor模式中,主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话立即通知工作线程(逻辑单元 ),读写数据、接受新连接及处理客户请求均在工作线程中完成。通常由同步I/O实现。

proactor模式中,主线程和内核负责处理读写数据、接受新连接等I/O操作,工作线程仅负责业务逻辑,如处理客户请求。

同步I/O模拟proactor模式的工作流程如下(epoll_wait为例):

  1. 主线程往epoll内核事件表注册socket上的读就绪事件。
  2. 主线程调用epoll_wait等待socket上有数据可读。
  3. 当socket上有数据可读,epoll_wait通知主线程,主线程从socket循环读取数据,直到没有更多数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列
  4. 睡眠在请求队列上某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件。
  5. 主线程调用epoll_wait等待socket可写。
  6. 当socket上有数据可写,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果。
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值