0%

Follow-TinyWebServer

GitHub上的项目:qinguoyi/TinyWebServer: Linux下C++轻量级Web服务器学习 (github.com),这篇博客记录一下follow的日程和更详细的注解和逻辑思考。整个工程作者没有透露完成顺序,我就根据自己的理解从一个部分开始逐步往下。

第一站

lock

服务器需要一些互斥操作,因为一些共享资源(如数据库连接池、线程池)被同时访问时会出现错误,需要互斥访问。因此互斥作为一个小的辅助功能,在前面这里先进行分析。

locker.h代码如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#ifndef LOCKER_H
#define LOCKER_H

#include <exception>
#include <pthread.h>//for mutex
#include <semaphore.h>//for sem

class sem
{
public:
sem()
{
if (sem_init(&m_sem, 0, 0) != 0)//初始化不成功都返回异常
{
throw std::exception();
}
}
sem(int num)
{
if (sem_init(&m_sem, 0, num) != 0)//初始化不成功都返回异常
{
throw std::exception();
}
}
~sem()
{
sem_destroy(&m_sem);
}
bool wait()
{
return sem_wait(&m_sem) == 0;//阻塞等待资源,资源获取后往下执行
}
bool post()
{
return sem_post(&m_sem) == 0;//执行完毕,释放资源
}

private:
sem_t m_sem;//信号量对象
};
class locker
{
public:
locker()
{
if (pthread_mutex_init(&m_mutex, NULL) != 0)//初始化不成功都返回异常
{
throw std::exception();
}
}
~locker()
{
pthread_mutex_destroy(&m_mutex);
}
bool lock()
{
return pthread_mutex_lock(&m_mutex) == 0;
}
bool unlock()
{
return pthread_mutex_unlock(&m_mutex) == 0;
}
pthread_mutex_t *get()//取类私有成员
{
return &m_mutex;
}

private:
pthread_mutex_t m_mutex;//互斥锁对象,注意它本身不是指针,当参数时要用引用传入地址
};
class cond
{
public:
cond()
{
if (pthread_cond_init(&m_cond, NULL) != 0)//初始化不成功都返回异常
{
//pthread_mutex_destroy(&m_mutex);
throw std::exception();
}
}
~cond()
{
pthread_cond_destroy(&m_cond);
}
bool wait(pthread_mutex_t *m_mutex)
{
int ret = 0;
//pthread_mutex_lock(&m_mutex);
ret = pthread_cond_wait(&m_cond, m_mutex);
//pthread_mutex_unlock(&m_mutex);
return ret == 0;
}
bool timewait(pthread_mutex_t *m_mutex, struct timespec t)
{
int ret = 0;
//pthread_mutex_lock(&m_mutex);
ret = pthread_cond_timedwait(&m_cond, m_mutex, &t);
//pthread_mutex_unlock(&m_mutex);
return ret == 0;
}
bool signal()
{
return pthread_cond_signal(&m_cond) == 0;
}
bool broadcast()
{
return pthread_cond_broadcast(&m_cond) == 0;
}

private:
//static pthread_mutex_t m_mutex;
pthread_cond_t m_cond;//条件变量对象
};
#endif

互斥锁mutex

互斥锁主要是让一个资源锁起来,同一时间只能有一个活动在使用这个资源,其他的请求全部被卡住。项目中具体的实现不是用零散的mutex类的函数操作,而是用一个locker类封装好,构造和析构函数分别执行初始化和注销,这使得用户不需要手动去做(RAII思想,同时可以简化API较长的函数名,其他两个类也是这样的思想)。

  • int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
    • pthread_mutex_init() 函数是以动态方式创建互斥锁的,参数attr指定了新建互斥锁的属性。如果参数attr为空(NULL),则使用默认的互斥锁属性,默认属性为快速互斥锁 。互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。
    • pthread_mutexattr_init() 函数成功完成之后会返回零,其他任何返回值都表示出现了错误。函数成功执行后,互斥锁被初始化为未锁住态。
  • *pthread_mutex_destroy()*用于注销一个互斥锁,API定义如下:int pthread_mutex_destroy(pthread_mutex_t *mutex*)*
    • 销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的 pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。
  • int pthread_mutex_lock(pthread_mutex_t *mutex):锁住,返回值为0成功;
  • int pthread_mutex_unlock(pthread_mutex_t *mutex):解锁,返回值为0成功;

简单来说:

  • pthread_mutex_init函数用于初始化互斥锁
  • pthread_mutex_destory函数用于销毁互斥锁
  • pthread_mutex_lock函数以原子操作方式给互斥锁加锁
  • pthread_mutex_unlock函数以原子操作方式给互斥锁解锁

信号量sem

信号量有数值大小,主要用来管理一个buffer,写入和读出都要满足buffer的边界,同样的取用资源也要在资源池满足的情况下进行。

  • int sem_init(sem_t *sem, int pshared, unsigned int value);
    • 该函数初始化由 sem 指向的信号对象,并给它一个初始的整数值 value。pshared 控制信号量的类型,值为 0 代表该信号量用于多线程间的同步,值如果大于 0 表示可以共享,用于多个相关进程间的同步:参数 pshared > 0 时指定了 sem 处于共享内存区域,所以可以在进程间共享该变量
  • int sem_destroy(sem_t *sem);
    • 该函数用于对用完的信号量的清理。
  • int sem_wait(sem_t *sem);
    • sem_wait 是一个阻塞的函数,测试所指定信号量的值,它的操作是原子的。若 sem value > 0,则该信号量值减去 1 并立即返回。若sem value = 0,则阻塞直到 sem value > 0,此时立即减去 1,然后返回。函数成功返回0,错误的话信号量的值不改动,返回-1。
    • 还有另一个函数:sem_trywait 函数是非阻塞的函数,它会尝试获取获取 sem value 值,如果 sem value = 0,不是阻塞住,而是直接返回一个错误 EAGAIN。
  • int sem_post(sem_t *sem);
    • 把指定的信号量 sem 的值加 1,唤醒正在等待该信号量的任意线程。成功时返回 0;错误时,信号量的值没有更改,-1 被返回。

简单来说:

  • sem_init函数用于初始化一个未命名的信号量
  • sem_destory函数用于销毁信号量
  • sem_wait函数将以原子操作方式将信号量减一,信号量为0时,sem_wait阻塞
  • sem_post函数以原子操作方式将信号量加一,信号量大于0时,唤醒调用sem_post的线程

条件变量cond

  • int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *cattr);

    • 返回值:函数成功返回0;任何其他返回值都表示错误。初始化一个条件变量。当参数cattr为空指针时,函数创建的是一个缺省的条件变量。否则条件变量的属性将由cattr中的属性值来决定。调用 pthread_cond_init函数时,参数cattr为空指针等价于cattr中的属性为缺省属性,只是前者不需要cattr所占用的内存开销。这个函数返回时,条件变量被存放在参数cv指向的内存中。
  • int pthread_cond_destroy(pthread_cond_t *cv);

    • 返回值:函数成功返回0;任何其他返回值都表示错误。释放条件变量。需要注意的是只有在没有线程在该条件变量上等待时,才可以注销条件变量,否则会返回EBUSY。同时Linux在实现条件变量时,并没有为条件变量分配资源,所以在销毁一个条件变量时,只要注意该变量是否仍有等待线程即可。
  • int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex);

    • 返回值:函数成功返回0;任何其他返回值都表示错误。

    • 为什么要关联一个mutex呢?无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的竞争条件(Race Condition)。

      • pthread_cond_wait(cond, mutex)的功能有3个:
        • 调用者线程首先释放mutex
        • 然后阻塞,等待被别的线程唤醒
        • 当调用者线程被唤醒后,调用者线程会再次获取mutex
      • pthread_cond_wait(cond)的功能只有1个:
        • 调用者线程阻塞,等待被别的线程唤醒。

      这里首先给一个简洁的回答:

      • 通常的应用场景下,当前线程执行pthread_cond_wait时,处于临界区访问共享资源,存在一个mutex与该临界区相关联,这是理解pthread_cond_wait带有mutex参数的关键
      • 当前线程执行pthread_cond_wait前,已经获得了和临界区相关联的mutex;因为缺少其他条件,执行pthread_cond_wait会阻塞,但是在进入阻塞状态前,必须释放已经获得的mutex,让其它线程能够进入临界区
      • 当前线程执行pthread_cond_wait后,阻塞等待的条件满足,条件满足时会被唤醒;被唤醒后,仍然处于临界区,因此被唤醒后必须再次获得和临界区相关联的mutex

      综上,调用pthread_cond_wait时,线程总是位于某个临界区,该临界区与mutex相关,pthread_cond_wait需要带有一个参数mutex,用于释放和再次获取mutex。

  • int pthread_cond_timedwait(pthread_cond_t *cv,pthread_mutex_t *mp, const struct timespec * abstime);

    • 返回值:函数成功返回0;任何其他返回值都表示错误
    • pthread_cond_timedwait()用于等待一个条件变量,等待条件变量的同时可以设置等待超时。这是一个非常有用的功能,如果不想一直等待某一条件变量,就可以使用这个函数。函数到了一定的时间,即使条件未发生也会解除阻塞。
    • 条件变量默认使用的时间是CLOCK_REALTIME。通过clock_gettime()接口获取时间。
  • int pthread_cond_signal(pthread_cond_t *cv);

    • 返回值:函数成功返回0;任何其他返回值都表示错误
    • 函数发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行。如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
  • int pthread_cond_broadcast(pthread_cond_t *cv);

    • 返回值:函数成功返回0;任何其他返回值都表示错误
    • 函数唤醒所有被pthread_cond_wait函数阻塞在某个条件变量上的线程,参数cv被用来指定这个条件变量。当没有线程阻塞在这个条件变量上时,pthread_cond_broadcast函数无效。

线程池threadpool

使用线程有动态方法和静态方法,动态方法是当一个工作需要完成时创建一个线程,当工作做完后释放线程。这种方式对资源的利用率高一些,但是耗费时间,因为要新创建、销毁线程。静态方法是使用线程池先创建好一系列等待请求的线程,当一个工作到来时直接分配空闲线程,工作完成后放回线程池。

线程池的代码放在threadpool.h中,代码如下:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#ifndef THREADPOOL_H
#define THREADPOOL_H

#include <list>
#include <cstdio>
#include <exception>
#include <pthread.h>
#include "../lock/locker.h"
#include "../CGImysql/sql_connection_pool.h"

template <typename T>
class threadpool
{
public:
/*thread_number是线程池中线程的数量,max_requests是请求队列中最多允许的、等待处理的请求的数量*/
threadpool(int actor_model, connection_pool *connPool, int thread_number = 8, int max_request = 10000);
~threadpool();
bool append(T *request, int state);//两种append,应该对应了不同的T的操作,实际上感觉重载就可以了
bool append_p(T *request);

private:
/*工作线程运行的函数,它不断从工作队列中取出任务并执行之*/
static void *worker(void *arg);//静态成员函数,是一个回调函数,后面会说明为什么要设置成静态
void run();

private:
int m_thread_number; //线程池中的线程数,即可同时工作的数量
int m_max_requests; //请求队列中允许的最大请求数,即最多同时等待的数量
pthread_t *m_threads; //描述线程池的数组,其大小为m_thread_number
std::list<T *> m_workqueue; //请求队列
locker m_queuelocker; //保护请求队列的互斥锁
sem m_queuestat; //是否有任务需要处理
connection_pool *m_connPool; //数据库
int m_actor_model; //模型切换
};
template <typename T>
threadpool<T>::threadpool( int actor_model, connection_pool *connPool, int thread_number, int max_requests) : m_actor_model(actor_model),m_thread_number(thread_number), m_max_requests(max_requests), m_threads(NULL),m_connPool(connPool)
{
if (thread_number <= 0 || max_requests <= 0) //一些不合理请求的判断
throw std::exception();
m_threads = new pthread_t[m_thread_number]; //线程池的实作是一个线程数组
if (!m_threads) //请求失败则m_threads是一个nullptr
throw std::exception();
for (int i = 0; i < thread_number; ++i)
{ //初始化线程池里的线程,返回值不为0说明失败
if (pthread_create(m_threads + i, NULL, worker, this) != 0)//m_threads+i与m_threads[i]没区别
{
delete[] m_threads;
throw std::exception();
}
//在创建线程后,实现线程从主线程(进程)分离,这使得线程能在工作完后自动回收资源,具体在后面有写
if (pthread_detach(m_threads[i]))//感觉这个if和上面那个if的风格好不一样...
{
delete[] m_threads;
throw std::exception();
}
}
}
template <typename T>
threadpool<T>::~threadpool()//析构,new出来的delete掉
{
delete[] m_threads;
}
//向请求队列添加请求
template <typename T>
bool threadpool<T>::append(T *request, int state)
{
m_queuelocker.lock();//多线程状态下工作,要互斥,否则request和list的修改会出现异常
if (m_workqueue.size() >= m_max_requests)//超出最大请求,非阻塞返回。如果要阻塞的话,可以用一个full信号量控制
{
m_queuelocker.unlock();
return false;
}
request->m_state = state;//赋予状态,指读还是写
m_workqueue.push_back(request);//添加队列
m_queuelocker.unlock();//解锁
m_queuestat.post();//信号量加一,告知线程池有任务在等待处理
return true;
}
//和上面一样
template <typename T>
bool threadpool<T>::append_p(T *request)
{
m_queuelocker.lock();
if (m_workqueue.size() >= m_max_requests)
{
m_queuelocker.unlock();
return false;
}
m_workqueue.push_back(request);
m_queuelocker.unlock();
m_queuestat.post();
return true;
}
//worker函数,传递参数给线程,然后调用真正的run函数工作。静态成员类外定义不用static
template <typename T>
void *threadpool<T>::worker(void *arg)//void 指针可以指向任意类型的数据
{
threadpool *pool = (threadpool *)arg;//arg = this,将参数强转为线程池类,调用成员方法
pool->run();//调用实例的run函数
return pool;//实际上run一直运行,估计不会return
}
template <typename T>
void threadpool<T>::run()
{
while (true)//为什么是while呢?因为每个线程其实在不断的运行,如果有任务就取出来做,没有就wait阻塞
{
m_queuestat.wait();//阻塞,要等有任务即前面post信号量了,才往下做
m_queuelocker.lock();//取出任务,要对list操作,那么要锁
if (m_workqueue.empty())//感觉没必要,前面用wait其实已经判断了工作池buffer了,有任务才会往下
{//m_workqueue的大小应该和m_queuestat信号量的大小绑定了(根据append函数来看)
m_queuelocker.unlock();
continue;
}
T *request = m_workqueue.front();//取第一个请求
m_workqueue.pop_front();//pop
m_queuelocker.unlock();//解锁,让下一个线程可以操作list
if (!request)//如果请求实际上是null
continue;
//切换模式,reactor==1,proactor==0
//非阻塞同步工作模式,读写均需要在线程里工作,调用read和write,并且进行最后的process业务逻辑处理。
//非阻塞是指有数据才进行,但数据处理过程仍在线程里执行
if (1 == m_actor_model)
{
if (0 == request->m_state)//读
{
if (request->read_once())//如果成功则不关闭定时器,不用关闭连接
{
request->improv = 1;
connectionRAII mysqlcon(&request->mysql, m_connPool);
request->process();
}
else//不成功要关闭连接和定时器
{
request->improv = 1;
request->timer_flag = 1;
}
}
else//写
{
if (request->write())
{
request->improv = 1;
}
else
{
request->improv = 1;
request->timer_flag = 1;
}
}
}
else//模拟proactor模式的IO在主循环处理(是同步的),线程只需要处理业务逻辑即可
{
connectionRAII mysqlcon(&request->mysql, m_connPool);
request->process();//处理业务逻辑
}
}
}
#endif
  • 线程池类使用模板,目前还没看出作用,猜测是后面会用于多种不同资源的分配使用,如处理http连接、处理数据库请求等等。
  • int pthread_create(pthread_t *tidp, const pthread_attr_t *attr,( void *)(*start_rtn)( void *), void *arg);
    • 第一个参数为指向线程 标识符的指针。
    • 第二个参数用来设置线程属性。
    • 第三个参数是线程运行函数的起始地址(函数指针)。
    • 最后一个参数是运行函数的参数。
    • 若线程创建成功,则返回0。若线程创建失败,则返回出错编号,并且*thread中的内容是未定义的。
  • int pthread_detach(pthread_t thread); 成功:0;失败:错误号
    • 使用pthread_create创建的线程有两种状态:joinable和unjoinable。默认是joinable 状态。
    • 线程创建后在线程中调用 pthread_detach, 如:pthread_detach(pthread_self()),将状态改为unjoinable状态,确保资源的释放。
    • pthread_detach()和pthread_join()就是控制子线程回收资源的两种不同的方式。同一进程间的线程具有共享和独立的资源,其中共享的资源有堆、全局变量、静态变量、文件等公用资源。而独享的资源有栈和寄存器,这两种方式就是决定子线程结束时如何回收独享的资源。
      • 如果是joinable状态,则该线程结束后(通过pthread_exit结束或者线程执行体任务执行完毕)不会释放线程所占用堆栈和线程描述符(总计8K多)等资源,除非在主线程调用了pthread_join函数之后才会释放。pthread_join函数一般应用在主线程需要等待子线程结束后才继续执行的场景。(pthread_join是一个阻塞函数,调用方会阻塞到pthread_join所指定的tid的线程结束后才被回收,但是在此之前,调用方是霸占系统资源的。 )
      • 如果是unjoinable状态,则该线程结束后会自动释放占用资源。实现方式是在创建时指定属性,或者在线程执行体的最开始处添加一行:pthread_detach(pthread_self());不会阻塞,调用它后,线程运行结束后会自动释放资源,后者非常方便。
    • 总结
      • pthread_detach()即主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收。
      • pthread_join()即是子线程合入主线程,主线程会一直阻塞,直到子线程执行结束,然后回收子线程资源,并继续执行。
  • 工作流程
    • 构造函数初始化线程池:创建线程和分离线程状态
    • 析构函数销毁线程池
    • append函数互斥地向list添加请求,并post信号量
    • 一个线程对应一个worker,worker函数调用run。
    • run函数从list互斥地获得请求并工作,不断循环
  • worker函数是一个成员函数,那么必须是一个静态的。它是一个回调函数,回调函数是通过指针调用的函数,最常使用的回调函数就是在创建线程时(pthread_create),以一个函数指针以及传递给这个函数多个参数来调用线程函数来创建线程。那么一般的类成员函数是不能用作回调函数的,因为在使用回调函数时,会传递指定的符合回调函数声明的的参数给回调函数,而类成员函数隐式包含一个this指针参数,所以把类成员函数当作回调函数编译时会因为参数不匹配会出错(回调后多了个this,与声明不一致)。
    • 静态成员函数就没有这个问题,里面没有this指针。
  • 那么为什么要用worker间接调用run函数呢?run设计成静态的直接调用不行吗?
    • 答案是不太方便,因为静态成员函数只能访问静态成员数据、其他静态成员和类外部的函数,因为没有this指针。不过我们这里手动传入了this指针使得它可以调用run成员函数。
    • this指针只能在类内部使用而不能在外部使用。可以访问类中所有public、private、protect的成员函数和变量。this指针是指向对象的实例,所以只有当对象被创建时this指针才有效。
    • 同一个模板类的不同实例共享静态成员函数,不同实例有不同的资源,这导致静态成员函数不能访问那些实例各有的资源,因为不知道要访问哪个。而run要操作不同实例的list等等资源,通过共享的worker使用从线程传入的this指针操作各个实例的run函数,run就能操作自己这个实例的资源了。但如果run是静态的,即使通过手动传入this参数,run里面所有的资源都要this->一下,太不方便了。
    • 因此,最好的方式就是静态成员函数通过this指针调用成员函数,这个成员函数就可以很方便地访问类实例的资源了(说白了就是使用资源不用this->了)。

第二站

单例模式

后面会用到单例模式,这里先详解一下,参考了许多文章。

什么是单例模式?

保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。

为什么要用单例模式?

1、单例模式节省公共资源

比如:大家都要喝水,但是没必要每人家里都打一口井是吧,通常的做法是整个村里打一个井就够了,大家都从这个井里面打水喝。

对应到我们计算机里面,像日志管理、打印机、数据库连接池、应用配置。

2、单例模式方便控制

