SbWebServer的几个设计
基于生产者消费者模型的线程池设计:
数据结构:
成员:
1.一个队列m_queue.其中存放Task,也就是任务,生产者线程,也就是主线程,往这个队列中push;消费者,也就是工作线程,不停地从其中拿走Task去工作。
关于Task,原型为boost::function<void()> Task;
2.一个表示线程数量的变量m_threadNum
3.一个表示队列,即流水线的最大长度变量m_maxSize
4.一个互斥量m_mutex,用于条件变量判断时对队列的状态进行保护
5.一个条件变量m_notFull,用于通知唤醒消费者线程
6.一个条件变量m_notEmpty,用于通知唤醒生产者线程
函数:
1.线程池构造函数ThreadPool(int threadNum,int maxSize)
threadNum为线程数量,maxSize为队列最大大小
在构造函数中创建threadNum个工作线程
2.静态函数startThread
工作线程实际上的工作内容,创建的工作线程从startThread开始工作。
首先pthread_detach,分离后线程的资源自动回收,不用join
startThread中传入线程池的this指针,这样静态函数startThread可以通过这个this指针调用线程池中的Work函数,Work才是真正的线程工作的主体
3.Work函数
Work内是一个for循环f(;;),循环内调用函数Take()从线程池的队列中取走任务,如果取来的任务可用,则执行这个任务
4.Take函数
典型的生产者消费者模型,首先用m_mutex加锁,加锁保护当前队列状态的原子性,用while(m_queue.empty())判断流水线中是否有工作,如果没有则wait陷入沉睡,直到生产者唤醒
如果有工作,则拿走,并返回这个Task;Work函数拿到这个Task后执行这个任务
5.append函数
参数传入bind的function
典型的生产者消费者模型,用于生产者往流水线中添加任务。依然是mutex加锁保护队列的状态,如果非满则将这个function加入到队列中
页面缓存设计
缓存Cache:
数据成员:
一个哈希表unordered_map:key为页面名,作为key;value为一个shared_ptr指针,指向FileInfo,FileInfo为封装了页面mmap到内存的类
一个互斥锁mutex,用于缓存满了删除时保证原子性
函数:
1.getFile用于在页面缓存中寻找请求的资源。
首先加锁,防止请求资源时该缓存被淘汰而出现问题。然后在哈希表中用文件名去寻找,如果未找到,则将这个页面加入缓存。如果此时缓存已满,则淘汰掉访问次数较少的页面,访问次数的统计被封装在FileInfo中。
2.清楚缓存的函数cleancache:
遍历哈希表,如果访问次数小于10的页面均淘汰掉。
页面封装FileInfo:
数据成员:
1.存储 通过mmap映射到内存的地址 的变量addr:所访问的页面都通过mmap映射到内存,然后记录这个地址,封装到FileInfo中,这样每次打开就不用了重新IO打开文件,只要通过这个文件对应的FileInfo中的addr即可寻址到页面对应的位置
2.size表示文件的大小
3.count用于统计页面的访问次数,当缓存满时以此为基准进行淘汰
方法:
1.构造函数:通过文件名打开对应文件,然后使用mmap将之映射到内存,mmap返回的结果赋予addr,用于寻址
2.析构函数:munmap来释放映射到内存的内容。当然,析构的时间由shared_ptr来掌握
Http_Handle封装:
数据成员:
1.文件描述符fd,用于表示对应的套接字
2.状态变量state:
state为Read,process转入processRead进行处理,表示此时应该将对端的数据存入readBuf缓冲区,并分析readBuf缓冲区的内容,根据url找到对应的文件,打开并加入缓存,并写好响应头到writeBuf
state为Write,process转入processWrite进行处理,将writeBuf内的响应信息写到对端,将请求的资源写入到对端。
3.两个缓存readBuf和writeBuf:readBuf用于存放对端发送过来的数据,比如Http头的信息,将之存放在readBuf中;writeBuf用于存放响应信息,比如响应行
4.一个指向所请求资源的FileInfo封装的shared_ptr指针,通过这个指针来找到资源内容,并写道对端。
整体流程:
首先服务端进入被动打开:
创建一个Socket,将这个socket绑定到8080端口,listen后转为监听套接字,用于监听请求
创建epoll,设定监听的文件描述符个数
将监听套接字listenfd加入epoll的监听范围
初始化线程池,创建若干工作线程,设定队列(流水线)大小
进入while(true)循环:
epoll_wait等待事件发生,当listenfd可读,代表有请求,由于使用的是ET模式,因此listenfd要一直accept到返回EAGAIN,每accept一个,返回套接字connfd,将这个套接字封装成Http_Handle,此时状态为Read,及等待对端像这个socket发送数据并分析。将这个套接字connfd也加入到epoll的监听里,监听时间为EPOLLIN,也就是可读。
当epoll_wait等待来的是其他事件,将这个事件对应的fd,把它的process函数bind到function上,并将这个function加入到流水线
工作线程从流水线上拿到这个Task,进行执行:
此时这个Task相当于绑定到某个Http_Handle上的Http_Handle::process,消费者执行process时,根据Http_Handle中state的状态来决定接下来的动作:
1.Read:代表读取对端发过来的连接包到readBuf,并分析readBuf的内容,找到URL指定的资源,这是通过缓存来找到的。找到后将响应行和响应头相关的信息写入writeBuf(因为writeBuf本来就被初始化为全0,所以不用在尾部加\0)。结束后将状态设置为Write,即只需要讲这些信息以及文件写入到对端就可以了。修改本fd在epoll中的监听事件,本来是监听可读,现在要写了,因此监听EPOLLOUT。
2.Write:代表此时应该向对端写数据了,转入执行processWrite:这个过程就是将writeBuf中的头信息写到对端,然后用Http_Handle中封装的FileInfo的智能指针找到映射到内存里的文件,将文件写到对端。一切结束后代表这个Socket的任务已经完成,断开连接。
这里主线程相当于生产者,不断的往队列中append不同fd的process函数,然后消费者从队列中拿走这些process函数, process函数会根据Http_Handle.中State的状态不同执行processRead或processWrite.