ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C++11服务器框架sylar入门:从编译到日志配置模块精读

C++11服务器框架sylar入门:从编译到日志配置模块精读 先声明一下我并不是什么框架源码研究专家平时主要用C写业务和中间件啃sylar纯粹是出于“这个项目怎么把C11用得这么狠”的好奇心。这篇是系列第一篇先聊清楚它是什么、值不值得学、环境怎么搭、以及从哪个模块入手最舒服。1. 我一个写业务C的人为什么回头啃sylar1.1 sylar到底是什么它能给你什么先给不了解的朋友科普一句sylar是一个基于C11的分布式/高性能服务器框架GitHub上可以搜到作者网名叫sylar整个项目用现代C重写了一套类似muduo的网络库但又不只是网络库——它还包含了日志、配置、协程、协程调度、Hook、Socket、HTTP协议封装、SQLite/Redis之类的集成模块。说人话就是你见过的开源服务器该有的东西它基本都有。我第一次打开它的源码时第一反应是这代码根本不是给人看的是给编译器看的。模板套模板YAML解析全靠模板特化日志模块的宏定义能绕晕人。但啃了两周后我的看法变了——sylar其实是很有教学价值的因为它的代码把C11/14的很多高级特性“用在了刀刃上”你学完它再回去看很多业务项目的代码会觉得处处眼熟、处处是套路。对于正在学C后端的人sylar能解决一个很现实的问题学完语法之后“怎么把线程、锁、网络、异步、配置这些零散的C知识点串成一个工业级项目”。教科书只会教你怎么写std::thread和std::lock_guard但不会教你怎么设计一个能支撑上万连接的服务端架构。sylar把这条路完整走了一遍。1.2 和muduo、workflow这类框架放一起怎么选说到学习Linux C高性能网络编程大部分人第一个想到的是muduo。我不否认muduo是经典但muduo是基于boost的后来陈硕出新版也没完全脱离boost的影子代码风格更偏现代C之前的“老派”。而且muduo的注释和文档确实多但它的学习曲线对新手不算友好——你得先熟悉boost::bind、boost::function那一套东西。workflow是腾讯开源的设计上更面向“任务流”编排思想很惊艳但它的抽象层级偏高如果你是想学底层系统编程workflow会让你觉得“学完还是很虚”。sylar恰好踩在两者之间它有完整的网络库实现从Socket封装到TcpServer、HTTP解析也有协程调度这么底层的设计基于ucontext自己实现协程切换代码风格是“现代C但不过度魔改”。最重要的是sylar是单作者项目代码一致性极强读起来不会像很多企业开源项目那样东一块西一块。我用一个表来说明三者差异方便你按需选择框架底层依赖核心卖点适合的学习人群muduoboost经典的Reactor多线程模型资料多想学网络编程经典范式的人workflow纯C少量依赖任务流编排、异步编程模型搞服务编排/在线异步业务的人sylarboost yaml-cpp openssl协程、Hook、完整HTTP、模块化设计想把C高级特性和网络编程一次串起来的人顺便说一句sylar对boost的依赖其实很克制主要用到了它的部分智能指针和线程库不需要学boost的复杂玩法。2. 从零编译sylar先把环境跑通再说2.1 依赖安装与CMake构建sylar 的编译不算复杂但没有坑是假的。我是在Ubuntu 22.04上编译的依赖主要是这几个CMake3.10以上应该都行boost库主要是boost/threadboost/systemyaml-cppopensslzlib可选Https和压缩相关会用到Ubuntu/Debian系一条命令装依赖sudo apt install -y cmake libboost-dev libboost-system-dev libyaml-cpp-dev libssl-dev zlib1g-dev然后克隆源码并编译git clone https://github.com/sylar-yin/sylar.git cd sylar mkdir -p build cd build cmake .. make -j$(nproc)如果你和我一样遇到“找不到yaml-cpp”或者“openssl头文件缺失”的报错先检查依赖包装没装全尤其是libyaml-cpp-dev和libssl-dev这是最容易漏的两个。另一个比较坑的点是sylar的CMakeLists.txt里对yaml-cpp的处理比较“原始”它直接find_package(yaml-cpp REQUIRED)。如果你是自己编译的yaml-cpp且安装路径不在默认目录建议把它安装到/usr/local否则CMake大概率找不到。2.2 阅读顶层目录先建立全局地图编译顺利通过之后千万不要急着一个文件一个文件往下读那样会迷失在宏和模板里。先花半个小时看目录结构sylar/ ├── CMakeLists.txt ├── README.md ├── examples/ # 示例代码HTTP服务器、TCP服务器 ├── src/ # 框架核心源码 │ ├── log.h # 日志模块 │ ├── config.h # 配置模块 │ ├── thread.h # 线程模块 │ ├── fiber.h # 协程模块 │ ├── scheduler.h # 协程调度器 │ ├── iomanager.h # IO协程调度器 │ ├── socket.h # Socket封装 │ ├── tcp_server.h # TCP服务端封装 │ ├── http/ # HTTP协议解析、Servlet等 │ └── ... ├── tests/ # 各模块的测试用例我的建议是先按“日志 - 配置 - 线程 - 协程 - 调度器 - IO调度 - Socket/TcpServer - HTTP”这个顺序读。道理很简单——日志模块不依赖其他模块是系统的地基配置模块依赖yaml-cpp和日志组成了框架的“基础设施”线程模块告诉你多线程环境怎么写才不出事再往后才是协程和调度那块是整个sylar最精彩也最难啃的部分放到前面纯属劝退。我强烈建议至少在第一个阶段只看tests/里的测试代码别读src的实现细节。sylar的测试用例虽然也写得比较简单但配合日志输出能很清楚看到每个模块的行为。3. 日志模块整个框架的敲门砖3.1 从日志器到输出地底层对象怎么协作从一开始就吃透日志模块另一个原因是框架其它模块都依赖它——配置加载要打日志协程切换要打日志网络事件要打日志。你要是能先看懂日志模块后面读任何模块都不会因为“日志哪来的”而卡住。sylar的日志模块围绕几个核心类展开Logger日志器、LogEvent日志事件、LogFormatter日志格式器、LogAppender日志输出地。还有个LogManager日志管理器做全局统一管理。它们的关系其实不难理解业务代码创建一条LogEvent记录时间、线程ID、文件名、行号、日志级别的快照把它交给LoggerLogger判断这条日志级别是否达到自己的门槛如果达到就调用LogFormatter把事件格式化成字符串最后把字符串交给LogAppender输出到指定位置控制台、文件等。有个细节值得注意LogAppender自己也可以持有独立的LogFormatter。这意味着你可以给文件输出设置一种格式带完整时间戳、线程ID给控制台设置另一种更紧凑的格式。这种设计在生产环境里非常实用比如你只想在屏幕上看[ERROR] xxx但落盘的日志需要含文件名:行号方便排查。3.2 自定义格式器的威力看日志模块的使用方式另一个让我觉得“这框架有点东西”的点是它的格式串设计。sylar日志支持像这样配置格式auto formatter std::make_sharedLogFormatter(%d{%Y-%m-%d %H:%M:%S}%T%t%T%F%T[%p]%T[%c]%T%f:%l%T%m%n);这段格式串的意思是输出日期时间、制表符、线程ID、协程ID%F是fiber id、日志级别、日志器名称、文件名、行号、消息、换行。这就是一个典型的工业级日志行模板。实际使用里这种设计帮了我大忙。有次要在线上排查一个问题我把格式切换成只包含%t、%p、%m日志瞬间从“沉重繁琐”变成“轻量可读”比重新编译换一套日志代码省太多事了。如果你要抄作业这个格式串的可配置思想非常值得抄进自己的项目。3.3 多线程环境下日志模块的坑sylar日志模块里还有一个很容易被人忽略但极其关键的设计LogAppender在输出前要加锁。原因很简单多个线程同时写日志如果不对文件输出做互斥日志内容会错行、串行。sylar里每个LogAppender内部都有一个Mutex在log()方法里用Mutex::Lock临时锁住当前输出地。我在自己项目里就吃过没加锁的亏。当时用了一个自定义的异步日志器为图省事没有对后端写文件的线程加锁结果高并发时日志文件里出现大量交叉片段——同一行日志前半截来自请求A后半截来自请求B。排查了很久最后发现是写日志队列的头部和尾部同时被两个线程消费。后来照着sylar的写法给每个appender挂了个mutex问题立刻消失。这里有个小经验日志模块是并发类项目里最容易被忽略线程安全的地方因为日志“偶尔乱一下”不容易造成线上故障但会让你排查其它问题的时候雪上加霜。4. 配置模块参数怎么从代码里解放出来4.1 ConfigVar一个模板类撑起整个配置体系看完日志模块我强烈建议你跟着看配置模块也就是config.h和config.cc。sylar的配置系统不像很多项目那样写一个啥都装的ConfigManager而是设计了一套以ConfigVarT模板类为核心的体系。ConfigVarT是一个模板类一个配置项就是一个ConfigVarT对象包含配置项名称如system.worker_num配置项描述当前值默认值一组监听回调值变更时触发全局的Config类内部维护了一张unordered_mapstring, ConfigVarBase::ptr把配置名和配置项对象关联起来。你在任何地方只要通过Config::LookupT(system.worker_num)就能拿到配置项改了值还能触发回调。为什么要这样设计直接读全局结构体不就行了答案是可以变相实现“运行时热更新”。配置模块加载文件后可以通过回调通知依赖配置的模块自行调整。举个例子你正在运行一个HTTP服务线程池大小原本是4你想把它调成8只需要改YAML文件并触发重载system.worker_num变更后线程池模块收到回调自动扩容或缩容不需要重启进程。4.2 YAML加载与变更回调的实际用途sylar配置模块的输入是YAML文件这是通过yaml-cpp库解析的。配置模块里用了一堆模板特化用于把YAML::Node转成标准类型vector、map、set等。我第一次看config.cc里那一大串模板特化时觉得“这代码真难看”后来实际需要给项目加一个std::vectorstd::pairint, string类型的配置项时才发觉sylar这种把“从YAML节点到具体类型”的转换逻辑集中在一起的设计多么省事。举个直白的例子我仿照sylar在自己的项目中定义了线程池大小配置static sylar::ConfigVarint::ptr g_thread_num sylar::Config::Lookupint(system.thread_num, 4, system thread number);然后绑定一个回调g_thread_num-addListener([](const int old_val, const int new_val) { SYLAR_LOG_INFO(SYLAR_LOG_ROOT()) thread num changed: old_val - new_val; // 在这里触发线程池扩容 });这样配置文件一改线程池就能动态调整。这个能力在很多开源框架里都属于“有但不开放”sylar直接把它做成了配置模块的基础能力非常良心。配置模块还有一个和日志模块的联动点日志级别本身可以被配置化。你可以通过配置项控制某个日志器的级别比如在线上把某个模块的级别从INFO调到DEBUG不用重新编译、重发代码这在高并发排查问题时省下的时间不是一点半点。5. 线程模块用工程化的方式封装pthread5.1 锁封装RAII的正确打开方式很多人写C多线程代码还是喜欢裸调pthread_mutex_lock/pthread_mutex_unlock这样遇到异常路径极易忘记解锁程序一抛异常就死锁。sylar的线程模块把这些都封装成了RAII风格的类核心就是Mutex和Mutex::Lock。简单说Mutex内部的lock()和unlock()封装了pthread_mutex_lock/unlock而Mutex::Lock是一个栈对象构造时调用lock()析构时调用unlock()锁的生命周期和作用域绑定。这就保证哪怕函数中途return或throw锁也能被正确释放。除了普通的Mutexsylar还提供了RWMutex读写锁、Spinlock自旋锁、Semaphore信号量。我最常抄的是Spinlock的用法当临界区非常短、等待时间远远小于线程切换开销时自旋锁比互斥锁性能好得多。sylar把这种对应关系封装得很直接——你不需要记住底层接口只要根据场景选择合适的类名即可。5.2 Thread类的设计与使用sylar的Thread类也很有借鉴意义。它内部封装了pthread_t提供join()、detach()等方法但你几乎看不到裸的pthread_create。更重要的是它支持给线程起名字在构造时传入线程名内部通过prctl或pthread_setname_npLinux下设置线程名。这个功能在线上用gdb或者top -H -p看线程列表的时候简直救命——一屏的线程名字像thread-1、thread-2总好过一堆Thread-xxxxx。我在自己的异步框架里照抄了这个设计。原本排查性能问题时需要靠 pid 才能定位具体线程加上线程名之后直接看线程名就知道是网络IO线程还是日志落盘线程定位问题的时间至少缩短了一半。线程模块还涉及一些比较进阶的东西比如ThreadLocal线程本地变量。sylar在协程调度里大量使用线程局部变量来保存当前线程的调度器和协程上下文。这个对初学者来说可以先放一放等读协程模块时再回头理解。6. 我踩过的坑以及接下来的学习路线6.1 环境与编译阶段的高频坑如果你也想照着sylar学先做好心理准备编译阶段有几个高频坑第一个是boost版本问题。sylar用到了boost::lexical_cast、boost::thread等组件有些老版本Ubuntu自带的boost库较旧可能在链接时因为符号版本不匹配报错。我后来干脆装了较新的boost库并且把/usr/local/lib加入库搜索路径才消停下来。第二个是openssl相关。sylar的src/http里有些文件依赖opensslCMakeLists里也有find_package(OpenSSL REQUIRED)。如果没装libssl-dev会在编译http模块时报头文件缺失。这不是大坑但如果你只装了libcurl没装libssl-dev很容易被这个报错迷惑。第三个是yaml-cpp自带版本和系统版本冲突。因为有些Linux发行版依赖了系统自带的yaml-cpp你手动编译一份新版本yaml-cpp后CMake可能找到的是系统的旧版导致链接错误或运行时行为不对。解决方法是编译yaml-cpp时指定安装前缀然后在sylar的CMakeLists里通过CMAKE_PREFIX_PATH指定。最后再提醒一个独立于编译但超级常见的坑第一次跑sylar的测试程序时记得设置环境变量SYLAR_HOME或配置好日志文件输出目录。sylar默认日志路径可能指向./logs如果目录不存在日志不会自动创建目录你会看到程序跑起来“啥也不输出”以为是代码挂了其实只是日志没写出来。6.2 从日志到HTTP服务器sylar的全景学习路线如果你完成了日志、配置、线程三个模块的学习恭喜你已经迈过了最枯燥的部分。接下来可以按这个路线继续协程模块fiber这是sylar最有技术含量的模块之一基于ucontext实现用户态协程切换。你需要理解“程序计数器栈寄存器上下文”怎么实现函数挂起和恢复。协程调度器scheduler一个线程里怎么调度多个协程谁负责调度谁负责执行这些都是经典操作系统课程里的线程调度问题sylar给了工程化的答案。IO协程调度器iomanager把epoll事件循环和协程调度结合让网络IO事件触发时自动唤醒对应协程。Socket和TcpServer把这些底层能力封装成易用的服务端组件。HTTP模块解析HTTP请求报文、构造响应、Servlet机制、HTTP Server集成。按这个顺序从头到尾走一遍你对“一个现代C服务器框架是怎么从零构建的”会有一个完整认知。之后再看别的网络库、再读开源中间件思路都会清晰很多。我自己学的时候前两三天在日志和配置模块磨了比较久因为宏和模板确实绕。后面到协程和调度器时反而顺畅了——因为已经适应了作者的风格而且协程切换是“硬核知识”原理上想通了代码就是那么件事。最后分享一点个人体会啃sylar的过程中我最大的收获不是“我会写一个HTTP服务器了”而是终于把C11那些平时用不上的特性——可变参数模板、完美转发、智能指针、模板特化、RAII、enable_shared_from_this——全部在一个真实项目里见到了“活的使用案例”。以前看教科书觉得“标准库这么设计就好了”看完sylar才明白底层框架为了让上层用户写得舒服自己必须承受大量的模板复杂度。所以如果你决定学sylar心态上要有准备不要指望第一个星期就能通读所有模块也别因为某个宏定义看不懂就烦躁。我的做法是“先会跑再深挖细节”——先编译、跑测试用例、看日志输出建立结果导向的认知然后回头精读一个具体模块推荐从日志开始的实现把自己的收获记录成笔记。下一篇我打算把协程模块和调度器这部分展开讲讲尤其是ucontext切换上下文时那些容易绕晕的细节以及sylar是怎么在用户态实现协程调度、避免递归栈溢出的。有对协程原理感兴趣的同学可以先自己翻一下fiber.h里的注释咱们下期见。
返回列表