moka项目开发过程记录
总结记录开发过程等于二次开发
!!
用法- 抽象基类里可以定义实现
默认参数
只在声明中出现- 重载最好用
override
关键字(若重写虚函数时函数名写错,编译器会报错),补充一下(final阻止进一步的派生和虚函数的进一步重写)。override
关键字只能在类内使用 - 纯虚函数可以实现
- stringstream类对象,当需要按预定的格式将程序中的数据保存在一个string中的时候
- stringstream.str()为返回string buffer的copy
#
为宏的字符串操作符std::get(i)
用于获取tuple的第i个元素- 继承
enable_shared_from_this<T>
才能在成员函数中安全获得this的智能指针版本,通过shared_from_this()
获得 stringstream
通过<<
将字符串放入缓冲区中(类似cout),.str()成员函数获取缓冲区中的数据- 时间用
localtime
先是从epoch到现在的时间UTC - C++11构造函数初始化列表的顺序和成员变量声明的顺序必须一致
- 使用wrap类作为临时对象,使用完后直接析构,触发日志写入,然而日志本身的智能指针,如果声明在主函数里面,程序不结束就永远无法释放。利用MOKA_LOG_LEVEL宏在if语句内部构造了局部变量,if语句结束后会调用LogEventWrap在if语句结束后会结束生命周期调用析构函数实现打印
- 日志器会初始化默认格式器,格式器会初始化
items
。日志事件的log
方法中调用对应子类appender的log,appender中的日志格式器会调用format方法遍历items,利用多态,调用对应item的format输出格式对应的信息 __VA_ARGS__
可变参数宏,将被替换为可变参数列表- 可变参数
va_list
宏用于支持可变参数,声明的变量用于存储可变参数列表va_start
第一个参数为va_list,第二个参数为可变参数之前的一个参数(即最后一个固定参数)va_arg
可用于获取可变参数列表中指定类型的参数(每次调用va_arg,可变参数列表的指针都会向后移动(从头开始))。如va_arg(args, int)
va_end
用于清理va_list变量
- 若直接父类声明了有参构造函数,则子类构造函数要传入直接父类的参数,并显式调用直接父类的构造函数
std::exception
。what
成员函数返回一个错误信息的解释性字符串- 可以通过
typeid(变量).name()
来获取对象的类型名称的字符串,与sizeof
相比,typeid是在运行时求值 - 选用
boost
库,在类型转换方面要方便,不需要stod
或stof
dynamic_pointer_cast
返回会使引用计数+1,用于智能指针模板类型的转换- 模板内嵌套使用模板必须要用
typename
指出,告诉编译器typename
后面的字符串为类型名称
,而不是成员变量或者成员函数名称
std::transform(name.begin(), name.end(), name.begin(), ::tolower)
将字符串转换成小写static
变量初始化时,要加上类名和作用域限定符(类型 类名::变量名
)- 配置系统原则,约定优于配置
- 模板全特化:模板参数列表没有参数,class 类名后限定死了类型,如
template<> class X<int, int>
- 模板偏特化(优先匹配更接近的版本):模板参数列表有参数,并未完全限定死类型,如
template<class T> class X<int, T>
- 使用
stringstream
可以简化数据类型的转换(可以将其它类型的值转换为字符串),.str("")
可以设置一个新的字符串缓冲区,str()可获取缓冲区的内容。通过ss <<
可以将其他类型转换为字符串,ss >>
可以将字符串转换为其他类型 - 局部静态变量也是一直存在的,可以一直被初始化,因此单例模式有一种
Meyers
实现方式,利用静态局部变量获取instance。C++11后,比那一起能保证局部静态变量的线程安全性 全局变量
和静态变量
在main函数之前初始化- 不同文件静态变量初始化顺序不一致,非局部的静态变量的初始化顺序是不确定的,解决方法是把它变成一个局部静态变量
ConfigVar
模板类在实例化时,都要提供其FromStr和ToStr两个仿函数(在类中实现operator()来实现),复杂类型要进行对应的偏特化(对LexicalCast)1
2template <class T, FromStr = LexicalCast<std::string, T>, ToStr = LexicalCast<T, std::string>>
class ConfigVar- 类模板调用优先级:全特化类 > 偏特化类 > 主版本模板类
- 调用LoadYaml之后LogDefine结构体发生更改,然后会调用这个LogIniter注册的回调函数,对logger日志器类的属性值进行更改
thread local
关键字修饰的变量,表示每一个线程都拥有这个变量pthread_self
获取的是pthread库对应的线程号(top命令是查不到的),而不是linux系统对应的编号volatile
防止编译器优化,每次都从内存中取数据- 继承
std::enable_shared_from_this<T>
类,调用shared_from_this
方法可以获取到当前类的智能指针 - 协程调度器:将协程指定到相应的线程上去执行
LogEventWrap
日志事件包装类,方便使用宏进行打log,在LogEventWrap的析构函数中打印Log,因为它在宏中为局部变量- 使用配置变更事件机制,可以方便配置日志,因为与yaml相关的日志信息使用LogDefine存储,修改配置项的值时会触发在main函数之前创建的logInit回调函数,在其中将变化的logDefine结构体的信息(增/删/改)更新到Logger类中
- 智能指针进行初始化时,可以用std::shared_ptr
::swap来进行赋值,两个操作数的引用计数不会改变。swap之后两个智能指针对象swap了一下,所以引用计数也swap了。所以在对智能指针进行赋值时可以使用std::move本质上和swap是一样的 - 智能指针的指针传递时并不会增加引用计数
- 经过检验,使用构造函数初始化列表传智能指针值进行赋值,引用计数会从1变成3,但最后会释放掉1个也就是初始化列表的那个。因此按值传参后可以reset一下,让引用计数减1
std::bind
绑定器返回类型是一个stl内部定义的仿函数类型,可以绑定函数的入口地址和参数,并可以使用std::function进行保存- 原子类型只保证线程安全,不保证线程同步?
stderror(errno)
将error number用字符串表示出来- 智能指针遇到的问题
1
2
3
4
5
6
7// GetThis后会返回当前协程的this的智能指针,这里yield之后,无法继续执行该函数体,故cur无法进行释放(没有执行出作用范围,即函数体,无法进行释放),所以cur需要显式子进行释放
{
Fiber::ptr cur = Fiber::GetThis();
auto raw_ptr = cur.get();
cur.reset();
raw_ptr->yield();
} - 扩容为fd的值*1.5,因为fd的值可能很大超过fd上下文集合的大小
- 在iomanager中调用schedule执行的notify、stop、idle,根据虚函数原理,都会执行IOManager::的相应的虚函数版本
- 若不使用caller线程,则在run函数中新建主协程为调度协程
- IOManager中的idle通过back回到事件循环之后,会直接跳过MainFunc中设置协程状态为TERM的过程,因此到事件循环后会被设置为HOLD。run中事件循环中重新启动后,HOLD状态的协程不会被放入事件队列,因此此时满足stop的条件,调度器就停止了
- IOManager中Addevent对某个fd注册的事件,会在epoll_wait返回之后,如果有addEvent注册的事件则调用回调函数
- IOManager析构时会调用stop
- idle事件循环中,在处理fd监听到的事件时,一个线程处理结束后会修改epoll内核事件表,然后下一个线程就不处理该事件了
- 实现一个定时器,利用epoll_wait的第四个参数(毫秒级),进行整合
- 友元类有当前类的访问权限
- 使用set进行自定义对象排序时,比较的时候得重载比较运算符
- set.insert()返回一个pair,<插入位置的迭代器,是否成功的bool变量>
- weak_ptr中
lock
方法能够返回当前weak_ptr对应的shared_ptr的对象,如果有则获取到shared_ptr对象,同时引用计数+1,如果没有则返回nullptr - std容器中,lower_bound返回的是第一个大于等于该值的迭代器的位置
- set中自定义比较器类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct MyCompare {
bool operator()(const int& a, const int& b) const {
// 按照元素的绝对值大小进行比较
return std::abs(a) < std::abs(b);
}
};
int main() {
std::set<int, MyCompare> s = {4, -2, 5, -3, 1};
for (auto it = s.begin(); it != s.end(); ++it) {
std::cout << *it << ' ';
}
std::cout << std::endl;
return 0;
} - std::function用内部可能使用智能指针来管理,如果可调用对象是一个函数对象或lambda表达式,就是new运算符在堆上动态分配一个函数对象;如果可调用对象是一个函数指针,就直接将其存储在函数指针成员中。
- 定时器采用最小堆来实现,根据超时时间点进行排序,超时时间到了之后,获取当前的绝对时间点,然后把最小堆里超时时间点小于这个时间点的定时器收集起来(这些定时器都超时了),执行他们的回调函数
- 如果系统时间发生了调整,则触发全部定时器
- hook模块,可以使一些不具备异步功能API展现出异步的性能。hook和IO协程调度联系起来之后,因为同一个线程中的协程是串行执行的,当处于同一个线程时,一个协程阻塞以后,HOOK模块可以使得该协程切换到另一个协程执行。因此hook是为了保证整个线程不会因为一个协程阻塞了而阻塞。(配合定时器)
- 配合定时器模块,可以实现单个线程中的协程异步执行
- 基于动态链接的hook有两种方式,第一种是外挂式hook(i.e.非侵入式hook),通过优先加载自定义的动态库来hook后加载的动态库,这种方式不需要重新编译代码
- hook,修改系统函数的实现行为
- c语言不支持重载
- 宏中
##
作为连字符 - 全局静态变量会在main函数之前初始化
- 两个文件中使用同一变量时要用extern显子声明外部链接
- 获取动态链接符号表中库函数的地址,并在hook.cc中重写该库函数,并封装起来
bool is_init_: 1
告诉编译器只为该变量分配1个bit- C++11类内初始化后,可不用构造函数初始化
- READY状态的协程会被调度器自动重新调度,而HOLD状态的协程需要显式地再次将协程加入调度
- 当使用main线程作为调度线程时,需要新建调度协程,此时的调度协程是需要栈空间的(因为需要运行run函数);新建线程作为调度线程时,调度协程则不需要栈空间(因为此时run函数由新建的线程来运行),这时候线程的主协程就是调度协程。不过主协程自始自终都不需要分配栈空间,调度协程的栈空间分配视情况而定。
- 如果使用caller线程作为调度线程,则在Fiber构造函数中,初始化主协程的同时会初始化调度协程(分配栈资源,执行run函数)
- epoll_wait如果在超时时间内有事件发生,则立即返回。
- 文件描述符上下文记录着epoll监听的事件集合,以及读写事件上下文(包括回调函数、任务协程、调度器指针等)。通过epoll.data存储文件描述符上下文指针。
- cancel和delEvent的区别在于,cancel在从epoll事件表中删除指定事件后会触发(调度执行)该事件的回调函数或者任务协程
- 个人觉得之所以要自定义一个fd的上下文,是为了在事件发生时让调度器去调度该回调函数或者任务协程,这样就能支持任务为协程或者回调函数了。
- 当一个文件描述符关闭时,相应的 epoll 事件表会将该文件描述符从事件队列中删除,以确保不再监听该文件描述符的事件。这种机制可以防止已关闭的文件描述符继续产生事件,从而导致 epoll 实例的无效阻塞和性能下降。同时,该机制还可以确保 epoll 事件表的正确性和一致性,从而保证 epoll 实例的稳定和可靠性。
- 注意close只能在最后一个回调函数体中调用(视情况而定),否则如果在addEvent事件体中进行调用,就会出现close了,此时也从fd相关的事件也从epoll内核事件表中删除。但这时候由于addEvent,pending的数量并没有减少,因此就一直阻塞在idle状态
- 循环定时,即这个定时器已经超时了,如果是循环定时,再重新放入到定时器堆中
- 定时器管理器的ticked变量,避免了还没有更新epoll的定时时间,就触发了onTimerInsertAtFirst,因此在get_expire时才将ticked置为false(避免频繁触发)
- gettimeofday不是系统调用,是在用户态实现的
- 如果系统时间被调整到一个小时之前,就触发所有定时器,执行所有定时器的回调函数。
- 抽象基类(存在纯虚函数的类)不能实例化
- 调用 sleep() 函数会使当前线程进入睡眠状态(也称为等待状态),并暂停其执行。在睡眠状态下,线程是暂停执行的,但仍然保持在运行队列中,并继续占用系统资源,例如内存。
- 在hook.cc中声明外部变量一定不要放到moka的命名空间中,否则链接时找不到
- fdmanager为了区分具体是文件还是socket,维护fd的状态信息
- 不能嵌套使用成员中的锁
- fstat函数用于获取文件描述符对应的元数据,
S_ISSOCK(stat.st_mode)
返回当前文件描述符是否为socket - 使用线程局部变量,不同线程的局部变量之间互不干扰(一个线程的局部变量如协程上下文,可以和另一个线程的上下文不一样)
- 在使用模板函数时,编译器会根据模板实参的类型,自动推导出模板函数的类型参数。模板函数声明时使用一个或多个类型参数,这些类型参数可以在函数体中使用。
- errno 11
Resource temporarily unavailable
表示资源暂时不可用,比如使用非阻塞socket,accept没有任何请求需要等待就会返回11。operation now in progress
表示非阻塞套接字正在进行连接操作 - std::forward完美转发(模板函数)可以保留参数左值和右值属性,和参数的是生命周期正确(直接将参数传递给另一个函数,可能会丢失参数的左值或右值属性),主要是为了保证参数的左值或右值属性保留(完美转发)。如果没有使用完美转发,右值引用会被强制转换为左值引用
- hook中do_io使用函数模板可变参数,保证匹配不同的IO系统调用
1
2
3
4template<typename... Args>
void foo(Args... args) {
// ...
} - 在hook住socket之后,新建sockfd时会将fd的上下文信息加入到fd信息集合中,因此通过get返回为空,说明该fd未hook(个人认为,那么fdctx中is_socket有用吗?)
- 也可能不是socketfd,如果是一般的文件描述符则需要手动fd上下文集合
- 所以结论是fdctx记录的是参与hook后操作的fd
- 切记范围锁在unlock时要保证上锁才进行释放,否则也会出现线程安全问题
- 对fd可以设置超时属性,如果在读写该fd时,超过时间未读写到数据则返回-1。
- do_io逻辑,考虑到超时,可以添加定时器,若不超时hook,则通过再次添加读写事件的方式来达到异步的效果。epoll_wait无论是超时事件还是读写事件都会触发返回,然后listallcbs可以读取所有的超时定时器的回调函数,并一一进行调度执行。
- 如果read超时了,设置超时时间resume执行,不可能会再次超时
- 不是套接字不需要hook(没必要)
- 如果读锁人数为0,再unlock就会设置为读的最大值
INADDR_ANY
表示Ipv4的地址,表示服务器可以接受来自任何网络接口的连接请求。该宏表示(0.0.0.0)通配地址- 在io协程调度中,idle函数内,epoll被唤醒之后,就会从idle协程中yield到调度协程执行,当然如果如果有定时事件或者是读写事件存在,调度器是不会停止的
- 普通协程调度器不能使用hook之后的版本,因为TimerManager并未初始化(IOManager继承了这个类,因此会在创建IO协程调度器的时候初始化它),因此在addTimer内操作TimerManager变量时会出现segFault。
- 如果是循环定时,则在listExpirecbs取出取出超时定时器事件的过程中,就再作为新的定时器插入到定时器堆中。
- 定时器堆使用的key是定时器指针,先用定时器的绝对到期时间比较,如果到期时间相等再比较地址。
- 当一个子类实现了其父类中的纯虚函数时,父类可以通过该纯虚函数调用子类实现的版本。如果父类有多个子类,那么调用哪一个子类版本的虚函数实现,取决于将哪一个子类对象的引用或指针赋给父类对象。如果是普通虚函数,要想调用父类版本的虚函数需要加类作用域限定直接调用,如
father::virtualfunc()
。 - memcmp如果比较字符,就会根据字典序来比较。具体用法
memcmp(str1, str2, len)
,len为比较的字节长度 - 有多个网卡,如何根据网卡地址去选择IP?
- 在使用动态库时,位置无关的代码(编译时加上-fpic)非常重要,因为动态库可以在内存中的任何位置加载和执行。
add_library(moka SHARED ${LIB_SRC})
将源文件编译成目标文件,生成动态库。在应用程序使用动态库时,不需要全部加载(只需要加载需要的实例即可),不需要将整个动态库链接到应用程序中。- char型数组和使用char*定义的字符串不会自动在末尾添加字符串结束符;但是
const char*
会在末尾自动加结束符 - Ip地址由(网络地址和主机地址构成)。CIDR表示法前缀一般称为网络地址的bit数
- 纯虚函数类(抽象基类),指的所有虚函数都是纯虚函数。如果直接基类不重写这些纯虚函数,那么它也将作为抽象基类
- 在 C++ 中,如果在不同文件中定义了同名的静态变量,每个文件中的静态变量都是独立的,它们具有各自的存储空间和作用域。静态变量的作用域限定在定义它的文件中,其他文件无法直接访问该变量。每个文件中的静态变量在程序运行时会分别存在内存中,它们互不干扰。
- 在有序容器中查找某个重复元素的范围可以用
std::equal_range
,会返回一个std::pair类型 - socket中的protocol参数为0,表示默认follow地址族的设置
- TLV(Type-Length-Value)协议,若字节最高位为1表示后续字节也属于该类型数据,最高位为0表示该字节为最后一个字节,将无符号整形数据拆分为多个7位字节进行编码,有符号整型需要转换为无符号,zigzag编码,将有符号整型转换为无符号整型数据进行VLE编码,然后将最高位的符号位翻转
- 固定长度需要考虑字节序,可变长度不需要
项目开发遇到的一些BUG
- 只能在调度器创建以后添加任务(因为协程载体的局限),因为调度器在构造时,才会初始化静态局部变量(use_caller时是在构造函数中初始化,非use_caller只有在run时才是初始化),当前调度器的调度协程,因此,Fiber初始化时返回link需要保证该变量是初始化的,这是个很严重的BUG。因此如果自己测试使用Fiber来作为任务,就会出现bug,之前没有崩是因为之前测试基本上都是函数,函数的Fiber载体都是在run函数中,在t_sched_fiber初始化之后才创建的,因此不会出现问题。
- 解决方案:在衍生一个MainFunc的版本(因为调用MainFunc时,肯定已经执行run函数了,即调调度协程已经被初始化),在Mainfunc里初始化返回的link,因为刚好mainfunc也是Fiber的成员函数,因此使用私有成员是没有问题的。故sched就调用MainFunc,call就调用MainFuncSched。(很遗憾MainFunc是static的,不过可以传入this指针)。
- 这个BUG解决之后就可以在调度器之前创建任务Fiber执行了
- 问题的原有之一,因为即便先start,也不能保证Fiber构造函数先于run函数中对t_sched的执行
- 解决方案不可行的原因:因为已经调用MainFunc函数了,这时候再link已经晚了,因为一般函数调用之前都会先将返回地址先push。
- 经过测验,协程作为任务添加是有BUG的,但函数作为任务添加不会出现
- 已经修复,通过再run函数中再新建一个Fiber作为载体,这个暂存协程任务的fiber在run中初始化,就可以正常link了。
- 最后不考虑uc_link了,直接区分不同版本的MainFunc,使用back或者yield返回即可,这里也要注意智能指针的处理
疑问
- 为什么IO多路复用要搭配非阻塞IO?
调试Skill
ps aux | grep {string}
查看名为string的线程信息,获取到进程id后top -Hp {proces id}
显示进程所包含的线程信息gdb attch {proces id}
调试已经在运行的进程(方便判断死锁)- 多线程调试
info thread
查看所有线程thread id
切换到指定id的线程set scheduler-locking on|off
锁定当前线程,即只在当前线程执行
- gcc指定链接库
-L<路径名> -l<库名> -I<头文件路径>
,注意库名不带lib前缀和.so后缀 - 如果使用动态库找不到路径可以通过
export LD_LIBRARY_PATH=/home/moksha/moka/lib
命令设置环境变量
常用工具
netstat
-l
显示所有listenning状态的端口-t
显示所有监听TCP的端口-p
显示PID和程序名称-n
以数字的形式显示地址,而不是尝试用主机或端口名-a
显示所有端口
man手册
- 1.用户命令(User Commands)
- 2.系统调用(System Calls)
- 3.库函数(C Library Functions)
协程的优势
- 减少了线程切换的成本(尤其是在高并发时,协程切换的效率高),适合于I/O密集型场景
- 协程不存在并发问题
- 协程更轻量,栈更小
协程调度
- 每个线程同一时间内只能有一个协程在执行
- 每个线程都有个主协程,主协程可以创建协程并调度协程,子协程只能回到主协程(非对称协程调度)
- 一个线程同一时刻只能运行一个协程
- 调度协程负责从调度器的任务队列中取任务执行,取出的任务即子协程,这里发生的主协程到子协程的切换为非对称协程。如果任务队列为空,调度协程会切换到idle协程,idle协程什么也不做
- 在执行调度任务时,还可以通过调度器的GetThis()方法获取到当前调度器,再通过schedule方法继续添加新的任务,这就变相实现了在子协程中创建并运行新的子协程的功能。
- main函数要等待调度线程结束后再退出
- 在非caller线程中,调度协程就是调度线程的主协程;在caller线程中,调度协程并不是caller线程的主协程(如main),相当于caller线程的子协程。main函数线程有三类协程:main函数对应的主协程,调度协程,待调度的任务协程
- 因此在初始化调度器时,use_caller表示使用当前调用者线程(调用构造函数的线程)来进行调度,caller线程会新建一个调度子协程来进行调度(注意这里执行调度的时机与执行start方法不在同一个地方)。如果是非caller线程,调度协程就是调度线程的主协程
- 在main函数线程里这三类协程(为什么有3类呢,因为main线程的主协程要负责添加任务schedule())运行的顺序是这样的:
- main函数主协程运行,创建调度器
- 仍然是main函数主协程运行,向调度器添加一些调度任务
- 开始协程调度,main函数主协程让出执行权,切换到调度协程,调度协程从任务队列里按顺序执行所有的任务
- 每次执行一个任务,调度协程都要让出执行权,再切到该任务的协程里去执行,任务执行结束后,还要再切回调度协程,继续下一个任务的调度
- 所有任务都执行完后,调度协程还要让出执行权并切回main函数主协程,以保证程序能顺利结束。
- 上面的case与非对称协程调度相悖?在caller线程中,调度子协程和任务子协程之间发生切换,怎么会到main主协程中?线程局部变量保存协程上下文,每个线程保存三个协程的上下文(正在执行的协程,调度协程,线程主协程)
- 调度器所在的线程称为caller线程(即调度线程)
- 若当前任务队列中没有任务执行才执行idle
- caller线程作为调度线程处理相对于非caller来说是比较复杂的,所幸每个线程都有这样几个线程局部变量来保存协程上下文:1.当前正在执行的协程,2.当前线程的主协程,3.当前线程的调度协程。额外有一个当前线程的调度器
- 任务协程执行过程中主动调用yield让出了执行权,调度器要怎么处理?再放进任务队列中
- 子协程(调度协程/任务协程),只有在其协程对象调用
sched
其中调用swapcontext时才会调用初始化Fiber对象时用makecontext设置的MainFunc
(其中执行run函数)发生调度 - 有两种调度case:1. 使用caller线程作为调度线程(即main线程) 2. 使用新建的线程作为调度线程
ucontext
int getcontext(ucontext_t *ucp);
使用当前执行的线程的上下文初始化协程上下文ucpint setcontext(const ucontext_t *);
恢复用户ucp的上下文void makecontext(ucontext_t *ucp, (void *func)(), int argc, ...);
,当使用setcontext或者swapcontext恢复ucp上下文时,调用func函数int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
交换用户上下文
CMake语法
project(demo)
会引入两个变量PROJECT_BINARY_DIR
和PROJECT_SOURCE_DIR
add_executable(demo demo.cpp)
生成可执行文件add_library(STATIC util.cpp)
生成静态库add_library(SHARED util.cpp)
生成动态库set(SRC_LIST main.cpp test.cpp)
直接设置变量的值,$SRC_LIST
就表示main.cpp和test.cpptarget_link_libraries(Demo MathFunctions)
添加链接库,表明demo需要链接一个MathFunctions的库- 常用变量
EXECUTABLE_OUTPUT_PATH
:重新定义目标二进制可执行文件的存放位置LIBRARY_OUTPUT_PATH
:重新定义目标链接库文件的存放位置PROJECT_SOURCE_DIR
:工程的根目录
yaml语法
- 大小写敏感,使用缩进(缩进只允许使用空格)表示层级关系,缩进的空格数量没有限制(相同层级的元素左对齐即可),
#
表示注释 - 数据类型
- 对象(使用
:
结构表示key/value):键值对的集合 - 数组(以
-
开 头) - 标量(scalars):不可再分的值
- 对象(使用
- 类型值
enum value {undefined, NULL, Scalar, Sequence, Map};
- 例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21# logs中包含两个数组,两个数组元素中又包含字典name、level、formatter
logs:
- name: root
level: info
formatter: '%d%T%m%n'
appender: # appender字典中包含两个数组(第一个数组中包含两个字典,第二个数组元素中包含一个字典)
- type: FileLogAppender
file: log.txt
- type: StdoutLogAppender
- name: system
level: debug
formatter: '%d%T%m%n'
appender:
- type: FileLogAppender
path: log.txt
- type: StdoutLogAppender
# system为map,其元素也为map
system:
port: 9900 # 9000为scalar
value: 15