0%

tinywebserver-代码详解

对tinywebserver总逻辑和各个模块的详细解释。

一些概念

触发模式

  • LT是指电平触发(level trigger),当IO事件就绪时,内核会一直通知,直到该IO事件被处理;
  • ET是指边沿触发(Edge trigger),当IO事件就绪时,内核只会通知一次,如果在这次没有及时处理,该IO事件就丢失了。

IO复用

  • 在单个进程中通过记录跟踪每一个Socket(I/O流)的状态来同时管理多个I/O流,但它本身是阻塞的。

  • 本项目是利用epoll IO复用技术实现对监听socket(listenfd)和连接socket(客户请求连接之后的socket)的同时监听。

  • why epoll in select/poll/epoll:

    • select和poll的最大开销来自内核判断是否有文件描述符就绪这一过程:每次执行select或poll调用时,它们会采用遍历的方式,遍历整个文件描述符集合去判断各个文件描述符是否有活动;epoll则不需要去以这种方式检查,当有活动产生时,会自动触发epoll回调函数通知epoll文件描述符,然后内核将这些就绪的文件描述符放到之前提到的ready list中等待epoll_wait调用后被处理。
    • 当监测的fd数量较小,且各个fd都很活跃的情况下,建议使用select和poll;当监听的fd数量较多,且单位时间仅部分fd活跃的情况下,使用epoll会明显提升性能

事件处理模式

  • Reactor模式:要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生(可读、可写),若有,则立即通知工作线程,将socket可读可写事件放入请求队列,读写数据、接受新连接及处理客户请求均在工作线程中完成。(需要区别读和写事件)
  • Proactor模式:主线程和内核负责处理读写数据、接受新连接等I/O操作,工作线程仅负责业务逻辑(给予相应的返回url),如处理客户请求。
  • 本项目中使用同步IO模拟proactor模式——线程池部分

main+WebServer

常量

  • MAX_FD:最大文件描述符(?最大http请求数量)

  • MAX_EVENT_NUMBER 最大事件数(最大操作请求数量?)

  • TIMESLOT:最小超时单位

  • TRIGMode:触发组合模式,默认listenfd LT + connfd LT

    • LISTENTrigmode:listenfd触发模式,默认LT
    • CONNTrigmode:connfd触发模式,默认LT
  • SOL_SOCKET 指定设置SOCKET的选项

    • SO_LINGER 指定close函数对 面向连接的协议(如TCP)如何操作
    • SO_REUSEADDR重用处于TIME_WAIT的socket

变量

  • users:
1
http_conn * users = new http_conn[MAX_FD];

该连接最多执行MAX_FD个http请求

  • linger:
1
struct linger tmp = {0, 1}|{1,1};

socket强制退出|优雅断开

  • address:
1
struct sockaddr_in address;