就像日志管理,如果多个人同时来写日志,你一笔我一笔那整个日志文件都乱七八糟,如果想要控制日志的正确性,那么必须要对关键的代码进行上锁,只能一个一个按照顺序来写,而单例模式只有一个人来向日志里写入信息方便控制,避免了这种多人干扰的问题出现。

实现单例模式的思路

1. 构造私有:

如果要保证一个类不能多次被实例化,那么我肯定要阻止对象被new 出来,所以需要把类的所有构造方法私有化

2.以静态方法返回实例

因为外界就不能通过new来获得对象,所以我们要通过提供类的方法来让外界获取对象实例。

3.确保对象实例只有一个

只对类进行一次实例化,以后都直接获取第一次实例化的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 单例模式案例
*/
public class Singleton {
//确保对象实例只有一个。
private static final Singleton singleton = new Singleton();
//构造方法私有
private Singleton() {
}
//以静态方法返回实例
public static Singleton getInstance() {
return singleton;
}
}

这里类的实例在类初始化的时候已经生成,不再进行第二次实例化了,而外界只能通过SingleCase.getInstance()方法来获取SingleCase对象, 所以这样就保证整个系统只能获取一个类的对象实例。

单例模式的两种实现模式

饿汉模式:就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。

1
2
优点:简单
缺点:可能会导致进程启动慢,且如果有多个单例类对象实例启动顺序不确定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Singleton
{
public:
static Singleton* GetInstance()
{
return &m_instance;
}
private:
// 构造函数私有
Singleton(){};
// C++98 防拷贝
Singleton(Singleton const&);
Singleton& operator=(Singleton const&);
// or
// C++11
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton m_instance;
};
Singleton Singleton::m_instance; // 在程序入口之前就完成单例对象的初始化

如果单例对象构造十分耗时或者占用很多资源,比如加载插件啊, 初始化网络连接啊,读取文件啊等等,而有可能该对象程序运行时不会用到,那么也要在程序一开始就进行初始化,就会导致程序启动时非常的缓慢。 所以这种情况使用懒汉模式(延迟加载)更好。


懒汉模式:等到用的的时候程序再创建实例对象

1
2
优点:第一次使用实例对象时,创建对象。进程启动无负载。多个单例实例启动顺序自由控制。
缺点:复杂
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
36
37
38
39
40
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
class Singleton
{
public:
static Singleton* GetInstance() {
// 注意多线程环境下一定要使用Double-Check的方式加锁,才能保证效率和线程安全
if (nullptr == m_pInstance) {
m_mtx.lock();
if (nullptr == m_pInstance) {
m_pInstance = new Singleton();
}
m_mtx.unlock();
}
return m_pInstance;
}
// 实现一个内嵌垃圾回收类
class CGarbo {
public:
~CGarbo(){
if (Singleton::m_pInstance)
delete Singleton::m_pInstance;
}
};
// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
static CGarbo Garbo;
private:
// 构造函数私有
Singleton(){};
// 防拷贝
Singleton(Singleton const&);
Singleton& operator=(Singleton const&);
static Singleton* m_pInstance; // 单例对象指针
static mutex m_mtx; //互斥锁
};
Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Garbo;
mutex Singleton::m_mtx;

添加一个类的静态对象,总是让人不太满意,所以有人用如下方法来重新实现单例和解决它相应的问题,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
}
CSingleton(const CSingleton &);
CSingleton & operator = (const CSingleton &);
public:
static CSingleton * GetInstance()
{
static CSingleton instance; //局部静态变量,在这个局部静态函数销毁才销毁,也就是当程序结束才销毁
return &instance;//不管怎么getinstance,都只定义一次instance,返回的都是同一个实例
}
};

使用局部静态变量是非常强大的方法,完全实现了单例的特性,而且代码量更少,也不用担心单例销毁的问题。

sql数据库连接

头文件

数据库连接的头文件声明了很多信息,我们先分析头文件的逻辑,再去看定义的实现

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#ifndef _CONNECTION_POOL_
#define _CONNECTION_POOL_

#include <stdio.h>
#include <list>
#include <mysql/mysql.h>
#include <error.h>
#include <string.h>
#include <iostream>
#include <string>
#include "../lock/locker.h"
#include "../log/log.h"

using namespace std;

class connection_pool
{
public:
MYSQL *GetConnection(); //获取数据库连接
bool ReleaseConnection(MYSQL *conn); //释放连接
int GetFreeConn(); //获取空闲连接数目
void DestroyPool(); //销毁所有连接

//单例模式
static connection_pool *GetInstance();

void init(string url, string User, string PassWord, string DataBaseName, int Port, int MaxConn, int close_log);

private:
connection_pool();
~connection_pool();

int m_MaxConn; //最大连接数
int m_CurConn; //当前已使用的连接数
int m_FreeConn; //当前空闲的连接数
locker lock;//互斥锁
list<MYSQL *> connList; //连接池
sem reserve;//信号量

public:
string m_url; //主机地址
string m_Port; //数据库端口号
string m_User; //登陆数据库用户名
string m_PassWord; //登陆数据库密码
string m_DatabaseName; //使用数据库名
int m_close_log; //日志开关
};

class connectionRAII{

public:
//双指针对MYSQL *con修改。数据库连接本身是指针类型,所以参数需要通过双指针才能对其进行修改。
connectionRAII(MYSQL **con, connection_pool *connPool);
~connectionRAII();

private:
MYSQL *conRAII;//这个RAII类拥有一个MYSQL连接
connection_pool *poolRAII;//且要有一个连接池指针指向那个单例对象,调用释放函数把MYSQL连接释放
};

#endif

头文件里主要是对connection_pool这个类的功能进行了声明:

  • 四种主要功能:获取数据库连接、获取空闲连接数目、释放连接、销毁所有连接。四个功能函数实际上从返回值就可以看出区别和要做什么事。
  • 数据库初始化init函数,它通过地址、端口、用户名密码、使用的数据库名称来进行数据库的连接。
  • 单例模式,把构造函数放private,使得只能用静态成员函数在类中创建类对象;把析构函数放private,使得无法在外部delete类对象,只能用内部的成员函数delete this,因为内部成员函数才能访问私有的析构函数。不过这里的单例模式不用new,因此也就没有对应的delete函数。

还有一个connectionRAII类,这个类对连接池对象进行RAII式的管理,前面可以看到有个释放连接的功能,我们不想手动释放,就可以在这个类的析构函数里释放,具体看实现就好了。

  • 为什么con是双指针,可以参考c/c++向函数传递指针并修改其指向的问题_AlanChaw292的博客-CSDN博客_c++改变指针指向。大概的意思就是,如果是单指针传进来,编译器也会为形参做一个备份,如传入一个p,会备份一个p1(我们实际上使用的是p1,跟值传递是一个意思),p和p1的值相同,都指向对象的地址。我们当然可以使用p1来修改指向的值,但无法通过修改p1修改p(就像形参无法影响实参),也就是说传入的指针不能修改指针本身的地址(不能修改指针的指向,不是不允许,而是没意义)。这种时候,就需要用双指针,指向我们想修改的指针的地址,这样就行了,那篇博客讲的很清楚。

.cpp实现

话不多说,先上源码

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#include <mysql/mysql.h>
#include <stdio.h>
#include <string>
#include <string.h>
#include <stdlib.h>
#include <list>
#include <pthread.h>
#include <iostream>
#include "sql_connection_pool.h"

using namespace std;

connection_pool::connection_pool()
{ //类内成员初始化
m_CurConn = 0;//已使用的是0
m_FreeConn = 0;//空闲的还不知道,但是没有init时就是0
}

connection_pool *connection_pool::GetInstance()//静态成员函数,单例模式
{
static connection_pool connPool;//创建静态的连接池对象,只定义一次,每次调用都返回它
return &connPool;//且是通过指针(地址)返回,不会导致拷贝构造
//这个静态对象销毁是在静态成员函数销毁时销毁,而这个函数在程序结束才销毁...
}

//构造初始化
void connection_pool::init(string url, string User, string PassWord, string DBName, int Port, int MaxConn, int close_log)
{
//给类成员赋值,这些类成员是为了以后访问连接池对象可以获取信息

m_url = url;
m_Port = Port;
m_User = User;
m_PassWord = PassWord;
m_DatabaseName = DBName;
m_close_log = close_log;
m_MaxConn = m_FreeConn;

for (int i = 0; i < MaxConn; i++)//一共(最多)可以有maxconn个连接
{
MYSQL *con = NULL;
con = mysql_init(con);//分配并初始化一个新对象

if (con == NULL)//NULL说明没有足够的内存分配
{
LOG_ERROR("MySQL Error");
exit(1);
}
//前面的init初始化了一个mysql的数据结构,现在real connect进行真正的连接
con = mysql_real_connect(con, url.c_str(), User.c_str(), PassWord.c_str(), DBName.c_str(), Port, NULL, 0);

if (con == NULL)//连接失败返回NULL
{
LOG_ERROR("MySQL Error");
exit(1);
}
connList.push_back(con);//成功则在连接池(list)里添加
++m_FreeConn;//空闲连接+1
}

reserve = sem(m_FreeConn);//给这个信号量赋值,实际上可以在for循环里post,不过逻辑有点怪就是了
}


//当有请求时,从数据库连接池中返回一个可用连接,更新使用和空闲连接数
MYSQL *connection_pool::GetConnection()
{
MYSQL *con = NULL;//创建一个指针,将要指向连接池已经创建的连接

if (0 == connList.size())//没有就没有了,不阻塞
return NULL;

reserve.wait();//有的话就让信号量减一,不过既然前面return了,不阻塞了还有信号量干啥嘞

lock.lock();//互斥访问这个连接,修改连接池(连接池是共享的),以及互斥修改一些表示buffer大小数据

con = connList.front();
connList.pop_front();

--m_FreeConn;//连接池容量buffer-1
++m_CurConn;//连接池buffer使用+1

lock.unlock();//解锁
return con;
}

//释放当前使用的连接
bool connection_pool::ReleaseConnection(MYSQL *con)
{
if (NULL == con)
return false;

lock.lock();//回收连接,放回连接池,既然访问连接池这个公共资源,要互斥锁住

connList.push_back(con);//放回
++m_FreeConn;
--m_CurConn;

lock.unlock();

reserve.post();//信号量+1,越发感觉信号量和freeconn是一个东西?以及connlist.size()...
return true;
}

//销毁数据库连接池
void connection_pool::DestroyPool()
{

lock.lock();//主线程要关闭连接池,要等连接池操作完再说,不然在销毁过程中可能又同时放回,会混乱
//且其他线程在获取连接时,也要等连接池销毁的操作,不然连接池都销毁了还拿到了一个连接
if (connList.size() > 0)
{
list<MYSQL *>::iterator it;
for (it = connList.begin(); it != connList.end(); ++it)
{
MYSQL *con = *it;
mysql_close(con);//一个一个关闭掉,但mysql对象、结构还在list里
}
m_CurConn = 0;//这些遍历的修改也要在临界区嘛
m_FreeConn = 0;
connList.clear();//移除所有元素,把那些关闭了的连接都删掉
}

lock.unlock();
}

//当前空闲的连接数
int connection_pool::GetFreeConn()
{
return this->m_FreeConn;//这个就不锁了,没什么意义,就放回“当下”的值就好了。
}

connection_pool::~connection_pool()
{
DestroyPool();//析构连接池
}


//这个RAII类是针对单个sql连接的,具体怎么使用还要看实际代码,
//注意这个双指针修改sql连接本身的值(指向连接的地址),使这个连接可以更改指向,(可能从null)指向连接池的可用的连接。
connectionRAII::connectionRAII(MYSQL **SQL, connection_pool *connPool){
*SQL = connPool->GetConnection();

conRAII = *SQL;//这个RAII类本身也要存一个备份,使得调用析构函数释放连接时知道要释放的连接的地址
poolRAII = connPool;
}

connectionRAII::~connectionRAII(){
poolRAII->ReleaseConnection(conRAII);//析构函数:释放连接
}
  • MYSQL *mysql_init(MYSQL *mysql)
    • 分配或初始化与mysql_real_connect()相适应的MYSQL对象。如果mysql是NULL指针,该函数将分配、初始化、并返回新对象。否则,将初始化对象,并返回对象的地址。如果mysql_init()分配了新的对象,当调用mysql_close()来关闭连接时。将释放该对象。
    • 返回值:初始化的MYSQL*句柄。如果无足够内存以分配新的对象,返回NULL。
  • MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd, const char *db, unsigned int port, const char *unix_socket, unsigned long client_flag)
    • mysql_real_connect()尝试与运行在主机上的MySQL数据库引擎建立连接。在你能够执行需要有效MySQL连接句柄结构的任何其他API函数之前,mysql_real_connect()必须成功完成。
    • 参数:
      • 第1个参数应是已有MYSQL结构的地址。调用mysql_real_connect()之前,必须调用mysql_init()来初始化MYSQL结构。通过mysql_options()调用,可更改多种连接选项。
      • “host”的值必须是主机名或IP地址。如果“host”是NULL或字符串”localhost”,连接将被视为与本地主机的连接。如果操作系统支持套接字(Unix)或命名管道(Windows),将使用它们而不是TCP/IP连接到服务器。
      • “user”参数包含用户的MySQL登录ID。如果“user”是NULL或空字符串””,用户将被视为当前用户。在UNIX环境下,它是当前的登录名。在Windows ODBC下,必须明确指定当前用户名。
      • “passwd”参数包含用户的密码。如果“passwd”是NULL,仅会对该用户的(拥有1个空密码字段的)用户表中的条目进行匹配检查。这样,数据库管理员就能按特定的方式设置MySQL权限系统,根据用户是否拥有指定的密码,用户将获得不同的权限。
      • “db”是数据库名称。如果db为NULL,连接会将默认的数据库设为该值。
      • 如果“port”不是0,其值将用作TCP/IP连接的端口号。注意,“host”参数决定了连接的类型。
      • 如果unix_socket不是NULL,该字符串描述了应使用的套接字或命名管道。注意,“host”参数决定了连接的类型。
      • client_flag的值通常为0,其他标志可以实现特定的功能
    • 返回值:如果连接成功,返回MYSQL*连接句柄。如果连接失败,返回NULL。对于成功的连接,返回值与第1个参数的值相同。
  • void mysql_close(MYSQL *mysql)
    • 关闭前面打开的连接。如果句柄是由mysql_init()或mysql_connect()自动分配的,mysql_close()还将解除分配由mysql指向的连接句柄。
  • string.c_str():
    • const char *c_str();
    • c_str()函数返回一个指向正规C字符串的指针常量, 内容与本string串相同。
    • 这是为了与c语言兼容,在c语言中没有string类型,故必须通过string类对象的成员函数c_str()把string 对象转换成c中的字符串样式。

第三站

日志系统

这里可以看看作者的讲解先理解一下:

最新版Web服务器项目详解 - 09 日志系统(上) (qq.com)

最新版Web服务器项目详解 - 10 日志系统(下) (qq.com)

阻塞队列

写入日志有同步的写入和异步的写入方式,同步的方式是当产生日志时就写入,主线程工作推迟;异步的写入方式是使用一个日志线程来管理,“写入日志”这个任务就需要有地方放,因此就要用一个阻塞队列来存放任务。为什么是阻塞的呢,日志线程有多个吗?实际上日志线程只有一个,但其他线程可以有多个,这就是一个多生产者–单消费者的模型。因此常规的解法就是用互斥锁+buffer信号量的组合。但这个阻塞队列还添加了超时处理的功能,信号量就需要改成条件变量,条件变量我没怎么使用过,之后回过头再整理一下,不过具体的功能在lock那章中写了。

下面是阻塞队列的代码,在头文件block_queue.h中

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/*************************************************************
*循环数组实现的阻塞队列,m_back = (m_back + 1) % m_max_size;
*线程安全,每个操作前都要先加互斥锁,操作完后,再解锁
**************************************************************/

#ifndef BLOCK_QUEUE_H
#define BLOCK_QUEUE_H

#include <iostream>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h>
#include "../lock/locker.h"
using namespace std;

template <class T>
class block_queue
{
public:
block_queue(int max_size = 1000)
{
if (max_size <= 0)
{
exit(-1);
}
//初始化
m_max_size = max_size;
m_array = new T[max_size];
m_size = 0;
m_front = -1;
m_back = -1;
}
//剩下的操作,涉及对队列内部元素的操作(插入删除)、对队列变量的访问(size,头尾指针等),都需要互斥访问
void clear()
{
m_mutex.lock();
m_size = 0;
m_front = -1;
m_back = -1;
m_mutex.unlock();
}

~block_queue()
{
m_mutex.lock();
if (m_array != NULL)//少见...不过健壮(也许多余?)
delete [] m_array;

m_mutex.unlock();
}
//判断队列是否满了
bool full()
{
m_mutex.lock();//访问msize,要锁
if (m_size >= m_max_size)
{

m_mutex.unlock();
return true;
}
m_mutex.unlock();
return false;
}
//判断队列是否为空
bool empty()
{
m_mutex.lock();//访问msize,要锁
if (0 == m_size)
{
m_mutex.unlock();
return true;
}
m_mutex.unlock();
return false;
}
//返回队首元素
bool front(T &value)//以参数形式返回
{
m_mutex.lock();
if (0 == m_size)
{
m_mutex.unlock();
return false;
}
value = m_array[m_front];
m_mutex.unlock();
return true;
}
//返回队尾元素
bool back(T &value)
{
m_mutex.lock();
if (0 == m_size)
{
m_mutex.unlock();
return false;
}
value = m_array[m_back];
m_mutex.unlock();
return true;
}

int size()
{
int tmp = 0;//不直接return,因为要加锁,return不能放锁里

m_mutex.lock();
tmp = m_size;

m_mutex.unlock();
return tmp;
}

int max_size()
{
int tmp = 0;

m_mutex.lock();
tmp = m_max_size;

m_mutex.unlock();
return tmp;
}


//往队列添加元素,需要将所有使用队列的线程先唤醒,这些线程除了等待锁,还要等待任务出现以pop,因此push要唤醒它们
//在应用上,调用pop的就一个日志线程

//当有元素push进队列,相当于生产者生产了一个元素
//若当前没有线程等待条件变量,则唤醒无意义
bool push(const T &item)
{

m_mutex.lock();
if (m_size >= m_max_size)//队列满了,赶紧让pop线程做事
{

m_cond.broadcast();//唤醒所有在wait的线程
//在wait说明之前队列空了,但怎么会从空->满呢?可能是一直被push抢了互斥锁
//因此这个唤醒让那些卡在while的pop从wait解放,然后一个一个等待抢占互斥锁做事(和pop抢也和push抢)
m_mutex.unlock();
return false;
}

m_back = (m_back + 1) % m_max_size;
m_array[m_back] = item;

m_size++;

m_cond.broadcast();
m_mutex.unlock();
return true;
}
//pop时,如果当前队列没有元素,将会等待条件变量
bool pop(T &item)
{

m_mutex.lock();//条件变量在临界区用,wait自身会解锁-等待唤醒-抢占锁
//pop被多个线程调用,前面push都唤醒了那么这里会竞争任务,可能只有一部分线程执行了这个m_size就=0了
//那么此时就要继续等待,因此用while而不是用if,if只能wait一次(这种情况是虚假唤醒)
while (m_size <= 0)
{

if (!m_cond.wait(m_mutex.get()))//等待唤醒
{
m_mutex.unlock();
return false;//wait出错就return
}
}
//条件变量被唤醒,抢到了互斥锁,且whlie正常退出,开始做事
m_front = (m_front + 1) % m_max_size;
item = m_array[m_front];
m_size--;
m_mutex.unlock();
return true;
}

//增加了超时处理
bool pop(T &item, int ms_timeout)//可以超时多少毫秒
{
//时间类下面介绍
struct timespec t = {0, 0};//一个秒,一个纳秒
struct timeval now = {0, 0};//一个秒,一个微秒
gettimeofday(&now, NULL);//获取系统当前时间
m_mutex.lock();
//如果要等待的话,就进去wait,注意这里不是一直等到可以调用,所以不用while
//如果超时就到下面的if返回,如果没超时就被唤醒,那么会有虚假唤醒的情况,
//因此下面还要if判断一下,虚假唤醒就直接返回,所以这里超时就不做、虚假唤醒也不做
if (m_size <= 0)
{
//t是前面获取的时间加上超时的时间
t.tv_sec = now.tv_sec + ms_timeout / 1000;//取秒位
t.tv_nsec = (ms_timeout % 1000) * 1000;//剩下没取到的毫秒(余数)弄成纳秒(为什么是*1000)
//整体时间计算是秒+纳秒
if (!m_cond.timewait(m_mutex.get(), t))//时间到了就不等待唤醒了,直接润
{
m_mutex.unlock();
return false;
}
}
//上面润完就到这里,注意因为抢了锁,所以不可能有push,这里一定是返回的
if (m_size <= 0)
{
m_mutex.unlock();
return false;
}
//正常干活
m_front = (m_front + 1) % m_max_size;
item = m_array[m_front];
m_size--;
m_mutex.unlock();
return true;
}

private:
locker m_mutex;//互斥锁
cond m_cond;//条件变量

T *m_array;//队列空间
int m_size;//队列目前大小
int m_max_size;//队列大小,使用这个变量就无需额外留一个空间
int m_front;//队列头部
int m_back;//队列尾部
};

