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

  • Post author:
  • Post category:其他




服务器介绍

本项目大部分参考社长的

TinyWebServer

。首先认识一下什么是服务器。

服务器就是一个

服务器软件

(程序),其主要功能是通过

HTTP协议

与客户端(通常是浏览器browser)进行通信,并对其请求做出HTTP响应,返回客户端所请求的内容(文件,网页等)。

通常用户使用Web浏览器与相应服务器进行通信。在浏览器中键入

“域名”



“IP地址:端口号”

,浏览器则先将你的域名解析成相应的IP地址或者直接根据你的IP地址向对应的Web服务器发送一个

HTTP请求

。这一过程首先要通过TCP协议的三次握手建立与目标Web服务器的连接,然后HTTP协议生成针对目标Web服务器的

HTTP请求报文

,通过TCP、IP等协议发送到目标Web服务器上。



服务器端整体运行流程



webserver的初始化

首先我们在服务器端通过

mysql

建立一个名叫“yourdb”的数据库,登录数据库的用户名和密码默认为

root,123456

。并且在“yourdb”中建立一张包含

user



password

两个字段的名叫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()**内部实现可以看出两种并发模式(

reactor



proactor

)的区别。简单来说:

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上写入服务器处理客户请求的结果。



版权声明:本文为qq_41561980原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。