- 服务器测试环境
- Ubuntu 20.04.3 LTS
- MySQL版本8.0.29
- 浏览器测试环境
- Windows、Linux均可
- Chrome
- FireFox
- 其他浏览器暂无测试
- 测试前确认已安装MySQL数据库
sudo apt-get install mysql-client sudo apt-get install mysql-server sudo apt-get install libmysql++-dev
// 建立yourdb库 create database yourdb; // 创建user表 USE yourdb; CREATE TABLE user( username char(50) NULL, passwd char(50) NULL )ENGINE=InnoDB; // 添加数据 INSERT INTO user(username, passwd) VALUES('name', 'passwd');
- 测试前确认已安装YAML-CPP
git clone https://github.com/jbeder/yaml-cpp.git cd yaml-cpp cd build && mkdir build make make install
- 测试前确认已安装boost
sudo apt install libdev-all-dev
-
修改
conf/TinyWebserverConfig.yml
中的服务器初始化信息host: "0.0.0.0:9006" # 主机地址 db: # 数据库信息 url: localhost user: root password: root databasename: yourdb port: 3306 maxconn: 15 logs: # 日志配置 - name: root level: info appenders: - type: FileLogAppender foramt: '%d{%Y-%m-%d %H:%M:%S} %t %N %F [%p] [%c] %f:%l %m%n' file: /home/pudge/workspace/pudge-server/logs/root.txt - type: StdoutLogAppender foramt: '%d{%Y-%m-%d %H:%M:%S} %t %N %F [%p] [%c] %f:%l %m%n' - name: system level: info appenders: - type: FileLogAppender foramt: '%d{%Y-%m-%d %H:%M:%S} %t %N %F [%p] [%c] %f:%l %m%n' file: /home/pudge/workspace/pudge-server/logs/system.txt - type: StdoutLogAppender foramt: '%d{%Y-%m-%d %H:%M:%S} %t %N %F [%p] [%c] %f:%l %m%n'
-
build
sh ./build.sh
-
启动server
./bin/tiny_webserver
-
浏览器端
ip:9006
支持流式日志风格写日志和格式化风格写日志。
流式日志使用:
pudge::Logger::ptr g_logger = PUDGE_LOG_NAME("log_name");
PUDGE_LOG_INFO(g_logger) << "this is a log";
格式化日志使用:
pudge::Logger::ptr g_logger = PUDGE_LOG_NAME("log_name");
PUDGE_LOG_FMT_INFO(g_logger, "%s", "this is a log");
日志格式器使用: 日志格式器,执行日志格式化,负责日志格式的初始化。 解析日志格式,将用户自定义的日志格式,解析为对应的FormatItem。
日志格式举例:%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n
格式解析:
%d{%Y-%m-%d %H:%M:%S} : %d 标识输出的是时间 {%Y-%m-%d %H:%M:%S}为时间格式,可选 DateTimeFormatItem
%T : Tab[\t] TabFormatItem
%t : 线程id ThreadIdFormatItem
%N : 线程名称 ThreadNameFormatItem
%F : 协程id FiberIdFormatItem
%p : 日志级别 LevelFormatItem
%c : 日志名称 NameFormatItem
%f : 文件名 FilenameFormatItem
%l : 行号 LineFormatItem
%m : 日志内容 MessageFormatItem
%n : 换行符[\r\n] NewLineFormatItem
str: 普通字符串 StringFormatItem
与日志模块相关的类:
LogEvent
: 日志事件,用于保存日志信息,比如,日期时间,线程/协程id,文件名/行号等,以及日志消息内容。
LogEventWarp
: 日志事件包装器,将日志事件与日志器包装在一起,通过宏定义简化日志模块的使用,当包装器析构时候,则调用日志器Logger
的log
方法进行输出;
LogFormatter
: 日志格式器,包含了FormatItem
的日志格式化内容的抽象基类及其派生类,新增加的格式内容通过继承该抽象基类,对日志格式化内容就行修改增加. 构造时可指定一个格式模板字符串m_pattern
,根据该模板字符串定义的模板项对一个日志事件进行格式化,解析出日志格式内容对应的FormatItem
对象,如普通字符串对应的是StringFormatItem
类型, 生成对应的m_items
解析结果。提供format
方法对LogEvent
对象进行格式化输出并返回对应的字符串(格式化日志)或流(流式日志),实质上是遍历m_iterms
调用FormatItem
的format
方法进行输出。
LogAppender
:日志输出器,用于将日志输出到不同的目的地,比如终端和文件等。LogAppender
内部包含一个LogFormatter
成员,提供log
方法对LogEvent
对象进行输出到不同的目的地。这是一个抽象基类,通过继承的方式可派生出不同的Appender,目前默认提供了StdoutAppender和FileAppender两个类,用于输出到终端和文件。
Logger
: 日志器,用于写日志,包含名称,日志级别两个属性,以及数个LogAppender成员对象,一个日志事件经过判断高于日志器自身的日志级别时即会启动Appender进行输出。日志器默认不带Appender,需要用户进行手动添加。
LoggerManager
:日志器管理类,单例模式,包含全部的日志器集合,用于创建或获取日志器。LoggerManager
初始化时自带一个root
日志器,这为日志模块提供一个初始可用的日志器。
采用约定优于配置的**。定义即可使用。支持变更通知功能。使用YAML文件做为配置内容,配置名称大小写不敏感。支持级别格式的数据类型,支持STL容器(vector,list,set,map等等), 支持自定义类型的支持(需要实现序列化和反序列化方法)。
使用方式如下:
pudge::ConfigVar<std::string>::ptr g_host_addr =
pudge::Config::Lookup("host", std::string("0.0.0.0:9006"), "host ip config");
定义了一个服务器主机地址参数,可以直接使用g_host_addr->getValue()
获取参数的值.
对应的yaml文件格式如下:
host: "0.0.0.0:9006"
与配置模块相关的类:
ConfigVarBase
: 配置参数基类,定义了一个配置参数的基本属性和方法,比如名称,描述,以及toString/fromString两个纯虚函数,配置参数的类型和值由继承类实现。
ConfigVar
: 由ConfigVarBase
派生于的配置参数类,这是一个模板类,有三个模板参数,一个是类型T,另外两个是FromStr
和ToStr
反序列化和序列化的类类型仿函数(支持偏特化方法),共中FromStr
用于将字符串转类型T
,ToStr
用于将T
转字符串。这两个模板函数以Boost
库的boost::lexical_cast
为基础,通过定义不同的偏特化仿函数类来实现基础数据类型的STL类
和的序列化与反序列化,通过定义不同的特例化仿函数类来实现自定义类型的转换,其中Log
模块、Mysql_pool
模块的参数配置均是利用了特例化仿函数进行读取配置。ConfigVar类包含了一个T
类型的配置参数值成员和一个参数变更回调函数数组. toString/fromString
用到了模板参数toStr/fromStr
。此外,ConfigVar还提供了setValue/getValue
方法用于设置/修改值,并触发回调函数数组,以及addListener/delListener
方法用于添加或删除回调函数。
Config
: ConfigVar的管理类,采用单例模式确保整个文件只有一个实例化对象。包含有ConfigVarMap
成员来管理全部的ConfigVar
对象。提供Lookup
方法,根据参数名称查询配置参数。如果调用Lookup查询时同时提供了默认值和配置描述信息,那么在未找到对应的配置时,会创建一个新的配置项,这样就保证了配置模块定义即可用的特性。除此外,Config类还提供了LoadFromYaml
和LoadFromConfDir
两个方法,用于从YAML结点或定义的配置文件路径中加载配置文件。。
线程模块,封装了pthread里面的一些常用功能,Thread,Semaphore,Mutex,RWMutex等对象,可以方便开发中对线程日常使用。
线程模块相关的类:
Thread
:线程类,构造函数传入线程入口函数和线程名称,线程入口函数类型为std::function<void()>
。在pthread_create
绑定Thread::run
函数,并将Thread
的this
指针作为参数传入,在Thread::run
中完成线程生存期变量t_thread
和t_thread_name
的赋值以及成员变量thread->m_id
的赋值.
Semaphore
: 为了确保构造线程池时,每个成员变量thread->m_id
已经初始化成功完成.
线程同步类(这部分被拆分到mutex.h)中:
Semaphore
: 计数信号量,基于sem_t实现
Mutex
: 互斥锁,基于pthread_mutex_t实现
RWMutex
: 读写锁,基于pthread_rwlock_t实现
Spinlock
: 自旋锁,基于pthread_spinlock_t实现
协程:用户态的线程,相当于线程中的线程,更轻量级。后续配置socket相关的系统调用的hook,可以把复杂的异步调用,封装成同步操作。降低业务逻辑的编写复杂度。该协程是基于ucontext_t
来实现的,利用makecontext
和swapcontext
等api实现协程的创建和切换. 协程栈大小可由g_fiber_stack_size
配置变量进行修改配置.
非对称协程调度模型:
子协程SubFiber只能和主协程MainFiber切换,而不能和另一个子协程切换.
Thread --> MainFiber <--> SubFiber
^
|
v
subFiber
实现方法: 利用如下两个变量进行调度切换:
static thread_local Fiber* t_fiber = nullptr; ///当前调用协程
static thread_local Fiber::ptr t_threadFiber = nullptr; ///主协程
t_threadFiber
在 MainFiber
的构造中进行初始化。
首次调用GetThis(), 此时无任何运行协程 -> new Fiber -> shared from this
-> t_threadFiber = main_fiber;
t_fiber
在 SetThis
的中进行赋值
三处SetThis
, 出现在新的协程切换进来时。
Fiber::Fiber() -> setThis()
Fiber::call() -> setThis()
Fiber::swapIn() -> setThis()
协程有五种状态
/// 初始化状态
INIT,
/// 暂停状态
HOLD,
/// 执行中状态
EXEC,
/// 结束状态
TERM,
/// 可执行状态
READY,
/// 异常状态
EXCEPT
- 协程类的构造函数初始化默认为
INIT
状态,主协程构造中直接赋值为EXEC
(默认是在运行的) HOLD
: 暂停状态,让出CPU执行权,完成异步系统调用的关键点,在Hook的Socket API中,如果出现阻塞状态,则切换为该状态.EXEC
:在swapIn
和call
以及Fiber()
中进行设置,标志该协程享有CPU执行权.TERM
: 在协程callBack
函数执行完毕后设置,标志协程已经执行完毕.READY
: 只在YieldToReady
中出现,尽量不使用该切换函数.因为协程的执行应该由协程自主决定,若是出现该状态,则调度器只是把它重新加入到任务队列中.EXCEPT
在协程callBack
中抛出异常时出现,同TERM同样处理。
Scheduler
协程调度器,管理协程的调度,内部实现为一个线程池,支持协程在多线程中切换,也可以指定协程在固定的线程中执行。是一个N-M的协程调度模型,N个线程,M个协程。重复利用每一个线程。
两种运行模式:
1.主线程参与调度,也用于消息队列的添加. 主线程负责任务的消息队列m_fibers
的添加, 副线程负责协程的调度. use_caller == true
,如下
MainThread --> MainFiber <--> m_rootFiber <--> workFiber (第 1 类)[需要陷入~IOManager中才进行调度]
| |
| |
v v
schedule run
SubThread --> MainFiber <--> workFiber (第 2 类)
|
|
v
run
...
2.主线程不参与调度, 可用于消息队列m_fibers
的添加. use_caller == false
,如下
MainThread --> MainFiber
|
|
v
schedule
SubThread --> MainFiber <--> workFiber
|
|
v
run
...
单独使用由于Idle
函数一直循环,造成调度线程一直占用CPU,最好不单独使用,可配合epoll
使用.
IOManager <- Scheduler
^
|
TimerManager
继承自协程调度器Scheduler
,封装了epoll(Linux)
,支持注册socket fd事件回调。只支持读写事件。IO协程调度器解决了协程调度器在idle情况下CPU占用率高的问题,当调度器idle时,调度器会阻塞在epoll_wait上,此时无协程任务执行,则让出线程的CPU执行权.当IO事件发生或添加了新调度任务时再返回, 其余阻塞的调度线程,可以通过一对pipe fd
来实现通知调度协程有新任务(tickle
)。
IOManager
的父类TimerManager
,功能是在指定超时时间结束之后执行回调函数。定时的实现机制是idle协程的epoll_wait
超时,大体思路是创建定时器时指定超时时间和回调函数,然后以当前时间加上超时时间计算出超时的绝对时间点,然后所有的定时器按这个超时时间点排序,从最早的时间点开始取出超时时间作为idle协程的epoll_wait超时时间,epoll_wait超时结束时把所有已超时的定时器收集起来,执行它们的回调函数。注意一点,如果出现更早需要执行的任务, 需要执行onTimerInsertedAtFront
(执行tickle
方法)去唤醒调度线程,对epoll_wait
的超时时间进行修正,确保更早的任务能完成。
hook系统底层和socket创建和设置相关的API,socket io相关的API,以及sleep系列的API。
- 通过hook模块,可以使一些不具异步功能的API,展现出异步的性能, 比如说 Mysql,通过hook改造后的
socket
和read
系统调用实现了异步操作. - 使得后台逻辑服务几乎不用修改逻辑代码就可以完成异步化改造。
- 可以用写同步代码的方式实现异步的性能,避免了实现异步带来回调地狱。
实现方法: 通过动态库的全局符号介入功能,用自定义的接口来替换掉同名的系统调用接口。由于系统调用接口基本上是由C标准函数库libc提供的,所以这里要做的事情就是用自定义的动态库来覆盖掉libc中的同名符号, 利用系统调用dlsym
找到被覆盖掉的libc中的实际系统调用函数,根据hook是否开启,来选择是否执行hook后的系统调用。
该框架对以下函数进行了hook,并且只对socket fd进行了hook,如果操作的不是socket fd,那会直接调用系统原本的api,而不是hook之后的api:
sleep
usleep
nanosleep
socket
connect
accept
read
readv
recv
recvfrom
recvmsg
write
writev
send
sendto
sendmsg
close
fcntl
ioctl
getsockopt
setsockopt
封装了Socket
类和统一的Address
地址(包含TCP套接字,UDP套接字,Unix域套接字)类,提供了如下方法:
- 创建各种类型的套接字对象的方法
- 设置套接字选项,比如超时参数
- bind/connect/listen方法,实现绑定地址、发起连接、发起监听功能
- accept方法,返回连入的套接字对象
- 发送、接收数据的方法
- 获取本地地址、远端地址的方法
- 获取套接字类型、地址类型、协议类型的方法
- 取消套接字读、写的方法
ByteArray二进制序列化模块,提供对二进制数据的常用操作。提供读写入基础类型int8_t,int16_t,int32_t,int64_t等,Protobuff(Varint和ZagZig)基础类型的读写,TLV格式序列化字符串的读写, 支持序列化到文件,以及从文件反序列化等功能。
ByteArray的底层存储是固定大小的块,以链表形式组织。每次写入数据时,将数据写入到链表最后一个块中,如果最后一个块不足以容纳数据,则分配一个新的块并添加到链表结尾,再写入数据。ByteArray会记录当前的操作位置,每次写入数据时,该操作位置按写入大小往后偏移,如果要读取数据,则必须调用setPosition重新设置当前的操作位置。
基于Socket类,封装了一个通用的TcpServer的服务器类,提供简单的API,使用便捷,可以快速绑定一个或多个地址,启动服务,监听端口,accept连接,处理socket连接等功能。具体业务功能更的服务器实现,只需要继承该类就可以快速实现
TcpServer类采用了Template Pattern设计模式,它的HandleClient是交由继承类来实现的。使用TcpServer时,必须从TcpServer派生一个新类,并重新实现子类的handleClient操作,example中的TinyWebserver
即是这样操作。
封装流式的统一接口。将文件,socket封装成统一的接口。使用的时候,采用统一的风格操作。基于统一的风格,可以提供更灵活的扩展。所有的流结构都继承自抽象类Stream,Stream类规定了一个流必须具备read/write接口(纯虚函数)和readFixSize/writeFixSize接口,继承自Stream的类必须实现备read/write接口。
现已实现套接字流结构SocketStream
,将套接字封装成流结构,以支持Stream接口规范,除此外,SocketStream还支持套接字关闭操作以及获取本地/远端地址的操作。
采用Ragel(有限状态机,性能媲美汇编),实现了HTTP/1.1的简单协议实现和uri的解析。基于SocketStream实现了HttpSession (HTTP服务器端的链接)。基于TcpServer实现了HttpServer。提供了完整的HTTP的客户端API请求功能,HTTP基础API服务器功能.
仿照java的servlet,实现了一套Servlet接口,实现了ServletDispatch,FunctionServlet。NotFoundServlet。支持uri的精准匹配,模糊匹配(fnmatch
)等功能。和HTTP模块,一起提供HTTP服务器功能