#endif

在C语言中可以使用函数gettimeofday()函数来得到精确时间。它的精度可以达到微妙,是C标准库的函数。

在gettimeofday()函数中tv或者tz都可以为空。如果为空则就不返回其对应的结构体。

函数执行成功后返回0,失败后返回-1,错误代码存于errno中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<sys/time.h>

int gettimeofday(struct timeval*tv,struct timezone *tz )

struct timeval{

long tv_sec; /*秒*/

long tv_usec; /*微妙*/

};

struct timezone{

int tz_minuteswest;/*和greenwich 时间差了多少分钟*/

int tz_dsttime; /*type of DST correction*/

}

说明:在使用gettimeofday()函数时,第二个参数一般都为空,因为我们一般都只是为了获得当前时间,而不用获得timezone的数值。

头文件

定义了Log类,其中使用宏来为其他程序提供接口。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#ifndef LOG_H
#define LOG_H

#include <stdio.h>
#include <iostream>
#include <string>
#include <stdarg.h>//与宏有关
#include <pthread.h>
#include "block_queue.h"

using namespace std;

class Log
{
public:
//C++11以后,使用局部变量懒汉不用加锁
static Log *get_instance()
{
static Log instance;
return &instance;
}

static void *flush_log_thread(void *args)//是一个worker函数
{
Log::get_instance()->async_write_log();//静态成员函数的调用:A::func()
}
//可选择的参数有日志文件、日志缓冲区大小、最大行数以及最长日志条队列
bool init(const char *file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0);

void write_log(int level, const char *format, ...);

void flush(void);

private:
Log();
virtual ~Log();
void *async_write_log()//write_log执行push任务的功能,这个函数只取任务写到文件
{
string single_log;
//从阻塞队列中取出一个日志string,写入文件
while (m_log_queue->pop(single_log))//取是互斥的,写也是互斥的,但是两个锁并不相同
{
m_mutex.lock();//写入m_fp中,共享的文件空间的要锁一下
fputs(single_log.c_str(), m_fp);
m_mutex.unlock();
}
}

private:
char dir_name[128]; //路径名
char log_name[128]; //log文件名
int m_split_lines; //日志最大行数
int m_log_buf_size; //日志缓冲区大小
long long m_count; //日志行数记录
int m_today; //因为按天分类,记录当前时间是那一天
FILE *m_fp; //打开log的文件指针
char *m_buf;
block_queue<string> *m_log_queue; //阻塞队列
bool m_is_async; //是否同步标志位
locker m_mutex;
int m_close_log; //关闭日志
};
//宏接口,调用write_log和flush
#define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}

#endif

.cpp实现

主要是一些string的操作,因为写日志就是把字符写入文件嘛

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <stdarg.h>
#include "log.h"
#include <pthread.h>
using namespace std;

Log::Log()
{
m_count = 0;//每次行数重置为0,包括天数的记录也会重置,所以如果关闭了的话前面的记录就不存在,重复写一个日志文件就可能出错,因此如果关掉程序再打开的话,最好换一个文件重新开始写
m_is_async = false;
}

Log::~Log()
{
if (m_fp != NULL)
{
fclose(m_fp);
}
}
//异步需要设置阻塞队列的长度,同步不需要设置
bool Log::init(const char *file_name, int close_log, int log_buf_size, int split_lines, int max_queue_size)
{
//如果设置了max_queue_size,则设置为异步,只有一个线程在取任务处理
if (max_queue_size >= 1)
{
m_is_async = true;//设置写入方式flag
m_log_queue = new block_queue<string>(max_queue_size);//创建并设置阻塞队列长度
pthread_t tid;
//flush_log_thread为回调函数,这里表示创建线程异步写日志
pthread_create(&tid, NULL, flush_log_thread, NULL);
}
//成员初始化
m_close_log = close_log;//1的话关闭日志功能
m_log_buf_size = log_buf_size;//缓冲区大小
m_buf = new char[m_log_buf_size];//缓冲区
memset(m_buf, '\0', m_log_buf_size);//缓冲区数值初始化
m_split_lines = split_lines;//最大行数

//见后面,实际上就是得到具体的本地的时间,年月日时分秒等等
time_t t = time(NULL);
struct tm *sys_tm = localtime(&t);
struct tm my_tm = *sys_tm;

//从后往前找到第一个/的位置
const char *p = strrchr(file_name, '/');//该函数见后面
char log_full_name[256] = {0};//接下来要生成一个具体的日志文件名


//接下来相当于自定义日志名
//若输入的文件名没有/,则直接将时间+文件名作为日志名
if (p == NULL)
{
snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name);//该函数见后面
//下面两行是我自己分析觉得要加上的地方,否则创建新日志的名字可能不协同
dir_name = "";
log_name = file_name;
}
//如果有的话,就是一个路径了,就要从/后面开始添加时间
else
{
//将/的位置向后移动一个位置,然后复制到logname中
//p - file_name + 1是文件所在路径文件夹的长度
strcpy(log_name, p + 1);//存一下log_name
strncpy(dir_name, file_name, p - file_name + 1);
snprintf(log_full_name, 255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name);//dirname相当于./,这里就比上面多一个路径名,不过把filename拆分为dirname和logname,补个时间
}

m_today = my_tm.tm_mday;//更新日期

m_fp = fopen(log_full_name, "a");//根据上面的一系列操作获得的名称打开文件或创建文件
//a表示追加到一个文件。写操作向文件末尾追加数据。如果文件不存在,则创建文件。
if (m_fp == NULL)
{
return false;
}

return true;
}


void Log::write_log(int level, const char *format, ...)//可变参数
{
//获取具体时间
struct timeval now = {0, 0};
gettimeofday(&now, NULL);//返回当前距离1970年的秒数和微妙数
time_t t = now.tv_sec;//取得从1970年1月1日至今的秒数。
struct tm *sys_tm = localtime(&t);//将time_t表示的时间转换为经过时区转换的UTC时间
struct tm my_tm = *sys_tm;

char s[16] = {0};//日志类型标头
switch (level)//日志分级
{
case 0:
strcpy(s, "[debug]:");
break;
case 1:
strcpy(s, "[info]:");
break;
case 2:
strcpy(s, "[warn]:");
break;
case 3:
strcpy(s, "[erro]:");
break;
default:
strcpy(s, "[info]:");
break;
}
//写入一个log,对m_count++
m_mutex.lock();//m_count和m_fp是共享的,要用锁修改,这就表明上面的时间是调用的时间而不是写的时间,因为锁要阻塞耗时
m_count++;//先++,因为是从0开始的,++后判断是否到最大行数了


//日志不是今天或写入的日志行数是最大行的倍数,这个时候要新换一个日志文件
//m_split_lines为最大行数
if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0)
{

char new_log[256] = {0};//日志内容
fflush(m_fp);//把缓冲区的内容强制写入文件,准备换新文件了
fclose(m_fp);//关闭
char tail[16] = {0};//时间信息

snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);//02表示月份和日期以两位数的形式呈现

if (m_today != my_tm.tm_mday)//新的一天,换一个文件
{
snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);//这里可能有点问题,因为dirname和logname不一定有,如果前面p是NULL的话,那么新的文件就只有日期了,前面最好更新一个logname
m_today = my_tm.tm_mday;
m_count = 0;
}
else//这一天的日志行数太多了,要分文件,m_count / m_split_lines表示这是第几份
{
snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines);
}
m_fp = fopen(new_log, "a");//打开新文件,把日志系统当前写入的文件更换
}

m_mutex.unlock();

va_list valst;//解决变参问题的宏,下面介绍
va_start(valst, format);//初始化,指向第一个参数地址

string log_str;
//接下来开始写内容
m_mutex.lock();//写缓冲区,要锁

//写入的具体时间内容格式
int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ",
my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,
my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);//前面分级的s在这里出现,它是内容开头

//时间、级别都写进缓冲区之后,把内容写入,内容就是可变参数,通过valst写入
int m = vsnprintf(m_buf + n, m_log_buf_size - 1, format, valst);//该函数后面介绍
m_buf[n + m] = '\n';//添加一个换行
m_buf[n + m + 1] = '\0';//这一个缓冲区字符串结束
log_str = m_buf;//变成string

m_mutex.unlock();

if (m_is_async && !m_log_queue->full())//如果是异步的且阻塞队列有空间
{
m_log_queue->push(log_str);//把写的任务推入队列,参数就是要写的全部内容,不执行写的功能
}
else//同步的话或者阻塞队列已经满了就直接写
{
m_mutex.lock();//互斥写入文件中
fputs(log_str.c_str(), m_fp);
m_mutex.unlock();
}

va_end(valst);//清空参数列表
}

void Log::flush(void)
{
m_mutex.lock();
//强制刷新写入流缓冲区
fflush(m_fp);
m_mutex.unlock();
}