[socket编程——sockaddr_in结构体操作 - 周人假的 - 博客园 (cnblogs.com)](https://www.cnblogs.com/zhouhbing/p/3844484.html#:~:text=sockaddr_in 结构体:struct,sockaddr_in中的in 表示internet,就是网络地址,这只是我们比较常用的地址结构,属于AF_INET地址族,他非常的常用) 给服务器的连接绑定地址和端口

  • events:
1
epoll_event events[MAX_EVENT_NUMBER];

epoll创建内核事件表 struct epoll_event轻飘风扬的博客-CSDN博客epoll_event结构体

  • m_epollfd:
1
m_epollfd = epoll_create(5);

创建一个epoll专用的socket

  • sigaction:
1
struct sigaction sa;

用来描述对信号的处理 linux中sigaction函数详解魏波-的博客-CSDN博客sigaction

  • **client_data *users_timer;**sockaddr_in+uti_timer

函数

  • WebServer()构造函数

  • users(http_conn类):数组,定义了http连接的最大连接数

  • m_root(WebServer类):html的路径

    • users_timer(client_data类):给每个用户连接分配一个定时器
    • sql_pool()初始化mysql连接,获得
    • m_connPool(connection_pool类)连接池中的一条连接
1
users->initmysql_result(m_connPool);
    • 通过读取user表测试连接
    • thread_pool()
1
m_pool = new threadpool<http_conn>()

-

  • **eventListen()**对服务端socket的一些预处理

  • socket():

1
m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
      • 设置socket的断开方式,根据linger tmp

        1
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    • 设置重用socket

      1
      setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
  • bind():

    • 绑定监听socket

      1
      ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
  • listen():

1
ret = listen(m_listenfd, 5);

server开始监听指定socket的活动,设置最大排队数为5

    • utils.init(TIMESLOT):定时器设置最小超时单位

    • utils.addfd()

      • **epoll_ctl()**用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。addfd中是为该socket设置注册事件

        • EPOLL_CTL_ADD注册、
        • EPOLL_CTL_MOD修改、
        • EPOLL_CTL_DEL删除;
      • **setnonblocking()**设置socket为非阻塞

    • **socketpair()**建立一对匿名的已经连接的套接字????暂时不知道有什么用

      • m_pipefd[2]用来存这一对socket,为m_pipefd[0]添加注册事件,设置m_pipefd[1]为非阻塞
    • **utils.addsig()**添加信号处理

  • sig_handler()信号处理函数

    • **alarm(TIMESLOT)**设置每隔TIMESLOT的时间就触发SIGALRM信号的函数
  • **dealclinetdata()**处理来自客户端的tcp请求(暂且只看默认模式,即0 == m_LISTENTrigmode)

  • accept():

1
connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);

接收客户端的请求,返回一个生成的新的socket(注意listenfd和connfd都是int类型,表示pid???

    • timer()

      • **users[connfd].init()**初始化一个http_conn连接
      • timer(util_timer类),user_data(client_data类)
      • 创建定时器,设置回调函数和超时时间,绑定用户数据,将定时器添加到链表
  • **dealwithsignal()**处理信号

    • SIGALRM 超时
    • SIGTERM停止server(stop_server = true)
  • dealwithread()处理线程(暂时只看默认模式,即actor_model = 0;并发模型,默认是proactor)

    • http_conn->read_once()
    • threadpool->append_p()
  • dealwithwrite()处理线程(暂时只看默认模式,即actor_model = 0;并发模型,默认是proactor)

  • http_conn->write()

    • adjust_timer
  • **eventLoop()**正式开始

  • epoll_wait():

1
number=epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);

轮询I/O事件的发生;

      • 返回发生的事件数

流程图

main.cpp:

img

webserver::eventListen

webserver-listen

webserver::eventLoop

img

CGImysql

模块逻辑:单例模式&RAII机制

img

变量

  • int m_MaxConn 最大连接数
  • int m_CurConn 当前已使用的连接数
  • int m_FreeConn 当前空闲的连接数
  • string m_url 主机地址
  • string m_Port 数据库端口号
  • string m_User 登陆数据库用户名
  • string m_DatabaseName 使用数据库名
  • string m_PassWord 登陆数据库密码
  • int m_close_log 日志开关

