# PartⅠ:mywebserver

# 一、locker.h,locker.cpp (与 Tiny 相同)

互斥锁类 locker
<u> 用于内存池 </u>:请求队列(工作队列)的互斥访问

向工作队列中添加任务 append () 时,会访问 / 修改请求队列

线程中运行的主要逻辑:从请求队列中取出请求并执行其 process () 函数,故会访问 / 修改请求队列

条件变量类 cond
项目中没有用到

信号量类 sem
<u> 用于内存池 </u>:请求队列 —— 生产者消费者模型

append () 向请求队列中添加请求,则信号量 ++(post ())

run () 从请求队列中取出请求,则信号量–(wait ())

# 二、threadpool.h 线程池

创建多个线程
创建一定数量(m_thread_number)的线程(pthread_create ()),线程的入口函数 worker () 需要是静态函数,原因:

  • https://blog.csdn.net/qq_39274501/article/details/117083175?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-0&spm=1001.2101.3001.4242

  • https://mp.weixin.qq.com/s/PB8vMwi8sB4Jw3WzAKpWOQ

  • 类中调用 pthread_create () 时,类的成员函数作为 pthread_create () 的参数时,必须是静态函数

  • 因为类的成员函数有一个隐藏的参数 ——this 指针,而线程的入口函数必须是接受一个 void 指针作为参数,故入口函数的参数中有一个隐藏的 this 指针是不允许的。为了解决矛盾,我们使用 static 成员函数,其独立于实例,参数中不会有 this 指针,故其可以用作线程的入口函数

将线程设为脱离
创建线程后,将线程设为脱离(pthread_detach ()),这是为了自动回收线程资源

pthread_detach()的作用——当线程终止时,线程的资源将会立即被回收,而不用等待另一个线程调用pthread_join
/* Indicate that the thread TH is never to be joined with PTHREAD\_JOIN. The resources of TH will therefore be freed immediately when it terminates, instead of waiting for another thread to perform PTHREAD\_JOIN on it. */
extern int pthread\_detach (pthread\_t \_\_th) \_\_THROW;

请求队列(工作队列)
请求队列 —— 以队列形式组织,其实现了:将主线程和工作线程解耦(主线程向请求队列中添加任务,工作线程通过竞争来取得任务并执行任务)

请求队列的访问:由于会有多个线程对其进行访问 / 修改,故通过互斥锁实现互斥访问

工作线程竞争请求队列中的任务:通过信号量实现生产者 / 消费者模型,任务交给哪个线程执行是随机的。(也可以用 Round Robin 算法让线程轮流获取任务)

工作线程们运行的函数
worker ()->run ()

  1. worker ():只是一个桥梁,静态成员函数,用作 pthread_create () 的参数;其中只是调用线程的主要逻辑 run ()
  2. run ():类的成员函数,需要静态成员函数 worker () 作为媒介来成为线程的工作函数。其主要逻辑:<u > 竟态获取请求队列中的任务 </u>,通过调用请求类的 process 函数处理请求,process 函数调用 process_read 函数和 process_write 函数分别完成报文解析与报文响应两个任务

只实现了 Proactor 模式

  1. run () 中只执行 process () 过程,数据的读写是在主线程 main () 中完成的
  2. reactor 模式的实现 —— 把数据读写放到工作线程中完成就行了(参考 Tinywebserver)

# 三、main.cpp

网络编程常规步骤

循环获取就绪事件并处理
这便是主线程的主要工作逻辑。

由于是 Proactor 模式,故可读可写事件发生时,main 中将数据读入 / 写出完毕,然后再通知工作线程进行后续逻辑处理

由于该项目中,写完成后,没有后续的处理逻辑,故只有读完成后才会通知工作线程(将任务 append 到线程池的请求队列)

