Linux高性能服务器开发小项目分析记录
参考书籍:《Linux高性能服务器编程》游双
基础班:Mywebserver,就是书上的服务器项目
进阶版:Tinywebserver
目录
PartⅠ:mywebserver
一、locker.h,locker.cpp (与Tiny相同)
互斥锁类locker
用于内存池:请求队列(工作队列)的互斥访问
-
向工作队列中添加任务append()时,会访问/修改请求队列
-
线程中运行的主要逻辑:从请求队列中取出请求并执行其process()函数,故会访问/修改请求队列
条件变量类cond
项目中没有用到
信号量类sem
用于内存池:请求队列——生产者消费者模型
-
append()向请求队列中添加请求,则信号量++(post())
-
run()从请求队列中取出请求,则信号量–(wait())
二、threadpool.h 线程池
创建多个线程
创建一定数量(m_thread_number)的线程(pthread_create()),线程的入口函数worker()需要是静态函数,原因:
-
类中调用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()
-
worker():只是一个桥梁,静态成员函数,用作pthread_create()的参数;其中只是调用线程的主要逻辑run()
-
run():类的成员函数,需要静态成员函数worker()作为媒介来成为线程的工作函数。其主要逻辑:
-
竟态获取请求队列中的任务,然后执行任务的process()函数
只实现了Proactor模式
run()中只执行process()过程,数据的读写是在主线程main()中完成的
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请求:
-
process_read()——正常流程下,process_read()返回的是do_request()的结果
-
parse_line()
-
get_line()
-
parse_request_line()
-
parse_hearders()
-
parse_content()
do_request():根据HTTP请求的解析结果,进行处理得到后续生成响应所需的前提状态
-
项目中只实现了GET:
-
获取请求资源的信息,并判断请求的合法性
-
若合法,则将文件只读地打开,并映射如内存
-
若要实现POST:需要在do_request()中添加POST的处理逻辑
生成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:
并发模式:半同步/半反应堆模式 —— 我认为应该叫“半同步/半模拟前摄器模式”
事件处理(事件分发)模型:同步IO模拟的Proactor模型
二、Tinywebserver:
并发模式:半同步/半反应堆模式、半同步/半模拟前摄器模式 都实现了(其实就是实现了下面的Reactor和模拟Proactor)
事件处理(事件分发)模型:同步IO模拟的Proactor模型、Reactor模型 都实现了(通过threadpool.h中的m_actor_model变量实现选择用哪种模型,就是选择read()和write()在主线程完成还是工作线程完成而已)
关于游双讲的服务器框架,我的疑问与理解:我的博客
PartⅢ:Tinywebserver做了哪些改进
坑,待填