函数

  • connection::connection_pool(),建立数据库连接池

    • 初始化未建立连接机当前已使用的连接数和空闲的连接数均为0

      1
      2
      3
      m_CurConn = 0;

      m_FreeConn = 0;
    • 使用以空间换时间的思想,使用连接池的模式初始化多个mysql连接

  • connection_pool *connection_pool::GetInstance(),建立连接池

    • 生成数据库连接池,将数据库连接池放入静态栈总

    • 单例模式

      采用单例懒汉模式创建连接池,使用信号量表示空闲连接数,管理连接出池入池,并且在入池出池操作前加锁,避免操作冲突。

  • void connection_pool::init,构造初始化

    • 初始化连接池,将空闲的连接数m_FreeConn设为初始化获取最大连接数MaxConn

    • 使用信号量sem同步空闲连接数

      1
      reserve = sem(m_FreeConn);
  • MYSQL *connection_pool::GetConnection(),请求建立一个可用连接

    等待空闲信号,上锁,front引用当前列表(先进先出)的第一个元素,……

    1
    2
    3
    4
    5
    6
    7
    reserve.wait(); 
    lock.lock();
    con = connList.front();
    connList.pop_front();
    --m_FreeConn;
    ++m_CurConn;
    lock.unlock();
    补充:为什么要使用互斥锁锁定列表?

    如果多个线程访问单个容器,并且至少有一个线程可能写入,则用户负责确保在容器访问期间线程之间的互斥。

    (可能两个线程同时访问,事务错误……)

  • bool connection_pool::ReleaseConnection(MYSQL *con),释放当前使用的连接

    上锁,push_back将元素添加到容器末尾,(将连接放回连接池)……解锁,post发送sem信号

    1
    2
    3
    4
    5
    6
    lock.lock();
    connList.push_back(con);
    ++m_FreeConn;
    --m_CurConn;
    lock.unlock();
    reserve.post();
  • void connection_pool::DestroyPool(),销毁数据库连接池

    连接池数据置0,clear清除连接池。

  • int connection_pool::GetFreeConn(),当前空闲的连接数

  • connectionRAII::connectionRAII(MYSQL *SQL, connection_pool connPool),数据库连接封装

    • 在RAII的构造函数中获取连接池中的连接,并将外部MYSQL连接指向该连接
    1
    2
    3
    SQL = connPool->GetConnection();
    conRAII = *SQL;
    poolRAII = connPool;
  • connectionRAII::~connectionRAII()

    在该类的析构函数中释放该连接,并将其入池。

    1
    poolRAII->ReleaseConnection(conRAII);

使用RAII机制,将外部获取连接封装到类connectionRAII中,这样使用,当该对象声明结束时,编译器会自动调用其析构函数,回收该连接,避免手动释放

timer

网络程序通常需要处理定时事件,例如定期检测客户连接的活动状态,因为非活跃连接占用了连接资源,需要定期检测释放非活跃连接。通常将定时事件封装为定时器类,然后使用排序链表、时间轮等数据结构管理定时器。

这里使用升序链表实现定时器

变量

  • struct client_data,连接的相关数据

    传递到超时处理函数的参数,主要在多个定时器同时使用时,区别是哪个timer超时。

    1
    2
    3
    sockaddr_in address; //连接地址
    int sockfd; //文件描述符
    util_timer *timer; //指向连接对应的定时器
  • time_t expire 超时时间

  • void ( cb_func*)(*client_data );

    回调函数,定时器超时处理函数

  • util_timer *prev 前继定时器

  • util_timer *next 后继定时器

  • util_timer *head;

    util_timer *tail; 定时器列表容器的头尾结点

  • static int *u_pipefd 管道

  • sort_timer_lst m_timer_lst

  • static int u_epollfd;

  • int m_TIMESLOT 闹钟设置的时间

