目录
代码开源:
https://github.com/PetterZhukov/webserver_HTTP
介绍:
webserver_HTTP
使用了线程池,通过epoll实现的Proactor版本的web服务器。参考了游双老师的《Linux高性能服务器编程》以及牛客网的《Linux高并发服务器开发》课程。在自己复现的基础上进行模块的整合并添加一些小更改。所有代码拥有完备的注释。访问的资源在 同级目录"resources"文件夹中
2.0 Linux多线程
2.0.1 Linux多线程概述
假如程序都使用多进程编程,则会造成很多弊端,比如需要拷贝大量内存,以及进程通信较为复杂等等,因此诞生了线程这个概念。Linux中的线程被称为LWP(light weight process),即轻量的进程。
线程共享内核以及全局内存区域,因此拷贝开销小,线程通信也容易。
2.0.2 Linux多线程编程的API
只列出本项目会用到的API
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- 功能:创建一个子线程
- 参数:
- thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中。
- attr : 设置线程的属性,一般使用默认值,NULL
- start_routine :void*(void*)类型的函数指针,这个函数是子线程需要处理的逻辑代码
- arg : 给第三个参数使用,传参
- 返回值:
成功:0
失败:返回错误号。这个错误号和之前errno不太一样。
获取错误号的信息: char * strerror(int errnum);
int pthread_detach(pthread_t thread);
- 功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统。
1.不能多次分离,会产生不可预料的行为。
2.不能去连接一个已经分离的线程,会报错。
- 参数:需要分离的线程的ID
- 返回值:
成功:0
失败:返回错误号
2.0.1 线程同步
2.0.1.1 线程同步概述
线程中可能会对某一变量同时进行操作,这样是很不安全的,因此需要线程同步。
可以使用诸如互斥锁,信号量,条件变量等来实现对应的功能。
2.0.1.2 线程同步的API
1. 互斥锁
对一个互斥锁进行加锁,则其他想对其加锁的操作都会失败,只有等到对该互斥锁进行解锁,加锁操作才可以成功,加锁操作有阻塞和非阻塞两种。
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
- 初始化互斥量
- 参数 :
- mutex : 需要初始化的互斥量变量
- attr : 互斥量相关的属性,NULL
- 返回值 :
- 成功: 0
- 失败:错误号
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 释放互斥量的资源
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 加锁(阻塞)如果有一个线程加锁了,那么其他的线程只能阻塞等待
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 解锁
2. 信号量
mutex是用在不能同时操作的时候,而信号量则是用在需要计数的地方,以保证计数值大于0的时候进程才会被允许继续,在项目中构建线程池的时候,我们需要保证消息队列不为空的时候才能进行pop操作,因此需要用到信号量。
需要注意的是信号量只保证进入的时候计数值大于0,而不保证互斥操作,因此经常和mutex一起用。
API:
信号量的类型 sem_t
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 初始化信号量
- 参数:
- sem : 信号量变量的地址
- pshared : 0 用在线程间 ,非0 用在进程间
- value : 信号量中的值
- return value
- 成功 : 0
- 失败 : -1
int sem_destroy(sem_t *sem);
- 释放资源
int sem_wait(sem_t *sem);
- 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞
int sem_post(sem_t *sem);
- 对信号量解锁,调用一次对信号量的值+1
2.1 线程池概述
线程池是由服务器预先创建的一组子线程。假如不使用线程池的话,有一个需要处理的队列就创建线程,没有就销毁,则频繁的创建与销毁线程会导致很大的开销。同时,线程的数量不稳定也不好控制。
相比起来,预先创建若干数量的线程,数量是可控的,同时,线程池只有休眠和运行两种状态,只有一开始的创建和最后的销毁,运行过程中没有这方面的开销。
面对网络服务器这样响应量大,响应时间短,对时间要求苛刻的任务,使用线程池是很合适的,若是遇到响应时间很长的任务,则线程池的作用就不大了。
2.2 线程池的组成部分
2.2.1 请求队列
如字面意思,请求队列就是一个队列,线程会持续从中取出业务来进行业务逻辑处理。
本项目中数据结构采用list<T*> ,即成员为指针的std::list来实现,因为只需要取头结点即可,用链表方便存取。
请求队列需要有几个功能:
- 从中取元素(若为空则阻塞等待)(这用信号量来实现),即pop
- 往队列中加入元素,即push
2.2.2 子线程的业务逻辑处理
因为是web服务器,以及如第一节所言,线程部分需要处理读写之后的业务逻辑。
因此线程需要处理的部分有如下几点:
- 分析读的内容,解析请求报文
- 生成对应的回应报文
线程需要处理的是不断从请求队列中取出的待处理业务,然后将其处理。
对应的设计是取出类型为T的请求业务,然后调用该类的process方法(因此该类必须要有process方法),至于process具体应该实现什么,则是接下来的内容。
2.2.3 线程池的相关实现
线程的组成成员:
请求队列,以及若干个线程
请求队列只需调用即可,而创建若干个线程,因为线程创建后其也只会通过修改epoll事件和处理业务逻辑产生结果来进行交互,所以和主线程没有什么需要通信的地方,因此只需创建并脱离即可。
2.3 代码实现
2.3.1 请求队列
#ifndef __QUEST_QUEUE_H_
#define __QUEST_QUEUE_H_
#include <list>
#include "locker.h"
// 模板类 请求队列
template <typename T>
class questqueue
{
private:
// 请求队列
std::list<T *> m_questqueue;
// 请求队列的最大长度
int m_max_queue;
// 保护请求队列的互斥锁
mutex m_queue_mutex;
// 是否有任务需要处理,信号量
sem m_queue_sem;
public:
questqueue(int max_queue);
~questqueue();
// 阻塞式取元素
T *pop();
// 阻塞式填入元素
bool push(T *quest);
};
template <typename T>
questqueue<T>::questqueue(int max_queue) : m_max_queue(max_queue)
{
if (max_queue <= 0)
throw "队列的大小错误";
}
template <typename T>
questqueue<T>::~questqueue()
{
for (auto it = m_questqueue.begin(); it != m_questqueue.end(); it++)
delete *it;
}
// 阻塞式填入元素
template <typename T>
bool questqueue<T>::push(T *quest)
{
// 操作请求队列加锁
m_queue_mutex.lock(); // lock
if (m_questqueue.size() >= m_max_queue)
{
return false;
}
// 添加quest,并且更新
m_questqueue.push_back(quest);
m_queue_sem.post();
m_queue_mutex.unlock(); // unlock
return true;
}
// 阻塞式取元素
template <typename T>
T *questqueue<T>::pop()
{
// 上锁
m_queue_sem.wait();
m_queue_mutex.lock();
if (m_questqueue.empty())
{
m_queue_mutex.unlock();
return NULL;
}
// 取出
T *quest = m_questqueue.front();
m_questqueue.pop_front();
// 解锁
m_queue_mutex.unlock();
return quest;
}
#endif
2.3.2 线程池
#ifndef _PTHREADPOOL_H_
#define _PTHREADPOOL_H_
#include <list>
#include "locker.h"
#include "questqueue.h"
// 模板类 线程池
template <typename T>
class threadpool
{
public:
threadpool(int poolsize = 8, int maxquest = 1000);
~threadpool();
// 增加请求
bool append(T *quest);
private:
// 子线程调用的的执行函数
static void *worker(void *arg);
// 因为worker是静态的,因此增加一个真正的执行函数
void run();
private:
// 请求队列
questqueue<T> m_questqueue;
// 线程池大小
int m_thread_poolsize;
// 大小为m_thread_poolsize的 线程池
pthread_t *m_threads;
// 是否结束线程
bool m_stoppool;
};
template <typename T>
threadpool<T>::threadpool(int poolsize, int maxquest) :
m_thread_poolsize(poolsize), m_stoppool(false),m_questqueue(maxquest)
{
// check size
if (poolsize <= 0)
throw "线程池的大小错误";
m_threads = new pthread_t[m_thread_poolsize];
// 初始化线程池的线程
for (int i = 0; i < m_thread_poolsize; i++)
{
#ifdef show_create_pool
printf( "create the %dth thread\n", i+1);
#endif
if (pthread_create(m_threads + i, NULL, worker, this) != 0)
{
delete[] m_threads;
throw "创建子线程时错误";
}
}
for (int i = 0; i < m_thread_poolsize; i++)
{
if (pthread_detach(m_threads[i]) != 0)
{
delete[] m_threads;
throw "子线程分离时错误";
}
}
printf("thread pool ready \n");
}
template <typename T>
threadpool<T>::~threadpool()
{
m_stoppool = true;
delete[] m_threads;
}
template <typename T>
bool threadpool<T>::append(T *quest)
{
return m_questqueue.push(quest);
}
template <typename T>
void *threadpool<T>::worker(void *arg)
{
threadpool *pool = (threadpool *)arg;
pool->run();
return pool;
}
template <typename T>
void threadpool<T>::run()
{
while (!m_stoppool)
{
// 从请求队列中阻塞取出待处理元素
T* quest=m_questqueue.pop();
// 检查是否为空
if (quest!=NULL)
// 调用quest
quest->process();
}
}
#endif