项目中只实现了 LT+ONESHOT 模式。虽然是 LT 模式,但是由于是 ONESHOT,故也要用非阻塞 IO 循环读取以保证将 TCP 缓存中的数据全部读出。(在 ET 模式下,事件触发后需要将缓冲区中的数据完全读完,否则会陷入死锁,故必须要用非阻塞 IO 循环读取)。
为什么必须要用 ONESHOT 呢?(ONESHOT 的作用)——《Linux 高性能服务器编程》P157:

  • 即使是用 ET 模式,一个 socket 上的事件还是可能被触发多次(LT 模式则更是如此),这在并发编程中就会引起一个问题:socket 上的数据被获取后,一个线程开始处理数据(此时 HTTP 请求报文可能并不完整);而在处理数据的过程中,又有新的数据可读(EPOLLIN 再次被触发),此时另一个线程被唤醒去处理新到来的数据。这就会出现同时有两个线程操作同一个 socket 的情况,这显然是不被期望的。
  • 正常情况应该是:数据到来–> 数据被读入 socket 对应的 http_conn 类对象 H 中–> 唤醒一个线程去操作这个类对象 H (处理数据),数据处理过程中不应再次触发事件,避免又有另一个线程开始对该类对象 H 进行操作。

# 四、http_conn.h,http_conn.cpp

# 主要逻辑

** 处理 HTTP 请求:** 通过主从状态机

  1. process_read ()—— 正常流程下,process_read () 返回的是 do_request () 的结果

  2. parse_line()

  3. get_line()

  4. parse_request_line()

  5. parse_hearders()

  6. parse_content()

do_request ():根据 HTTP 请求的解析结果,进行处理得到后续生成响应所需的前提状态

  1. 首先对 GET 请求和不同 POST 请求(登录,注册,请求图片,视频等等)做不同的预处理

  2. 获取请求资源的信息,并判断请求的合法性:分析目标文件的属性,若目标文件存在、对所有用户可读且不是目录

  3. 若合法:则将文件只读地打开,使用 mmap 将其映射到内存地址 m_file_address 处,并告诉调用者获取文件成功。

处理 CGI

根据标志判断是登录检测还是注册检测

将用户名和密码提取出来

生成 HTTP 响应数据并放入发送缓存

  • process_write ()—— 根据 process_read ()(do_request ())获得的文件合法性,生成响应报文放入发送缓冲区,并设置发送缓冲区的标志信息
  • process_write () 处理完成之后,注册 EPOLLOUT 事件,等待主线程调用 write () 发送缓冲区中的数据

其他函数

  • write ()—— 采用分散写 writev (),客户请求的资源(响应体)和 process_write () 生成的报文数据(状态行和响应头),分别放在两块内存中,前者在文件映射到的内存中,后者在发送缓存中
  • setnonblocking ()、addfd ()、removefd ()、modfd () 等 socket 编程常用函数也定义在 http_conn.cpp 中(因为类中也会用到这些函数)

有限状态机 - 处理 HTTP 请求中使用

  • 主状态机:当前解析到哪部分(请求首行、请求头部、请求体)

  • 第一行是首行,解析完直接跳到解析头部状态

  • 头部解析时若遇到空行,则说明头部解析完毕,若有请求体则跳到解析请求体状态(头部中会有 Content-Length 来告诉你有没有请求体)

  • 请求体解析

  • 从状态机:请求数据的每一行的处理状态(完整的一行、不完整的一行、错误的一行)

  • 请求分析结果状态机:用于标记请求报文解析的结果,并用于决定后续的报文解析和响应生成

  • 请求方法状态机:用于标记请求报文中的请求方法,该项目中只支持 GET

# PartⅡ:整个项目的模型

# 一、mywebserver:

  1. 并发模式:半同步 / 半反应堆模式 —— 我认为应该叫 “半同步 / 半模拟前摄器模式”
  2. 事件处理(事件分发)模型:同步 IO 模拟的 Proactor 模型

# 二、Tinywebserver:

  1. 并发模式:半同步 / 半反应堆模式、半同步 / 半模拟前摄器模式 都实现了(其实就是实现了下面的 Reactor 和模拟 Proactor)
  2. 事件处理(事件分发)模型:同步 IO 模拟的 Proactor 模型、Reactor 模型 都实现了(通过 threadpool.h 中的 m_actor_model 变量实现选择用哪种模型,就是选择 read () 和 write () 在主线程完成还是工作线程完成而已)