函数

  • sort_timer_lst::sort_timer_lst(),定时器容器类的构造函数

    1
    2
    head = NULL;
    tail = NULL;
  • 添加计时器

    • void sort_timer_lst::add_timer(util_timer *timer)

      • 定时器按照expire(定时器定时的滴答数)从小到大排序
      • 如果新的定时器超时时间小于当前头部结点,直接将当前定时器结点作为头部节点
        1
        2
        3
        4
        5
        6
        7
        8
        if (timer->expire < head->expire)
        {
        timer->next = head;
        head->prev = timer;
        head = timer;
        return;
        }
        add_timer(timer, head);//对于非头结点的计时器

      对于不是头结点的定时器,使用c++的优先队列实现定时器,即用时间堆的方式实现,如下

    • void sort_timer_lst::add_timer(util_timer *timer, util_timer *lst_head)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    if (timer->expire < tmp->expire)  //tmp是头结点的下一结点     
    {
    prev->next = timer;
    timer->next = tmp;
    tmp->prev = timer;
    timer->prev = prev;
    break;
    }
    prev = tmp;
    tmp = tmp->next;

    如果遍历完tmp==NULL(!tmp)即定时器的超时时间比最后一个结点还要大,则将要加入的定时器放到尾结点处

  • void sort_timer_lst::adjust_timer(util_timer *timer),调整定时器

    任务发生变化时,调整定时器在链表中的位置

    • 被调整的定时器在链表尾部或者定时器超时值仍小于下一定时器的超时值时,不调整
    1
    2
    3
    4
    5
    util_timer *tmp = timer->next;
    if (!tmp || (timer->expire < tmp->expire))
    {
    return;
    }
    • 被调整定时器是链表头结点,将定时器取出,重新插入

      1
      2
      3
      4
      5
      6
      7
      if (timer == head)
      {
      head = head->next;
      head->prev = NULL;
      timer->next = NULL;
      add_timer(timer, head);
      }
    • 被调整定时器在内部,将定时器取出,重新插入

      1
      2
      3
      4
      5
      6
      else
      {
      timer->prev->next = timer->next;
      timer->next->prev = timer->prev;
      add_timer(timer, timer->next);
      }
  • void sort_timer_lst::del_timer(util_timer *timer),删除定时器

    • 结点位置不同,指针的变化不同

      • 链表中只有一个定时器,需要删除该定时器

        1
        2
        3
        4
        5
        6
        7
        if ((timer == head) && (timer == tail))
        {
        delete timer;
        head = NULL;
        tail = NULL;
        return;
        }
      • 被删除的定时器为头结点

        1
        2
        head = head->next;
        head->prev = NULL;
      • 被删除的定时器为尾结点

        1
        2
        tail = tail->prev;
        tail->next = NULL;
      • 被删除的定时器在链表内部,常规链表结点删除

        1
        2
        timer->prev->next = timer->next;
        timer->next->prev = timer->prev;
  • void sort_timer_lst::tick(),定时任务处理

    • 获取当前时间time_t cur=time(NULL),遍历定时器链表,对应不同的处理方法

    • 当前时间小于定时器时间,后面的定时器也没有到期,直接break,等待时间到了再执行?

    • 当前定时器到期,则调用回调函数,执行定时事件,将处理后的定时器从链表容器中删除,并重置头结点

  • void cb_func(client_data *user_data),定时器回调

    • 删除非活动连接在socket上的注册事件,关闭文件描述符

      epoll_ctl -> close(user_data->sockfd)

    • 减少连接数

      http_conn::m_user_count–;

  • void Utils::init(int timeslot),定时

  • int Utils::setnonblocking(int fd),对文件描述符设置非阻塞

    要先把fd对应的标志flags读出来,然后加上非阻塞标志O_NONBLOCK,再设置回去

    1
    2
    3
    4
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
    补充介绍fcntl

    fcntl()函数,是Linux用来管理文件描述符的API。

    读取fd的标志用F_GETFL,设置fd的标志用F_SETFL。

    (添加标志用或运算,清除标志用按位取反之后的与运算)

  • void Utils::addfd(int epollfd, int fd, bool one_shot, int TRIGMode)

    将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT

  • void Utils::sig_handler(int sig),事件处理

    为保证函数的可重入性,保留原来的error

    将信号值从管道写端写入,使用send

  • send(u_pipefd[1], (char *)&msg, 1, 0);void Utils::addsig(int sig, void(handler)(int), bool restart),设置信号

    补充介绍 struct sigaction结构体 和 函数 memset、 sigfillset、sigaction
    1
    2
    3
    4
    5
    6
    7
    struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
    }
    • sa_handler代表新的信号处理函数,仅仅发送信号值,不做对应逻辑处理

    • sa_mask 用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置

    • sa_flags 用来设置信号处理的其他相关操作,下列的数值可用。

    • SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL

    • SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用

    • SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号

    • memset函数为初始化函数,可以将一段连续的内存初始化为某个值。但它是以字节为单位进行初始化的。

    • sigfillset函数将所有的信号都添加到信号集中。

    • sigaction函数的功能是检查或修改与指定信号相关联的处理动作(可同时两种操作)

  • void Utils::timer_handler()

    • 调用tick定时处理任务

    • 利用alarm函数重新定时以不断触发SIGALRM信号

      补充介绍alarm函数

      alarm函数是设置一个计时器, 在计时器超时的时候, 产生SIGALRM信号。

      它的主要功能是设置信号传送闹钟。其主要功能用来设置信号SIGALRM在经过seconds指定的秒数后传送给目前的进程,如果在定时未完成的时间内再次调用了alarm函数,则后一次定时器设置将覆盖前面的设置,当seconds设置为0时,定时器将被取消。它返回上次定时器剩余时间,如果是第一次设置则返回0。

