2. 从服务器的主接口入手
Webserver 的主函数 main.cpp
,完成了哪些功能?
#include "config.h"
int main(int argc, char *argv[])
{
string user = "";
string passwd = "";
string databasename = "";
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;
}
上面是服务器的主函数,初始化并启动服务器。它的主要功能包括解析命令行参数、初始化服务器、设置日志、数据库连接池、线程池、触发模式等。
数据库信息设置
这里是数据库连接的用户名、密码和数据库名称,根据实际需要修改为正确的数据库配置信息。
string user = "";
string passwd = "";
string databasename = "";
命令行参数解析
创建 Config
类对象 config
并调用 parse_arg
函数解析命令行参数,例如端口号、日志模式、触发模式、线程池线程数等设置。
Config config; // 创建 Config 类对象 config
config.parse_arg(argc, argv); // 调用 parse_arg 函数解析命令行参数
服务器初始化
初始化 WebServer
对象,传入多个配置信息,包括:
- 端口号
- 数据库信息:1 数据库信息设置 中设置的数据库用户名、密码和数据库名称
- 日志配置
- 触发模式
- 数据库连接池大小
- 线程池线程数
- 日志关闭选项
- 并发模型(
actor_model
)。
这一步会完成服务器的基本配置。
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();
触发模式
设置触发模式,配置事件监听的触发方式( LT 模式和 ET 模式),用于控制 I/O 多路复用的触发行为。
server.trig_mode();
监听端口
监听端口,准备接受客户端的连接请求。
server.eventListen();
事件循环
进入事件循环,开始处理客户端连接请求及其他事件。eventLoop
通常会运行在一个循环中,处理网络事件、请求解析、响应生成等操作。
server.eventLoop();
Config 类
Config
类,用于配置服务器项目中的一些参数,并通过命令行参数来动态调整这些配置。
class Config
{
public:
Config();
~Config(){};
void parse_arg(int argc, char*argv[]); // 使用 getopt 函数来解析命令行参数。
// 成员函数
int PORT; // 端口号 (默认为9006)
int LOGWrite; // 日志写入方式 (默认同步写入且不关闭)
int TRIGMode; // 触发组合模式
int LISTENTrigmode; // listenfd触发模式
int CONNTrigmode; // connfd触发模式
int OPT_LINGER; // 优雅关闭链接
int sql_num; // 数据库连接池数量
int thread_num; // 线程池内的线程数量
int close_log; // 是否关闭日志
int actor_model; // 并发模型选择 (默认使用 Proactor 模式)
};
构造函数
/*构造函数*/
Config::Config(){
PORT = 9006; // 端口号,默认9006
LOGWrite = 0; // 日志写入方式,默认同步
TRIGMode = 0; // 触发组合模式,默认listenfd LT + connfd LT
LISTENTrigmode = 0; // listenfd触发模式,默认LT
CONNTrigmode = 0; // connfd触发模式,默认LT
OPT_LINGER = 0; // 优雅关闭链接,默认不使用
sql_num = 8; // 数据库连接池数量,默认8
thread_num = 8; // 线程池内的线程数量,默认8
close_log = 0; // 关闭日志,默认不关闭
actor_model = 0; // 并发模型,默认是proactor
}
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;
}
}
}
使用 parse_arg
方法解析命令行参数
选项字符串 str
:"p:l:m:o:s:t:c:a:"
定义了可接受的选项,每个字母代表一个选项,后面的冒号 :
表示该选项需要一个参数。
解析循环:使用 while
循环调用 getopt
,逐个解析命令行参数。
选项处理:根据解析到的选项字符,使用 atoi(optarg)
将字符串转换为整数,并赋值给对应的配置变量。
如果不传入任何命令行参数,程序将会使用在 Config
类构造函数中设置的默认值。因为在 parse_arg
函数中,如果没有提供任何参数,getopt
函数会立即返回 -1
,因此不会对配置对象的成员变量进行任何修改。
/* 命令行参数解析 */
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;
}
}
}
getopt
函数
getopt
函数是用于解析命令行参数的函数,通常在 C/C++ 程序中使用。它可以方便地处理带有选项的命令行参数,使程序支持类似于 Unix 风格的命令行接口。
#include <unistd.h>
int getopt(int argc, char * const argv[], const char *optstring);
参数说明
argc
:命令行参数的数量。argv
:命令行参数的数组。optstring
:选项字符,定义了可接受的选项字符及其是否需要参数。
返回值
- 成功时,返回解析到的选项字符。
- 如果所有选项解析完毕,返回
-1
。 - 如果遇到未定义的选项字符,返回
?
。
optstring
格式
- 每个字符代表一个可接受的选项。例如,
"abc"
表示接受-a
,-b
,-c
选项。 - 如果一个选项需要参数,在其后加上冒号
:
。例如,"a:b"
表示-a
需要参数,-b
不需要参数。 - 如果一个选项的参数是可选的,在其后加上两个冒号
::
。这种用法是 GNU 扩展,不是标准 C 库的一部分。
使用步骤
- 定义选项字符串:根据程序需要,定义可接受的选项及其参数要求。
- 循环调用
getopt
:使用while
循环,不断调用getopt
,直到返回-1
。 - 处理选项:在循环内,使用
switch
语句,根据返回的选项字符执行相应的操作。 - 处理非选项参数:在所有选项解析完毕后,
optind
指向第一个非选项参数,可以继续处理剩余的命令行参数。
示例代码
以下面的代码为例:
#include <iostream>
#include <unistd.h>
int main(int argc, char *argv[]) {
int opt;
/*
a: 需要参数
b: 可选参数
c: 不需要参数
*/
std::string optString = "a:b::c";
// 禁止 getopt 自动打印错误信息
opterr = 0;
while ((opt = getopt(argc, argv, optString.c_str())) != -1) {
switch (opt) {
case 'a':
std::cout << "选项 -a,参数值:" << optarg << std::endl;
break;
case 'b':
if (optarg)
std::cout << "选项 -b,参数值:" << optarg << std::endl;
else
std::cout << "选项 -b,没有提供参数" << std::endl;
break;
case 'c':
std::cout << "选项 -c,不需要参数" << std::endl;
break;
case '?':
std::cerr << "未知选项:" << char(optopt) << std::endl;
break;
default:
break;
}
}
// 处理非选项参数
for (int index = optind; index < argc; index++)
std::cout << "非选项参数:" << argv[index] << std::endl;
return 0;
}
运行:./getopt -a value1 -b value2 -c arg1 arg2
结果如下:
运行:./getopt -a value1 -bvalue2 -c arg1 arg2
结果如下:
在使用 getopt
函数并且定义了可选参数(使用 ::
)的选项时,如果选项参数与选项之间有空格(例如 -b value2
),那么 getopt
不会将 value2
视为 -b
的参数,而是将其作为非选项参数处理。
函数优化
原来的代码,只是实现了一个很简单的命令行参数解析逻辑,在一些情况下,如:
- 提供了未知选项。改进方法:
?
分支中 处理getopt
返回的?
,提示用户输入了未知选项。 - 提供了选项但是没有提供对应的参数。改进方法:在
optstring
的开头加入冒号
在
optstring
(选项字符串)中,开头的冒号:
非常重要。它改变了getopt
函数的错误处理方式。
当选项需要参数但未提供参数时:
如果optstring
以冒号开头(如:p:l:m:o:s:t:c:a:h
),getopt
将返回:
,并将optopt
设置为缺少参数的选项字符。
如果optstring
不以冒号开头(如p:l:m:o:s:t:c:a:h
),getopt
将返回?
,并将optopt
设置为出错的选项字符。
- -p 提供的端口号非法(端口号小于 0 或者大于 65535)。改进方法:
PORT <= 0 || PORT > 65535
,限制-p
提供的参数范围 - 参数格式非法(传入的字符串不是数字字符串):
atoi
对非数字字符串会返回0
,无法检测输入是否为有效数字。如果用户输入非数字字符(如-p abc
),程序可能会继续运行,但使用了错误的参数值。 改进方法:使用strtol
这些情况下,可能导致无法正确解析参数。
此外,还可以加入一个帮助选项-h
,用于打印帮助信息:
/* 显示帮助信息 */
void Config::display_usage() {
printf(
"用法: server [选项]...\n"
"选项列表:\n"
" -p <端口号> 设置服务器监听端口号 (默认: 9006)\n"
" -l <日志写入方式> 设置日志写入方式 (0: 同步, 1: 异步, 默认: 0)\n"
" -m <触发模式> 设置触发模式 (0~3, 默认: 0)\n"
" 0: listenfd LT + connfd LT\n"
" 1: listenfd LT + connfd ET\n"
" 2: listenfd ET + connfd LT\n"
" 3: listenfd ET + connfd ET\n"
" -o <优雅关闭连接> 是否使用优雅关闭连接 (0: 不使用, 1: 使用, 默认: 0)\n"
" -s <数据库连接数> 设置数据库连接池连接数 (默认: 8)\n"
" -t <线程数> 设置线程池内线程数量 (默认: 8)\n"
" -c <关闭日志> 是否关闭日志 (0: 不关闭, 1: 关闭, 默认: 0)\n"
" -a <并发模型> 选择并发模型 (0: Proactor, 1: Reactor, 默认: 0)\n"
" -h 显示此帮助信息\n"
"示例:\n"
" server -p 8080 -t 16 -c 1\n"
);
}
改进后的完整代码如下:
#include "config.h"
/* 构造函数 */
Config::Config(){
PORT = 9006; // 端口号,默认9006
LOGWrite = 0; // 日志写入方式,默认同步
TRIGMode = 0; // 触发组合模式,默认listenfd LT + connfd LT
LISTENTrigmode = 0; // listenfd触发模式,默认LT
CONNTrigmode = 0; // connfd触发模式,默认LT
OPT_LINGER = 0; // 优雅关闭链接,默认不使用
sql_num = 8; // 数据库连接池数量,默认8
thread_num = 8; // 线程池内的线程数量,默认8
close_log = 0; // 关闭日志,默认不关闭
actor_model = 0; // 并发模型,默认是proactor
}
/* 显示帮助信息 */
void Config::display_usage() {
printf(
"用法: server [选项]...\n"
"选项列表:\n"
" -p <端口号> 设置服务器监听端口号 (默认: 9006)\n"
" -l <日志写入方式> 设置日志写入方式 (0: 同步, 1: 异步, 默认: 0)\n"
" -m <触发模式> 设置触发模式 (0~3, 默认: 0)\n"
" 0: listenfd LT + connfd LT\n"
" 1: listenfd LT + connfd ET\n"
" 2: listenfd ET + connfd LT\n"
" 3: listenfd ET + connfd ET\n"
" -o <优雅关闭连接> 是否使用优雅关闭连接 (0: 不使用, 1: 使用, 默认: 0)\n"
" -s <数据库连接数> 设置数据库连接池连接数 (默认: 8)\n"
" -t <线程数> 设置线程池内线程数量 (默认: 8)\n"
" -c <关闭日志> 是否关闭日志 (0: 不关闭, 1: 关闭, 默认: 0)\n"
" -a <并发模型> 选择并发模型 (0: Proactor, 1: Reactor, 默认: 0)\n"
" -h 显示此帮助信息\n"
"示例:\n"
" server -p 8080 -t 16 -c 1\n"
);
}
void Config::parse_arg(int argc, char* argv[]) {
int opt;
// 设置 optstring:选项字符
const char *str = ":p:l:m:o:s:t:c:a:h";
while ((opt = getopt(argc, argv, str)) != -1) {
switch (opt) {
case 'p': // 设置端口号,范围应在 1 到 65535 之间。
{
char *endptr;
PORT = strtol(optarg, &endptr, 10);
if (*endptr != '\0' || PORT <= 0 || PORT > 65535) {
fprintf(stderr, "无效的端口号:%s\n", optarg);
exit(EXIT_FAILURE);
}
}
break;
case 'l':
{
char *endptr;
LOGWrite = strtol(optarg, &endptr, 10);
if (*endptr != '\0' ) {
fprintf(stderr, "无效的日志写入方式:%s\n", optarg);
exit(EXIT_FAILURE);
}
}
break;
case 'm':
{
char *endptr;
TRIGMode = strtol(optarg, &endptr, 10);
if (*endptr != '\0' ) {
fprintf(stderr, "无效的触发模式:%s\n", optarg);
exit(EXIT_FAILURE);
}
}
break;
case 'o':
{
char *endptr;
OPT_LINGER = strtol(optarg, &endptr, 10);
if (*endptr != '\0' ) {
fprintf(stderr, "无效的优雅关闭选项:%s\n", optarg);
exit(EXIT_FAILURE);
}
}
break;
case 's':
{
char *endptr;
sql_num = strtol(optarg, &endptr, 10);
if (*endptr != '\0' || sql_num <= 0) {
fprintf(stderr, "无效的数据库连接池数量:%s,应为正整数\n", optarg);
exit(EXIT_FAILURE);
}
}
break;
case 't':
{
char *endptr;
thread_num = strtol(optarg, &endptr, 10);
if (*endptr != '\0' || thread_num <= 0) {
fprintf(stderr, "无效的线程数量:%s,应为正整数\n", optarg);
exit(EXIT_FAILURE);
}
}
break;
case 'c':
{
char *endptr;
close_log = strtol(optarg, &endptr, 10);
if (*endptr != '\0') {
fprintf(stderr, "无效的日志关闭选项:%s\n", optarg);
exit(EXIT_FAILURE);
}
}
break;
case 'a':
{
char *endptr;
actor_model = strtol(optarg, &endptr, 10);
if (*endptr != '\0' ) {
fprintf(stderr, "无效的并发模型选项:%s\n", optarg);
exit(EXIT_FAILURE);
}
}
break;
case 'h': // 显示帮助信息
display_usage();
exit(EXIT_SUCCESS);
case ':': // 缺少参数
fprintf(stderr, "选项 -%c 缺少一个参数。\n", optopt);
exit(EXIT_FAILURE);
case '?':
fprintf(stderr, "输入了未定义的选项:-%c\n", optopt);
exit(EXIT_FAILURE);
default:
fprintf(stderr, "解析选项时遇到未知错误\n");
exit(EXIT_FAILURE);
}
}
// 检查是否有非选项参数
if (optind < argc) {
fprintf(stderr, "存在多余的非选项参数:\n");
for (int i = optind; i < argc; i++) {
fprintf(stderr, " %s\n", argv[i]);
}
exit(EXIT_FAILURE);
}
}
优化效果
优化前,输入一个非法端口号启动服务器,如:./server -p -1
终端没有报错,但是服务器拒绝连接。
优化后,针对错误情况都提供了报错和退出处理:
优化后,输入 ./server -h
显示帮助信息: