TinywebServer代码详解– 服务器主程序(14)

原项目地址(点击跳转)

博主添加注释后项目地址(点击跳转)


本文将介绍TinyWebServer服务器主程序的代码



一、代码详解

1.头文件

webserver.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
#pragma once
#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, std::string user, std::string passWord, std::string databaseName,
int log_write, int opt_linger, int trigmode, int sql_num,
int thread_num, int close_log, int actor_model,int db_port = 3306);

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;
// 事件处理模式 Reactor/Proactor
int m_actormodel;
/********************基础信息******************/

/********************网络信息******************/
// 管道套接字,用于定时器设计的统一事件源
int m_pipefd[2];
// epoll对象
int m_epollfd;
// http连接对象指针,指向http连接对象数组
http_conn *users;
/********************网络信息******************/

/********************数据库相关******************/
connection_pool *m_connPool;
// 登陆数据库的用户名
std::string m_user;
// 登陆数据库的密码
std::string m_passWord;
// 使用数据库名
std::string m_databaseName;
// 数据库连接池中数据库连接数量
int m_sql_num;
// 数据库服务器端口
int m_db_port;
/********************数据库相关******************/

/********************线程池相关******************/
threadpool<http_conn> *m_pool;
// 线程池工作线程数量
int m_thread_num;
/********************线程池相关******************/

/********************epoll_event相关******************/
// IO复用系统,用于存储就绪的文件描述符信息
epoll_event events[MAX_EVENT_NUMBER];
// 用于监听客户端连接请求的套接字
int m_listenfd;
// 是否优雅下线
int m_OPT_LINGER;
// epoll的(事件触发模式)工作模式 电平ET/边沿LT
int m_TRIGMode;
// 监听套接字的事件触发模式
int m_LISTENTrigmode;
// 连接套接字的事件触发模式
int m_CONNTrigmode;
/********************epoll_event相关******************/

/********************定时器相关******************/
// 定时器连接资源指针,指向定时器连接资源数组
client_data *users_timer;
// 定时器中的工具类
Utils utils;
/********************定时器相关******************/
};


2.源文件

(1)WebServer()

该函数是服务器构造函数,用于对服务器进行初始化,创建http连接对象数组、设置资源根目录、创建定时器连接资源数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
WebServer::WebServer()
{
// http_conn连接对象数组,MAX_FD为最大的连接数量
users = new http_conn[MAX_FD];

// root资源文件根目录
char server_path[200];
// getcwd获取当前工作目录的绝对路径
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);
// 此时m_root指向资源文件根目录

// 定时器连接资源数组
users_timer = new client_data[MAX_FD];
}

(2)trig_mode()

该函数通过WebServer类的成员变量m_TRIMode设置epoll上挂载的套接字的事件触发模式

0 == m_TRIGMode

  • 监听连接套接字,水平工作模式LT
  • 通信套接字,水平工作模式LT

1 == m_TRIGMode

  • 监听连接套接字,水平工作模式LT
  • 通信套接字,边沿工作模式ET

2 == m_TRIGMode

  • 监听连接套接字,边沿工作模式ET
  • 通信套接字,水平工作模式LT

3 == m_TRIGMode

  • 监听连接套接字,边沿工作模式ET
  • 通信套接字,边沿工作模式ET
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
void WebServer::trig_mode()
{
// LT + LT (监听套接字/通信套接字均为水平工作模式)
if(0 == m_TRIGMode)
{
// 0表示水平工作模式
m_LISTENTrigmode = 0;
m_CONNTrigmode = 0;
}
// LT + ET
else if(1 == m_TRIGMode)
{
m_LISTENTrigmode = 0;
// 1表示边沿工作模式
m_CONNTrigmode = 1;
}
// ET + LT
else if(2 == m_TRIGMode)
{
m_LISTENTrigmode = 1;
m_CONNTrigmode = 0;
}
else if(3 == m_TRIGMode)
{
m_LISTENTrigmode = 1;
m_CONNTrigmode = 1;
}
}

(3)log_write()