涉及到的与时间类相关的知识:

  • struct tm *localtime(const time_t *timer)

  • timer – 这是指向表示日历时间的 time_t 值的指针。

  • C 库函数 struct tm *localtime(const time_t *timer) 使用 timer 的值来填充 tm 结构。timer 的值被分解为 tm 结构,并用本地时区表示。

  • 该函数返回指向 tm 结构的指针,该结构带有被填充的时间信息。下面是 tm 结构的细节:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct tm {
    int tm_sec; /* 秒,范围从 0 到 59 */
    int tm_min; /* 分,范围从 0 到 59 */
    int tm_hour; /* 小时,范围从 0 到 23 */
    int tm_mday; /* 一月中的第几天,范围从 1 到 31 */
    int tm_mon; /* 月份,范围从 0 到 11 */
    int tm_year; /* 自 1900 起的年数 */
    int tm_wday; /* 一周中的第几天,范围从 0 到 6 */
    int tm_yday; /* 一年中的第几天,范围从 0 到 365 */
    int tm_isdst; /* 夏令时 */
    };
  • char *strrchr(const char *str, int c)

  • C 库函数 char *strrchr(const char *str, int c) 在参数 str 所指向的字符串中搜索最后一次出现字符 c(一个无符号字符)的位置。

  • 该函数返回 str 中最后一次出现字符 c 的位置。如果未找到该值,则函数返回一个空指针。

  • int snprintf ( char * str, size_t size, const char * format, … );

  • C 库函数 int snprintf(char *str, size_t size, const char *format, …) 设将可变参数**(…)按照 format 格式化成字符串,并将字符串复制到 str 中,size** 为要写入的字符的最大数目,超过 size 会被截断。

  • 返回值

    • 1、如果格式化后的字符串长度小于 size,则会把字符串全部复制到 str 中,并给其后添加一个字符串结束符 \0
    • 2、如果格式化后的字符串长度大于等于 size,超过 size 的部分会被截断,只将其中的 (size-1) 个字符复制到 str 中,并给其后添加一个字符串结束符 \0,返回值为欲写入的字符串长度。
  • VA_LIST 解决变参问题的一组宏,所在头文件:#include <stdarg.h>,用于获取不确定个数的参数,这种获取是根据参数类型对应的大小,找到对应的内存地址,然后获取参数来实现的

  • typedef char *va_list;
    
    获取可变参数列表的第一个参数的地址(list是类型为va_list的指针,param1是可变参数最左边的参数)
    #define va_start(list,param1) 
    
    获取可变参数的当前参数,返回指定类型并将指针指向下一参数(mode参数描述了当前参数的类型)
    #define va_arg(list,mode)
    
    清空va_list可变参数列表
    #define va_end(list)
    
    1
    2
    3
    4
    5
    6
    7

    * ```
    va_list的使用方法:
    a) 首先在函数中定义一个具有va_list型的变量,这个变量是指向参数的指针。
    b) 然后用va_start宏初始化变量刚定义的va_list变量,使其指向第一个可变参数的地址。
    c) 然后va_arg返回可变参数,va_arg的第二个参数是你要返回的参数的类型(如果多个可变参数,依次调用va_arg获取各个参数)。
    d) 最后使用va_end宏结束可变参数的获取。
  • int vsnprintf (char * s, size_t n, const char * format, va_list arg );

  • 将格式化的数据从变量参数列表写入大小已设置的缓冲区

  • 参数

    • s

      指向存储结果C-string的缓冲区的指针。 缓冲区的大小至少应为n字符。

    • n

      缓冲区中要使用的最大字节数。 生成的字符串的长度最大为n-1,为其他终止空字符留出空间。

    • format

      包含格式字符串的C字符串,其格式与prinf相同。

    • arg

      一个值,该值标识用初始化的变量参数列表。

  • 返回值

    • 成功打印到sbuf中的字符的个数,不包括末尾追加的\0。如果格式化解析失败,则返回负数。

第四站

http——前置知识

这部分内容很多,耐心些,别急

作者写的三篇介绍:

最新版Web服务器项目详解 - 04 http连接处理(上) (qq.com)

最新版Web服务器项目详解 - 05 http连接处理(中) (qq.com)

最新版Web服务器项目详解 - 06 http连接处理(下) (qq.com)

epoll

epoll是linux新内核中替换select来做事件触发的机制,效率非常高,底层使用红黑树实现。这篇博客讲的非常清楚,强烈推荐:epoll使用详解(精髓)_ljx0305的博客-CSDN博客_epoll。下面简单介绍下API,头文件#include <sys/epoll.h>

  • int epoll_create(int size)

    • 创建一个指示epoll内核事件表的文件描述符,该描述符将用作其他epoll系统调用的第一个参数,size不起作用。(从Linux 2.6.8开始,max_size参数将被忽略,但必须大于零。)
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

    • 该函数用于操作内核事件表监控的文件描述符上的事件:注册、修改、删除

    • epfd:为epoll_creat的句柄

    • op:表示动作,用3个宏来表示:

      • EPOLL_CTL_ADD (注册新的fd到epfd),相当于把fd加到epfd这棵红黑树上
      • EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
      • EPOLL_CTL_DEL (从epfd删除一个fd);
    • fd:文件描述符

    • event:告诉内核需要监听的事件,结构如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      typedef union epoll_data {
      void *ptr;
      int fd;
      __uint32_t u32;
      __uint64_t u64;
      } epoll_data_t;

      struct epoll_event {
      __uint32_t events; /* Epoll events,是一串比特,设置类型时把类型或起来 */
      epoll_data_t data; /* User data variable */
      };
    • events描述事件类型,其中epoll事件类型有以下几种

      • EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
      • EPOLLOUT:表示对应的文件描述符可以写
      • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
      • EPOLLERR:表示对应的文件描述符发生错误
      • EPOLLHUP:表示对应的文件描述符被挂断;
      • EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
      • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
  • int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)

    • 该函数用于等待所监控文件描述符上有事件的产生,返回就绪的文件描述符个数

      • events:用来存内核得到事件的集合,

      • maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,

      • timeout:是超时时间

        • -1:阻塞
        • 0:立即返回,非阻塞
        • >0:指定毫秒
      • 返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1

触发模式:

  • LT水平触发模式

    • 当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll还会再次向应用程序通知此事件,直到该事件被处理完毕。
  • ET边缘触发模式

    • 当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。
    • 必须要一次性将数据读取完,使用非阻塞I/O,读取到出现eagain
  • ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,故效率要比LT模式高。LT模式是epoll的默认工作模式

  • EPOLLONESHOT

    • 一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket
    • 我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件

项目中epoll相关代码部分包括非阻塞模式、内核事件表注册事件、删除事件、重置EPOLLONESHOT事件四种。

  • 非阻塞模式

  • //对文件描述符设置非阻塞
    int setnonblocking(int fd)
    {
        int old_option = fcntl(fd, F_GETFL);//返回fd的状态标志,是一串比特位
        int new_option = old_option | O_NONBLOCK;//设置非阻塞的比特位,把前面获得的flag和它或起来就可以了
        fcntl(fd, F_SETFL, new_option);//重新设置
        return old_option;
    }
    /*
    阻塞方式是文件读写操作的默认方式,但是应用程序员可通过使用O_NONBLOCK 标志来人为
    的设置读写操作为非阻塞方式 .( 该标志定义在 < linux/fcntl.h > 中,在打开文件时指定 ) .
     
    如果设置了 O_NONBLOCK 标志,read 和 write 的行为是不同的 ,如果进程没有数据就绪时调用了 read ,
    或者在缓冲区没有空间时调用了 write ,系统只是简单的返回 EAGAIN,而不会阻塞进程.
    */
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性

    ```c++
    #include<unistd.h>
    #include<fcntl.h>
    int fcntl(int fd, int cmd);
    int fcntl(int fd, int cmd, long arg);
    int fcntl(int fd, int cmd ,struct flock* lock);
    fcntl函数功能依据cmd的值的不同而不同。参数对应功能如下:
    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
    (1)F_DUPFD
    与dup函数功能一样,复制由fd指向的文件描述符,调用成功后返回新的文件描述符,与旧的文件描述符共同指向同一个文件。

    (2)F_GETFD
    读取文件描述符close-on-exec标志。
    close_on_exec 是一个进程所有文件描述符(文件句柄)的位图标志,每个比特位代表一个打开的文件描述符,用于确定在调用系统调用execve()时需要关闭的文件句柄(参见include/fcntl.h)。当一个程序使用fork()函数创建了一个子进程时,通常会在该子进程中调用execve()函数加载执行另一个新程序。此时子进程将完全被新程序替换掉,并在子进程中开始执行新程序。若一个文件描述符在close_on_exec中的对应比特位被设置,那么在执行execve()时该描述符将被关闭,否则该描述符将始终处于打开状态。
    试想一下这样的场景:在Webserver中,首先会使用root权限启动,以此打开root权限才能打开的端口、日志等文件。然后降权到普通用户,fork出一些worker进程,这些进程中再进行解析脚本、写日志、输出结果等进一步操作。
    然而这里,就会发现隐含一个安全问题:子进程中既然继承了父进程的FD,那么子进程中运行的脚本只需要继续操作这些FD,就能够使用普通权限“越权”操作root用户才能操作的文件。

    (3)F_SETFD
    将文件描述符close-on-exec标志设置为第三个参数arg的最后一位

    (4)F_GETFL
    获取文件打开方式的标志,标志值含义与open调用一致

    (5)F_SETFL
    设置文件打开方式标志为arg指定方式

    (6)F_SETLK
    此时fcntl函数用来设置或释放锁。当short_l_type为F_RDLCK为读锁,F_WDLCK为写锁,F_UNLCK为解锁。
    如果锁被其他进程占用,则返回-1;
    这种情况设的锁遇到锁被其他进程占用时,会立刻停止进程。

    (7)F_SETLKW
    此时也是给文件上锁,不同于F_SETLK的是,该上锁是阻塞方式。当希望设置的锁因为其他锁而被阻止设置时,该命令会等待相冲突的锁被释放。

    (8)F_GETLK
    第3个参数lock指向一个希望设置的锁的属性结构,如果锁能被设置,该命令并不真的设置锁,而是只修改lock的l_type为F_UNLCK,然后返回该结构体。如果存在一个或多个锁与希望设置的锁相互冲突,则fcntl返回其中的一个锁的flock结构。
    返回值:与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列三个命令有特定返回值:F_DUPFD,F_GETFD,F_GETFL以及F_GETOWN。第一个返回新的文件描述符,第二个返回相应标志,最后一个返回一个正的进程ID或负的进程组ID。
  • 内核事件表注册新事件,开启EPOLLONESHOT,针对客户端连接的描述符,listenfd不用开启

  •  void addfd(int epollfd, int fd, bool one_shot)
     {
         epoll_event event;
         event.data.fd = fd;
     
     #ifdef ET
         event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;//告知要监听可读事件、文件描述符被挂断事件
     #endif
     
    #ifdef LT
        event.events = EPOLLIN | EPOLLRDHUP;//可读、文件描述符被挂断
    #endif
    
        if (one_shot)
            event.events |= EPOLLONESHOT;
        epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);//处理好描述的事件类型后,添加进内核事件表的文件描述符epfd
        setnonblocking(fd);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    * 内核事件表删除事件

    * ```c++
    void removefd(int epollfd, int fd)
    {
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);//标志是delete,删除这个fd
    close(fd);
    }
  • 重置EPOLLONESHOT事件

  • ```c++
    void modfd(int epollfd, int fd, int ev)
    {
    epoll_event event;
    event.data.fd = fd;
    #ifdef ET
    event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
    #endif

    #ifdef LT
    event.events = ev | EPOLLONESHOT | EPOLLRDHUP;
    #endif

    epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
    

    }

    1
    2
    3
    4
    5

    ### http细节

    请求报文:get和post,报文的请求头部不一定全部都有,但可以有:

    GET /562f25980001b1b106000338.jpg HTTP/1.1
    Host:img.mukewang.com
    User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
    AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
    Accept:image/webp,image/,/*;q=0.8
    Referer:http://www.imooc.com/
    Accept-Encoding:gzip, deflate, sdch
    Accept-Language:zh-CN,zh;q=0.8
    空行
    请求数据为空

1

POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

- **请求行**,用来说明请求类型,要访问的资源以及所使用的HTTP版本。
GET说明请求类型为GET,/562f25980001b1b106000338.jpg(URL)为要访问的资源,该行的最后一部分说明使用的是HTTP1.1版本。

- **请求头部**,紧接着请求行(即第一行)之后的部分,用来说明服务器要使用的附加信息。

- - HOST,给出请求资源所在服务器的域名。
- User-Agent,HTTP客户端程序的信息,该信息由你发出请求使用的浏览器来定义,并且在每个请求中自动发送等。
- Accept,说明用户代理可处理的媒体类型。
- Accept-Encoding,说明用户代理支持的内容编码。
- Accept-Language,说明用户代理能够处理的自然语言集。
- Content-Type,说明实现主体的媒体类型。
- Content-Length,说明实现主体的大小。
- Connection,连接管理,可以是Keep-Alive或close。

- **空行**,请求头部后面的空行是必须的即使第四部分的请求数据为空,也必须有空行。

- **请求数据**也叫主体,可以添加任意的其他数据。

响应报文:

HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8
空行

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248

- 状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成。
第一行为状态行,(HTTP/1.1)表明HTTP版本为1.1版本,状态码为200,状态消息为OK。
- 消息报头,用来说明客户端要使用的一些附加信息。
第二行和第三行为消息报头,Date:生成响应的日期和时间;Content-Type:指定了MIME类型的HTML(text/html),编码类型是UTF-8。
- 空行,消息报头后面的空行是必须的。
- 响应正文,服务器返回给客户端的文本信息。空行后面的html部分为响应正文。

状态码:

HTTP有5种类型的状态码,具体的:

- 1xx:指示信息--表示请求已接收,继续处理。

- 2xx:成功--表示请求正常处理完毕。

- - 200 OK:客户端请求被正常处理。
- 206 Partial content:客户端进行了范围请求。

- 3xx:重定向--要完成请求必须进行更进一步的操作。

- - 301 Moved Permanently:永久重定向,该资源已被永久移动到新位置,将来任何对该资源的访问都要使用本响应返回的若干个URI之一。
- 302 Found:临时重定向,请求的资源现在临时从不同的URI中获得。

- 4xx:客户端错误--请求有语法错误,服务器无法处理请求。

- - 400 Bad Request:请求报文存在语法错误。
- 403 Forbidden:请求被服务器拒绝。
- 404 Not Found:请求不存在,服务器上找不到请求的资源。

- 5xx:服务器端错误--服务器处理请求出错。

- - 500 Internal Server Error:服务器在执行请求时出现错误。

http报文处理流程:

- 浏览器端发出http连接请求,主线程创建http对象接收请求并将所有数据读入对应buffer,将该对象插入任务队列,工作线程从任务队列中取出一个任务进行处理。
- 工作线程取出任务后,调用process_read函数,通过主、从状态机对请求报文进行解析。
- 解析完之后,跳转do_request函数生成响应报文,通过process_write写入buffer,返回给浏览器端。

### http类

这一部分代码在TinyWebServer/http/http_conn.h中,主要是http类的定义。

```c++
#ifndef HTTPCONNECTION_H
#define HTTPCONNECTION_H
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <stdarg.h>
#include <errno.h>//存储错误
#include <sys/wait.h>
#include <sys/uio.h>
#include <map>

#include "../lock/locker.h"
#include "../CGImysql/sql_connection_pool.h"
#include "../timer/lst_timer.h"
#include "../log/log.h"

class http_conn
{
public:
//设置读取文件的名称m_real_file大小
static const int FILENAME_LEN = 200;
//设置读缓冲区m_read_buf大小
static const int READ_BUFFER_SIZE = 2048;
//设置写缓冲区m_write_buf大小
static const int WRITE_BUFFER_SIZE = 1024;

//报文的请求方法,本项目只用到GET和POST
enum METHOD
{
GET = 0,
POST,
HEAD,
PUT,
DELETE,
TRACE,
OPTIONS,
CONNECT,
PATH
};
//主状态机的状态
enum CHECK_STATE
{
CHECK_STATE_REQUESTLINE = 0,
CHECK_STATE_HEADER,
CHECK_STATE_CONTENT
};
//报文解析的结果
enum HTTP_CODE
{
NO_REQUEST,
GET_REQUEST,
BAD_REQUEST,
NO_RESOURCE,
FORBIDDEN_REQUEST,
FILE_REQUEST,
INTERNAL_ERROR,
CLOSED_CONNECTION
};
//从状态机的状态
enum LINE_STATUS
{
LINE_OK = 0,
LINE_BAD,
LINE_OPEN
};

public:
http_conn() {}
~http_conn() {}

public:
//初始化套接字地址,函数内部会调用私有方法init
void init(int sockfd, const sockaddr_in &addr, char *, int, int, string user, string passwd, string sqlname);
//关闭http连接
void close_conn(bool real_close = true);

void process();

//读取浏览器端发来的全部数据
bool read_once();
//响应报文写入函数
bool write();

sockaddr_in *get_address()
{
return &m_address;
}

//同步线程初始化数据库读取表
void initmysql_result(connection_pool *connPool);

int timer_flag;
int improv;


private:
void init();
//从m_read_buf读取,并处理请求报文
HTTP_CODE process_read();
//向m_write_buf写入响应报文数据
bool process_write(HTTP_CODE ret);
//主状态机解析报文中的请求行数据
HTTP_CODE parse_request_line(char *text);
//主状态机解析报文中的请求头数据
HTTP_CODE parse_headers(char *text);
//主状态机解析报文中的请求内容
HTTP_CODE parse_content(char *text);
//生成响应报文
HTTP_CODE do_request();

//m_start_line是已经解析的字符
//get_line用于将指针向后偏移,指向未处理的字符
char *get_line() { return m_read_buf + m_start_line; };

//从状态机读取一行,分析是请求报文的哪一部分
LINE_STATUS parse_line();
void unmap();

//根据响应报文格式,生成对应8个部分,以下函数均由do_request调用
bool add_response(const char *format, ...);
bool add_content(const char *content);
bool add_status_line(int status, const char *title);
bool add_headers(int content_length);
bool add_content_type();
bool add_content_length(int content_length);
bool add_linger();
bool add_blank_line();

public:
static int m_epollfd;//内核事件表,类共享的
static int m_user_count;//记录总数,静态变量的形式
MYSQL *mysql;
int m_state; //读为0, 写为1

private:
int m_sockfd;
sockaddr_in m_address;

//存储读取的请求报文数据
char m_read_buf[READ_BUFFER_SIZE];
//缓冲区中m_read_buf中数据的最后一个字节的下一个位置
int m_read_idx;
//m_read_buf读取的位置m_checked_idx
int m_checked_idx;
//m_read_buf中已经解析的字符个数
int m_start_line;

//存储发出的响应报文数据
char m_write_buf[WRITE_BUFFER_SIZE];
//指示buffer中的长度
int m_write_idx;

//主状态机的状态
CHECK_STATE m_check_state;
//请求方法
METHOD m_method;

//以下为解析请求报文中对应的6个变量
//存储读取文件的名称
char m_real_file[FILENAME_LEN];
char *m_url;
char *m_version;
char *m_host;
int m_content_length;
bool m_linger;

//读取服务器上的文件地址
char *m_file_address;

//stat iovec后面介绍
struct stat m_file_stat;//获取文件的信息
//io向量机制iovec
struct iovec m_iv[2];
int m_iv_count;
int cgi; //是否启用的POST
char *m_string; //存储请求头数据
int bytes_to_send;//剩余发送字节数
int bytes_have_send;//已发送字节数
char *doc_root;

map<string, string> m_users;
int m_TRIGMode;
int m_close_log;

char sql_user[100];
char sql_passwd[100];
char sql_name[100];
};

#endif

在http请求接收部分,会涉及到init和read_once函数,但init仅仅是对私有成员变量进行初始化,不用过多讲解。

这里,对read_once进行介绍。read_once读取浏览器端发送来的请求报文,直到无数据可读或对方关闭连接,读取到m_read_buffer中,并更新m_read_idx。

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
 //循环读取客户数据,直到无数据可读或对方关闭连接
bool http_conn::read_once()
{
if(m_read_idx>=READ_BUFFER_SIZE)
{
return false;
}
int bytes_read=0;
while(true)
{
//从套接字接收数据,存储在m_read_buf缓冲区
bytes_read=recv(m_sockfd,m_read_buf+m_read_idx,READ_BUFFER_SIZE-m_read_idx,0);//该函数后面介绍
if(bytes_read==-1)
{
//非阻塞ET模式下,需要一次性将数据读完
if(errno==EAGAIN||errno==EWOULDBLOCK)//这种错误是系统告知要再尝试一次,可能是因为没有数据了,break返回true
break;
return false;
}
else if(bytes_read==0)
{
return false;
}
//修改m_read_idx的读取字节数
m_read_idx+=bytes_read;
}
return true;
}
  • int recv(int sockfd, char * buf, int len, int flags);

    • sockfd:连接的fd
    • buf:用于接收数据的缓冲区
    • len:缓冲区长度,一般是参数2的字节数-1,把\0字符串结尾留出来
    • flags:指定调用方式,一般设置为0
    • 返回值:成功返回实际读到的字节数。如果recv在copy时出错,那么它返回err,err小于0;如果recv函数在等待协议接收数据时网络中断了,那么它返回0 。
  • 在Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。EAGAIN和 EWOULDBLOCK等效!

    • 从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。例如,以O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。
    • 这个错误不会破坏socket的同步,不用管它,下次循环接着recv就可以。对非阻塞socket而言,EAGAIN不是一种错误。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK。
  • struct iovec 结构体定义了一个向量元素,通常这个 iovec 结构体用于一个多元素的数组,对于每一个元素,iovec 结构体的字段 iov_base 指向一个缓冲区,这个缓冲区存放的是网络接收的数据(read),或者网络将要发送的数据(write)。iovec 结构体的字段 iov_len 存放的是接收数据的最大长度(read),或者实际写入的数据长度(write)。

    • struct iovec {
          /* Starting address (内存起始地址)*/
          void  *iov_base;   
      
          /* Number of bytes to transfer(这块内存长度) */
          size_t iov_len;  
      };
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20

      * struct stat这个结构体是用来描述一个linux系统文件系统中的文件属性的结构。

      * ```c++
      struct stat
      {
      dev_t st_dev; /* ID of device containing file -文件所在设备的ID*/
      ino_t st_ino; /* inode number -inode节点号*/
      mode_t st_mode; /* protection -保护模式?*/
      nlink_t st_nlink; /* number of hard links -链向此文件的连接数(硬连接)*/
      uid_t st_uid; /* user ID of owner -user id*/
      gid_t st_gid; /* group ID of owner - group id*/
      dev_t st_rdev; /* device ID (if special file) -设备号,针对设备文件*/
      off_t st_size; /* total size, in bytes -文件大小,字节为单位*/
      blksize_t st_blksize; /* blocksize for filesystem I/O -系统块的大小*/
      blkcnt_t st_blocks; /* number of blocks allocated -文件所占块数*/
      time_t st_atime; /* time of last access -最近存取时间*/
      time_t st_mtime; /* time of last modification -最近修改时间*/
      time_t st_ctime; /* time of last status change - */
      };
    • //_stat函数用来获取指定路径的文件或者文件夹的信息。
      
      
      //! 需要包含de头文件  
      #include <sys/types.h>    
      #include <sys/stat.h>   
      int stat(
        const char *filename    //文件或者文件夹的路径
        , struct stat *buf      //获取的信息保存在内存中
      ); //! prototype,原型     
      
      //正确——返回0
      //错误——返回-1,具体错误码保存在errno中
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      * 一般情况下,我们关心文件大小和创建时间、访问时间、修改时间。

      * #### mmap

      * 用于将一个文件或其他对象映射到内存,提高文件的访问速度。

      * ```c++
      void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
      int munmap(void* start,size_t length);
    • start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址

    • length:映射区的长度

    • prot:期望的内存保护标志,不能与文件的打开模式冲突

      • PROT_READ 表示页内容可以被读取
    • flags:指定映射对象的类型,映射选项和映射页是否可以共享

      • MAP_PRIVATE 建立一个写入时拷贝的私有映射,内存区域的写入不会影响到原文件
    • fd:有效的文件描述符,一般是由open()函数返回

    • off_toffset:被映射对象内容的起点

    • 返回值:成功返回创建的映射区的首地址;失败返回宏MAP_FAILED。

  • writev

    • writev函数用于在一次函数调用中写多个非连续缓冲区,有时也将这该函数称为聚集写。

    • ```c++
      #include <sys/uio.h>
      ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);

      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
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174

      * filedes表示文件描述符

      * iov为前述io向量机制结构体iovec

      * iovcnt为结构体的个数

      * 若成功则返回已写的字节数,若出错则返回-1。`writev`以顺序`iov[0]`,`iov[1]`至`iov[iovcnt-1]`从缓冲区中聚集输出数据。`writev`返回输出的字节总数,通常,它应等于所有缓冲区长度之和。

      * **特别注意:** 循环调用writev时,需要重新处理iovec中的指针和长度,该函数不会对这两个成员做任何处理。writev的返回值为已写的字节数,但这个返回值“实用性”并不高,因为参数传入的是iovec数组,计量单位是iovcnt,而不是字节数,我们仍然需要通过遍历iovec来计算新的基址,另外写入数据的“结束点”可能位于一个iovec的中间某个位置,因此需要调整临界iovec的io_base和io_len。


      补充:

      Linux中系统调用的错误都存储于 `errno`中,`errno`由操作系统维护,存储就近发生的错误,即下一次的错误码会覆盖掉上一次的错误。

      > *PS: 只有当系统调用或者调用lib函数时出错,才会置位`errno`!*

      查看系统中所有的`errno`所代表的含义,可以采用如下的代码:

      ```c++
      /* Function: obtain the errno string
      * char *strerror(int errno)
      */

      #include <stdio.h>
      #include <string.h> //for strerror()
      //#include <errno.h>
      int main()
      {
      int tmp = 0;
      for(tmp = 0; tmp <=256; tmp++)
      {
      printf("errno: %2d\t%s\n",tmp,strerror(tmp));
      }
      return 0;
      }
      //输出信息如下:
      errno: 0 Success
      errno: 1 Operation not permitted
      errno: 2 No such file or directory
      errno: 3 No such process
      errno: 4 Interrupted system call
      errno: 5 Input/output error
      errno: 6 No such device or address
      errno: 7 Argument list too long
      errno: 8 Exec format error
      errno: 9 Bad file descriptor
      errno: 10 No child processes
      errno: 11 Resource temporarily unavailable
      errno: 12 Cannot allocate memory
      errno: 13 Permission denied
      errno: 14 Bad address
      errno: 15 Block device required
      errno: 16 Device or resource busy
      errno: 17 File exists
      errno: 18 Invalid cross-device link
      errno: 19 No such device
      errno: 20 Not a directory
      errno: 21 Is a directory
      errno: 22 Invalid argument
      errno: 23 Too many open files in system
      errno: 24 Too many open files
      errno: 25 Inappropriate ioctl for device
      errno: 26 Text file busy
      errno: 27 File too large
      errno: 28 No space left on device
      errno: 29 Illegal seek
      errno: 30 Read-only file system
      errno: 31 Too many links
      errno: 32 Broken pipe
      errno: 33 Numerical argument out of domain
      errno: 34 Numerical result out of range
      errno: 35 Resource deadlock avoided
      errno: 36 File name too long
      errno: 37 No locks available
      errno: 38 Function not implemented
      errno: 39 Directory not empty
      errno: 40 Too many levels of symbolic links
      errno: 41 Unknown error 41
      errno: 42 No message of desired type
      errno: 43 Identifier removed
      errno: 44 Channel number out of range
      errno: 45 Level 2 not synchronized
      errno: 46 Level 3 halted
      errno: 47 Level 3 reset
      errno: 48 Link number out of range
      errno: 49 Protocol driver not attached
      errno: 50 No CSI structure available
      errno: 51 Level 2 halted
      errno: 52 Invalid exchange
      errno: 53 Invalid request descriptor
      errno: 54 Exchange full
      errno: 55 No anode
      errno: 56 Invalid request code
      errno: 57 Invalid slot
      errno: 58 Unknown error 58
      errno: 59 Bad font file format
      errno: 60 Device not a stream
      errno: 61 No data available
      errno: 62 Timer expired
      errno: 63 Out of streams resources
      errno: 64 Machine is not on the network
      errno: 65 Package not installed
      errno: 66 Object is remote
      errno: 67 Link has been severed
      errno: 68 Advertise error
      errno: 69 Srmount error
      errno: 70 Communication error on send
      errno: 71 Protocol error
      errno: 72 Multihop attempted
      errno: 73 RFS specific error
      errno: 74 Bad message
      errno: 75 Value too large for defined data type
      errno: 76 Name not unique on network
      errno: 77 File descriptor in bad state
      errno: 78 Remote address changed
      errno: 79 Can not access a needed shared library
      errno: 80 Accessing a corrupted shared library
      errno: 81 .lib section in a.out corrupted
      errno: 82 Attempting to link in too many shared libraries
      errno: 83 Cannot exec a shared library directly
      errno: 84 Invalid or incomplete multibyte or wide character
      errno: 85 Interrupted system call should be restarted
      errno: 86 Streams pipe error
      errno: 87 Too many users
      errno: 88 Socket operation on non-socket
      errno: 89 Destination address required
      errno: 90 Message too long
      errno: 91 Protocol wrong type for socket
      errno: 92 Protocol not available
      errno: 93 Protocol not supported
      errno: 94 Socket type not supported
      errno: 95 Operation not supported
      errno: 96 Protocol family not supported
      errno: 97 Address family not supported by protocol
      errno: 98 Address already in use
      errno: 99 Cannot assign requested address
      errno: 100 Network is down
      errno: 101 Network is unreachable
      errno: 102 Network dropped connection on reset
      errno: 103 Software caused connection abort
      errno: 104 Connection reset by peer
      errno: 105 No buffer space available
      errno: 106 Transport endpoint is already connected
      errno: 107 Transport endpoint is not connected
      errno: 108 Cannot send after transport endpoint shutdown
      errno: 109 Too many references: cannot splice
      errno: 110 Connection timed out
      errno: 111 Connection refused
      errno: 112 Host is down
      errno: 113 No route to host
      errno: 114 Operation already in progress
      errno: 115 Operation now in progress
      errno: 116 Stale file handle
      errno: 117 Structure needs cleaning
      errno: 118 Not a XENIX named type file
      errno: 119 No XENIX semaphores available
      errno: 120 Is a named type file
      errno: 121 Remote I/O error
      errno: 122 Disk quota exceeded
      errno: 123 No medium found
      errno: 124 Wrong medium type
      errno: 125 Operation canceled
      errno: 126 Required key not available
      errno: 127 Key has expired
      errno: 128 Key has been revoked
      errno: 129 Key was rejected by service
      errno: 130 Owner died
      errno: 131 State not recoverable
      errno: 132 Operation not possible due to RF-kill
      errno: 133 Memory page has hardware error
      errno: 134~255 unknown error!

