# da4qi4_public **Repository Path**: maolala/da4qi4_public ## Basic Information - **Project Name**: da4qi4_public - **Description**: C++ 高性能 Web Server 快速开发框架 - **Primary Language**: C++ - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: https://www.d2school.com - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 78 - **Created**: 2019-11-14 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README - [零、几个原则](#零几个原则) * [0.1 自己的狗粮自己吃](#01-自己的狗粮自己吃) * [0.2 紧抱牛人的大腿](#02-紧抱牛人的大腿) * [0.3 易用优于性能](#03-易用优于性能) - [一、快速了解](#一快速了解) * [1.1 一个空转的Web Server](#11-一个空转的web-server) * [1.2 Hello World!](#12-hello-world) + [1.2.1 针对指定URL的响应](#121-针对指定url的响应) + [1.2.2 返回HTML](#122-返回html) * [1.3 处理请求](#13-处理请求) * [1.4 引入“Application”](#14-引入application) * [1.5 运行日志](#15-运行日志) * [1.6 HTML 模板](#16-html模板) * [1.7 更多](#17-更多) - [二、如何构建](#二如何构建) * [2.1 基于生产环境构建](#21-基于生产环境构建) * [2.2 准备编译工具](#22-准备编译工具) * [2.3 准备第三库](#23-准备第三方库) * [2.4 下载大器源代码](#24-下载大器源代码) * [2.5 编译“大器”库](#25-编译大器库) * [2.6 在你的项目中使用 da4qi4库](#26--在你的项目中使用-da4qi4-) - [三、运行时外部配套系统](#三运行时外部配套系统) * [3.1 运行时依赖说明](#31-运行时信赖说明) * [3.2 Redis的安装](#32-redis的安装) * [3.3 数据库](#33-数据库) # 零、几个原则 ## 0.1 自己的狗粮自己吃 官网 [第2学堂 www.d2school.com](http://www.d2school.com) 后台使用 da4qi4作为Web Server开发。(nginx + da4qi4 + redis + mysql)。 ## 0.2 紧抱牛人的大腿 使用成熟的,广泛应用(最好有大公司参与)的开源项目作为框架基础组成部件。 da4qi4 Web 框架优先使用成熟的、C/C++开源项目的搭建。其中: - HTTP 基础协议解析: Node.JS / http-parser, 纯C语言 [nodejs/http-parser](https://github.com/nodejs/http-parser) - HTTP multi-part : multipart-parsr [multipart-parser-c](https://github.com/iafonov/multipart-parser-c) - 网络异步框架: C++ boost.asio [boostorg/asio](https://github.com/boostorg/asio) (预计进入C++标准库) - JSON : [nlohmann-json JSON for Modern C++](https://github.com/nlohmann/json) (github 上搜索JSON出来的第一个) - 日志: [splogs](https://github.com/gabime/spdlog) 一个高性能的C++日志库 (微软公司将它绑定到 Node.JS) - 模板引擎: [inja](https://github.com/pantor/inja) 是模板引擎 [Jinja](https://palletsprojects.com/p/jinja/) 的 C++ 实现版本,和 nlohmann-json 完美配合实现C++内嵌的动态数据结构 - Redis 客户端: 基于[nekipelov/redisclient](https://github.com/nekipelov/redisclient),为以类node.js访问redis进行专门优化(实现单线程异步访问,去锁)。 ,da4qi4默认使用redis缓存session等信息,以优先支持负载均衡下的节点无状态横向扩展。 - TLS/加密: OpenSSL - 静态文件服务: da4qi4自身支持静态文件(包括前端缓存支持)。实际项目部署建议与nginx配合。由nginx提供更高性能、更安全的接入及提从静态文件服务。 数据库访问方式不作绑定,建议使用 Oracle 官方 C++ Connector。 ## 0.3 易用优于性能 使用C++开发,基于异步框架,目的就是为了有一个较好的原生性能起点,开发者不要过于费心性能。框架易用性设计高于性能设计。 暂时仅与 Tomcat 做了一个比较。由于Tomcat似乎是“Per Connection Per Thread”,所以这个对比非常“胜之不武”;但考虑到Tomcat曾经被广泛使用,所以和它对比的数据反倒更容易让读者知道da4qi4框架的性能基准。 **基准测试环境:** - ubuntu 18.04 - 4核心8线程 、8G内存 - 测试工具: Jmeter - 测试工具和服务端运行于同一机器(显然会影响服务端性能,不过本次测试重点是做相对性的对比) - 后台无业务,不访问数据库,仅返回简短字符串(造成吞吐量严重不足) - 不走nginx等Web Server的反向代理 **Tomcat 运行配置** - JVM 1.8G 内存 - 最大线程数:10000 - 最大连接数:20000 - 最大等待队列长度 200 _对 Tomcat不算熟,因此以上配置基本照着网上的相关测试指南设置,有不合理之处,望指正。_ | - | 并发数 | 平均响应(ms) | 响应时间中位数(ms) | 99% 用户响应时间(ms) |最小响应(ms) |最大响应(ms)|错误率|吞吐量(s)|每秒接收字节(KB)| |--- | --- | --- | --- | --- | --- | --- | --- | --- | --- | |tomcat| 1000 | 350 | 337 | 872 | 1 | 879 | 0 | 886.7 | 273 | |da4qi4| 1000 | 1 | 1 | 20 | 0 | 24 | 0 | 1233 | 286.6| 另,官网 www.d2school.com 一度以 1M带度、1核CPU、1G 内存的一台服务器作为运行环境(即:同时还运行MySQL、redis服务);后因线上编译太慢,做了有限的升级。 后续会给出与其他Web Server的更多对比。但总体上,da4qi4 的当前阶段开发,基本不会以极端性能提升作为目标。 # 一、快速了解 ## 1.1 一个空转的Web Server 像所有的C++程序,我们至少需要一个C++文件,假设名字还是熟悉的“main.cpp”,在本例中,它的内容如下: ```C++ #include "daqi/da4qi4.hpp" using namespace da4qi4; int main() { auto svc = Server::Supply(4098); svc->Run(); } ``` 不到10行代码,我们创建了一个空转的,似乎不干活的Web Server。虽然被污蔑为不干活,但其实个Web Server是在正常运行中,它之所以表现成不干活,只是因为它要忠于创建它的主人,也就是我们(程序员):我们没有指示它如何响应,所以它对所有的请求,都只能回应一句:“Not Found”(没错,就是404)。 编译、运行,然后在浏览器地址栏输入:http://127.0.0.1:4098 ,而后回车,浏览器将显示一个页面,上面写着: ``` Not Found ``` > 小提示:代码中的“Supply(4098)”调用,如果不提供4098这个入参,那么Web Server将在HTTP默认的80端口监听。我们使用4098是考虑到在许多程序员的开发电脑上,80端口可能已经被别的应用占用了。 ## 1.2 Hello World! 不管访问什么都回一句“Not Found”这令人沮丧。接下来实现这么一个功能:当访问网站的根路径时,它能答应一声:“Hello World!”。 ### 1.2.1 针对指定URL的响应 这需要我们大代码中指示框架遇上访问网站根路径时,做出必要的响应。响应可以是函数指针、std::function或C++11引入的lambda,我们使用最后者: ```C++ #include "daqi/da4qi4.hpp" using namespace da4qi4; int main() { auto svc = Server::Supply(4098); svc->AddHandler(_GET_, "/", [](Context ctx) { ctx->Res().ReplyOk("Hello World!"); ctx->Pass(); }); svc->Run(); } ``` 例中使用到的Server类的AddHandler()方法,并提供三个入参: 1. 指定的HTTP访问方法: \_GET\_; 2. 指定的访问URL: /,即根路径 ; 3. 匿名的lambda表达式。 三个入参以及方法名,表达一个意思:如果用户以GET方法访问网站的根路径,框架就调用lambda 表达式以做出响应。 编译、运行。现在用浏览器访问 http://127.0.0.1:4098 ,将看到: ``` Hello World! ``` ### 1.2.2 返回HTML 以上代码返回给浏览器纯文本内容,接下来,应该来返回HTML格式的内容。出于演示目的,我们干了一件有“恶臭”的事:直接在代码中写HTML字符串。放心,后面很快会演示正常的做法:使用静态文件,或者基于网页模板文件来定制网页的页面内容;但现在,让我们来修改第11行代码调用ReplyOK()函数的入参,原来是“Hello World!”,现在将它改成一串HTML: ```c++ …… ctx->Res().ReplyOk("