该函数,根据成员变量m_close_log判断是否开启系统日志,若开启,则通过变量m_log_write判断日志开启的类型,异步或者同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void WebServer::log_write()
{
// 不关闭日志
if(0 == m_close_log)
{
// 日志类型:异步日志
if(1 == m_log_write)
{
Log::get_instance()->init("./ServerLog", m_close_log, 200, 800000, 800);
}
// 日志类型:同步日志
else
{
Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 0);
}
}
}

(4)sql_pool()

该函数用于初始化数据库连接池:

通过获取数据库连接池唯一实例对象,并对其进行初始化,为连接池创建一定数量的数据库连接,将这些连接push进阻塞的连接队列中

读取数据库中的user表,将其存储至本地的map中,用于CGI注册登录校验

1
2
3
4
5
6
7
8
9
10
void WebServer::sql_pool()
{
// 静态接口函数,获取单例模式的唯一实例对象
m_connPool = connection_pool::GetInstance();
// 初始化数据库连接池,数据库服务器默认端口为3306
m_connPool->init("localhost", m_user, m_passWord, m_databaseName, m_db_port, m_sql_num, m_close_log);
// 初始化数据库,读取user表,用于cgi注册登陆验证
users->initmysql_result(m_connPool);

}

(5)eventListen()

该函数:

用于创建用于监听客户端连接的套接字,并且为套接字设置IP以及端口快速复用等属性

IO复用系统epoll实例的创建与设置,将用于监听的套接字挂载到epoll实例上,进行监测

为定时器系统创建管道套接字,用于于主循环进行通信,并且将管道的读端挂载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
void WebServer::eventListen()
{
// SOCK_STREAM 表示使用面向字节流的TCP协议
m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(m_listenfd >= 0);

// 是否优雅下线
// 控制套接字关闭时的行为
// struct linger 是一个用于控制套接字关闭行为的结构体
// struct linger 结构体中的字段 int l_onoff 表示是否开启 linger 选项
// struct linger 结构体中的字段 int l_linger 套接字关闭时等待的时间(以秒为单位)
if(0== m_OPT_LINGER)
{
// tmp.l_onoff 关闭,表示当这个套接字被关闭时
// 它不会等待未发送的数据发送完毕,而是立即关闭套接字
struct linger tmp = {0, 1};
setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
}
else if(1 == m_OPT_LINGER)
{
// tmp.l_onoff 打开,表示当这个套接字被关闭时
// 会等待1s再关闭套接字
struct linger tmp = {1, 1};
setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
}

// 设置服务器主机ip相关信息
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof address);
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(m_port);

// 设置套接字快速复用
int flag = 1;
setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof flag);
// 套接字绑定本地IP与端口
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);

// 将用于监听的套接字在内核事件表上注册读事件(挂到epoll实例上)
utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);
http_conn::m_epollfd = m_epollfd;

// socketpair()函数用于创建一对匿名的、相互连接的管道套接字,用于进程间通信
// socketpait创建的管道套接字是双向通信的
ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);
assert(ret != -1);

// 设置管道写端为非阻塞
// send是将信息发送给套接字缓冲区,如果缓冲区满了,
// 则会阻塞,这时候会进一步增加信号处理函数的执行时间,为此,将其修改为非阻塞
utils.setnonblocking(m_pipefd[1]);
// 将读端的套接字文件描述符添加到m_epoll实例中,监听读事件
// 而且是ET,边沿工作模式下,非阻塞,没有阻塞EPOLLONESHOT事件
utils.addfd(m_epollfd, m_pipefd[0], false, 0);
// 如此完成了定时器设计中提到的统一事件源

// 定时器alarm函数传递给主循环的信号值,这里只关注SIGALRM和SIGTERM
// SIGALRM/SIGTERM 设置信号处理函数
// SIGPIPE 信号忽视
utils.addsig(SIGPIPE, SIG_IGN);
utils.addsig(SIGALRM, utils.sig_handler, false);
utils.addsig(SIGTERM, utils.sig_handler, false);
// utils.sig_handler函数就是将alarm系统调用产生的定时信号SIGALRM,通过管道套接字发送给主循环
// utils.sig_handler函数就是将终端发送的终止信号SIGTERM,通过管道套接字发送给主循环

// alarm系统调用进行定时
alarm(TIMESLOT);

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

(6)dealclientdata()