Linux中,在头文件 /usr/include/asm-generic/errno-base.h 对基础常用errno进行了宏定义

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
36
37
38
39
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H

#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */
#define ENOEXEC 8 /* Exec format error */
#define EBADF 9 /* Bad file number */
#define ECHILD 10 /* No child processes */
#define EAGAIN 11 /* Try again */
#define ENOMEM 12 /* Out of memory */
#define EACCES 13 /* Permission denied */
#define EFAULT 14 /* Bad address */
#define ENOTBLK 15 /* Block device required */
#define EBUSY 16 /* Device or resource busy */
#define EEXIST 17 /* File exists */
#define EXDEV 18 /* Cross-device link */
#define ENODEV 19 /* No such device */
#define ENOTDIR 20 /* Not a directory */
#define EISDIR 21 /* Is a directory */
#define EINVAL 22 /* Invalid argument */
#define ENFILE 23 /* File table overflow */
#define EMFILE 24 /* Too many open files */
#define ENOTTY 25 /* Not a typewriter */
#define ETXTBSY 26 /* Text file busy */
#define EFBIG 27 /* File too large */
#define ENOSPC 28 /* No space left on device */
#define ESPIPE 29 /* Illegal seek */
#define EROFS 30 /* Read-only file system */
#define EMLINK 31 /* Too many links */
#define EPIPE 32 /* Broken pipe */
#define EDOM 33 /* Math argument out of domain of func */
#define ERANGE 34 /* Math result not representable */

#endif

/usr/include/asm-asm-generic/errno.h 中,对剩余的errno做了宏定义

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#ifndef _ASM_GENERIC_ERRNO_H
#define _ASM_GENERIC_ERRNO_H

#include <asm-generic/errno-base.h>

#define EDEADLK 35 /* Resource deadlock would occur */
#define ENAMETOOLONG 36 /* File name too long */
#define ENOLCK 37 /* No record locks available */
#define ENOSYS 38 /* Function not implemented */
#define ENOTEMPTY 39 /* Directory not empty */
#define ELOOP 40 /* Too many symbolic links encountered */
#define EWOULDBLOCK EAGAIN /* Operation would block */
#define ENOMSG 42 /* No message of desired type */
#define EIDRM 43 /* Identifier removed */
#define ECHRNG 44 /* Channel number out of range */
#define EL2NSYNC 45 /* Level 2 not synchronized */
#define EL3HLT 46 /* Level 3 halted */
#define EL3RST 47 /* Level 3 reset */
#define ELNRNG 48 /* Link number out of range */
#define EUNATCH 49 /* Protocol driver not attached */
#define ENOCSI 50 /* No CSI structure available */
#define EL2HLT 51 /* Level 2 halted */
#define EBADE 52 /* Invalid exchange */
#define EBADR 53 /* Invalid request descriptor */
#define EXFULL 54 /* Exchange full */
#define ENOANO 55 /* No anode */
#define EBADRQC 56 /* Invalid request code */
#define EBADSLT 57 /* Invalid slot */

#define EDEADLOCK EDEADLK

#define EBFONT 59 /* Bad font file format */
#define ENOSTR 60 /* Device not a stream */
#define ENODATA 61 /* No data available */
#define ETIME 62 /* Timer expired */
#define ENOSR 63 /* Out of streams resources */
#define ENONET 64 /* Machine is not on the network */
#define ENOPKG 65 /* Package not installed */
#define EREMOTE 66 /* Object is remote */
#define ENOLINK 67 /* Link has been severed */
#define EADV 68 /* Advertise error */
#define ESRMNT 69 /* Srmount error */
#define ECOMM 70 /* Communication error on send */
#define EPROTO 71 /* Protocol error */
#define EMULTIHOP 72 /* Multihop attempted */
#define EDOTDOT 73 /* RFS specific error */
#define EBADMSG 74 /* Not a data message */
#define EOVERFLOW 75 /* Value too large for defined data type */
#define ENOTUNIQ 76 /* Name not unique on network */
#define EBADFD 77 /* File descriptor in bad state */
#define EREMCHG 78 /* Remote address changed */
#define ELIBACC 79 /* Can not access a needed shared library */
#define ELIBBAD 80 /* Accessing a corrupted shared library */
#define ELIBSCN 81 /* .lib section in a.out corrupted */
#define ELIBMAX 82 /* Attempting to link in too many shared libraries */

#define ELIBEXEC 83 /* Cannot exec a shared library directly */
#define EILSEQ 84 /* Illegal byte sequence */
#define ERESTART 85 /* Interrupted system call should be restarted */
#define ESTRPIPE 86 /* Streams pipe error */
#define EUSERS 87 /* Too many users */
#define ENOTSOCK 88 /* Socket operation on non-socket */
#define EDESTADDRREQ 89 /* Destination address required */
#define EMSGSIZE 90 /* Message too long */
#define EPROTOTYPE 91 /* Protocol wrong type for socket */
#define ENOPROTOOPT 92 /* Protocol not available */
#define EPROTONOSUPPORT 93 /* Protocol not supported */
#define ESOCKTNOSUPPORT 94 /* Socket type not supported */
#define EOPNOTSUPP 95 /* Operation not supported on transport endpoint */
#define EPFNOSUPPORT 96 /* Protocol family not supported */
#define EAFNOSUPPORT 97 /* Address family not supported by protocol */
#define EADDRINUSE 98 /* Address already in use */
#define EADDRNOTAVAIL 99 /* Cannot assign requested address */
#define ENETDOWN 100 /* Network is down */
#define ENETUNREACH 101 /* Network is unreachable */
#define ENETRESET 102 /* Network dropped connection because of reset */
#define ECONNABORTED 103 /* Software caused connection abort */
#define ECONNRESET 104 /* Connection reset by peer */
#define ENOBUFS 105 /* No buffer space available */
#define EISCONN 106 /* Transport endpoint is already connected */
#define ENOTCONN 107 /* Transport endpoint is not connected */
#define ESHUTDOWN 108 /* Cannot send after transport endpoint shutdown */
#define ETOOMANYREFS 109 /* Too many references: cannot splice */
#define ETIMEDOUT 110 /* Connection timed out */
#define ECONNREFUSED 111 /* Connection refused */
#define EHOSTDOWN 112 /* Host is down */
#define EHOSTUNREACH 113 /* No route to host */
#define EALREADY 114 /* Operation already in progress */
#define EINPROGRESS 115 /* Operation now in progress */
#define ESTALE 116 /* Stale file handle */
#define EUCLEAN 117 /* Structure needs cleaning */
#define ENOTNAM 118 /* Not a XENIX named type file */
#define ENAVAIL 119 /* No XENIX semaphores available */
#define EISNAM 120 /* Is a named type file */
#define EREMOTEIO 121 /* Remote I/O error */
#define EDQUOT 122 /* Quota exceeded */

#define ENOMEDIUM 123 /* No medium found */
#define EMEDIUMTYPE 124 /* Wrong medium type */
#define ECANCELED 125 /* Operation Canceled */
#define ENOKEY 126 /* Required key not available */
#define EKEYEXPIRED 127 /* Key has expired */
#define EKEYREVOKED 128 /* Key has been revoked */
#define EKEYREJECTED 129 /* Key was rejected by service */

/* for robust mutexes */
#define EOWNERDEAD 130 /* Owner died */
#define ENOTRECOVERABLE 131 /* State not recoverable */

#define ERFKILL 132 /* Operation not possible due to RF-kill */

#define EHWPOISON 133 /* Memory page has hardware error */

#endif

http的调用

这不是http的实现,实现后面再说,这里是使用epoll调用的运行代码

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
 //创建MAX_FD个http类对象
http_conn* users=new http_conn[MAX_FD];

//创建内核事件表
epoll_event events[MAX_EVENT_NUMBER];
epollfd = epoll_create(5);
assert(epollfd != -1);

//将listenfd放在epoll树上
addfd(epollfd, listenfd, false);

//将上述epollfd赋值给http类对象的m_epollfd属性
http_conn::m_epollfd = epollfd;

while (!stop_server)
{
//等待所监控文件描述符上有事件的产生
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
break;
}
//对所有就绪事件进行处理
for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;//通过epollfd监听到的就绪事件会放在events数组

//处理新到的客户连接
if (sockfd == listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
//LT水平触发
#ifdef LT
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
if (connfd < 0)
{
continue;
}
if (http_conn::m_user_count >= MAX_FD)
{
show_error(connfd, "Internal server busy");
continue;
}
#endif

//ET非阻塞边缘触发
#ifdef ET
//需要循环接收数据
while (1)
{
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
if (connfd < 0)
{
break;
}
if (http_conn::m_user_count >= MAX_FD)
{
show_error(connfd, "Internal server busy");
break;
}
users[connfd].init(connfd, client_address);
}
continue;
#endif
}

//处理异常事件
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
//服务器端关闭连接
}

//处理信号
else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
{
}

//处理客户连接上接收到的数据
else if (events[i].events & EPOLLIN)
{
//读入对应缓冲区
if (users[sockfd].read_once())
{
//若监测到读事件,将该事件放入请求队列
pool->append(users + sockfd);
}
else
{
//服务器关闭连接
}
}

}
}

http实现

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
#include "http_conn.h"

#include <mysql/mysql.h>
#include <fstream>

//定义http响应的一些状态信息
const char *ok_200_title = "OK";
const char *error_400_title = "Bad Request";
const char *error_400_form = "Your request has bad syntax or is inherently impossible to staisfy.\n";
const char *error_403_title = "Forbidden";
const char *error_403_form = "You do not have permission to get file form this server.\n";
const char *error_404_title = "Not Found";
const char *error_404_form = "The requested file was not found on this server.\n";
const char *error_500_title = "Internal Error";
const char *error_500_form = "There was an unusual problem serving the request file.\n";

locker m_lock;
map<string, string> users;

void http_conn::initmysql_result(connection_pool *connPool)
{
//先从连接池中取一个连接
MYSQL *mysql = NULL;
connectionRAII mysqlcon(&mysql, connPool);

//在user表中检索username,passwd数据,浏览器端输入
if (mysql_query(mysql, "SELECT username,passwd FROM user"))//从mysql这个接口输入查询语句
{
LOG_ERROR("SELECT error:%s\n", mysql_error(mysql));
}

//从表中检索完整的结果集
MYSQL_RES *result = mysql_store_result(mysql);

//返回结果集中的列数
int num_fields = mysql_num_fields(result);

//返回所有字段结构的数组
MYSQL_FIELD *fields = mysql_fetch_fields(result);

//从结果集中获取下一行,将对应的用户名和密码,存入map中
while (MYSQL_ROW row = mysql_fetch_row(result))
{
string temp1(row[0]);
string temp2(row[1]);
users[temp1] = temp2;
}
}

//对文件描述符设置非阻塞
int setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}

//将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
//这可以把一个fd绑定在epollfd上,接下来对内核事件的操作(唤醒什么的)都是针对fd文件描述符的。
void addfd(int epollfd, int fd, bool one_shot, int TRIGMode)
{
epoll_event event;
event.data.fd = fd;

if (1 == TRIGMode)//边缘触发模式
event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
else
event.events = EPOLLIN | EPOLLRDHUP;

if (one_shot)
event.events |= EPOLLONESHOT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);//add一个
setnonblocking(fd);
}

//从内核时间表删除描述符
void removefd(int epollfd, int fd)
{
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
close(fd);//从epollfd中删掉fd,然后关闭
}

//将事件重置为EPOLLONESHOT
void modfd(int epollfd, int fd, int ev, int TRIGMode)
{
epoll_event event;
event.data.fd = fd;

if (1 == TRIGMode)
event.events = ev | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
else
event.events = ev | EPOLLONESHOT | EPOLLRDHUP;

epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);//修改
}
//静态变量定义
int http_conn::m_user_count = 0;
int http_conn::m_epollfd = -1;

//关闭连接,关闭一个连接,客户总量减一
void http_conn::close_conn(bool real_close)
{
if (real_close && (m_sockfd != -1))
{
printf("close %d\n", m_sockfd);
removefd(m_epollfd, m_sockfd);
m_sockfd = -1;
m_user_count--;
}
}

//初始化连接,外部调用初始化套接字地址
void http_conn::init(int sockfd, const sockaddr_in &addr, char *root, int TRIGMode,
int close_log, string user, string passwd, string sqlname)
{
m_sockfd = sockfd;
m_address = addr;

addfd(m_epollfd, sockfd, true, m_TRIGMode);//注册一个连接
m_user_count++;

//当浏览器出现连接重置时,可能是网站根目录出错或http响应格式出错或者访问的文件中内容完全为空
doc_root = root;
m_TRIGMode = TRIGMode;
m_close_log = close_log;

strcpy(sql_user, user.c_str());
strcpy(sql_passwd, passwd.c_str());
strcpy(sql_name, sqlname.c_str());

init();
}

//初始化新接受的连接
//check_state默认为分析请求行状态
void http_conn::init()
{
mysql = NULL;
bytes_to_send = 0;
bytes_have_send = 0;
m_check_state = CHECK_STATE_REQUESTLINE;
m_linger = false;
m_method = GET;
m_url = 0;
m_version = 0;
m_content_length = 0;
m_host = 0;
m_start_line = 0;
m_checked_idx = 0;
m_read_idx = 0;
m_write_idx = 0;
cgi = 0;
m_state = 0;
timer_flag = 0;
improv = 0;

memset(m_read_buf, '\0', READ_BUFFER_SIZE);
memset(m_write_buf, '\0', WRITE_BUFFER_SIZE);
memset(m_real_file, '\0', FILENAME_LEN);
}

//从状态机,用于分析出一行内容,见作者的分析中篇
//返回值为行的读取状态,有LINE_OK,LINE_BAD,LINE_OPEN
http_conn::LINE_STATUS http_conn::parse_line()
{
char temp;
for (; m_checked_idx < m_read_idx; ++m_checked_idx)
{
temp = m_read_buf[m_checked_idx];//temp为将要分析的字节
if (temp == '\r')//如果当前是\r字符,则有可能会读取到完整行
{
if ((m_checked_idx + 1) == m_read_idx)//下一个字符达到了buffer结尾,则接收不完整,需要继续接收
return LINE_OPEN;
else if (m_read_buf[m_checked_idx + 1] == '\n')//下一个字符是\n,将\r\n改为\0\0
{
m_read_buf[m_checked_idx++] = '\0';
m_read_buf[m_checked_idx++] = '\0';
return LINE_OK;
}
return LINE_BAD;//如果都不符合,则返回语法错误
}
else if (temp == '\n')//如果当前字符是\n,也有可能读取到完整行
{//一般是上次读取到\r就到buffer末尾了,没有接收完整,再次接收时会出现这种情况
if (m_checked_idx > 1 && m_read_buf[m_checked_idx - 1] == '\r')//前一个字符是\r,则接收完整
{
m_read_buf[m_checked_idx - 1] = '\0';
m_read_buf[m_checked_idx++] = '\0';
return LINE_OK;
}
return LINE_BAD;
}
}
return LINE_OPEN;//并没有找到\r\n,需要继续接收
}

//循环读取客户数据,直到无数据可读或对方关闭连接
//非阻塞ET工作模式下,需要一次性将数据读完
bool http_conn::read_once()
{
if (m_read_idx >= READ_BUFFER_SIZE)
{
return false;
}
int bytes_read = 0;

//LT读取数据
if (0 == m_TRIGMode)
{
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
m_read_idx += bytes_read;

if (bytes_read <= 0)
{
return false;
}

return true;
}
//ET读数据
else
{
while (true)//循环读取
{
bytes_read = recv(m_sockfd, m_read_buf + m_read_idx, READ_BUFFER_SIZE - m_read_idx, 0);
if (bytes_read == -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)//这时说明读完了
break;
return false;
}
else if (bytes_read == 0)
{
return false;
}
m_read_idx += bytes_read;
}
return true;
}
}

//解析http请求行,获得请求方法,目标url及http版本号
//在HTTP报文中,请求行用来说明请求类型,要访问的资源以及所使用的HTTP版本,其中各个部分之间通过\t或空格分隔。
http_conn::HTTP_CODE http_conn::parse_request_line(char *text)
{ //请求行中最先含有空格和\t任一字符的位置并返回
m_url = strpbrk(text, " \t");//检索字符串 str1 中第一个匹配字符串 str2 中字符的字符
if (!m_url)//如果没有空格或\t,则报文格式有误
{
return BAD_REQUEST;
}
*m_url++ = '\0';//将该位置改为\0,用于将前面数据取出
char *method = text;
if (strcasecmp(method, "GET") == 0)//取出数据,并通过与GET和POST比较,以确定请求方式
m_method = GET;
else if (strcasecmp(method, "POST") == 0)
{
m_method = POST;
cgi = 1;
}
else
return BAD_REQUEST;

//检索字符串 str1 中第一个不在字符串 str2 中出现的字符下标,该函数返回 str1 中第一个不在字符串 str2 中出现的字符下标。
m_url += strspn(m_url, " \t");//因为报文后面可能还有空格,跳过这些空格
m_version = strpbrk(m_url, " \t");

//使用与判断请求方式的相同逻辑,判断HTTP版本号
if (!m_version)
return BAD_REQUEST;
*m_version++ = '\0';
m_version += strspn(m_version, " \t");
if (strcasecmp(m_version, "HTTP/1.1") != 0)//只支持1.1版本
return BAD_REQUEST;

//这里主要是有些报文的请求资源中会带有http://,这里需要对这种情况进行单独处理
if (strncasecmp(m_url, "http://", 7) == 0)//对请求资源前7个字符进行判断
{
m_url += 7;
m_url = strchr(m_url, '/');
}
//同样增加https情况
if (strncasecmp(m_url, "https://", 8) == 0)
{
m_url += 8;
m_url = strchr(m_url, '/');
}

if (!m_url || m_url[0] != '/')//一般的不会带有上述两种符号,直接是单独的/或/后面带访问资源
return BAD_REQUEST;
//当url为/时,显示判断界面
if (strlen(m_url) == 1)
strcat(m_url, "judge.html");
m_check_state = CHECK_STATE_HEADER;//请求行处理完毕,将主状态机转移处理请求头
return NO_REQUEST;
}

//解析http请求的一个头部信息
http_conn::HTTP_CODE http_conn::parse_headers(char *text)
{
if (text[0] == '\0')//判断是空行还是请求头
{
if (m_content_length != 0)//判断是GET还是POST请求
{
m_check_state = CHECK_STATE_CONTENT;//POST需要跳转到消息体处理状态
return NO_REQUEST;
}
return GET_REQUEST;
}
else if (strncasecmp(text, "Connection:", 11) == 0)//解析请求头部连接字段
{
text += 11;
text += strspn(text, " \t");//跳过空格和\t字符
if (strcasecmp(text, "keep-alive") == 0)//如果是长连接,则将linger标志设置为true
{
m_linger = true;
}
}
else if (strncasecmp(text, "Content-length:", 15) == 0)//解析请求头部内容长度字段
{
text += 15;
text += strspn(text, " \t");
m_content_length = atol(text);
}
else if (strncasecmp(text, "Host:", 5) == 0)//解析请求头部HOST字段
{
text += 5;
text += strspn(text, " \t");
m_host = text;
}
else
{
LOG_INFO("oop!unknow header: %s", text);
}
return NO_REQUEST;
}