使用方法

timer

  • 创建定时器时需要先定义users_timer = new client_data[MAX_FD]

  • 初始化定时器init_timer

  • 在超时处理函数结尾重新加载定时器时间adjust_timer

  • 如果自己编写的驱动中有中断,需要在中断入口处del_timer,并且在入口处重新重新加载定时器时间adjust_timer

http

img

步骤

● 连接处理:浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,等待工作线程从任务队列中取出一个任务进行处理。
● 处理报文请求:工作线程取出任务后,调用进程处理函数,通过主、从状态机对请求报文进行解析。
● 返回响应报文:解析完之后,生成响应报文,返回给浏览器端。

READ_BUFFER_SIZE: 读取缓冲区大小

WRITE_BUFFER_SIZE: 写入缓冲区大小

enum: enum是计算机编程语言中的一种数据类型。枚举类型:在实际问题中,有些变量的取值被限定在一个有限的范围内。例如,一个星期内只有七天,一年只有十二个月,一个班每周有六门课程等等。如果把这些量说明为整型,字符型或其它类型显然是不妥当的。为此,C语言提供了一种称为“枚举”的类型。在“枚举”类型的定义中列举出所有可能的取值,被说明为该“枚举”类型的变量取值不能超过定义的范围。应该说明的是,枚举类型是一种基本数据类型,而不是一种构造类型,因为它不能再分解为任何基本类型。

METHOD:枚举可能出现的连接的方法

CHECK_STATE: ?

img

HTTP_CODE: 判断请求是什么?

LIME_STATUS:线路状态:OK,BAD,OPEN

概念

EPOLL: 文件监听事件

epoll使用详解:epoll_create、epoll_ctl、epoll_wait、close - 雾穹 - 博客园 (cnblogs.com)

HTTP 状态信息定义

const char *ok_200_title = “OK”;

const char *error_400_title = “Bad Request”;

const char *error_400_form = “Your request has bad syntax or is inherently impossible to staisfy.\n”;

const char *error_403_title = “Forbidden”;

const char *error_403_form = “You do not have permission to get file form this server.\n”;

const char *error_404_title = “Not Found”;

const char *error_404_form = “The requested file was not found on this server.\n”;

const char *error_500_title = “Internal Error”;

const char *error_500_form = “There was an unusual problem serving the request file.\n”;

HTTP逻辑框架

img

变量

static const int FILENAME_LEN = 200;//设置读取文件的名称m_real_file大小

​ static const int READ_BUFFER_SIZE = 2048;//设置读缓冲区m_read_buf大小

​ static const int WRITE_BUFFER_SIZE = 1024; //设置写缓冲区m_write_buf大小

char m_read_buf[READ_BUFFER_SIZE];//存储读取的请求报文数据

​ int m_read_idx;//缓冲区中m_read_buf中数据的最后一个字节的下一个位置

​ int m_checked_idx;//m_read_buf读取的位置

​ int m_start_line;//m_read_buf中已经解析的字符个数

​ char m_write_buf[WRITE_BUFFER_SIZE];//存储发出的响应报文数据

​ int m_write_idx;//指示buffer中的长度

​ CHECK_STATE m_check_state; //主状态机状态

​ METHOD m_method;//请求的方法

​ char m_real_file[FILENAME_LEN]; //文件目录?

​ char *m_url; //报文的url