Hello World!

"); …… ``` ## 1.3 处理请求 接下来,我们希望请求和响应的内容都能够有点变化,并且二者的变化存在一定的匹配关系。具体是:在请求的URL中,加一个参数,假设是“name=Tom”,则我们希望后台能返回“Hello Tom!”。 这就需要用到“Request/请求”和“Response/响应”: ```C++ #include "daqi/da4qi4.hpp" using namespace da4qi4; int main() { auto svc = Server::Supply(4098); svc->AddHandler(_GET_, "/", [](Context ctx) { std::string name = ctx->Req("name"); std::string html = "

Hello " + name + "!

"; ctx->Res().ReplyOk(html); ctx->Pass(); }); svc->Run(); } ``` 编译、运行。通过浏览器访问 “http://127.0.0.1:4098/?name=Tom” ,将得到带有HTML格式控制的 “Hello Tom!”。 > 小提示:试试看将“Tom”改为汉字,比如“张三”,通常你的浏览器会在“Hello ”后面显示成一团乱码;这不是大器框架的问题,这是我们手工写的那段html内容不够规范。 ## 1.4 引入“Application” Server代表一个Web 服务端,但同一个Web Server系统很可能可分成多个不同的人群。 比如写一个在线商城,第一类用户,也是主要的用户,当然就是来商城在线购物的买家,第二类用户则是卖家和商城的管理员。这种区别,也可以称作是:一个服务端,多个应用。在大器框架中,应用以Application表达, 就当前而言,还不到演示一个Server上挂接多个Application的复杂案例,那我们为什么要开始介绍Application呢?Application才是负责应后台行为的主要实现者。在前面的例子中,虽然没有在代码中虽然只看到Server,但背后是由Server帮我们创建一个默认的 Application 对象,然后依靠该默认对象以实现演示中的相关功能。 现在我们要做的是:显式创建一个Application对象,并代替Server对象来实前面最后一个例子的功能。 ```C++ #include "daqi/da4qi4.hpp" using namespace da4qi4; int main() { auto svc = Server::Supply(4098); auto web_app = Application::Customize("web", "/", "./log"); web_app->AddHandler(_GET_, "/", [](Context ctx) { std::string name = ctx->Req("name"); std::string html = "

