在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
开源软件名称:da4qi4_public开源软件地址:https://gitee.com/zhuangyan-stone/da4qi4_public开源软件介绍:零、几个原则0.1 自己的狗粮自己吃官网 第2学堂 www.d2school.com 后台使用 da4qi4作为Web Server开发。(nginx + da4qi4 + redis + mysql)。给一个在手机上运行的网站效果: 样式丑,但这只和差劲的UI师,也就是我的美感有关,和后台使用什么Web框架没有关系。 0.2 站在巨人的肩膀上da4qi4 Web 框架优先使用成熟的、C/C++开源项目的搭建。它的关键组成:
注:
0.3 易用优于性能使用C++开发,基于异步框架,目的就是为了有一个较好的原生性能起点,开发者不要过于费心性能。当然,性能也不能差,因为性能差必将影响产品的易用性)。暂时仅与 Tomcat 做了一个比较。由于Tomcat似乎是“Per Connection Per Thread/每连接每线程”,所以这个对比会有些胜之不武;但考虑到Tomcat曾广泛应用于实际系统,所以和它的对比数据有利于表明da4qi4在性能上的可用性。 基准测试环境:
Tomcat 运行配置
另,官网 www.d2school.com 一度以 1M带度、1核CPU、1G 内存的一台服务器作为运行环境(即:同时还运行MySQL、redis服务);后因线上编译太慢,做了有限的升级。 后续会给出与其他Web Server的更多对比。但总体上,da4qi4 的当前阶段开发,基本不会以极端性能提升作为目标。 0.4 简单胜过炫技众所周知C++语言很难,非常适于C++程序员“炫技”;所以有一票C++开源项目虽然技术上很优秀,但却很容易吓跑普通的C++程序员。比如,超爱用“模板元”……da4qi4 的代码强烈克制了这种“炫技”冲动,尽量代码看上去毫无技巧,特别是对外接口,遵循KISS原则,不会让你产生任何“惊奇”(头回看到程序员把无技可炫写得这么清新脱俗?)。 不管怎样,在C++所大范围支持的“面向过程”、“基于对象”、“面向对象”和“泛型”等编程模式中,你只需熟悉“面向过程”,并且会一点“基于对象”,就可以放心地用这个库。 0.5 紧跟国内生产环境用哪个版本的C++?用哪个版本的boost库?用哪个版本的OpenSSL?用哪个版本的CMake? 就一个标准:当前国内主要云计算提供商,已经提供哪些现成的版本,我们就用那个版本——这意味着你几乎只需编译好你写的代码可以完成在线构建、部署了。不用编译boost、不用编译OpenSSL、不用下载编译新版的CMake…… 阿里云、腾讯云、百度云、华为云、七牛云……无论哪家,只要你在上面申请一台Ubuntu 18.04 (或更高版本)的服务器,简单向行指令就能在线编译、部署好,让它成为一台跑着“大器 INSIDE”的WEB 服务器,为你的用户提供网站服务。对服务器配置的最低要求是:4G内存、1M带宽、1核CPU。 一、快速了解1.1 一个空转的Web Server我们需要一个C++文件,假设名为“main.cpp”,内容如下: #include "daqi/da4qi4.hpp"using namespace da4qi4;int main(){ auto svc = Server::Supply(4098); svc->Run();} 不到10行代码,我们创建了一个空转的,似乎不干活的Web Server。 编译、运行,然后在浏览器地址栏输入:http://127.0.0.1:4098 ,而后回车,浏览器将显示一个页面,上面写着: Not Found 虽然说它“不干活”,但这个Web Server的运行完全合乎逻辑:我们没有为它配备任何资源或响应操作、,所以对它的任何访问,都返回404页面:“Not Found”。 1.2 Hello World!接下来实现这么一个功能:当访问网站的根路径时,它能响应:“Hello World!”。 1.2.1 针对指定URL的响应这需要我们大代码中指示框架遇上访问网站根路径时,做出必要的响应。响应可以是函数指针、std::function、类方法或C++11引入的lambda,我们先来使用最后者: #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()方法,并提供三个入参:
三个入参以及方法名合起来表达:如果用户以GET方法访问网站的根路径,框架就调用lambda表达式以做出响应。 编译、运行。现在用浏览器访问 http://127.0.0.1:4098 ,将看到: Hello World! 作为对比,下面给出同样功能使用自由函数的实现: #include "daqi/da4qi4.hpp"using namespace da4qi4;void hello(Context ctx){ ctx->Res().ReplyOk("Hello World!"); ctx->Pass();}int main(){ auto svc = Server::Supply(4098); svc->AddHandler(_GET_, "/", hello); svc->Run();} 为节省代码篇幅,后续演示均使用lambda表达式来表达HTTP的响应操作。实际系统显然不可能将代码全塞在main()函数中,因此平实的自由函数会用得更多。不仅lambda不是必需,实际是连“class/类”都很少使用——这符号Web Server基本的要求:尽量不要带状态;自由函数相比类的成员函数(或称方法),更“天然的”不带状态。 1.2.2 返回HTML以上代码返回给浏览器纯文本内容,接下来,应该来返回HTML格式的内容。出于演示目的,我们干了一件有“恶臭”的事:直接在代码中写HTML字符串。后面很快会演示正常的做法:使用静态文件,或者基于网页模板文件来定制网页的页面内容;但现在,让我们来修改第11行代码调用ReplyOK()函数的入参,原来是“Hello World!”,现在将它改成一串HTML: …… ctx->Res().ReplyOk("<html><body><h1>Hello World!</h1></body></html>");…… 1.3 处理请求接下来,我们希望请求和响应的内容都能够有点变化,并且二者的变化存在一定的匹配关系。具体是:在请求的URL中,加一个参数,假设是“name=Tom”,则我们希望后台能返回“Hello Tom!”。 这就需要用到“Request/请求”和“Response/响应”: #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 = "<html><body><h1>Hello " + name + "!</h1></body></html>"; ctx->Res().ReplyOk(html); ctx->Pass(); }); svc->Run();}
编译、运行。通过浏览器访问 “http://127.0.0.1:4098/?name=Tom” ,将得到带有HTML格式控制的 “Hello Tom!”。 1.4 引入Application编译、运行。通过浏览器访问 “http://127.0.0.1:4098/?name=Tom” ,将得到带有HTML格式控制的 “Hello Tom!”。 Server代表一个Web 服务端,但同一个Web Server系统很可能可分成多个不同的人群。
就当前而言,还不到演示一个Server上挂接多个Application的复杂案例,那我们为什么要开始介绍Application呢?Application才是负责应后台行为的主要实现者。在前面的例子中,虽然没有在代码中虽然只看到Server,但背后是由Server帮我们创建一个默认的 Application 对象,然后依靠该默认对象以实现演示中的相关功能。 下面我们就通过“Server | 服务”对象,取出这个“Application | 应用”,并代替前者实现前面最后一个例子的功能。 #include "daqi/da4qi4.hpp"using namespace da4qi4;int main(){ auto svc = Server::Supply(4098); auto app = svc->DefaultApp(); //取出自动生成的默认应用对象 app->AddHandler(_GET_, "/", [](Context ctx) { std::string name = ctx->Req("name"); std::string html = "<html><body><h1>Hello " + name + "!</h1></body></html>"; ctx->Res().ReplyOk(html); ctx->Pass(); }); svc->Run();} 除了“AddHandler()”的实施对象以前是svc,现在是“app”以外,基本没有什么变化。代码和前面没有显式引入Application之前功能一致。但为什么我们一定要引入Application呢?除了前述的,为将来一个Server对应多个Application做准备之外,从设计及运维上讲,还有一个目的:让Server和Application各背责任。 Application负责较为高层的逻辑,重点是具体的某类业务,而Server则负责服务器较基础的逻辑,重点是网络方面的功能 。下一小节将要讲到日志,正好是二者分工的一个典型体现。 1.5 运行日志一个Web Server在运行时,当然容易遇到或产生各种问题。这时候后台能够输出、存储运行时的各种日志是大有必要的功能。并且,最最重要的是,如果你写一个服务端程序,运行大半年没有什么屏幕输出,看起来实在是“不够专业”,很有可能会影响你的工资收入…… 结合前面所说的Server与Application的分工。日志在归集上就被分成两大部分:服务日志和应用日志。
其中,相对底层的Server日志由框架自动创建;而应用层日志自然是每个应用对应一套日志。程序可以为服务层和应用层日志创建不同的日志策略。事实上,如果有多个应用,那自然可以为每个应用定制不同的日志策略。如果不主动为某个应用创建日志记录器,则该应用只管全速运行,不输出任何日志——听起很酷,但你不应该对自己写的代码这么有信心。 #include "daqi/da4qi4.hpp"using namespace da4qi4;int main(){ //初始化服务日志,需指定日志文件要存在哪里?以及日志记录的最低级别 log::InitServerLogger("你希望/服务日志文件/要存储的/目录/" , log::Level::debug)); auto svc = Server::Supply(4098); log::Server()->info("服务已成功加载."); //强行输出一条服务日志 auto app = svc->DefaultApp(); //再来初始化应用日志 app->InitLogger("你希望/当前应用的/要存储的/目录/"); app->AddHandler(_GET_, "/", [](Context ctx) { std::string name = ctx->Req("name"); std::string html = "<html><body><h1>Hello " + name + "!</h1></body></html>"; ctx->Res().ReplyOk(html); ctx->Pass(); }); svc->Run(); log::Server()->info("再见!"); //强行再输出一条服务日志} 所有日志功能都在“log::”名字空间之下。以上日志配置不仅会将信息输出到终端(控制台),也会自动输出指定目录下的文件中,服务日志和各应用日志是独立的文件。文件带有默认的最大尺寸和最大个数限制。实际在linux服务器上运行时,程序通常在后台运行并将本次运行的屏幕输出重定向到某个文件。 日志的输出控制,支持常见的:跟踪(trace)、调试(debug)、信息(info)、警告(warn)、错误(err)、致命错误(critical)等级别。例中对“app->InitLogger()” 使用默认级别:info。 下面是运行日志截图示例。 看起来有点像个后台程序,可以申请领导过来视察你的工作成果了。 1.6 HTML模板是时候解决在代码中直接写HTML的问题了。 用户最终看到的网页的内容,有一些在系统设计阶段就很清楚,有一些则必须等用户访问时才知道。比如前面的例子中,在设计时就清楚的有:页面字体格式,以及“Hello, _ _ _ _ !”;而需要在运行时用户访问后才能知道的,就是当中的下划线处所要填写的内容。 下面是适用于本例的,一个相当简单的的HTMl网页模板: <!DOCTYPE html><html lang="zh"><head> <title>首页</title> <meta content="text/html; charset=UTF-8"></head><body> <h1>你好,{=_URL_PARAMETER_("name")=} !</h1> <p>您正在使用的浏览器: {=_HEADER_("User-Agent")=}</p> <p>您正在通过该网址访问本站:{=_HEADER_("Host")=}</p></body></html> “你好,”后面的特定格式{=URL_PARAMETER("name")=} ,将会被程序的模板解析引擎识别,并填写上运行时的提供的name的值。 解释:
假设这个文件被存放在 “你的/网页模板/目录”。下面代码中的 “app->SetTemplateRoot()”将用到这个目录的路径。 #include "daqi/da4qi4.hpp"using namespace da4qi4;int main(){ log::InitServerLogger("你希望/服务日志文件/要存储的/目录/", log::Level::debug)); auto svc = Server::Supply(4098); log::Server()->info("服务已成功加载."); auto app = svc->DefaultApp(); //新增的两行: app->SetTemplateRoot("你的/网页模板/目录/"); //模板文件根目录 app->InitTemplates(); //加载并将模板文件“编译成” 字节码 app->InitLogger("你希望/当前应用的/要存储的/目录/"); //下面这行让sever定时检测模板文件的变动(包括新增) svc->EnableDetectTemplates(5); //5秒,实际项目请设置较大间隔,如10分钟 svc->Run(); log::Server()->info("再见!");} 现在,使用火狐浏览器访问URL并带上“name”参数:http://127.0.0.1:4098?name=大器da4qi4 ,将得到以下HTML内容: <h1>你好,大器da4qi4 !</h1><p>您正在使用的浏览器:Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:71.0) Gecko/20100101 Firefox/71.0 </p><p>您正在通过该网址访问本站:127.0.0.1:4098</p> 小提示:“为什么代码更短了?” 你应该注意到,基于模板响应后,代码原有“AddHandler()” 都不见了。因为这个例子没有实质业务逻辑:用户访问一个URL地址,并且带参数,服务依据事先定义的模板样式,将这个参数原样展现出来。实际业务系统当然不可能这么简单(否则要我们后端程序员干什么?),但是,当我们在快速搭建一个系统时,在初始开发过程中,这种情况非常常见,不需要修改源代码,不需要重启服务程序,就能直接看到新增或修改的网页内容,带给我们很大的方便。 框架提供的模板引擎,不仅能替换数据,也支持基本的条件判断、循环、自定函数等功能,类似一门“脚本”。
接下来,我们应该有一个带业务逻辑的例子。这个业务逻辑非常的复杂,并且严重依赖于CPU的计算速度……我们要做一个加法器。用户在浏览器地址栏输入: http://127.0.0.1:4098/add?a=1&b=2 浏览器将显示a+b的结果。显然,业务逻辑就是计算两个整数相加,我们的强大的,计算力过剩的C++语言终于可以派上用场。 首先,准备一个用于显示加法结果的页面模板,文件名为 “add.daqi.HTML”: <!DOCTYPE html><html lang="zh"><head> <title>加法</title> <meta content="text/html; charset=UTF-8"></head><body> <p> {=c=} </p></body></html> 重点在 “{=c=}”身上。 {==} 仍然用来标识一个可变内容,但其内不再是一个内置函数,而是一个普通的变量名称:c。为此我们在C++代码中要做的事变成两件:一是计算 a 加 b的和,二是将和以 c 为名字,填入模板对应位置。 然后需要一个add的自由函数: void add(Context ctx){ //第一步:取得用户输入的参数 a 和 b: std::string a = ctx->Req().GetUrlParameter("a"); std::string b = ctx->Req().GetUrlParameter("b"); //第二步:把字符串转换为整数: int na = std::stoi(a); //stoi 是 C++11新标中的字符串转换整数的函数 int nb = std::stoi(b); //第三步:核心核心核心业务逻辑:加法计算 int c = na + nb; //第四步:把结果按模板指定的名字"c",设置到“Model”数据中: ctx->ModelData()["c"] = c; //最后一步:渲染,并把最终页面数据传回浏览器: (即:输出结果 = 模板 + 数据) ctx->Render().Pass(); //Render 是动词:渲染} 暂时为了简化,我们不写日志、不作错误处理,现在,除了add函数的内部实现外,完整的main.cpp文件内容是: #include "daqi/da4qi4.hpp"using namespace da4qi4;void add(Context ctx){ /* 实现见上 */}int main(){ auto svc = Server::Supply("127.0.0.1", 4098); auto app = svc->DefaultApp(); app->SetTemplateRoot("你的/网页模板/目录/"); app->InitTemplates(); //AddHandler 又回来了: app->AddHandler(_GET_, "/add", add); svc->EnableDetectTemplates(5); svc->Run();} 如前所述通过浏览器访问 .../add?a=1&b=2 ,将看一个简单的3。 甲方说这也太不人性化了,好歹显示一个 “1 + 2 = 3” 啊! 太好了,我们正好借此演示如何不修改代码,不重启服务程序就达成目标。 需要修改的是模板文件:“add.daqi.HTML”: <!DOCTYPE html><html lang="zh"><head> <title>加法</title> <meta content="text/html; charset=UTF-8"></head><body> <p> <!-- 展示内容类似:1 + 2 = 3 --> {=_URL_PARAMETER_("a")=} + {=_URL_PARAMETER_("b")=} = {=c=} </p></body></html> 修改、保存,5秒过后再访问,就看到新成果了。 会有人担心C++写的程序容易出错,并且一出错就直接挂掉——上面程序,如果用户无意有意或干脆就是恶意搞破坏,输入 “.../add?a=A&b=BBBB”……会怎样呢? add 函数中的 “std::stoi()” 调用可能抛出异常?不管怎样,请放心,程序并不会挂掉,它会继续运行,只是:
很简单,对add的业务逻辑加上异常处理,出现异常时,向客户回复一句相对友好点的内容,并且留下应用日志即可。以下是关注异常后的add函数: #include "daqi/da4qi4.hpp"using namespace da4qi4;void add(Context ctx){ try { std::string a = ctx->Req().GetUrlParameter("a"); std::string b = ctx->Req().GetUrlParameter("b"); int na = std::stoi(a); int nb = std::stoi(b); int c = na + nb; ctx->ModelData()["c"] = c; } catch(std::exception const& e) { ctx->Logger()->warn("hand add exception. {}. {}. {}.", a, b, e.what()); ctx->ModelData()["c"] = std::string("同学,不要乱输入加数嘛!") + e.what(); } ctx->Render().Pass();} 关键在异常处理。第一行是: ctx->Logger()->warn("hand add exception. {}. {}. {}.", a, b, e.what()); 三个重点:
第二行是: ctx->ModelData()["c"] = std::string("同学,不要乱输入加数嘛!") + e.what(); 重点在于:赋值操作的右值,是一个字符串。“ModelData”,即“模型数据”在这里指的是即将写往页面模板的数据,和C++的强类型相比,页面上的数据不用太区分类型。所以,“c” 本是a+b之和,按理说是整数类型,但我们却可以往里写入一行字符串,这样,当用户捣乱造成 a + b 无法执行时,他就会看到一行出错信息。
1.7 WebSocket1.7.1 HTTP 对比 WebSocket先简单说下在业务与技术上,传统HTTP访问和WebSocket访问的核心区别。 HTTP访问讲究“无状态”,当然,一个业务系统怎么可能无状态,只不过是将状态都放在数据中(缓存、数据库),所谓的无状态是指业务逻辑相关的“类/class”应该无状态——这正好和“class/类”或“object/对象”本质是一个“状态机”相冲突——幸好C++支持多范式开发,所以在前面的例子中,我们几乎不设计“class”,而是使用天生无状态的自由函数。“类与对象”想写成不带状态的状态,难;而自由函数想写出带“状态”来,还真不简单。 到了WebSocket,长连接,通常这 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论