​ char *m_version; //报文的http协议类型

​ char *m_host; //报文的host

​ int m_content_length; //消息体长度

​ bool m_linger;

​ char *m_file_address; //读取服务器上的文件地址

​ struct iovec m_iv[2];//io向量机制iovec

​ int cgi; //是否启用的POST

​ char *m_string; //存储请求头数据

​ int bytes_to_send;//剩余发送字节数

​ int bytes_have_send;//已发送字节数

函数

initmysql_result()

void http_conn::initmysql_result(connection_pool *connPool)先从连接池取出一个连接

connectionRAII mysqlcon(&mysql, connPool);setnonblocking()

对文件描述符设置非堵塞

阻塞操作:

​ 是指在执行设备操作时,若不能获得资源则挂起进程,直到满足可操作的条件后进行操作,

​ 被挂起的进程进入睡眠状态,被从调度器的运行队列移走,直到等待的条件被满足.

非阻塞操作:

​ 进程不能进行设备操作时并不挂起,他或者放弃,或者不停的查询,直到可以进行操作为止.

addfd()

将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT

epoll_event event;

event.data.fd = fd;创造一个事件

if (1 == TRIGMode)

​ event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;

else

​ event.events = EPOLLIN | EPOLLRDHUP;

if (one_shot)

​ event.events |= EPOLLONESHOT;设置文件描述符

EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)

  • EPOLLOUT:表示对应的文件描述符可以写

  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)

  • EPOLLERR:表示对应的文件描述符发生错误

  • EPOLLHUP:表示对应的文件描述符被挂断;

  • EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的

  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

  • epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);

    setnonblocking(fd);

  • (注册新的fd到epfd),对文件描述符设置非堵塞态

removefd()

从内核时间表删除描述符

modfd()

将事件重置为EPOLLONESHOT

if (1 == TRIGMode)

​ event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;

else

event.events = ev | EPOLLONESHOT | EPOLLRDHUP;

http_conn::close_conn()

关闭连接,关闭一个连接,客户总量减一

http_conn::init(int sockfd, const sockaddr_in &addr, char *root, int TRIGMode,int close_log, string user, string passwd, string sqlname)

初始化连接,外部调用初始化套接字地址

http_conn::init()

初始化新接受的连接,check_state默认为分析请求行状态

http_conn::LINE_STATUS http_conn::parse_line()

从状态机,用于分析出一行内容,返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN

if (temp == ‘\r’)

​ {

​ if ((m_checked_idx + 1) == m_read_idx)

​ return LINE_OPEN;

如果当前是\r字符,则有可能会读取到完整行,下一个字符达到了buffer结尾,则接收不完整,需要继续接收

else if (m_read_buf[m_checked_idx + 1] == ‘\n’)

​ {

​ m_read_buf[m_checked_idx++] = ‘\0’;

​ m_read_buf[m_checked_idx++] = ‘\0’;

​ return LINE_OK;

}下一个字符是\n,将\r\n改为\0\0

都不符合则返回语法错误,LINE_BAD

else if (temp == ‘\n’)

​ {

​ //前一个字符是\r,则接收完整

​ if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == ‘\r’)

​ {

​ m_read_buf[m_checked_idx - 1] = ‘\0’;

​ m_read_buf[m_checked_idx++] = ‘\0’;

​ return LINE_OK;

​ }

​ return LINE_BAD;

​ }

}

//并没有找到\r\n,需要继续接收

return LINE_OPEN;如果当且字符是\n的处理方法

http_conn::read_once()

循环读取客户数据,直到无数据可读或对方关闭连接,非阻塞ET工作模式下,需要一次性将数据读完

http_conn::HTTP_CODE http_conn::parse_request_line(char *text)

解析http请求行,获得请求方法,目标url及http版本号

  • 主状态机的初始状态,调用parse_request_line函数解析请求行
  • 解析函数从m_read_buf中解析HTTP请求行,获得请求方法、目标URL及HTTP版本号
  • 解析完成后主状态机的状态变为CHECK_STATE_HEADER