Hello " + name + "!

"; ctx->Res().ReplyOk(html); ctx->Pass(); }); svc->Mount(web_app); svc->Run(); } ``` 发生的变化:使用Aplication类的静态成员函数,定制(Customize)了一个应用,例中命名为web_app;然后改用它来添加前端以GET方法访问网站根URL路径时的处理方法。最后在svc运行之前,需要先将该应用挂接(Mount)上去。 这段代码和前面没有显式引入Application的代码,功能一致,输出效果也一致。但为什么我们一定要引入Application呢?除了前述的,为将来一个Server对应多个Application做准备之外,从设计及运维上讲,还有一个目的:让Server和Application各背责任。Application负责较为高层的逻辑,重点是具体的某类业务,而Server则负责服务器较基础的逻辑,重点是网络方面的功能。下一小节将要讲到日志,正好是二者分工的一个典型体现。 ## 1.5 运行日志 一个Web Server在运行时,当然容易遇到或产生各种问题。这时候后台能够输出、存储运行时的各种日志是大有必要的功能。 结合前面所说的Server与Application的分工。日志在归集上就被分成两大部分:服务日志和应用日志。 - 服务层日志:记录底层网络、相关的周边运行支撑环境(缓存/Redis、数据库/MySQL)等基础设施的运行状态。 - 应用层日志:记录具体应用的运行日志。 其中,相对底层的Server日志全局唯一,由框架自动创建;而应用层日志自然是每个应用对应一套日志。程序可以为服务层和应用层日志创建不同的日志策略。事实上,如果有多个应用,那自然可以为每个应用定制不同的日志策略。 在前面例子中,服务层日志对象已经存在,只是我们没有主动配置它,也没有主动使用它记录日志。而唯一的应用web_app则采用默认的日记策略:即没有日志。没错,应用允许不记录日志。 为了使用服务层日志,我们需要在程序启动后,服务还未创建时,就初始化服务层的日志对象,这样才有机会记录服务创建过程中的日志(比如最常见的,服务端口被占用的问题)。假设初始化日志这一步都失败,记录失败信息的人,肯定不能是日志对象,只能是我们熟悉的std::cerr或std::cout对象。 ```C++ #include "daqi/da4qi4.hpp" using namespace da4qi4; int main() { if (!log::InitServerLogger("/home/zhuangyan/Projects/CPP/daqi_demo/www/logs", log::Level::debug)) { std::cerr << "Create server logger fail." << std::endl; return -1; } auto svc = Server::Supply(4098); log::Server()->info("准备web应用中……"); auto web_app = Application::Customize("web", "/", "./log"); web_app->AddHandler(_GET_, "/", [](Context ctx) { std::string name = ctx->Req("name"); std::string html = "