http_conn::HTTP_CODE http_conn::parse_content(char *text)
{//判断http请求是否被完整读入
if (m_read_idx >= (m_content_length + m_checked_idx))
{
text[m_content_length] = '\0';
//POST请求中最后为输入的用户名和密码
m_string = text;
return GET_REQUEST;
}
return NO_REQUEST;
}
//主状态机
http_conn::HTTP_CODE http_conn::process_read()
{
LINE_STATUS line_status = LINE_OK;//初始化从状态机状态、HTTP请求解析结果
HTTP_CODE ret = NO_REQUEST;
char *text = 0;

//在GET请求报文中,每一行都是\r\n作为结束,所以对报文进行拆解时,仅用从状态机的状态line_status=parse_line())==LINE_OK语句即可。
//在POST请求报文中,消息体的末尾没有任何字符,所以不能使用从状态机的状态,这里转而使用主状态机的状态作为循环入口条件。
//解析完消息体后,报文的完整解析就完成了,但此时主状态机的状态还是CHECK_STATE_CONTENT,符合循环入口条件,还会再次进入循环,这并不是我们所希望的
//为此,增加了该语句,并在完成消息体解析后,将line_status变量更改为LINE_OPEN,此时可以跳出循环,完成报文解析任务。

//只有当从状态机处理好了,主状态机才运行
while ((m_check_state == CHECK_STATE_CONTENT && line_status == LINE_OK) || ((line_status = parse_line()) == LINE_OK))
{
text = get_line();
//m_start_line是每一个数据行在m_read_buf中的起始位置
//m_checked_idx表示从状态机在m_read_buf中读取的位置
m_start_line = m_checked_idx;
LOG_INFO("%s", text);
switch (m_check_state)//主状态机的三种状态转移逻辑
{
case CHECK_STATE_REQUESTLINE:
{ //解析请求行
ret = parse_request_line(text);
if (ret == BAD_REQUEST)
return BAD_REQUEST;
break;
}
case CHECK_STATE_HEADER:
{ //解析请求头
ret = parse_headers(text);
if (ret == BAD_REQUEST)
return BAD_REQUEST;
//完整解析GET请求后,跳转到报文响应函数
else if (ret == GET_REQUEST)
{
return do_request();
}
break;
}
case CHECK_STATE_CONTENT:
{ //解析消息体
ret = parse_content(text);
//完整解析POST请求后,跳转到报文响应函数
if (ret == GET_REQUEST)
return do_request();//正确的请求就转调用
line_status = LINE_OPEN;//从状态机没处理好,退出循环,openline
break;
}
default:
return INTERNAL_ERROR;
}
}
return NO_REQUEST;
}
//这里跟html有关系
http_conn::HTTP_CODE http_conn::do_request()
{
strcpy(m_real_file, doc_root);//将初始化的m_real_file赋值为网站根目录
int len = strlen(doc_root);
//printf("m_url:%s\n", m_url);
const char *p = strrchr(m_url, '/');//找到m_url中/的位置

//处理cgi
if (cgi == 1 && (*(p + 1) == '2' || *(p + 1) == '3'))//实现登录和注册校验
{

//根据标志判断是登录检测还是注册检测
char flag = m_url[1];

char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/");
strcat(m_url_real, m_url + 2);
strncpy(m_real_file + len, m_url_real, FILENAME_LEN - len - 1);
free(m_url_real);

//将用户名和密码提取出来
//user=123&passwd=123
char name[100], password[100];
int i;
for (i = 5; m_string[i] != '&'; ++i)
name[i - 5] = m_string[i];
name[i - 5] = '\0';

int j = 0;
for (i = i + 10; m_string[i] != '\0'; ++i, ++j)
password[j] = m_string[i];
password[j] = '\0';

if (*(p + 1) == '3')
{
//如果是注册,先检测数据库中是否有重名的
//没有重名的,进行增加数据
char *sql_insert = (char *)malloc(sizeof(char) * 200);
strcpy(sql_insert, "INSERT INTO user(username, passwd) VALUES(");
strcat(sql_insert, "'");
strcat(sql_insert, name);
strcat(sql_insert, "', '");
strcat(sql_insert, password);
strcat(sql_insert, "')");

if (users.find(name) == users.end())//没有这个名字
{
m_lock.lock();//操作数据库,互斥
int res = mysql_query(mysql, sql_insert);//数据库,没有给mysql变量赋一个连接啊?
users.insert(pair<string, string>(name, password));//map
m_lock.unlock();

if (!res)
strcpy(m_url, "/log.html");
else
strcpy(m_url, "/registerError.html");
}
else
strcpy(m_url, "/registerError.html");
}
//如果是登录,直接判断
//若浏览器端输入的用户名和密码在表中可以查找到,返回1,否则返回0
else if (*(p + 1) == '2')
{
if (users.find(name) != users.end() && users[name] == password)
strcpy(m_url, "/welcome.html");
else
strcpy(m_url, "/logError.html");
}
}

if (*(p + 1) == '0')//如果请求资源为/0,表示跳转注册界面
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/register.html");
//将网站目录和/register.html进行拼接,更新到m_real_file中
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}
else if (*(p + 1) == '1')//如果请求资源为/1,表示跳转登录界面
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/log.html");
//将网站目录和/log.html进行拼接,更新到m_real_file中
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}
else if (*(p + 1) == '5')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/picture.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}
else if (*(p + 1) == '6')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/video.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}
else if (*(p + 1) == '7')
{
char *m_url_real = (char *)malloc(sizeof(char) * 200);
strcpy(m_url_real, "/fans.html");
strncpy(m_real_file + len, m_url_real, strlen(m_url_real));

free(m_url_real);
}
else//如果以上均不符合,即不是登录和注册,直接将url与网站目录拼接,这里的情况是welcome界面,请求服务器上的一个图片
strncpy(m_real_file + len, m_url, FILENAME_LEN - len - 1);

//通过stat获取请求资源文件信息,成功则将信息更新到m_file_stat结构体
//失败返回NO_RESOURCE状态,表示资源不存在
if (stat(m_real_file, &m_file_stat) < 0)
return NO_RESOURCE;

//判断文件的权限,是否可读,不可读则返回FORBIDDEN_REQUEST状态
if (!(m_file_stat.st_mode & S_IROTH))
return FORBIDDEN_REQUEST;

//判断文件类型,如果是目录,则返回BAD_REQUEST,表示请求报文有误
if (S_ISDIR(m_file_stat.st_mode))
return BAD_REQUEST;

//以只读方式获取文件描述符,通过mmap将该文件映射到内存中
int fd = open(m_real_file, O_RDONLY);
m_file_address = (char *)mmap(0, m_file_stat.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);//避免文件描述符的浪费和占用
return FILE_REQUEST;//表示请求文件存在,且可以访问
}
void http_conn::unmap()//解除内存映射
{
if (m_file_address)
{
munmap(m_file_address, m_file_stat.st_size);
m_file_address = 0;
}
}
bool http_conn::write()
{
int temp = 0;

//若要发送的数据长度为0
//表示响应报文为空,一般不会出现这种情况
if (bytes_to_send == 0)
{
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);
init();
return true;
}

while (1)
{
//将响应报文的状态行、消息头、空行和响应正文发送给浏览器端
temp = writev(m_sockfd, m_iv, m_iv_count);

if (temp < 0)//发送失败
{ //判断缓冲区是否满了
if (errno == EAGAIN)
{ //重新注册写事件
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);
return true;
}
//如果发送失败,但不是缓冲区问题,取消映射
unmap();
return false;
}

//正常发送,temp为发送的字节数
bytes_have_send += temp;//更新已发送字节
bytes_to_send -= temp;//偏移文件iovec的指针

//第一个iovec头部信息的数据已发送完,发送第二个iovec数据
if (bytes_have_send >= m_iv[0].iov_len)
{
m_iv[0].iov_len = 0;//不再继续发送头部信息
m_iv[1].iov_base = m_file_address + (bytes_have_send - m_write_idx);
m_iv[1].iov_len = bytes_to_send;
}
else//继续发送第一个iovec头部信息的数据
{
m_iv[0].iov_base = m_write_buf + bytes_have_send;
m_iv[0].iov_len = m_iv[0].iov_len - bytes_have_send;
}
//判断条件,数据已全部发送完
if (bytes_to_send <= 0)
{
unmap();
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);//在epoll树上重置EPOLLONESHOT事件

if (m_linger)//浏览器的请求为长连接
{
init();//重新初始化HTTP对象
return true;
}
else
{
return false;
}
}
}
}
bool http_conn::add_response(const char *format, ...)
{
if (m_write_idx >= WRITE_BUFFER_SIZE)//如果写入内容超出m_write_buf大小则报错
return false;
va_list arg_list;//定义可变参数列表
va_start(arg_list, format);//将变量arg_list初始化为传入参数
//将数据format从可变参数列表写入缓冲区,返回写入数据的长度
int len = vsnprintf(m_write_buf + m_write_idx, WRITE_BUFFER_SIZE - 1 - m_write_idx, format, arg_list);
//如果写入的数据长度超过缓冲区剩余空间,则报错
if (len >= (WRITE_BUFFER_SIZE - 1 - m_write_idx))
{
va_end(arg_list);
return false;
}
m_write_idx += len;//更新m_write_idx位置
va_end(arg_list);//清空可变参列表

LOG_INFO("request:%s", m_write_buf);

return true;
}
bool http_conn::add_status_line(int status, const char *title)//添加状态行
{
return add_response("%s %d %s\r\n", "HTTP/1.1", status, title);
}
bool http_conn::add_headers(int content_len)//添加消息报头,具体的添加文本长度、连接状态和空行
{
return add_content_length(content_len) && add_linger() &&
add_blank_line();
}
bool http_conn::add_content_length(int content_len)//添加Content-Length,表示响应报文的长度
{
return add_response("Content-Length:%d\r\n", content_len);
}
bool http_conn::add_content_type()//添加文本类型,这里是html
{
return add_response("Content-Type:%s\r\n", "text/html");
}
bool http_conn::add_linger()//添加连接状态,通知浏览器端是保持连接还是关闭
{
return add_response("Connection:%s\r\n", (m_linger == true) ? "keep-alive" : "close");
}
bool http_conn::add_blank_line()//添加空行
{
return add_response("%s", "\r\n");
}
bool http_conn::add_content(const char *content)//添加文本content
{
return add_response("%s", content);
}
bool http_conn::process_write(HTTP_CODE ret)//逻辑上处理要写什么
{
switch (ret)
{
case INTERNAL_ERROR://内部错误,500
{
add_status_line(500, error_500_title);//状态行
add_headers(strlen(error_500_form));//消息报头
if (!add_content(error_500_form))
return false;
break;
}
case BAD_REQUEST://报文语法有误,404
{
add_status_line(404, error_404_title);
add_headers(strlen(error_404_form));
if (!add_content(error_404_form))
return false;
break;
}
case FORBIDDEN_REQUEST://资源没有访问权限,403
{
add_status_line(403, error_403_title);
add_headers(strlen(error_403_form));
if (!add_content(error_403_form))
return false;
break;
}
case FILE_REQUEST://文件存在,200
{
add_status_line(200, ok_200_title);
if (m_file_stat.st_size != 0)//如果请求的资源存在
{
add_headers(m_file_stat.st_size);
//第一个iovec指针指向响应报文缓冲区,长度指向m_write_idx
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
//第二个iovec指针指向mmap返回的文件指针,长度指向文件大小
m_iv[1].iov_base = m_file_address;
m_iv[1].iov_len = m_file_stat.st_size;
m_iv_count = 2;
//发送的全部数据为响应报文头部信息和文件大小
bytes_to_send = m_write_idx + m_file_stat.st_size;
return true;
}
else
{ //如果请求的资源大小为0,则返回空白html文件
const char *ok_string = "<html><body></body></html>";
add_headers(strlen(ok_string));
if (!add_content(ok_string))
return false;
}
}
default:
return false;
}
//除FILE_REQUEST状态外,其余状态只申请一个iovec,指向响应报文缓冲区
m_iv[0].iov_base = m_write_buf;
m_iv[0].iov_len = m_write_idx;
m_iv_count = 1;
bytes_to_send = m_write_idx;
return true;
}
void http_conn::process()//对读事件完成最终处理并发送响应报文
{
HTTP_CODE read_ret = process_read();
if (read_ret == NO_REQUEST)//NO_REQUEST,表示请求不完整,需要继续接收请求数据
{
modfd(m_epollfd, m_sockfd, EPOLLIN, m_TRIGMode);//注册并监听读事件
return;
}
bool write_ret = process_write(read_ret);//调用process_write完成报文响应
if (!write_ret)
{
close_conn();//如果写错误就关闭连接,会把fd删除且关闭
}
//?都关闭了且没有注册怎么修改
modfd(m_epollfd, m_sockfd, EPOLLOUT, m_TRIGMode);//注册并监听写事件
}
  • MYSQL_RES *mysql_store_result(MYSQL *mysql)
    • 对于成功检索了数据的每个查询(SELECT、SHOW、DESCRIBE、EXPLAIN、CHECK TABLE等),必须调用mysql_store_result()或mysql_use_result() 。
    • 对于其他查询,不需要调用mysql_store_result()或mysql_use_result(),但是如果在任何情况下均调用了mysql_store_result(),它也不会导致任何伤害或性能降低。通过检查mysql_store_result()是否返回0,可检测查询是否没有结果集(以后会更多)。
    • 如果希望了解查询是否应返回结果集,可使用mysql_field_count()进行检查。
  • unsigned int mysql_field_count(MYSQL *mysql)
    • 返回作用在连接上的最近查询的列数。
  • MYSQL_FIELD *mysql_fetch_field(MYSQL_RES *result)
    • 返回采用MYSQL_FIELD结构的结果集的列。重复调用该函数,以检索关于结果集中所有列的信息。
  • MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)
    • 检索结果集的下一行。在mysql_store_result()之后使用时,如果没有要检索的行,mysql_fetch_row()返回NULL。在mysql_use_result()之后使用时,如果没有要检索的行或出现了错误,mysql_fetch_row()返回NULL。行内值的数目由mysql_num_fields(result)给出。
  • int munmap(void *start,size_t length);
    • 函数说明 munmap()用来取消参数start所指的映射内存起始地址,参数length则是欲取消的内存大小。当进程结束或利用exec相关函数来执行其他程序时,映射内存会自动解除,但关闭对应的文件描述词时不会解除映射。
    • 返回值 如果解除映射成功则返回0,否则返回-1,错误原因存于errno中错误代码EINVAL参数 start或length 不合法。

第五站

项目中使用的是SIGALRM信号,具体的,利用alarm函数周期性地触发SIGALRM信号,信号处理函数利用管道通知主循环,主循环接收到该信号后对升序链表上所有定时器进行处理,若该段时间内没有交换数据,则将该连接关闭,释放所占用的资源。

定时器处理非活动连接模块,主要分为两部分,其一为定时方法与信号通知流程,其二为定时器及其容器设计与定时任务的处理。

定时器与信号API

1
2
3
4
5
6
7
- Linux中的信号是一种消息处理机制,它本质上是一个整数,不同的信号对应不同的值,信号在系统中的优先级是非常高的。
- 项目中使用的信号
1. SIGALRM:定时器超时信号,超时的时间由系统调用alarm设置,默认终止进程。
2. SIGTERM:程序结束信号,kill或Ctrl+C触发,默认终止进程。
- 两个特殊信号
1. SIGKILL:9号信号,无条件终止进程,不能被捕捉、阻塞和忽略。
2. SIGSTOP:19号信号,无条件暂停进程,不能被捕捉、阻塞和忽略。
  • 还有一个信号:SIGPIPE:当服务器close一个连接时,若client端接着发数据。根据TCP 协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。
    • TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条. 当对端调用close时, 虽然本意是关闭整个两条信道, 但本端只是收到FIN包. 按照TCP协议的语义, 表示对端只是关闭了其所负责的那一条单工信道, 仍然可以继续接收数据. 也就是说, 因为TCP协议的限制, 一个端点无法获知对端的socket是调用了close还是shutdown.
    • 对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出.
    • 为了避免进程退出, 可以捕获SIGPIPE信号, 或者忽略它, 给它设置SIG_IGN信号处理函数:signal(SIGPIPE, SIG_IGN);SIG_IGN表示忽略信号
  • Linux中的每个信号产生之后都会有对应的默认处理行为,如果想要忽略某些信号或者修改某些信号的默认行为就需要在程序中捕捉该信号。
  • 程序中的信号捕捉是一个注册的动作,提前告诉应用程序信号产生之后的处理动作,当进程中对应的信号产生了,这个处理动作也就被调用了。
  • sigaction结构体:

    • struct sigaction
      {
          void (*sa_handler)(int);                        // 函数指针,指向信号处理函数。
          void (*sa_sigaction)(int, siginfo_t *, void *); // 函数指针,指向信号处理函数,有三个参数。
          sigset_t sa_mask;                               // 在信号处理函数执行期间,临时屏蔽的信号。
          int sa_flags;                                   // 用于指定信号处理的行为
          void (*sa_restorer)(void);                      // 被废弃的成员,一般不使用
      };
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17

      * flag有以下几个:

      * SA_RESTART,使被信号打断的系统调用自动重新发起
      * SA_NOCLDSTOP,使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号
      * SA_NOCLDWAIT,使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程
      * SA_NODEFER,使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
      * SA_RESETHAND,信号处理之后重新设置为默认的处理方式

      * sigaction函数:

      * ```c++
      int sigaction(
      int signum, // 要捕捉的信号。
      const struct sigaction *act, // 对信号设置新的处理方式。
      struct sigaction *oldact // 上一次信号处理方式,一般指定为NULL。
      );
    • 返回值,0 表示成功,-1 表示有错误发生。

  • sigfillset函数:

    • ```c++
      int sigfillset(sigset_t * set);
      1
      2
      3
      4
      5
      6
      7
      8

      * sigfillset()用来将参数set信号集初始化,然后把所有的信号加入到此信号集里,即将所有的信号标志位置为1,屏蔽所有的信号。信号集是在执行信号处理程序时被阻塞的信号集。因此,当执行信号处理程序时,所有信号都被阻塞,不必担心另一个信号会中断信号处理程序。

      * SIGALRM、SIGTERM信号,是整形数

      * ```c++
      #define SIGALRM 14 //由alarm系统调用产生timer时钟信号
      #define SIGTERM 15 //终端发送的终止信号
  • alarm函数

    • #include<unistd.h>
      unsigned int alarm(unsigned int seconds);
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16

      * alarm也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它向进程发送SIGALRM信号。可以设置忽略或者不捕获此信号,如果采用默认方式其动作是终止调用该alarm函数的进程。

      * 要注意的是,一个进程只能有一个闹钟时间,如果在调用alarm之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。需要注意的是,经过指定的秒数后,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需要一些时间。

      * 返回值:成功:如果调用此alarm()前,进程已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。

      * socketpair函数

      * 在linux下,使用socketpair函数能够创建一对套接字进行通信,项目中使用管道通信。socketpair创建的描述符任意一端既可以读也可以写。

      * ```c++
      #include <sys/types.h>
      #include <sys/socket.h>

      int socketpair(int domain, int type, int protocol, int sv[2]);
      * domain表示协议族,PF_UNIX或者AF_UNIX,AF = Address Family、PF = Protocol Family。PF_UNIX (也称作 PF_LOCAL ) 套接字族用来在同一机器上的提供有效的进程间通讯。AF\_和PF\_的值直接可以替换,没有其它区别。 * type表示协议,可以是SOCK_STREAM或者SOCK_DGRAM,SOCK_STREAM基于TCP,SOCK_DGRAM基于UDP * protocol表示类型,只能为0 * sv[2]表示套节字柄对,该两个句柄作用相同,均能进行读写双向操作
    • 返回结果, 0为创建成功,-1为创建失败

  • send函数,当套接字发送缓冲区变满时,send通常会阻塞,除非套接字设置为非阻塞模式,当缓冲区变满时,返回EAGAIN或者EWOULDBLOCK错误,此时可以调用select函数来监视何时可以发送数据。

    • ```c++
      #include <sys/types.h>
      #include <sys/socket.h>

      ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);

      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
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134

      * sockfd:指定发送端套接字描述符。
      * buff: 存放要发送数据的缓冲区
      * nbytes: 实际要改善的数据的字节数
      * flags: 一般设置为0

      * 1) send先比较发送数据的长度nbytes和套接字sockfd的发送缓冲区的长度,如果nbytes > 套接字sockfd的发送缓冲区的长度, 该函数返回SOCKET_ERROR。

      2) 如果nbtyes <= 套接字sockfd的发送缓冲区的长度,那么send先检查协议是否正在发送sockfd的发送缓冲区中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送sockfd的发送缓冲区中的数据或者sockfd的发送缓冲区中没有数据,那么send就比较sockfd的发送缓冲区的剩余空间和nbytes。

      3) 如果 nbytes > 套接字sockfd的发送缓冲区剩余空间的长度,send就一起等待协议把套接字sockfd的发送缓冲区中的数据发送完。

      4) 如果 nbytes < 套接字sockfd的发送缓冲区剩余空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意:并不是send把套接字sockfd的发送缓冲区中的数据传到连接的另一端的,而是协议传送的。send仅仅是把buf中的数据copy到套接字sockfd的发送缓冲区的剩余空间里)。

      5) 如果send函数copy成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果在等待协议传送数据时网络断开,send函数也返回SOCKET_ERROR。

      6) 如果send函数copy成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果在等待协议传送数据时网络断开,send函数也返回SOCKET_ERROR。

      7) 在unix系统下,如果send在等待协议传送数据时网络断开,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的处理是进程终止。



      ## 头文件

      lst_timer.h

      ```c++
      #ifndef LST_TIMER
      #define LST_TIMER

      #include <unistd.h>
      #include <signal.h>
      #include <sys/types.h>
      #include <sys/epoll.h>
      #include <fcntl.h>
      #include <sys/socket.h>
      #include <netinet/in.h>
      #include <arpa/inet.h>
      #include <assert.h>
      #include <sys/stat.h>
      #include <string.h>
      #include <pthread.h>
      #include <stdio.h>
      #include <stdlib.h>
      #include <sys/mman.h>
      #include <stdarg.h>
      #include <errno.h>
      #include <sys/wait.h>
      #include <sys/uio.h>

      #include <time.h>
      #include "../log/log.h"

      //连接资源结构体成员需要用到定时器类
      //前向声明
      class util_timer;

      //连接资源
      struct client_data
      {
      //客户端socket地址,项目中未使用
      sockaddr_in address;
      int sockfd;//socket文件描述符
      util_timer *timer;//定时器
      };
      //定时器类
      class util_timer
      {
      public:
      util_timer() : prev(NULL), next(NULL) {}

      public:
      time_t expire;//超时时间

      void (* cb_func)(client_data *);//回调函数指针,这个回调函数会删除client_data的资源连接
      client_data *user_data;//连接资源,嵌套类使用指针,相当于内部成员指针互相指向对方实例
      util_timer *prev;//前向定时器
      util_timer *next;//后继定时器
      };
      //定时器容器类,双向链表
      class sort_timer_lst
      {
      public:
      sort_timer_lst();
      ~sort_timer_lst();//常规销毁链表

      void add_timer(util_timer *timer);//添加定时器,内部调用私有成员add_timer
      void adjust_timer(util_timer *timer);//调整定时器,任务发生变化时,调整定时器在链表中的位置
      void del_timer(util_timer *timer);//删除定时器
      void tick();//定时任务处理函数,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器。

      private:
      void add_timer(util_timer *timer, util_timer *lst_head);

      //创建头尾指针,方便管理
      util_timer *head;
      util_timer *tail;
      };

      class Utils//资源管理类
      {
      public:
      Utils() {}
      ~Utils() {}

      void init(int timeslot);

      //对文件描述符设置非阻塞
      int setnonblocking(int fd);

      //将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
      void addfd(int epollfd, int fd, bool one_shot, int TRIGMode);

      //信号处理函数
      static void sig_handler(int sig);

      //设置信号函数
      void addsig(int sig, void(handler)(int), bool restart = true);

      //定时处理任务,重新定时以不断触发SIGALRM信号
      void timer_handler();

      void show_error(int connfd, const char *info);

      public:
      static int *u_pipefd;//管道描述符
      sort_timer_lst m_timer_lst;//定时器容器
      static int u_epollfd;//事务描述符
      int m_TIMESLOT;
      };

      void cb_func(client_data *user_data);//回调函数

      #endif