m_url = strpbrk(text, “ \t”);请求行中最先含有空格和\t任一字符的位置并返回,如果没有目标字符则表示报文格式有问题,return BAD_REQUEST

*m_url++ = ‘\0’;用于将前面的数据取出

char *method = text;

if (strcasecmp(method, “GET”) == 0)

​ m_method = GET;

else if (strcasecmp(method, “POST”) == 0)

{

​ m_method = POST;

​ cgi = 1;

}

else

​ return BAD_REQUEST;

取出数据确定请求方式

m_url += strspn(m_url, “ \t”);m_url此时跳过了第一个空格或者\t字符,但是后面还可能存在 不断后移找到请求资源的第一个字符

if (strcasecmp(m_version, “HTTP/1.1”) != 0)

​ return BAD_REQUEST;目前仅支持http1.1

if (strncasecmp(m_url, “http://“, 7) == 0)

{

​ m_url += 7;

​ m_url = strchr(m_url, ‘/‘);

}对请求资源的前七个字符进行判断 对某些带有http://的报文进行单独处理

http_conn::HTTP_CODE http_conn::parse_header(char*text)

解析http请求的一个头部信息

  • 调用parse_headers函数解析请求头部信息
  • 判断是空行还是请求头,若是空行,进而判断content-length是否为0,如果不是0,表明是POST请求,则状态转移到CHECK_STATE_CONTENT,否则说明是GET请求,则报文解析结束。
  • 若解析的是请求头部字段,则主要分析connection字段,content-length字段,其他字段可以直接跳过,各位也可以根据需求继续分析。
  • connection字段判断是keep-alive还是close,决定是长连接还是短连接
  • content-length字段,这里用于读取post请求的消息体长度

m_check_state = CHECK_STATE_CONTENT;

​ return NO_REQUEST;

  • post请求需要改变主状态机的状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
else if (strncasecmp(text, "Connection:", 11) == 0)
{
text += 11;
//跳过空格和\t字符
text += strspn(text, " \t");
if (strcasecmp(text, "keep-alive") == 0)
{
//判断是否为长连接
m_linger = true;
}

}解析头部连接字段
else if (strncasecmp(text, "Content-length:", 15) == 0)
{
text += 15;
text += strspn(text, " \t");
m_content_length = atol(text);
}

解析请求头 内容长度字段

http_conn::HTTP_CODE http_conn::parse_content(char *text)

  • 仅用于解析POST请求,调用parse_content函数解析消息体
  • 用于保存POST请求消息体,为后面的登陆和注册做准备
1
2
3
4
5
6
7
8
if (m_read_idx >= (m_content_length + m_checked_idx))
{
text[m_content_length] = '\0';
//POST请求中最后为输入的用户名和密码
m_string = text;
return GET_REQUEST;
}
return NO_REQUEST;判断是否读取了消息体

http_conn::HTTP_CODE http_conn::process_read()

process_read返回值是对请求文件的分析结果,一部分是语法错误的BAD_REQUEST,一部分则是我们认可的规则然后作出的对应的响应

主状态机初始状态是CHECK_STATE_REQUESTLINE,通过调用从状态机来驱动主状态机

从状态机以及将每一行的末尾\r\n符号改为\0\0,主状态机直接去除对应字符串进行处理

LINE_STATUS line_status = LINE_OK;

HTTP_CODE ret = NO_REQUEST;

char *text = 0;初始化从状态机的状态

ret = parse_request_line(text);

ret = parse_headers(text)

ret = parse_content(text);三种状态转换逻辑,分别解析请求行,解析请求头,解析消息体

else if (ret == GET_REQUEST)

​ {

​ return do_request();

​ }如果是get请求则需要跳转到报文响应函数

如果是post请求,跳转到报文响应函数,更新并跳出循环,维持line_open的状态

http_conn::HTTP_CODE http_conn::do_request()

strcpy(m_real_file, doc_root);

int len = strlen(doc_root);将初始化的m_real_file赋值为网站根目录

const char *p = strrchr(m_url, ‘/‘);找到m_url中/的位置

void http_conn::unmap()

bool http_conn::write()

img

bool http_conn::process_write(HTTP_CODE ret)

void http_conn::process()

if (read_ret == NO_REQUEST)

{

​ //注册并监听读事件

​ modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);

​ return;

}NO_REQUEST,表示请求不完整,需要继续接收请求数据

bool write_ret = process_write(read_ret);

if (!write_ret)

{

​ close_conn();

}

如果接受到了数据则调用process_write完成报文响应

modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);

