webServer-note
杂项
项目中为什么要处理
SIGPIPE
信号?
Unix的五种IO模型?
- 阻塞IO
- 非阻塞IO,轮询什么时候可以读写(即什么时候数据从网卡抵达内核缓冲区),系统调用开销大
- IO多路复用,为了使得一个线程可以同时处理多个请求
- 信号驱动式IO,内核会告诉程序什么时候可以读写
- 异步IO,内核会告诉程序什么时候读写好了
两种事件处理模式?
- Reactor模式:主线程只负责监听读写事件,工作线程负责读写以及业务处理
- Proactor模式:主线程负责监听以及读写,工作线程只负责业务处理
还需要注意两种模式下,线程池的run函数写法不同
讲讲Select,poll,epoll的区别?
- 对于select和poll,每次都要把文件描述符数组copy到内核,而epoll是每次添加或删除一个内核维护的文件描述符数组中的一个元素,则会导致如果短时间有多次活跃链接,会导致epoll效果比另两者效果差
- select用数组维护fd,poll用链表维护,epoll在内核中用红黑树维护,并将就绪的fd放入ready-list返还给用户程序
怎么实现ET和LT的?
实现ET和LT需要注意ET模式下,多个socket请求连接时,epoll仍然只会发出一次连接事件,所以需要循环使用accept防止遗漏某些连接请求。同时ET模式决定了每次读写就要全部读完,而LT模式下可以不需要全部读写完
用了什么方法来优化呢?
维护了一个双向链表并使用定时器来淘汰不活跃的连接,注册SIGALRM信号与SIGTERM信号以及sig_handler函数,并创建管道用于接收SIG_ALRM信号,当一定时间后连接仍然不活跃,则将该socket对应的timer从链表中删除,其中删除是O(1)的操作,加入链表则为O(n),可以考虑使用小根堆使得删除与增加操作均为O(logn)
项目流程图
![](/2024/02/18/webServer-note/pic1.jpg)
HTTP相关
HTTP报文
- 请求报文:请求行+请求头部+空行+请求数据
1
2
3
4
5
6
7POST /user HTTP/1.1 // 请求行
Host: www.user.com
Content-Type: application/x-www-form-urlencoded
Connection: Keep-Alive
User-agent: Mozilla/5.0. // 以上是请求头
(此处必须有一空行 | // 空行分割header和请求内容
name=world // 请求体(可选,如get请求时可选) - 响应报文:状态行+响应头部+空行+响应数据
1
2
3
4HTTP/1.1 304 Not Modified
Date:Sat, 15 Oct 2011 15:39:29
(空行)
(空响应体)
HTTP处理流程
- 连接处理,主线程用于连接请求,并根据事务模型决定是否读写完再交给工作线程
- 解析请求报文
使用主从状态机解析报文,主状态机为FUNC:process_read
,从状态机为FUNC:parse_line
,主状态机有三个状态分别对应处理请求行,请求头,和请求数据,从状态机用于读取报文的一行数据(将其中的\r
,\n
替换成\0
) - 返回响应报文
使用iovec数组以及writev函数来支持多个缓冲区的写入,从而将响应头与响应数据一并写入到套接字中
日志系统
使用单例模式创建日志系统,并可以选择同步或异步写入(使用生产者-消费者模型封装一个阻塞队列)
单例模式
通用实现思路:私有化其构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例
- 经典的线程安全懒汉模式(懒指的是在用的时候才去尝试初始化)
1
2
3
4
5
6
7
8
9
10
11
12single* single::p = NULL;
single* single::getinstance(){
if (NULL == p){
pthread_mutex_lock(&lock);
if (NULL == p){
p = new single;
}
pthread_mutex_unlock(&lock);
}
return p;
} - 局部静态变量之线程安全懒汉模式(C++11后可用)
1
2
3
4
5
6
7
8
9
10
11
12
13class single{
private:
single(){}
~single(){}
public:
static single* getinstance();
};
single* single::getinstance(){
static single obj;
return &obj;
} - 饿汉模式
程序开始时就定义了对象。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class single{
private:
static single* p;
single(){}
~single(){}
public:
static single* getinstance();
};
single* single::p = new single();
single* single::getinstance(){
return p;
}
//测试方法
int main(){
single *p1 = single::getinstance();
single *p2 = single::getinstance();
if (p1 == p2)
cout << "same" << endl;
system("pause");
return 0;
}
多线程编程注意点
pthread_cond_wait
函数需要传入一个互斥锁作为参数,需要注意的是此函数内部会执行一次解锁,加锁过程,解锁是为了防止产生死锁。内部操作分为以下几步
- 将线程放在条件变量的请求队列中,内部解锁
- 线程等待被
pthread_cond_broadcast
信号或者pthread_cond_signal
信号唤醒,唤醒后去竞争锁 - 若竞争到互斥锁,则内部再次加锁
- 为什么在
生产者-消费者模型
中,消费者需要使用while
?
一般来说,在多线程资源竞争的时候,在一个使用资源的线程里面(消费者)判断资源是否可用,不可用,便调用pthread_cond_wait
,在另一个线程里面(生产者)如果判断资源可用的话,则调用pthread_cond_signal
发送一个资源可用信号。
在wait
成功之后,资源就一定可以被使用么?答案是否定的,如果同时有两个或者两个以上的线程正在等待此资源,wait
返回后,资源可能已经被使用了。
再具体点,有可能多个线程都在等待这个资源可用的信号,信号发出后只有一个资源可用,但是有A,B两个线程都在等待,B比较速度快,获得互斥锁,然后加锁,消耗资源,然后解锁,之后A获得互斥锁,但A回去发现资源已经被使用了,它便有两个选择,一个是去访问不存在的资源,另一个就是继续等待,那么继续等待下去的条件就是使用while,要不然使用if的话pthread_cond_wait
返回后,就会顺序执行下去。 生产者-消费者模型
以《Unix环境高级编程》为准1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35#include <pthread.h>
struct msg {
struct msg *m_next;
/* value...*/
};
struct msg* workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
void process_msg() {
struct msg* mp;
for (;;) {
pthread_mutex_lock(&qlock);
while (workq == NULL) {
pthread_cond_wait(&qready, &qlock);
}
mp = workq;
workq = mp->m_next;
pthread_mutex_unlock(&qlock);
/* now process the message mp */
}
}
void enqueue_msg(struct msg* mp) {
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
/** 此时另外一个线程在signal之前,执行了process_msg,刚好把mp元素拿走*/
pthread_cond_signal(&qready);
/** 此时执行signal, 在pthread_cond_wait等待的线程被唤醒,
但是mp元素已经被另外一个线程拿走,所以,workq还是NULL ,因此需要继续等待*/
}
webServer-note
http://bugeater.space/2024/02/18/webServer-note/