.cpp实现

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#include "lst_timer.h"
#include "../http/http_conn.h"

sort_timer_lst::sort_timer_lst()
{
head = NULL;
tail = NULL;
}
sort_timer_lst::~sort_timer_lst()//常规销毁链表
{
util_timer *tmp = head;
while (tmp)
{
head = tmp->next;
delete tmp;
tmp = head;
}
}

void sort_timer_lst::add_timer(util_timer *timer)
{
if (!timer)//没有timer要加
{
return;
}
if (!head)//链表中一个节点都没有
{
head = tail = timer;
return;
}

//如果新的定时器超时时间小于当前头部结点
//直接将当前定时器结点作为头部结点
if (timer->expire < head->expire)
{
timer->next = head;
head->prev = timer;
head = timer;
return;
}
//否则要插入链表中间或结尾,调用私有方法
add_timer(timer, head);
}

//调整定时器,任务发生变化时,调整定时器在链表中的位置
void sort_timer_lst::adjust_timer(util_timer *timer)
{
if (!timer)
{
return;
}
util_timer *tmp = timer->next;
//被调整的定时器在链表尾部,不调整
//定时器超时值仍然小于下一个定时器超时值,不调整(定时器刷新时间只可能更大,不用和前面的节点比较)
if (!tmp || (timer->expire < tmp->expire))
{
return;
}
//被调整定时器是链表头结点,将定时器取出,重新插入
if (timer == head)
{
head = head->next;
head->prev = NULL;
timer->next = NULL;
add_timer(timer, head);
}
//被调整定时器在内部,将定时器取出,重新插入
else
{
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
add_timer(timer, timer->next);//因为timer要比next大,所以next当头节点往后add就可以
}
}
//删除定时器
void sort_timer_lst::del_timer(util_timer *timer)
{
if (!timer)
{
return;
}
//链表中只有一个定时器,需要删除该定时器
if ((timer == head) && (timer == tail))
{
delete timer;
head = NULL;
tail = NULL;
return;
}
//被删除的定时器为头结点
if (timer == head)
{
head = head->next;
head->prev = NULL;
delete timer;
return;
}
//被删除的定时器为尾结点
if (timer == tail)
{
tail = tail->prev;
tail->next = NULL;
delete timer;
return;
}
//被删除的定时器在链表内部,常规链表结点删除
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
delete timer;
}

//定时任务处理函数,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器。
void sort_timer_lst::tick()
{
if (!head)
{
return;
}
//获取当前时间,定时器的超时时间是以前设置的时间+n个单位超时时间(成为未来时间),所以用当前时间来比较判断是否超时
time_t cur = time(NULL);
util_timer *tmp = head;
//遍历定时器链表
while (tmp)
{
//链表容器为升序排列
//当前时间小于定时器的超时时间,后面的定时器也没有到期
if (cur < tmp->expire)
{
break;
}
//当前定时器到期,则调用回调函数,执行定时事件
tmp->cb_func(tmp->user_data);
//将处理后的定时器从链表容器中删除,并重置头结点
head = tmp->next;
if (head)
{
head->prev = NULL;
}
delete tmp;
tmp = head;
}
}

//私有成员,被公有成员add_timer和adjust_time调用
//主要用于调整链表内部结点
void sort_timer_lst::add_timer(util_timer *timer, util_timer *lst_head)
{
util_timer *prev = lst_head;
util_timer *tmp = prev->next;//不可能比头节点小,所以比较后面的一个节点
//遍历当前结点之后的链表,按照超时时间找到目标定时器对应的位置,常规双向链表插入操作
while (tmp)
{
if (timer->expire < tmp->expire)//可以插到tmp前面
{
prev->next = timer;
timer->next = tmp;
tmp->prev = timer;
timer->prev = prev;
break;
}
prev = tmp;
tmp = tmp->next;
}
//遍历完发现,目标定时器需要放到尾结点处
if (!tmp)
{
prev->next = timer;
timer->prev = prev;
timer->next = NULL;
tail = timer;
}
}

void Utils::init(int timeslot)
{
m_TIMESLOT = timeslot;//单位时间
}

//对文件描述符设置非阻塞
int Utils::setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}

//将内核事件表注册读事件,ET模式,选择开启EPOLLONESHOT
void Utils::addfd(int epollfd, int fd, bool one_shot, int TRIGMode)
{
epoll_event event;
event.data.fd = fd;

if (1 == TRIGMode)
event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
else
event.events = EPOLLIN | EPOLLRDHUP;

if (one_shot)
event.events |= EPOLLONESHOT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}

//信号处理函数
void Utils::sig_handler(int sig)
{
//为保证函数的可重入性,保留原来的errno
//可重入性表示中断后再次进入该函数,环境变量与之前相同,不会丢失数据
int save_errno = errno;
int msg = sig;
//将信号值从管道写端写入,传输字符类型,而非整型
send(u_pipefd[1], (char *)&msg, 1, 0);
errno = save_errno;
}

//设置信号函数,信号是指SIGALRM这些信号,本质是一个int
//当超时时(比如alarm)会产生这个信号,这里的设置(注册)就是让这个信号的处理按照这里设置的方式,比如flag和处理函数
void Utils::addsig(int sig, void(*handler)(int), bool restart)//handler是sig_handler
{
//创建sigaction结构体变量
struct sigaction sa;
memset(&sa, '\0', sizeof(sa));
//信号处理函数中仅仅发送信号值,不做对应逻辑处理
sa.sa_handler = handler;
if (restart)
sa.sa_flags |= SA_RESTART;
//将所有信号添加到信号集sa_mask中,屏蔽所有信号
sigfillset(&sa.sa_mask);
//执行sigaction函数
assert(sigaction(sig, &sa, NULL) != -1);//这个sig信号会屏蔽其他的信号
}

//定时处理任务,重新定时以不断触发SIGALRM信号
void Utils::timer_handler()
{
m_timer_lst.tick();
alarm(m_TIMESLOT);
}

void Utils::show_error(int connfd, const char *info)
{
send(connfd, info, strlen(info), 0);
close(connfd);
}

int *Utils::u_pipefd = 0;
int Utils::u_epollfd = 0;

class Utils;//在这声明是什么意思?

//定时器回调函数,tick函数调用
void cb_func(client_data *user_data)
{
//删除非活动连接在socket上的注册事件
epoll_ctl(Utils::u_epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
assert(user_data);
//关闭文件描述符
close(user_data->sockfd);
//减少连接数
http_conn::m_user_count--;
}

使用逻辑

首先注册(设置)好信号,比如SIGALRM信号,这使得产生这个信号时,能有对应的方式和处理(默认是终止进程),这里的方式是restart、屏蔽其他信号,处理是通过管道向主循环发这个信号。产生的方式是alarm(),主循环中每次尝试从管道获取信号,如果有这个信号,则设置timeout为true说明有超时事件要处理,因为是非必须事件,在这轮循环读写完再进行处理。

处理会调用timer_handler(),首先调用tick(),把定时器超时的都关了(调用cb_func),然后重新alarm()。

关于SIGTERM:程序结束信号,kill或Ctrl+C触发,默认终止进程。

也就是:

  • 信号

    • 1.先知道有些动作会产生一些信号

    • 2.设置(注册)这些信号产生后的动作——方式(flag)和处理函数(handler)

    • 3.处理函数只是通知主循环有个信号产生,主循环要做对应的处理。

  • 主循环,我们看看会发生什么

    • 1.当一个连接到来时,要创建一个定时器给它,初始化时间和回调函数等变量;
    • 2.如果连接有读写,更新时间;
    • 3.无论有没有定时器超时,每隔timeslot(时隙)会alarm一次,触发信号后主循环得知信号产生,去查看有哪些连接的定时器超时了,超时就关闭连接,然后重新设置alarm,循环往复。
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//定时处理任务,重新定时以不断触发SIGALRM信号
void timer_handler()
{
timer_lst.tick();
alarm(TIMESLOT);
}

//创建定时器容器链表
static sort_timer_lst timer_lst;

//创建连接资源数组
client_data *users_timer = new client_data[MAX_FD];

//超时默认为False
bool timeout = false;

//alarm定时触发SIGALRM信号
alarm(TIMESLOT);

while (!stop_server)
{
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
break;
}

for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;

//处理新到的客户连接
if (sockfd == listenfd)
{
//初始化客户端连接地址
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);

//该连接分配的文件描述符
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);

//初始化该连接对应的连接资源
users_timer[connfd].address = client_address;
users_timer[connfd].sockfd = connfd;

//创建定时器临时变量
util_timer *timer = new util_timer;
//设置定时器对应的连接资源
timer->user_data = &users_timer[connfd];
//设置回调函数
timer->cb_func = cb_func;

time_t cur = time(NULL);
//设置绝对超时时间
timer->expire = cur + 3 * TIMESLOT;
//创建该连接对应的定时器,初始化为前述临时变量
users_timer[connfd].timer = timer;
//将该定时器添加到链表中
timer_lst.add_timer(timer);
}
//处理异常事件
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
//服务器端关闭连接,移除对应的定时器
cb_func(&users_timer[sockfd]);

util_timer *timer = users_timer[sockfd].timer;
if (timer)
{
timer_lst.del_timer(timer);
}
}

//处理定时器信号
else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
{
//接收到SIGALRM信号,timeout设置为True
}

//处理客户连接上接收到的数据
else if (events[i].events & EPOLLIN)
{
//创建定时器临时变量,将该连接对应的定时器取出来
util_timer *timer = users_timer[sockfd].timer;
if (users[sockfd].read_once())
{
//若监测到读事件,将该事件放入请求队列
pool->append(users + sockfd);

//若有数据传输,则将定时器往后延迟3个单位
//对其在链表上的位置进行调整
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
timer_lst.adjust_timer(timer);
}
}
else
{
//服务器端关闭连接,移除对应的定时器
cb_func(&users_timer[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
}
else if (events[i].events & EPOLLOUT)
{
util_timer *timer = users_timer[sockfd].timer;
if (users[sockfd].write())
{
//若有数据传输,则将定时器往后延迟3个单位
//并对新的定时器在链表上的位置进行调整
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
timer_lst.adjust_timer(timer);
}
}
else
{
//服务器端关闭连接,移除对应的定时器
cb_func(&users_timer[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
}
}
//处理定时器为非必须事件,收到信号并不是立马处理
//完成读写事件后,再进行处理
if (timeout)
{
timer_handler();
timeout = false;
}
}

第六站

内容比较少

config

头文件config.h

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
36
37
38
39
40
41
42
43
44
45
46
47
48
#ifndef CONFIG_H
#define CONFIG_H

#include "webserver.h"

using namespace std;

class Config
{
public:
Config();
~Config(){};

void parse_arg(int argc, char*argv[]);

//端口号
int PORT;

//日志写入方式
int LOGWrite;

//触发组合模式
int TRIGMode;

//listenfd触发模式
int LISTENTrigmode;

//connfd触发模式
int CONNTrigmode;

//优雅关闭链接
int OPT_LINGER;

//数据库连接池数量
int sql_num;

//线程池内的线程数量
int thread_num;

//是否关闭日志
int close_log;

//并发模型选择
int actor_model;
};

#endif

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include "config.h"

Config::Config(){
//端口号,默认9006
PORT = 9006;

//日志写入方式,默认同步
LOGWrite = 0;

//触发组合模式,默认listenfd LT + connfd LT
TRIGMode = 0;

//listenfd触发模式,默认LT
LISTENTrigmode = 0;

//connfd触发模式,默认LT
CONNTrigmode = 0;

//优雅关闭链接,默认不使用
OPT_LINGER = 0;

//数据库连接池数量,默认8
sql_num = 8;

//线程池内的线程数量,默认8
thread_num = 8;

//关闭日志,默认不关闭
close_log = 0;

//并发模型,默认是proactor,这个后面介绍
actor_model = 0;
}

void Config::parse_arg(int argc, char*argv[]){//命令行形式获取参数
int opt;
const char *str = "p:l:m:o:s:t:c:a:";
while ((opt = getopt(argc, argv, str)) != -1)//这个函数下面介绍
{
switch (opt)//会重新排列参数顺序,所以要switch
{
case 'p':
{
PORT = atoi(optarg);
break;
}
case 'l':
{
LOGWrite = atoi(optarg);
break;
}
case 'm':
{
TRIGMode = atoi(optarg);
break;
}
case 'o':
{
OPT_LINGER = atoi(optarg);
break;
}
case 's':
{
sql_num = atoi(optarg);
break;
}
case 't':
{
thread_num = atoi(optarg);
break;
}
case 'c':
{
close_log = atoi(optarg);
break;
}
case 'a':
{
actor_model = atoi(optarg);
break;
}
default:
break;
}
}
}

  • getopt() 方法是用来分析命令行参数的,该方法由 Unix 标准库提供,包含在 <unistd.h> 头文件中。

  • ```c++
    int getopt(int argc, char * const argv[], const char *optstring);
    extern char *optarg; //选项的参数指针
    extern int optind, //下一次调用getopt的时,从optind存储的位置处重新开始检查选项。
    extern int opterr, //当opterr=0时,getopt不向stderr输出错误信息。
    extern int optopt; //当命令行选项字符不包括在optstring中或者选项缺少必要的参数时,该选项存储在optopt中,getopt返回’?’、

    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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71

    * argc:通常由函数直接传入,表示参数的数量

    * argv:通常也由函数直接传入,表示参数的字符串变量数组

    * optstring:一个包含正确的参数选项字符串,用于参数的解析。例如 “abc:”,其中 -a,-b 就表示两个普通选项,-c 表示一个必须有参数的选项,因为它后面有一个冒号

    * 1.单个字符,表示选项,
    * 2.单个字符后接一个冒号:表示该选项后必须跟一个参数。参数紧跟在选项后或者以空格隔开。该参数的指针赋给optarg。
    * 3 单个字符后跟两个冒号,表示该选项后必须跟一个参数。参数必须紧跟在选项后不能以空格隔开。该参数的指针赋给optarg。(这个特性是GNU的扩张)。

    * getopt处理以'-’开头的命令行参数,如optstring="ab:c::d::",命令行为getopt.exe -a -b host -ckeke -d haha
    在这个命令行参数中,-a和-h就是选项元素,去掉'-',a,b,c就是选项。host是b的参数,keke是c的参数。但haha并不是d的参数,因为它们中间有空格隔开。

    * getopt()用来分析命令行参数。参数argc和argv是由main()传递的参数个数和内容。参数optstring 则代表欲处理的选项字符串。此函数会返回在argv 中下一个的选项字母(指针不断移动),此字母会对应参数optstring 中的字母。如果选项字符串里的字母后接着冒号“:”,则表示还有相关的参数,全域变量optarg 即会指向此额外参数。如果getopt()找不到符合的参数则会印出错信息,并将全域变量optopt设为“?”字符,如果不希望getopt()印出错信息,则只要将全域变量opterr设为0即可。

    * 还要注意的是默认情况下getopt会重新排列命令行参数的顺序,所以到最后所有不包含选项的命令行参数都排到最后。
    如getopt.exe -a ima -b host -ckeke -d haha, 都最后命令行参数的顺序是: -a -b host -ckeke -d ima haha



    关于proactor模式,小林coding的这篇分析写得很好,推荐看一看:[如何深刻理解Reactor和Proactor? - 知乎 (zhihu.com)](https://www.zhihu.com/question/26943938)

    项目中用的是假reactor和模拟proactor(同步的):[(29条消息) 两种高效的事件处理模式:Reactor模式和Proactor模式_ZY-JIMMY的博客-CSDN博客_reactor模式和proactor](https://blog.csdn.net/ZYZMZM_/article/details/98049471)

    ## main

    main.cpp

    ```c++
    #include "config.h"

    int main(int argc, char *argv[])
    {
    //需要修改的数据库信息,登录名,密码,库名
    string user = "root";
    string passwd = "root";
    string databasename = "qgydb";

    //命令行解析
    Config config;
    config.parse_arg(argc, argv);

    WebServer server;//websever在config.h中导入了websever.h

    //初始化
    server.init(config.PORT, user, passwd, databasename, config.LOGWrite,
    config.OPT_LINGER, config.TRIGMode, config.sql_num, config.thread_num,
    config.close_log, config.actor_model);


    //日志
    server.log_write();

    //数据库
    server.sql_pool();

    //线程池
    server.thread_pool();

    //触发模式
    server.trig_mode();

    //监听
    server.eventListen();

    //运行
    server.eventLoop();

    return 0;
    }

makefile&g++

先看这个入门:(29条消息) Makefile 语法入门_阿飞__的博客-CSDN博客_makefile语法

再看:(29条消息) Makefile文件语法规则及用法总结_fangye945a的博客-CSDN博客_makefile语法规则

Makefile 条件判断 - ifeq、ifneq、ifdef、ifndef - Makefile 简明教程 | 宅学部落 (zhaixue.cc)

g++参数:C/C++专题—gcc g++ 参数详解 - 知乎 (zhihu.com)

rm命令:Linux rm命令:删除文件或目录 (biancheng.net)

项目中的makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CXX ?= g++

DEBUG ?= 1
ifeq ($(DEBUG), 1)
CXXFLAGS += -g
else
CXXFLAGS += -O2

endif

server: main.cpp ./timer/lst_timer.cpp ./http/http_conn.cpp ./log/log.cpp ./CGImysql/sql_connection_pool.cpp webserver.cpp config.cpp
$(CXX) -o server $^ $(CXXFLAGS) -lpthread -lmysqlclient

clean:
rm -r server

第七站

顶层实现

头文件websever.h

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#ifndef WEBSERVER_H
#define WEBSERVER_H

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <cassert>
#include <sys/epoll.h>

#include "./threadpool/threadpool.h"
#include "./http/http_conn.h"

const int MAX_FD = 65536; //最大文件描述符,即最大连接数
const int MAX_EVENT_NUMBER = 10000; //最大事件数
const int TIMESLOT = 5; //最小超时单位

class WebServer
{
public:
WebServer();
~WebServer();

void init(int port , string user, string passWord, string databaseName,
int log_write , int opt_linger, int trigmode, int sql_num,
int thread_num, int close_log, int actor_model);

void thread_pool();
void sql_pool();
void log_write();
void trig_mode();
void eventListen();
void eventLoop();
void timer(int connfd, struct sockaddr_in client_address);
void adjust_timer(util_timer *timer);
void deal_timer(util_timer *timer, int sockfd);
bool dealclinetdata();
bool dealwithsignal(bool& timeout, bool& stop_server);
void dealwithread(int sockfd);
void dealwithwrite(int sockfd);

public:
//基础
int m_port;//端口
char *m_root;//根目录地址
int m_log_write;//是否要异步写日志,异步写用一个阻塞队列
int m_close_log;//是否关闭日志
int m_actormodel;//模型切换

int m_pipefd[2];//管道通信
int m_epollfd;//内核描述符
http_conn *users;

//数据库相关
connection_pool *m_connPool;
string m_user; //登陆数据库用户名
string m_passWord; //登陆数据库密码
string m_databaseName; //使用数据库名
int m_sql_num;

//线程池相关
threadpool<http_conn> *m_pool;
int m_thread_num;

//epoll_event相关
epoll_event events[MAX_EVENT_NUMBER];

int m_listenfd;//监听文件描述符
int m_OPT_LINGER;
int m_TRIGMode;//控制连接和读写的触发模式
int m_LISTENTrigmode;
int m_CONNTrigmode;

//定时器相关
client_data *users_timer;
Utils utils;
};
#endif

.cpp实现

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
#include "webserver.h"

WebServer::WebServer()
{
//http_conn类对象
users = new http_conn[MAX_FD];

//root文件夹路径
char server_path[200];
getcwd(server_path, 200);
char root[6] = "/root";
m_root = (char *)malloc(strlen(server_path) + strlen(root) + 1);
strcpy(m_root, server_path);
strcat(m_root, root);

//定时器
users_timer = new client_data[MAX_FD];//每个连接都对应一个定时器
}

WebServer::~WebServer()
{
close(m_epollfd);
close(m_listenfd);
close(m_pipefd[1]);
close(m_pipefd[0]);
delete[] users;
delete[] users_timer;
delete m_pool;
}

void WebServer::init(int port, string user, string passWord, string databaseName, int log_write,
int opt_linger, int trigmode, int sql_num, int thread_num, int close_log, int actor_model)
{
m_port = port;
m_user = user;
m_passWord = passWord;
m_databaseName = databaseName;
m_sql_num = sql_num;
m_thread_num = thread_num;
m_log_write = log_write;
m_OPT_LINGER = opt_linger;
m_TRIGMode = trigmode;
m_close_log = close_log;
m_actormodel = actor_model;
}

void WebServer::trig_mode()
{
//LT + LT
if (0 == m_TRIGMode)
{
m_LISTENTrigmode = 0;//读写的模式
m_CONNTrigmode = 0;//连接的模式
}
//LT + ET
else if (1 == m_TRIGMode)
{
m_LISTENTrigmode = 0;
m_CONNTrigmode = 1;
}
//ET + LT
else if (2 == m_TRIGMode)
{
m_LISTENTrigmode = 1;
m_CONNTrigmode = 0;
}
//ET + ET
else if (3 == m_TRIGMode)
{
m_LISTENTrigmode = 1;
m_CONNTrigmode = 1;
}
}

void WebServer::log_write()
{
if (0 == m_close_log)
{
//初始化日志
if (1 == m_log_write)//是否异步写日志
Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 800);//800是阻塞队列长度
else
Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 0);
}
}

