Skip to content

Latest commit

 

History

History
252 lines (148 loc) · 9.02 KB

架构分析.md

File metadata and controls

252 lines (148 loc) · 9.02 KB

TKeed整体设计

如果该部分阅读有障碍可以先看下面的各部分结构定义及联系。

初始化

  • 读取配置。

  • 绑定地址与监听(LISTENQ = 1024)。

  • 创建epoll并添加监听描述符。

  • 初始化线程池:

    • 初始化线程池各参数。

    • 创建线程。

    • 调用threadpool_worker函数(每个worker线程循环执行以下几步):

      • 每个worker线程进入线程池时都先对线程池互斥锁。

      • 通过条件变量判断是否有待处理任务,只要没有任务则阻塞并打开互斥锁。

      • 如有任务(通过threadpool_add添加),则取出队列中第一个节点。

      • 对线程池开锁,之后其他线程可访问线程池。

      • 执行每个任务对应的操作并删除该任务。

      注:这里先开锁后执行对应任务,因为在此情况下已经完成对该worker线程任务的分配,不可能将其他任务再分配给此线程,其他线程的并发操作不会对其有任何影响。另外,如果将开锁过程置于任务执行之后,则线程池会被加锁至前一个任务执行结束,整个线程池处于被锁定状态。任务会退化成顺序执行,所以这里先开锁非常重要。

  • 初始化定时器:

    • 初始化定时器优先队列。

    • 更新当前时间。

任务处理

  • 请求连接(事件:有连接请求):

    • 接受连接并返回连接描述符。

    • 向epoll中注册新描述符。

    • 新增时间戳信息,新时间戳被加入到优先队列:

      • 最早超时节点在优先队列头。
  • 响应任务(事件:请求到服务器):

    • 将新建的任务加入到线程池中,处理函数为do_request,参数为请求结构(tk_request_t)。

      • 完成tk_task_t节点初始化,任务数量queue_size加1。

      • 将新任务挂在线程池task队列。

      • 调用pthread_cond_signal激活一个等待该条件的线程(只激活一个,避免惊群效应)。在初始化时,线程池中多个线程因为任务队列为空,所以调用pthread_cond_wait被休眠。一旦有新任务被加入线程池,条件变量条件(任务队列非空)即可被满足,此时会唤醒工作线程来处理该任务。

具体处理用户请求过程

某一具体任务被添加进task队列中就会激活一个worker线程去处理请求,入口函数为do_request。

  • 删除该请求定时器:

    因为该任务已经响应,且未超时,不再需要定时器去处理该连接的超时情况,接下来在定时器队列中删除该请求的定时器。这也就是为什么tk_request_t和tk_timer_t结构中互有指向彼此指针的原因。

  • 读取用户请求:

    • 数据读取发生错误(errno非EAGAIN)则关闭连接,释放相应数据结构。

    • 若为errno为EAGAIN:

      • 跳出循环(外层使用循环主要是为了触发EAGAIN条件)。

      • 将该请求重设定时器并与于epoll中重新注册。

      • 结束do_request函数,释放worker线程控制权(TCP未断开)。

  • 解析请求并填充tk_request_t各变量。

  • 获取用户请求文件名,判断默认目录下该文件权限等基本信息:

    • 如果有错误信息返回错误响应体。
  • 返回响应体:

  • 若为长连接,则不关闭TCP连接并重新回到循环中。若为短连接则断开连接。

结构定义及联系

线程池结构定义

关于线程池首先需要了解TKeed中线程池结构体的定义:

typedef struct threadpool{
	pthread_mutex_t lock;    // 互斥锁
	pthread_cond_t cond;    // 条件变量
	pthread_t *threads;    // 线程
	tk_task_t *head;    // 任务链表
	int thread_count;     // 线程数
	int queue_size;    // 任务队列长
	int shutdown;    // 关机方式
	int started;
}tk_threadpool_t;

除了线程的锁机制,线程池还有指向任务队列头节点的head指针。

  • 互斥锁lock

    互斥锁lock在每次访问临界区时都需要先检查互斥锁,第一个执行pthread_mutex_lock()的线程会得到互斥锁,其他线程会一直等待直到第一个线程执行pthread_mutex_unlock()才会释放。为了保证每个任务的原子性(同一个task不会被多个线程获取并执行),线程池中线程数虽然大于1,但某一具体时刻只能有一个线程在取任务。(对应生产者 - 消费者模型中只能有一个消费者线程取任务)

  • 条件变量cond

    多线程情况下,如果某个线程已经进入临界区,其他线程会一直检查是否已经开锁,但这是在浪费时间和系统资源,于是就设置条件变量cond来解决这种忙等的问题。某个线程一旦监测到有线程已经得到互斥锁就是进程进入休眠状态,一旦满足条件就会唤醒休眠的线程,之后被唤醒的线程再去检查互斥锁。

  • 线程数组threads

    初始化时根据配置文件中设定的worker线程数分配线程缓冲池。

  • 任务列表头head

    所有任务以链表形式组织,head指针指向任务队列的首节点。具体每个任务节点的变量会在下面分析到tk_task_t结构时补充。

  • 线程数thread_count

    初始化时从用户配置中获得,用于标识线程池中worker线程数量。

  • 任务队列大小queue_size

    用于标识当前未处理的任务数,设置其目的是为了快速判断是否任务队列已经为空。(也可以判断通过"head->next == NULL"来判断,但不够直观)

  • 关机方式shutdown

    有立即关机(immediate_shutdown)和平滑关机(graceful_shutdown)两种模式。

任务结构定义

上述线程池中,其他均为"原子变量",只有head变量是tk_task_t是复合型的,tk_task_t的定义如下:

typedef struct tk_task{
	void (*func)(void *);    // 处理函数的函数指针
	void *arg;    // 函数变量
	struct tk_task *next;    // 任务链表(下一节点指针)
}tk_task_t;

这里使用无类型函数指针和无类型变量指针也是为了程序扩展性。

  • 处理函数指针func

    每个任务创建时设置函数指针func,func为该任务的执行函数。

  • 函数参数指针arg

    函数参数指针指向任务处理函数func的变量。

  • 链表下一节点指针next

    所有新增的任务以链表形式组织,next指向下一个任务节点。

请求节点定义

所有HTTP请求解析的参数都以tk_request_t结构定义,上述arg指针会被强制转为tk_request_t类型的指针

typedef struct tk_http_request{
    char* root;    // 配置目录
    int fd;    // 描述符(监听、连接)
    int epoll_fd;    // epoll描述符
    char buff[MAX_BUF];    // 用户缓冲
    int method;    // 请求方法
    int state;    // 请求头解析状态
    void *request_start;
    void *method_end;
    void *uri_start;
    void *uri_end;
    void *path_start;
    void *path_end;
    void *timer;    // 指向时间戳结构
    .....
    .....
}tk_http_request_t;

本结构用作任务中用户请求处理函数的参数,之后timer节点为非原子结构。

  • 默认文件目录指针root

    root指向默认文件目录。该目录在读取配置后被设置。

  • 连接描述符fd

    服务器接受请求创建连接后返回的客户端连接描述符,该描述符在向客户机发回响应文件时候会被用到。

  • epoll描述符epoll_fd

    epoll描述符。

  • 用户缓冲buff

    用户请求的到达客户端后,需要从内核缓冲区读出,读到用户缓冲区buff中。请求行等均会被先读入buff中,之后再进行解析操作。

  • 用户请求方法method

    解析buff中用户请求行时填入method,如GET、POST等。

  • 解析状态state

    使用状态机解析用户请求行,state用于记录每个解析状态。

  • 解析指针

    解析用户请求行时,设置各指针指向buff中某一部分收尾部位,比如uri_start指向uri首地址,uri_end指向uri最后一个字节。设置指针方式是为了操作方便。

  • 时间结构timer

    用于各个请求时间相关数据,具体描述下面会说。

时间结构定义

该结构记录各请求时间戳信息。

typedef struct tk_timer{
    size_t key;    // 标记超时时间
    int deleted;    // 标记是否被删除
    timer_handler_pt handler;    // 超时处理
    tk_http_request_t *request;    // 指向对应的request请求
} tk_timer_t;
  • 超时时间key

    添加请求时先更新当前时间,key的值为当前时间加上超时时间(默认timeout为500ms)。

  • 标记是否被删除deleted

    标记该请求是否需要被关闭。每次删除时并不是直接删除该请求,而是先置deleted为1,之后在检查超时时会统一处理,实现惰性删除。

  • 超时回调函数handler

    发生超时需要处理时,处理方法为调用回调函数handler。这里这么处理也是为了扩展性考虑。

  • 请求节点指针request

    指向tk_thread_t节点的指针,每个timer和请求节点一一对应。