Hello " + name + "!

"; ctx->Res().ReplyOk(html); ctx->Pass(); }); svc->Mount(web_app); svc->Run(); } ``` > 小提示:为方便演示,上面代码暂时使用绝对路径以指定服务层日志文件的存储位置,实际项目通常以相对路径,或读取外部配置的方式以方便部署。 一旦“InitServerLogger()”调用成功,并且设置低于INFO的日志输出级别(例中为DEBUG级别,见该函数的第2个入参:log::Level::debug),框架中有关服务层的许多日志,就会打印到屏幕(控制台)上及相应的日志文件里。 ## 1.6 HTML 模板 是时候解决在代码中直接写HTML的问题了。 用户最终看到的网页的内容,有一些在系统设计阶段就很清楚,有一些则必须等用户访问时才知道。比如前面的例子中,在设计时就清楚的有:页面字体格式,以及“Hello, _ _ _ _ !”;而需要在运行时用户访问后才能知道的,就是当中的下划线处所要填写的内容。 下面是适用于本例的,一个相当简单的的HTMl网页模板: ```html 首页

你好,{=name=}!

``` “你好,”后面的特定格式{=name=},将会被程序的模板解析引擎识别,并填写上运行时的提供的name的值。使用模板后,现在用于产生的网页内容的完整C++代码如下: ```C++ #include "daqi/da4qi4.hpp" using namespace da4qi4; int main() { //网站根目录 (同样,我们暂使用绝对路径) std::string www_root = "/home/zhuangyan/Projects/CPP/daqi_demo/www/"; //初始化服务层日志 if (!log::InitServerLogger(www_root + "logs", log::Level::debug)) { std::cerr << "Create server logger fail." << std::endl; return -1; } auto svc = Server::Supply(4098); log::Server()->info("准备web应用中……"); //纯粹是为了演示日志…… auto web_app = Application::Customize("web", "/", www_root + "logs/", www_root + "static/", www_root + "view/", www_root + "upload/"); //初始化web应用日志 if (!web_app->Init(Application::ActualLogger::yes, log::Level::debug)) { log::Server()->critical("Init application {} fail.", web_app->GetName()); return -2; } //添加请求处理 web_app->AddHandler(_GET_, "/", [](Context ctx) { std::string name = ctx->Req().GetUrlParameter("name"); ctx->ModelData()["name"] = name; ctx->Render().Pass(); }); svc->Mount(web_app); svc->Run(); } ``` 由于我们所写的模板文件正确地指定了相关编码,所以现在如果访问 http://127.0.0.1:4098?name=大器da4qi4 。将得到带有HTML格式的: ``` 你好,大器da4qi4! ``` 框架提供的模板引擎,不仅能替换数据,也支持基本的条件判断、循环、自定函数等功能,类似一门“脚本”。 > 小提示:大多数情况下,我们写的C++程序用以高性能地、从各种来源(数据库、缓存、文件、网络等)、以各种花样(同步、异步)获取数据、处理数据。而HTML模板引擎在C++程序中以解释的方式运行,因此正常的做法是不要让一个模板引擎干太复杂的,毕竟,在C++这种“彪形大汉”的语言面,它就是个小孩子。 ## 1.7 更多 前面未提及的,但框架本身集成功能还包括: 1. cookie支持 2. 前端(浏览器)缓存支持 3. Redis 缓存支持 4. Session 支持 5. 静态文件 6. 模板文件更新检测 7. HTTP 客户端组件 8. POST响应支持 9. 文件上传、下载 10. 访问限流 11. HTTPS 12. JSON 13. 纯数据输出的API接口,与前端AJAX配合 14. 框架全方式集成:(a) 基于源代码集成、(b) 基于动态库集成、(c) 基于静态库集成 15. …… 而框架外围当前可供集成或实现的功能有: 1. 数据库访问 2. 和nginx配合(实现负载均衡的快速横向扩展) 3. 阿里短信云异步客户端 4. 微信扫一扫登录异步客户端 5. 基于OpenSSL的数据加密工具 6. 常用字符串处理 7. 常用编码转换(UTF-8、UCS、GBK、GB18030) 8. …… # 二、如何构建 ## 2.1 基于生产环境构建 大器 当前支持在Linux下环境编译。 为方便构建,大器的相关构建工具及外部信赖库,与“阿里云”(“腾讯云”、“华为云”、“七牛云”等国内云计算商类似)的Ubuntu 服务器版本保持基本同步。 因此,如果你有一台2019年或更新的云服务器,那么在其上构建大器,则所需的软件、信赖库等,只要Ubuntu仓库中存在,均只需使用“apt”指令从云厂商为该版本的Ubuntu提供的软件仓库拉取即可。当前仅“iconv”库需要手动下载编译。 当前国内各云计算提供商,均提供 Ubuntu Server 版本为 18.04 LTS 版本。以下内容均以 Ubuntu 18.04 为例,考虑日常开发不会直接使用Server版,因此严格讲,以下内容均假设系统环境为 Ubuntu 18.04 桌面版。 > 小提示-服务器与开发机的区别: > > 因此也假设你的开发机使用的是Ubuntu 桌面版 18.04 LTS 版本。以下涉及apt指令时,以开发机(桌面版)为例,因为如有需要,均带着“sudo ”前缀。当在服务器(Ubuntu Server)上编译,并且你使用的是默认的root用户,只需去掉 “sudo”前缀即可。例如: > > 开发机: sudo apt install git > > 服务器: apt install git ## 2.2 准备编译工具 1. 如果未安装或不知道有没有安装(以下简称为“准备”) GCC 编译器: ```shell sudo apt install gcc g++ ``` 2. 准备 CMake构建套件,请: ```shell sudo apt install cmake ``` ## 2.3 准备第三库 1. 准备 boost 开发库: ```shell sudo apt install libboost-dev libboost-filesystem libboost-system ``` > 小提示-安装全部boost库: > > boost中需要编译的库,大器只用到上述的“filesytem”和“system”。如果你想一次性安装所有boost库,可以使用:sudo apt install libboost-dev libboost-all-dev 2. 准备openssl及其开发库: ```shell sudo apt install openssl libssl-dev ``` 3. 准备 libiconv 库 我们使用汉字,而汉字有多种编码方案,因此,汉字的编码转换,是开发包括Web应用在内各种软件系统的常见需求。大器框架通过集成iconv库用以实现支持多国语言多种编码的转换功能。 先下载:https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.16.tar.gz 在Ubuntu图形界面中,双击该.gz文件,再点击其内.tar文件,解压后得到 “libiconv-1.16”文件夹。在终端进入该文件夹,依次输入以下三行,完成安装: ```shell ./configure --prefix=/usr/local make sudo make install ``` 4. 最后,确保生效所安装的动态库在系统中能被找到: ```shell sudo ldconfig ``` ## 2.4 下载大器源代码 通常你应该安装 git 工具,如果没有或不确定,请打开终端(Ctrl + Alt + T),按如下指令安装。 ```shell sudo apt install git ``` 然后在本地新建一文件夹,命名为 daqi,打开终端进入该目录,执行如下指令,以从github上克隆大器项目(此站服务器在国外,较慢,为获得更快速速度,请往下看小提示): ```shell git clone https://github.com/d2school/da4qi4.git ``` > 小提示-从国内服务器下载: > > 做为代替,也可以使用位于国内的的GITEE (开源中国)仓库(速度快很多)。仓库名是“da4qi4_public”: > > git clone https://gitee.com/zhuangyan-stone/da4qi4_public.git 如果你就是不喜欢使用git,请进入 https://github.com/d2school/da4qi4 或 https://gitee.com/zhuangyan-stone/da4qi4_public,点击“Clone or download”按钮,然后选择“download / 下载”,得到 zip 压缩文件后,再于本地解压至前述的“daqi4”目录下。 无论从哪个仓库中克隆,还是手工下载解压,最终,你**将在前述的“daqi”目录下,得到一个子目录“da4qi4”**。大器项目的代码位于后者内,其内你应该能看到“src”、“include”等多个子目录。 > 如有余力,建议在以上两个网站均为本开源项目打个星 ## 2.5 编译“大器”库 **重要:以下假设大器源代码位于“daqi/da4qi4”目录下。** 1. 准备构建目录 请在“daqi”之下(和“da4qi4”平级)的位置,新建一目录,名为“build”: ```shell mkdir build ``` 进入该当目录: ```shell cd build ``` 2. 执行CMake ``` shell cmake -D_DAQI_TARGET_TYPE_=SHARED_LIB -DCMAKE_BUILD_TYPE=Release ../da4qi4/ ``` 将生成目标为“发行版(Release)”的大器“动态库(SHARED_LIB)”。 * 如果希望生成调试版本,请将“Release”替换为“Debug”; * 如果希望生成静态库版本,请将“SHARED_LIB”替换为“STATIC_LIB”; * 更多编译目标设置,请到本项目官网“www.d2school.com” 一切正常的看,将看到终端上输出“Generating done”等字样。其中更多内容中,包含有boost库的版本号、库所在路径,以及一行“\~BUILD DAQI AS SHARED LIB\~”字样以指示大器的编译形式(SHARED LIB)。 3. 开始编译 ```shell make ``` > 小提示:并行编译 > > 如果你的电脑拥有多核CPU,并且内存足够大(至少8G),可以按如下方式并行编译(其中 -j 后面的数字,指明并行编译的核数,以下以四核数例): > > make -j4 完成make之后,以上过程将在build目录内,得到“libda4qi4.so”; 如果是调试版,将得到 “libda4qi4_d.so”。如果是静态库,则扩展为“.a”。 ## 2.6 在你的项目中使用 da4qi4库 现在,你可以使用你熟悉IDE(Code::Blocks、Qt Creator、CodeLite等)中,构建你的项目,然后以类型使用其它开发库的方式,添加大器的库文件(就是前一步构建所得的.so或.a文件),及大器的头文件。 * 大器库文件目录:即前面的“.../daqi/build”。(建议另外建一目录,将.so或.a文件复制过去。); * 大器头文件目录:即前面的“.../daqi/da4qi4/include”。 现在,你可以从之前“1.1 一个空转的Web Server”重新看起。 # 三、运行时外部配套系统 ## 3.1 运行时依赖说明 一个Web系统常用到缓存系统和数据库系统。大器框架对二者依赖情况如下: * 完全不依赖数据库; * 简单例子不依赖缓存系统,但一旦需要用到Web 系统常见的“会话/SESSION”功能,则需要依赖redis缓存库。 ## 3.2 Redis的安装 显然,这已经不是本开源项目的自己的说明内容。不过,反正在Ubuntu Linux下安装Redis就一行话: ```C++ sudo apt install redis-server ``` 这不仅会安装redis服务,而且会顺便在本机redis的命令行客户端,可以如下运行: ``` shell redis-cli ``` * 有关如何在你写的大器Web Server中实现SESSION,请参看本项目官网www.d2school.com 相关(免费视频)课程; * 有关Redis的学习,请关注www.d2school.com 课程。 ## 3.3 数据库 * 可以使用 mysql 官方的 MySQL C++ Connector; * 新人强烈推荐: 相对传统的C++封装 : [MySQL++](https://tangentsoft.com/mysqlpp/home) (注:欢迎关注《白话 C++》下册,有详细的 MySQL 数据库及 MySQL++使用的章节; * 新人推荐: [CppDB](http://cppcms.com/sql/cppdb/) * 到 [github](https://github.com)上,搜索 “MySQL C++”,你将找到大量国内或国外的MySQL C++连接库; * 有经验的C++程序员推荐:[sqlpp11](https://github.com/rbock/sqlpp11)