void WebServer::sql_pool()
{
//初始化数据库连接池
m_connPool = connection_pool::GetInstance();
m_connPool->init("localhost", m_user, m_passWord, m_databaseName, 3306, m_sql_num, m_close_log);

//初始化数据库读取表
users->initmysql_result(m_connPool);
}

void WebServer::thread_pool()
{
//线程池,前面创建了users一组http_conn对象,每个对象的工作处理由线程池调用
m_pool = new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num);//只有这一个实例
}

void WebServer::eventListen()
{
//网络编程基础步骤
m_listenfd = socket(PF_INET, SOCK_STREAM, 0);//创建一个套接字,监听套接口,socket函数看后面
assert(m_listenfd >= 0);

//优雅关闭连接
if (0 == m_OPT_LINGER)
{
struct linger tmp = {0, 1};
setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
}
else if (1 == m_OPT_LINGER)
{
struct linger tmp = {1, 1};
setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
}

int ret = 0;
//地址配置
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);//本函数将一个32位数从主机字节顺序转换成网络字节顺序。INADDR_ANY见后面
address.sin_port = htons(m_port);//将整型变量从主机字节顺序转变成网络字节顺序,就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。
/*
网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,
从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用big-endian排序方式。
*/
int flag = 1;
//打开地址复用功能,允许服务器bind一个地址,即使这个地址当前已经存在已建立的连接
setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));//见后面
assert(ret >= 0);
ret = listen(m_listenfd, 5);
assert(ret >= 0);

utils.init(TIMESLOT);//初始化资源管理类

//epoll创建内核事件表
epoll_event events[MAX_EVENT_NUMBER];//事件集合
m_epollfd = epoll_create(5);//事件表描述符
assert(m_epollfd != -1);

utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);//内核事件表注册读事件,监听socket
http_conn::m_epollfd = m_epollfd;

ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);//创建管道套接字
assert(ret != -1);
utils.setnonblocking(m_pipefd[1]);//信号写端,设置非阻塞,当缓冲区满了时候不阻塞,减少send执行时间
utils.addfd(m_epollfd, m_pipefd[0], false, 0);//设置管道读端为ET非阻塞

//传递给主循环的信号值,这里为信号注册处理函数,restart是false,在程序中手动重新设置
utils.addsig(SIGPIPE, SIG_IGN);//忽略连接断开信号
utils.addsig(SIGALRM, utils.sig_handler, false);//超时信号
utils.addsig(SIGTERM, utils.sig_handler, false);//终止信号

alarm(TIMESLOT);//开始计时

//工具类,信号和描述符基础操作
Utils::u_pipefd = m_pipefd;
Utils::u_epollfd = m_epollfd;
}

void WebServer::timer(int connfd, struct sockaddr_in client_address)//获取一个连接后,初始化计时器和用户数据
{
users[connfd].init(connfd, client_address, m_root, m_CONNTrigmode, m_close_log, m_user, m_passWord, m_databaseName);

//初始化client_data数据
//创建定时器,设置回调函数和超时时间,绑定用户数据,将定时器添加到链表中
users_timer[connfd].address = client_address;
users_timer[connfd].sockfd = connfd;
util_timer *timer = new util_timer;
timer->user_data = &users_timer[connfd];
timer->cb_func = cb_func;
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
users_timer[connfd].timer = timer;
utils.m_timer_lst.add_timer(timer);
}

//若有数据传输,则将定时器往后延迟3个单位
//并对新的定时器在链表上的位置进行调整
void WebServer::adjust_timer(util_timer *timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
utils.m_timer_lst.adjust_timer(timer);

LOG_INFO("%s", "adjust timer once");
}

void WebServer::deal_timer(util_timer *timer, int sockfd)
{
timer->cb_func(&users_timer[sockfd]);
if (timer)
{
utils.m_timer_lst.del_timer(timer);
}

LOG_INFO("close fd %d", users_timer[sockfd].sockfd);
}

bool WebServer::dealclinetdata()
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
if (0 == m_LISTENTrigmode)//LT模式
{
int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);//返回值是连接描述符
if (connfd < 0)
{
LOG_ERROR("%s:errno is:%d", "accept error", errno);
return false;
}
if (http_conn::m_user_count >= MAX_FD)
{
utils.show_error(connfd, "Internal server busy");
LOG_ERROR("%s", "Internal server busy");
return false;
}
timer(connfd, client_address);
}

else//ET模式
{
while (1)//必须一次把监听到的连接读取完,因此循环读取、初始化,直至缓冲区为空
{
int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
if (connfd < 0)
{
LOG_ERROR("%s:errno is:%d", "accept error", errno);
break;
}
if (http_conn::m_user_count >= MAX_FD)
{
utils.show_error(connfd, "Internal server busy");
LOG_ERROR("%s", "Internal server busy");
break;
}
timer(connfd, client_address);//读取一个连接就初始化
}
return false;
}
return true;
}

bool WebServer::dealwithsignal(bool &timeout, bool &stop_server)
{
int ret = 0;
int sig;
char signals[1024];
ret = recv(m_pipefd[0], signals, sizeof(signals), 0);//从读管道读取信号,放到signals缓冲区
//recv的作用,就是通过fdt找到这个缓冲区,并把数据复制到咱们的参数2指向的地址,复制参数3个
//返回:读出来的字节大小;客户端下线,返回0;执行失败,返回-1
//正常情况下,这里的ret返回值总是1,只有14和15两个ASCII码对应的字符
if (ret == -1)
{
return false;
}
else if (ret == 0)
{
return false;
}
else
{
for (int i = 0; i < ret; ++i)
{
switch (signals[i])
{
case SIGALRM:
{
timeout = true;
break;
}
case SIGTERM:
{
stop_server = true;
break;
}
}
}
}
return true;
}

void WebServer::dealwithread(int sockfd)
{
util_timer *timer = users_timer[sockfd].timer;

//reactor,非阻塞同步
if (1 == m_actormodel)
{
if (timer)
{
adjust_timer(timer);
}

//若监测到读事件,将该事件放入请求队列(线程池的请求队列中),同步模式让线程处理整个读过程和相响应报文生成过程
m_pool->append(users + sockfd, 0);//users[sockfd]

while (true)//一直等待这个读事件完成,很多评论说是作者偷懒了,
//这不是reactor模式,这相当于阻塞了,最多只有一个http请求。完全没有发挥线程池的作用
{
if (1 == users[sockfd].improv)//完成标志
{
if (1 == users[sockfd].timer_flag)//如果要关闭的话
{
deal_timer(timer, sockfd);//关闭连接
users[sockfd].timer_flag = 0;
}
users[sockfd].improv = 0;
break;
}
}
}
else//异步,主线程和内核执行IO,工作线程负责业务处理
{
//proactor,实际上这里是模拟proactor模式,是同步的模式,只有主线程串行IO
if (users[sockfd].read_once())//读数据成功的话(无论是LT还是ET),返回true
{
LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));

//若监测到读事件,将该事件放入请求队列
m_pool->append_p(users + sockfd);

if (timer)
{
adjust_timer(timer);
}
}
else
{
deal_timer(timer, sockfd);
}
}
}

void WebServer::dealwithwrite(int sockfd)
{
util_timer *timer = users_timer[sockfd].timer;
//reactor
if (1 == m_actormodel)
{
if (timer)
{
adjust_timer(timer);
}

m_pool->append(users + sockfd, 1);

while (true)
{
if (1 == users[sockfd].improv)
{
if (1 == users[sockfd].timer_flag)
{
deal_timer(timer, sockfd);
users[sockfd].timer_flag = 0;
}
users[sockfd].improv = 0;
break;
}
}
}
else
{
//proactor
if (users[sockfd].write())
{
LOG_INFO("send data to the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));

if (timer)
{
adjust_timer(timer);
}
}
else
{
deal_timer(timer, sockfd);
}
}
}

void WebServer::eventLoop()//主循环
{
bool timeout = false;
bool stop_server = false;

while (!stop_server)
{
int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
LOG_ERROR("%s", "epoll failure");
break;
}

for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;

//处理新到的客户连接
if (sockfd == m_listenfd)
{
bool flag = dealclinetdata();
if (false == flag)
continue;
}
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
//服务器端关闭连接,移除对应的定时器
util_timer *timer = users_timer[sockfd].timer;
deal_timer(timer, sockfd);
}
//处理信号
else if ((sockfd == m_pipefd[0]) && (events[i].events & EPOLLIN))
{
bool flag = dealwithsignal(timeout, stop_server);
if (false == flag)
LOG_ERROR("%s", "dealclientdata failure");
}
//处理客户连接上接收到的数据
else if (events[i].events & EPOLLIN)
{
dealwithread(sockfd);
}
else if (events[i].events & EPOLLOUT)
{
dealwithwrite(sockfd);
}
}
if (timeout)
{
utils.timer_handler();

LOG_INFO("%s", "timer tick");

timeout = false;
}
}
}

  • int socket(int af, int type, int protocol);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    * af 为地址族(Address Family),也就是 IP 地址类型,常用的有 AF_INET 和 AF_INET6。AF 是“Address Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,例如 127.0.0.1;AF_INET6 表示 IPv6 地址,例如 1030::C9B4:FF12:48AA:1A2B。
    * type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。
    * protocol 表示传输协议,常用的有 IPPROTO_TCP 和 IPPTOTO_UDP,分别表示 TCP 传输协议和 UDP 传输协议。
    * 为什么还需要第三个参数呢?一般情况下有了 af 和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

    * 若无错误发生,socket()返回引用新套接口的描述字。



    * ```c++
    int listen(int sockfd, int backlog);
  • listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

  • 成功返回0

  • int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    
    1
    2
    3
    4
    5

    * connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

    * ```c++
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

  • accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为客户端协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。


  • struct linger用法:

  • Linux下tcp连接断开的时候调用close()函数,有优雅断开和强制断开两种方式。那么如何设置断开连接的方式呢?是通过设置socket描述符一个linger结构体属性。

  • ```c++
    #include <arpa/inet.h>

    struct linger {
      int l_onoff;
      int l_linger;
    };

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    * 三种断开方式:

    * 1. l_onoff = 0; l_linger忽略:close()立刻返回,底层会将未发送完的数据发送完成后再释放资源,即优雅退出。
    2. l_onoff != 0; l_linger = 0:close()立刻返回,但不会发送未发送完成的数据,而是通过一个REST包强制的关闭socket描述符,即强制退出。
    3. l_onoff != 0; l_linger > 0:close()不会立刻返回,内核会延迟一段时间,这个时间就由l_linger的值来决定。如果超时时间到达之前,发送完未发送的数据(包括FIN包)并得到另一端的确认,close()会返回正确,socket描述符优雅性退出。否则,close()会直接返回错误值,未发送数据丢失,socket描述符被强制性退出。需要注意的时,如果socket描述符被设置为非堵塞型,则close()会直接返回值。

    -----

    使用完linger之后,就用setsockopt()设置

    ```c++
    #include <sys/socket.h>

    int setsockopt( int socket, int level, int option_name,const void *option_value, size_t ,ption_len);

第一个参数socket是套接字描述符。第二个参数level是被设置的选项的级别,如果想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET。 option_name指定准备设置的选项,len是选项的长度。option_name可以有哪些取值,这取决于level,以linux 2.6内核为例(在不同的平台上,这种关系可能会有不同),在套接字级别上(SOL_SOCKET),option_name可以有以下取值:

  • SO_LINGER,如果选择此选项, close或 shutdown将等到所有套接字里排队的消息成功发送或到达延迟时间后>才会返回. 否则, 调用将立即返回。该选项的参数(option_value)是一个linger结构:

    1
    2
    3
    4
    5
    6
    7
    8
    struct linger {
    int l_onoff;
    int l_linger;
    };
    /*
    如果linger.l_onoff值为0(关闭),则清 sock->sk->sk_flag中的SOCK_LINGER位;
    否则,置该位,并赋sk->sk_lingertime值为 linger.l_linger。
    */
  • SO_DEBUG,打开或关闭调试信息。当option_value不等于0时,打开调试信息,否则,关闭调试信息。它实际所做的工作是在sock->sk->sk_flag中置 SOCK_DBG(第10)位,或清SOCK_DBG位。

  • SO_REUSEADDR,打开或关闭地址复用功能。当option_value不等于0时,打开,否则,关闭。它实际所做的工作是置sock->sk->sk_reuse为1或0。

    • SO_REUSEADDR是一个很有用的选项,一般服务器的监听socket都应该打开它。它的大意是允许服务器bind一个地址,即使这个地址当前已经存在已建立的连接,比如:
      • 服务器启动后,有客户端连接并已建立,如果服务器主动关闭,那么和客户端的连接会处于TIME_WAIT状态,此时再次启动服务器,就会bind不成功,报:Address already in use。

      • 服务器父进程监听客户端,当和客户端建立链接后,fork一个子进程专门处理客户端的请求,如果父进程停止,因为子进程还和客户端有连接,所以再次启动父进程,也会报Address already in use。

  • SO_DONTROUTE,打开或关闭路由查找功能。当option_value不等于0时,打开,否则,关闭。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_LOCALROUTE位。

  • SO_BROADCAST,允许或禁止发送广播数据。当option_value不等于0时,允许,否则,禁止。它实际所做的工作是在sock->sk->sk_flag中置或清SOCK_BROADCAST位。

  • 等等…太多了。


  • sockaddr_in

  • struct sockaddr_in {
        __uint8_t sin_len;
        sa_family_t sin_family;
        in_port_t sin_port;
        struct in_addr sin_addr;
        char sin_zero[8];
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    * sin_family指代协议族,在socket编程中只能是AF_INET
    * sin_port存储端口号(使用网络字节顺序)
    * sin_addr存储IP地址,使用in_addr这个数据结构
    * sin_zero是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
    * addr.sin_len=sizeof(addr);//socket字节长度

    * sockaddr_in 结构体:struct sockaddr_in中的in 表示internet,就是网络地址,这只是我们比较常用的地址结构,属于AF_INET地址族,非常地常用

    * sin_zero 初始值应该使用函数 bzero() 来全部置零。一般采用下面语句

    * ```c++
    struct sockaddr_in cliaddr;
    bzero(&cliaddr,sizeof(cliaddr));
  • sockaddr_in结构体变量的基本配置

    • ```c++
      struct sockaddr_in ina;
      bzero(&ina,sizeof(ina));
      ina.sin_family=AF_INET;
      ina.sin_port=htons(23);
      ina.sin_addr.s_addr = inet_addr(“132.241.5.10”);
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10



      * sockaddr

      * ```c++
      struct sockaddr {
      unsigned short sa_family; /* address family, AF_xxx */
      char sa_data[14]; /* 14字节,包含目标地址和端口信息 */
      };
  • sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了

  • sockaddr_in和sockaddr二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。二者是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr。

  • sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。

  • sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。


  • INADDR_ANY
  • 转换过来就是0.0.0.0,泛指本机的意思,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。
    比如一台电脑有3块网卡,分别连接三个网络,那么这台电脑就有3个ip地址了,如果某个应用程序需要监听某个端口,那他要监听哪个网卡地址的端口呢?
  • 如果绑定某个具体的ip地址,你只能监听你所设置的ip地址所在的网卡的端口,其它两块网卡无法监听端口,如果我需要三个网卡都监听,那就需要绑定3个ip,也就等于需要管理3个套接字进行数据交换,这样岂不是很繁琐?
  • 所以出现INADDR_ANY,你只需绑定INADDR_ANY,管理一个套接字就行,不管数据是从哪个网卡过来的,只要是绑定的端口号过来的数据,都可以接收到。

  • bind:服务端用于将把用于通信的地址和端口绑定到 socket上。

  • int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
    
    • 参数 sockfd ,需要绑定的socket。
    • 参数 addr ,存放了服务端用于通信的地址和端口。ip地址和端口号是放在 socketaddr_in 结构体里面的。
    • 参数 addrlen ,表示 addr 结构体的大小。
  • 返回值:成功则返回0 ,失败返回-1,错误原因存于 errno 中。如果绑定的地址错误,或者端口已被占用,bind 函数一定会报错,否则一般不会返回错误

压力测试

先安装依赖

image-20220919164341581

然后到webbench-1.5的目录下:make clean,再make。

测试:

image-20220919164437394