为了弄清楚具体的业务逻辑,我们直接从主函数开始看源代码:
#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;
//初始化
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;
}
可以看到第一个需要弄明白的就是Config文件,那么现在我们进入头文件:
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;
};
可以看到都是一些标志,用来配置服务器的熟悉。
首先我们来熟悉两个模式:ET和LT,以socket的读事件为例,水平模式只要socket上有未读完的数据就会一直产生EPOLLIN事件;对于边缘模式,socket上每新来一次数据就会触发一次,如果上一次触发后,没有将socket上的数据读完,也不会再次触发,除非再来新的数据。对于socket写事件,如果socket的TCP窗口一致不饱和,会一直出发EPOLLOUT事件,对于边缘模式,只会触发一次,除非TCP窗口从不饱和变成饱和再一次变成不饱和才会触发。(饱和=写缓冲区已经满了)
在LT模式下,读事件触发后,可以按需收取想要的字节数,不用吧本次接收到的数据一次性拿走。而ET模式下则必须将数据读取干净,因为你不一定有下次机会收取数据,即使收取也是上次没读完的,造成客户端响应延迟。在LT模式下,不需要写事件一定要移除,避免不必要的触发,浪费CPU资源,ET模式下,写事件触发后,如果还要下一次的写事件触发来驱动任务,需要继续注册检测可写事件。
I/O | LT | ET |
读 | socket上无数据->有数据 | socket上无数据->有数据 |
socket上有数据 | socket上新来一次数据 | |
写 | socket可写 | socket不可写->可写 |
socket不可写->可写 |
现在大部分属性可以直到是什么意思了,再来看OPT_LINGER(优雅连接)。当我们服务器关闭时采用优雅连接时采取下面的步骤:
1.停止接受新的连接请求,继续处理已建立连接的请求;
2.等待未完成的数据传输;
3.文件传输完成,服务器关闭连接,释放相关资源。
而非优雅连接则是不保证数据的完整性和安全性,直接将连接断开。
还有就是并发模型,常用的并发模型有两个,Proactor和Reactor:
Reactor | 要求主线程只负责监听文件描述符是否有事件发生,有则通知工作单元将socket可读可写事件放入请求队列,交由工作现场处理 |
Proactor | 将所有的I/O操作都交给主线程和内核来完成,工作线程只负责处理逻辑,例如主线程完成read后,选择一个工作线程来处理具体请求 |
现在我们聚焦于Config提供的 void Config::parse_arg(int argc, char*argv[])这个函数
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)
{
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;
}
}
}
里面使用了下面的函数和optarg:
int getopt(int argc, char * const argv[], const char *optstring);
这里讲一下,getopt函数是用于解析命令行参数的C库函数,允许程序从命令行获取选项和参数,根据用户提供的选项规则进行解析:
argc:命令行参数的数量;
argv:命令行参数的数组;
str:一个包含选项字符的字符串,表示程序所支持的命令行选项,每个选项后面加上一个冒号代表该选项需要一个参数。
比如在本项目中,默认指定端口号是9006,也可以在启动是手动设置:
./server -p 9999
在程序运行时,会进入下面的代码块中:
case 'p':
{
PORT = atoi(optarg);
break;
}
这样我们输入的'-p'和上面的case 'p'对应起来,optarg的值就是输入的9999,然后将PORT的值赋值为9999。