该函数,根据监听套接字事件触发方式,接收客户端的连接

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
bool WebServer::dealclientdata()
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof client_address;
// LT水平工作模式
if (0 == m_LISTENTrigmode)
{
// 接受客户端连接
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;
}
// 创建一个定时器节点,将http连接信息挂载
timer(connfd, client_address);
}

// ET 边沿工作模式
else
{
// 边沿触发需要一直accept直到为空
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;
}

(7)dealwithsignal()

该函数用于处理定时器通过管道套接字发送给主循环的信号

在管道的读端,通过recv读取读端套接字读缓冲区的数据,若信号为

  • SIGALRM,将timeout=true,之后可以通过该标志,在主循环中,触发定时器处理任务函数,对定时器双向链表容器进行检查,查看是否有超时的定时器,并且重新使用alarm函数进行定时
  • SIGTERM,将stop_server,表示关闭服务器,主循环可以通过该标志结束循环
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
bool WebServer::dealwithsignal(bool &timeout, bool &stop_server)
{
int ret = 0;
int sig;
char signals[1024];
//从管道读端读出信号值,成功返回字节数,失败返回-1
//正常情况下,这里的ret返回值总是1,只有14和15两个ASCII码对应的字符
ret = recv(m_pipefd[0], signals, sizeof(signals), 0);
if (ret == -1)
{
// handle the error
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;
}

(8)dealwithread()

该函数用于处理与客户端通信接收到的数据(通信套接字读操作)

根据成员变量m_actormodel判断事件处理模式(Reactor/Proactor)

  • Reactor模式下,主线程只需要负责epoll实例中的文件描述符监听,若有套接字就绪,则将套接字上的可读事件进行封装放到线程池的请求队列中,由线程池中的工作线程进行竞争执行,IO数据读取以及数据的逻辑处理
  • Proactor模式下,主线程负责epoll实例中的文件描述符监听,以及IO的读操作;而工作线程仅仅负责数据的逻辑处理
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
void WebServer::dealwithread(int sockfd)
{
// 创建定时器临时变量,将该连接对应的定时器取出来
util_timer *timer = users_timer[sockfd].timer;

// Reactor事件处理模式
// Reactor模式,仅负责文件描述符的事件监听
if(1 == m_actormodel)
{
if(timer)
{
//将定时器(超时时间)往后延迟3个单位
adjust_timer(timer);
}

// 若监测到读事件,将该事件放入请求队列
m_pool->append(users + sockfd, 0);
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;
}
}
}

// Proactor事件处理模式
// Proactor模式,负责文件描述符的事件监听以及IO数据读写
else
{
// 先读取数据,再放进请求队列
if (users[sockfd].read_once())
{
LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));
// 将该事件放入请求队列
m_pool->append_p(users + sockfd);
if (timer)
{
// 将该http连接对应的定时器超时时间延长3个单位
adjust_timer(timer);
}
}
else
{
// 删除定时器节点,关闭连接
deal_timer(timer, sockfd);
}
}
}

(9)dealwithwrite()

该函数用于处理与客户端套接字的写操作

根据成员变量m_actormodel判断事件处理模式(Reactor/Proactor)

  • Reactor模式下,主线程只需要负责epoll实例中的文件描述符监听,若有套接字就绪,则将套接字上的可写事件进行封装放到线程池的请求队列中,由线程池中的工作线程进行竞争执行,IO数据写入至通信套接字的写缓冲区(将响应报文发送给客户端)以及数据的逻辑处理
  • Proactor模式下,主线程负责epoll实例中的文件描述符监听,以及IO的写操作;而工作线程仅仅负责数据的逻辑处理
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
void WebServer::dealwithwrite(int sockfd)
{
// 创建定时器临时变量,将该连接对应的定时器取出来
util_timer *timer = users_timer[sockfd].timer;
// Reactor事件处理模式
// 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;
}
}
}

// Proactor事件处理模式
// Proactor模式,负责文件描述符的事件监听以及IO数据读写
else
{
// 将响应报文写入到通信套接字的写缓冲区,发送给浏览器(客户)端
if (users[sockfd].write())
{
LOG_INFO("send data to the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));

if (timer)
{
// 将该http连接对应的定时器超时时间延长3个单位
adjust_timer(timer);
}
}
else
{
// 删除定时器节点,关闭连接
deal_timer(timer, sockfd);
}
}
}