注册并监听事件

threadpool

整体框架

img

变量

  • m_thread_number 线程池中线程的数量

  • m_max_requests 请求队列中最多允许的、等待处理的请求的数量

  • m_threads 描述线程池的数组,其大小为m_thread_number

m_threads = new pthread_t[m_thread_number];

  • sem m_queuestat; 是否有任务需要处理
  • locker m_queuelocker; 保护请求队列的互斥锁
  • std::list<T *> m_workqueue 请求队列
  • connection_pool *m_connPool 数据库
  • m_actor_model 模型切换

函数

  • threadpool()线程池构造函数

    • 循环创建线程

pthread_create(m_threads + i, NULL, worker, this) != 0

​ 创建成功应该返回0,如果线程池在线程创建阶段就失败,那就应该关闭线程池了

    • 更改线程属性为unjoinable,分离线程

      • pthread_detach(m_threads[i]);

创建一个线程之后需要调用pthread_detech(),原因在于: linux线程有两种状态joinable状态和unjoinable状态。

      • 当线程为joinable状态,线程函数退出都不会释放线程所占用堆栈和线程描述符。只有当调用了pthread_join,主线程阻塞等待子线程结束,才会回收子线程资源。
      • unjoinable属性可以在pthread_create时指定,或在线程创建后在线程中pthread_detach,主线程与子线程分离子线程结束后,资源自动回收
  • append_p()Proactor模式下的任务请求入队

    • 当epoll检测到端口有事件激活时,即将该事件放入请求队列中,并注意互斥,等待工作线程处理

    • 本项目所实现的是一个基于半同步/半反应堆式的并发结构,以Proactor模式为例的工作流程

    • 主线程充当异步线程,负责监听所有socket上的事件

      • 若有新请求到来,主线程接收之以得到新的连接socket,然后往epoll内核事件表中注册该socket上的读写事件
      • 如果连接socket上有读写事件发生,主线程从socket上接收数据,并将数据封装成请求对象插入到请求队列中
      • 所有工作线程睡眠在请求队列上,当有任务到来时,通过竞争(如互斥锁)获得任务的接管权

img

  • worker()线程回调函数

    • template
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  void *threadpool<T>::worker(void *arg)

{

//调用时 *arg是this!

//将参数强转为线程池类,调用成员方法,获取threadpool对象地址

threadpool *pool = (threadpool *)arg;

//线程池中每一个线程创建时都会调用run(),睡眠在队列中

pool->run();

return pool;

}
  • run()

    • 看作一个回环事件:等待m_queuestat()信号变量post,即新任务进入请求队列,然后就加锁取任务->取到任务解锁->执行任务
    • 从请求队列中取出第一个任务,将任务从请求队列删除
    • 每调用一次pthread_create就会调用一次run(),因为每个线程是相互独立的,都睡眠在工作队列上,仅当信号变量更新才会唤醒进行任务的竞争

lock

为便于实现同步类的RAII机制,该项目在pthread库的基础上进行了封装

引用

  • #include <semaphore.h> POSIX信号量
  • #include <pthread.h> 互斥锁 与条件变量

  • sem 简单的信号量类,包括对信号量的构造与析构和wait与post的设置
  • locker 简单的互斥锁类,包括对互斥锁的构造与析构和加锁与解锁的设置
  • cond 简单的条件变量类,包含对条件变量的构造与析构和wait、timewait、signal、broadcast的设置