(10)eventLoop()

该函数用于事件回环,即服务器主线程循环。通过epoll_wait对套接字进行监听,并且根据就绪的套接字的类型以及套接字对应的就绪事件进行处理

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
void WebServer::eventLoop()
{
bool timeout = false;
bool stop_server = false;

while (!stop_server)
{
// 等待所监控文件描述符上有事件的产生
int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
// EINTR错误的产生:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。
// 例如:在socket服务器端,设置了信号捕获机制,有子进程,
// 当在父进程阻塞于慢系统调用时由父进程捕获到了一个有效信号时,
// 在epoll_wait时,因为设置了alarm定时触发警告,导致每次返回-1,errno为EINTR,对于这种错误返回
// 忽略这种错误,让epoll报错误号为4时,再次做一次epoll_wait
// EINTR错误的产生(系统调用被打断,产生的假错误)---当发现是假错误,就需要重新epoll_wait再次进行系统调用
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 = dealclientdata();
if (false == flag)
continue;
}
// 处理异常事件
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
// 服务器端关闭该http连接,移除对应的定时器
util_timer *timer = users_timer[sockfd].timer;
deal_timer(timer, sockfd);
}
// 处理定时器信号(就绪的套接字是发送定时器信号的管道套接字读端)
else if ((sockfd == m_pipefd[0]) && (events[i].events & EPOLLIN))
{
// 接收到SIGALRM信号,timeout设置为True
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)
{
// 定时处理任务,重新定时以不断触发SIGALRM信号
utils.timer_handler();
LOG_INFO("%s", "timer tick");
timeout = false;
}
}
}


3.主函数

main.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
#include "config.h"
#include <iostream>

int main(int argc, char *argv[])
{

Config config;
// 读取数据库配置文件dbconf.json,修改数据库信息
config.parseJsonFile();
// 命令行解析
config.parse_arg(argc,argv);
std::cout << config.user <<std::endl;
std::cout << config.password << std::endl;
std::cout << config.databasename << std::endl;
std::cout << config.db_Port << std::endl;

WebServer server;

// 初始化服务器相关变量
server.init(config.PORT, config.user, config.password, config.databasename,
config.LOGWrite, config.OPT_LINGER, config.TRIGMode, config.sql_num, config.thread_num,
config.close_log, config.actor_model, config.db_Port);

// 初始化日志系统
server.log_write();

// 初始化数据库连接池
server.sql_pool();

// 初始化线程池
server.thread_pool();

// 设置epoll实例上挂载的监听与通信的套接字文件描述符事件触发模式
server.trig_mode();

// 服务器用于监听的套接字ip等属性的设置
// IO复用系统epoll实例的创建与设置,将用于监听的套接字挂到epoll实例上,进行监测
// 定时器系统用于统一事件源的管道套接字创建设置
server.eventListen();

// 运行
server.eventLoop();

return 0;
}


4.CMakeLists.txt

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
cmake_minimum_required(VERSION 3.16)
project(TinyWebServerBymyself)

set(CMAKE_CXX_STANDARD 14)

# 指定可执行文件与CMakeLists.txt位于同一级目录
set(EXECUTABLE_FILE_OUTPUT ../)
set(EXECUTABLE_OUTPUT_PATH ${EXECUTABLE_FILE_OUTPUT})

# 查找JSONCPP库
find_package(jsoncpp REQUIRED)
# 添加JSON库头文件
include_directories(${JSONCPP_INCLUDE_DIRS})

# 手动指定MYSQL路径
include_directories(/usr/include/mysql)
# 指定MySQL库文件的位置
link_directories(/usr/lib/mysql)


add_executable(TinyWebServerBymyself main.cpp ./timer/lst_timer.cpp ./log/log.cpp
http/http_conn.cpp ./CGImysql/sql_connection_pool.cpp
./config.cpp ./webserver.cpp)

# 链接 MySQL 客户端库
target_link_libraries(TinyWebServerBymyself mysqlclient)
# 链接JSONCPP库
target_link_libraries(TinyWebServerBymyself jsoncpp_lib)
# 链接线程库
target_link_libraries(TinyWebServerBymyself pthread)