Skip to content
master
Switch branches/tags
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
 
 
 
 
src
 
 
 
 
 
 
 
 

零、几个原则

0.1 自己的狗粮自己吃

官网 第2学堂 www.d2school.com 后台使用 da4qi4作为Web Server开发。(nginx + da4qi4 + redis + mysql)。 给一个在手机上运行的网站效果:

第2学堂手机版

样式丑,但这只和差劲的UI师,也就是我的美感有关,和后台使用什么Web框架没有关系。

0.2 站在巨人的肩膀上

da4qi4 Web 框架优先使用成熟的、C/C++开源项目的搭建。它的关键组成:

  • HTTP 基础协议解析:Node.JS/llhttp。 一直使用Node.JS底层的HTTP解析器,Node.JS v12 之前是nodejs/http-parser;之后升级迁移到 llhttp 。Node.JS 官方说法解析性能提升156%;
  • HTTP multi-part : multipart-parsr multipart-parser-c
  • 网络异步框架: C++ boost.asio boostorg/asio (可能进入C++2x标准库)
  • JSON : nlohmann-json JSON for Modern C++ (github上搜索JSON结果中第一个);
  • 日志: splogs 高性能的C++日志库 (微软公司选择将它绑定到 Node.JS 作日志库);
  • 模板引擎: inja 模板引擎 Jinja 的 C++ 实现版本,名气不大,但能和nlohmann-json完美配合实现C++内嵌的动态数据结构,加上我为它解决过bug,比较熟悉、放心。
  • TLS/SSL/数据加密: OpenSSL (TLS);
  • Redis 客户端: 基于nekipelov/redisclient,为以类node.js访问redis进行专门优化(实现单线程异步访问,去锁)。 da4qi4默认使用redis缓存session等信息(以优先支持负载均衡下的节点无状态横向扩展)。
  • 静态文件服务: da4qi4自身支持静态文件(包括前端缓存逻辑)。实际项目部署建议与nginx配合。由nginx提供更高性能、更安全的接入及提从静态文件服务。

注:

  1. 框架未绑定数据库访问方式。用户可使用 Oracle 官方 C++ Connector,或MySQL++,或 三、运行时外部配套系统提及的各类数据库连接器;
  2. 框架自身使用 redis 作为默认的(可跨进程的)SESSION支持。上层应用可选用框架的redis接口,也可以使用自己喜欢、 顺手 的redis C++客户端。

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 的当前阶段开发,基本不会以极端性能提升作为目标。

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()方法,并提供三个入参:

  1. 指定的HTTP访问方法: _GET_;

  2. 指定的访问URL: /,即根路径 ;

  3. 匿名的lambda表达式。

三个入参以及方法名合起来表达:如果用户以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();
}

重要: 这里为方便演示而使用 lambda 表达式,但实际系统不可能把所有代码都放在main()函数中写。所以肯定是一个个函数。用编程语言中最最基础的函数并不丢人,因为,我们要的是实用,而不是非在代码秀一下“我会lambda哦!”。(参看:0.4 简单胜过炫技

编译、运行。通过浏览器访问 “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系统很可能可分成多个不同的人群。

举例:比如写一个在线商城,第一类用户,也是主要的用户,当然就是来商城在线购物的买家,第二类用户则是卖家和商城的管理员。这种区别,也可以称作是:一个服务端,多个应用。在大器框架中,应用以Application表达。

就当前而言,还不到演示一个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的分工。日志在归集上就被分成两大部分:服务日志和应用日志。

  • 服务层日志:全局唯一,记录底层网络、相关的周边运行支撑环境(缓存/Redis、数据库/MySQL)等基础设施的运行状态。

  • 应用层日志:每个应用都对应一个日志记录器,记录该应用的运行日志。

其中,相对底层的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的值。

解释:

  • _URL_PARAMETER_() 是网页模板脚本内置提供的一个函数,它将自动得取浏览器地址栏输入URL后,所带的参数。
  • _HEADER_() 同样是网页模板脚本内置的一个函数,用以获得当前HTTP请求的报头数据项。在本例中无实际业务作用,常用于辅助页面调试。

假设这个文件被存放在 “你的/网页模板/目录”。下面代码中的 “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地址,并且带参数,服务依据事先定义的模板样式,将这个参数原样展现出来。实际业务系统当然不可能这么简单(否则要我们后端程序员干什么?),但是,当我们在快速搭建一个系统时,在初始开发过程中,这种情况非常常见,不需要修改源代码,不需要重启服务程序,就能直接看到新增或修改的网页内容,带给我们很大的方便。

框架提供的模板引擎,不仅能替换数据,也支持基本的条件判断、循环、自定函数等功能,类似一门“脚本”。

重要:多数情况下我们写C++程序用以高性能地、从各种来源(数据库、缓存、文件、网络等)、以各种花样(同步、异步)获取数据、处理数据。而HTML模板引擎在C++程序中以解释的方式运行,因此正常的做法是不要让一个模板引擎干太复杂的,毕竟,在C++这种 “彪形大汉”的语言面,页面模板引擎“语言”无论在功能还是性能上,都只能算是一个小孩子。

接下来,我们应该有一个带业务逻辑的例子。这个业务逻辑非常的复杂,并且严重依赖于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()” 调用可能抛出异常?不管怎样,请放心,程序并不会挂掉,它会继续运行,只是:

  • 一来、用户只会看到一个典型的HTTP 500 错误 “Internal Server Error ” (即:服务内部错误),这对用户来说,不太友好。

  • 二来,后台什么日志记录也没有,对系统的维护人员来说,也不友好。

很简单,对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());

三个重点:

  • 一是如何通过上下文(Context)得到当前应用的日志记录器:ctx->Logger()。它实际上是 ctx->App().GetLogger() 的简写。

  • 二是得益于“spdlog”的语法,记日志就这么简单:要显示三个信息:a、b和异常,就在前面的格式字符串中,写上三个 {} ,最终就可以在日志中看到一行完整的内容。

  • 三是我们使用“warn()”,而不是“error()”,这体现了服务程序在此刻的淡定内心:不就是用户输入错误嘛?有什么因结什么果。用户输入错误,就返回给他一行出错信息。何事慌张?警告而已。

第二行是:

ctx->ModelData()["c"] = std::string("同学,不要乱输入加数嘛!") + e.what();

重点在于:赋值操作的右值,是一个字符串。“ModelData”,即“模型数据”在这里指的是即将写往页面模板的数据,和C++的强类型相比,页面上的数据不用太区分类型。所以,“c” 本是a+b之和,按理说是整数类型,但我们却可以往里写入一行字符串,这样,当用户捣乱造成 a + b 无法执行时,他就会看到一行出错信息。

main() 函数中如何初始化日志,已经演示过,不再给出代码。

1.7 WebSocket

1.7.1 HTTP 对比 WebSocket

先简单说下在业务与技术上,传统HTTP访问和WebSocket访问的核心区别。

HTTP访问讲究“无状态”,当然,一个业务系统怎么可能无状态,只不过是将状态都放在数据中(缓存、数据库),所谓的无状态是指业务逻辑相关的“类/class”应该无状态——这正好和“class/类”或“object/对象”本质是一个“状态机”相冲突——幸好C++支持多范式开发,所以在前面的例子中,我们几乎不设计“class”,而是使用天生无状态的自由函数。“类与对象”想写成不带状态的状态,难;而自由函数想写出带“状态”来,还真不简单。

到了WebSocket,长连接,通常这时候就有状态——甚至此时底层的网络连接保持或断开本身就是一种状态。比如使用WebSocket写一个页面聊天室,有人连线,就是上线了(进聊天室);有人断线,那就是下线了(出聊天室)。再比如,假设我们的“聊天室”要限制“潜水”用户,就至少得记录每个用户这些状态:上线多久一直没有说话?反过来,如果要限制话痨用户,也至少需要记录一个用户说话记录的记数——这些都是状态。

结论:HTTP访问后端讲究无状态,所以很适合使用“面向过程”的自由函数,而WebSocket 的后端往往需要保持状态,所以这时候“面向对象”比较合适。da4qi4的WebSocket 在保持对无状态的支持下,增加并且主要使用“有状态”的类设计做支持。

1.7.2 大器WebSocket后台实现特性

  • 支持直接接入WebSokcet支持,也支持从nginx继续反向代理。

  • 支持一个端口同时响应HTTP和WebSocket请求。

  • 支持ws和wss。

  • 支持服务端推送(其实是WebSocket的要求)。

  • 支持大报文分段传输(其实也是WebSocket的要求)。

  • 支持群发。

  • 保持和HTTP相对一致的概念与设计,比如上下文:Context。

  • WebSocket连接时,可以方便获得连接升级(upgrade)前的Cookie、HTTP报头、URL等信息。

1.7.3 使用示例

一、先演示面向对象思路的写法。

  1. 先写一个类,派生自 da4qi4::Websocket::EventsHandler。
using namespace da4qi4;

class MyEventsHandler : public Websocket::EventsHandler
{
public:
    bool Open(Websocket::Context ctx) { return true; }   //允许该ws连接
    
    void OnText(Websocket::Context ctx, std::string&& data, bool isfinish)
    {
        ctx->Logger()->info("收到: {}.", data);
        ctx->SendText("已阅!"); 
    }
    
    void OnBinary(Websocket::Context ctx, std::string&& data, bool isfinish)
    {
        //此时data是二进制数据,比如图片什么的,可以保存下来...
    }
    
    void OnError(Websocket::Context ctx
                , Websocket::EventOn evt //在哪个环节出错,读或写?
                , int code //出错编号
                , std::string const& msg //出错信息
                )    
    {
        ctx->Logger()->error("出错了. {} - {}.", code, msg);
    }
    
    void OnClose(Websocket::Context ctx, Websocket::EventOn evt) 
    {
        ctx->Logger()->info("Websocket连接已经关闭.");
    }   
};
  1. 在主函数中,在某个“Application”上注册一个WebSocket的后台处理方法。这个方法用来创建(new)出刚刚定义的那个“MyEventsHandler”的对象,我们使用lambda实现:
#include "daqi/da4qi4.hpp"
#include "daqi/websocket/websocket.hpp" //引入websocket相关定义

using namespace da4qi4;

class MyEventsHandler : public Websocket::EventsHandler
{
     //见上
};

int main()
{
    auto svc = Server::Supply(4098);
    auto app = svc->DefaultApp();
    app->InitLogger("log/");
 
    //在某个app的指定URL下,挂接一个websocket响应处理
    app->RegistWebSocket("/ws", UrlFlag::url_full_path, 
           [](){ return new MyEventsHandler; }
    );

    svc->Run();
}

现在,让你的前端开发人员,在HTML页面里,用JS写一段代码,类似于:

var ws = new WebSocket("ws://127.0.0.1:4098/ws");

ws.onopen = function(evt) {
    this.send("Hello WebSocket.");
}

ws.onmessage = function (evt) {
    console.log(evt.data);
}
……

前后端就可以聊起来了。

二、如果后台业务逻辑确实很简单,那写一个类,还派生什么的确实显得很笨。此时也可以使用简单的函数、labmbda来快速响应。

方法是定义一个大器预定的 “ Websocket::EventHandleFunctor” 变量:

#include "daqi/da4qi4.hpp"
#include "daqi/websocket/websocket.hpp" //引入websocket相关定义

using namespace da4qi4;

/*不需要类定义了*/

int main()
{
    auto svc = Server::Supply(4098);
    auto app = svc->DefaultApp();
    app->InitLogger("log/");

    Websocket::EventHandleFunctor functor;
    functor.DoOnText = [] (Websocket::Context ctx, std::string&& data, bool isfinished)
    {
        ctx->Logger()->info("收到: {}.", data);
        ctx->SendText("已阅!");
    }

    app->RegistWebSocket("/ws", UrlFlag::url_full_path, functor);

    svc->Run();
}

1.8 更多

1.8.1 框架更多集成功能

  1. cookie支持

  2. 前端(浏览器)缓存支持

  3. Redis 缓存支持

  4. Session 支持

  5. 静态文件

  6. 模板文件更新检测及热加载

  7. HTTP/HTTPS 客户端组件(已基于此实现微信扫码登录、阿里云短信的C++SDK,见下)

  8. POST响应支持

  9. 文件上传、下载

  10. 访问限流

  11. JSON

  12. 纯数据输出的API接口,与前端AJAX配合

  13. 框架全方式集成:(a) 基于源代码集成、(b) 基于动态库集成、(c) 基于静态库集成

  14. 常用编码转换(UTF-8、UCS、GBK、GB18030)

  15. ……

1.8.2 框架外围可供集成的工具

  1. 数据库访问

  2. 和nginx配合(实现负载均衡的快速横向扩展)

  3. 阿里短信云异步客户端

  4. 微信扫一扫登录异步客户端

  5. 基于OpenSSL的数据加密工具

  6. 常用字符串处理

  7. ……

二、如何构建

2.1 基于生产环境构建

尽管使用的组件都支持跨平台,但大器当前仅支持在Linux下环境编译;大多数实际项目的服务,都运行在Linux下。

Web 服务端部署在Linux上,这是有原因的: (1) 前述的外围组件:nginx、mysql、redis 在Linux下安装都是一行命令的事,远比Windows方便。(2) 如果你使用当前非常流行的Docker,更是如此。 (3) 事实上Web 端的开源大杀器都是先提供Linux版,然后再考虑出Windows版,甚至有官方拒绝出Windows版(比如Redis 作者就“无情”地拒绝了微软提供的,将Redis变成也可以在Windows执行的补丁包)。

大器框架同样会在后续某个时间,提供Windows版本。

当前国内各云计算提供商,均提供 Ubuntu Server 版本为 18.04 LTS 版本。如“0.5 紧跟国内生产环境” 小节所述,你有一台2019年或更新的Ubuntu云服务器,那么在其上构建大器,则所需的软件、依赖库等,暂时只有一个用于中文编码转换的 iconv 库,需要手动下载编译之外,其它的都可以从Ubuntu 软件仓库中获取。

以下内容均以 Ubuntu 18.04 为例,考虑日常开发不会直接使用Server版,因此严格讲,以下内容均假设系统环境为 Ubuntu 18.04 桌面版。

小提示-服务器与开发机的区别:

开发机(桌面版)为安装组件时,需临时提升用户权限,即相关指令前面多出个“sudo ”。如果是在服务版的Ubuntu操作,默认就是拥有更高管理权限的根用户,因此不需要该指令。例如: 开发机:sudo apt install git 服务器:apt install git

2.2 准备编译工具

  1. 如果未安装或不知道有没有安装(以下简称为“准备”) GCC 编译器:
sudo apt install build-essential
  1. 准备 CMake构建套件,请:
sudo apt install cmake

2.3 准备第三方库

  1. 准备 boost 开发库:
sudo apt install libboost-dev libboost-filesystem libboost-system
  1. 准备openssl及其开发库:
sudo apt install openssl libssl-dev
  1. 准备 libiconv 库

我们使用汉字,而汉字有多种编码方案,因此,汉字的编码转换,是开发包括Web应用在内各种软件系统的常见需求。大器框架通过集成iconv库用以实现支持多国语言多种编码的转换功能。

先下载:https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.16.tar.gz

在Ubuntu图形界面中,双击该.gz文件,再点击其内.tar文件,解压后得到 “libiconv-1.16”文件夹。在终端进入该文件夹,依次输入以下三行,完成安装:

./configure --prefix=/usr/local
make
sudo make install
  1. 最后,确保生效所安装的动态库在系统中能被找到:
sudo ldconfig

2.4 下载大器源代码

通常你应该安装 git 工具,如果没有或不确定,请打开终端(Ctrl + Alt + T),按如下指令安装。

sudo apt install git 

然后在本地新建一文件夹,假设命名为 daqi,打开终端进入该目录,从github克隆代码:

git clone https://github.com/d2school/da4qi4.git

或者从国内的gitee克隆代码(速度比较快):

git  clone  https://gitee.com/zhuangyan-stone/da4qi4_public.git

最终,你将在前述的“daqi”目录下,得到一个子目录“da4qi4”(也可能是别的,看git源)。大器项目的代码位于后者内,其内你应该能看到“src”、“include”等多个子目录。

感谢你看到这里。如有余力,建议在以上两个网站均为本开源项目打个星 。

2.5 编译“大器”库

重要:以下假设大器源代码位于“daqi/da4qi4”目录下。

  1. 准备构建目录

请在“daqi”之下(和“da4qi4”平级)的位置,新建一目录,名为“build”:

mkdir build

进入该当目录:

cd build
  1. 执行CMake

如果你使用的是1.66或更高版本的boost库,请先打开项目下的CMakefile.txt文件,找到第13行:set(USE_LOCAL_BOOST_VERSION OFF) 将OFF改为ON: set(USE_LOCAL_BOOST_VERSION ON)

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)。

  1. 开始编译
make

小提示:并行编译 如果你的电脑拥有多核CPU,并且内存足够大(至少8G),可以按如下方式并行编译(其中 -j 后面的数字,指明并行编译的核数,以下以四核数例): make -j4

完成make之后,以上过程将在build目录内,得到“libda4qi4.so”;如果是调试版,将得到 “libda4qi4_d.so”。 如果是静态库,则相应的扩展名为 “.a”。

以上工作都是一次性的,以后你再使用da4qi4开发新项目,都从下面的步骤开始。

2.6 在你的项目中使用da4qi4库

现在,你可以使用你熟悉IDE(Code::Blocks、Qt Creator、CodeLite等)中,构建你的项目,然后以类型使用其它开发库的方式,添加大器的库文件(就是前一步构建所得的.so或.a文件),及大器的头文件。

  1. da4qi4库文件。 即前面编译大器库得到的库文件,如“libda4qi4.so”或“libda4qi4.a”,“libda4qi4_d.so”、“libda4qi4_d.a”等文件。
  2. da4qi4库依赖的文件。 在Linux下,它们是 pthread、ssl、crypto、boost_filesystem、boost_system
  3. da4qi4头文件:“大器项目目录”、“大器项目目录/include”及“大器项目目录/nlohmann_json/include/”

下面以CMake的CMakefiles.txt为例:

cmake_minimum_required(VERSION 3.5)

project(hello_daqi LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -Wall")

# 此处设置大器项目目录
set(_DAQI_PROJECT_PATH_ "你的大器项目所在目录")
# 此处设置大器项目编译后得到的 .so 文件所在目录
set(_DAQI_LIBRARY_PATH_ "你的大器项目动态库所在目录")

include_directories(${_DAQI_PROJECT_PATH_})
include_directories(${_DAQI_PROJECT_PATH_}/include)
include_directories(${_DAQI_PROJECT_PATH_}/nlohmann_json/include/)

find_package(Boost 1.65.0 REQUIRED COMPONENTS filesystem system)
link_directories(${_DAQI_LIBRARY_PATH_})

link_libraries(da4qi4)

link_libraries(pthread)
link_libraries(ssl)
link_libraries(crypto)
link_libraries(boost_filesystem)
link_libraries(boost_system)

add_executable(hello_daqi main.cpp)

现在你可以从之前 1.1 一个空转的Web Server 重新看起。

三、运行时外部配套系统

3.1 运行时依赖说明

一个Web系统常用到缓存系统和数据库系统。大器框架对二者依赖情况如下:

  • 完全不依赖数据库;
  • 简单例子不依赖缓存系统,但一旦需要用到Web 系统常见的“会话/SESSION”功能,则需要依赖redis缓存库。

3.2 Redis的安装

显然,这已经不是本开源项目的自己的说明内容。不过,反正在Ubuntu Linux下安装Redis就一行话:

sudo apt install redis-server

这不仅会安装redis服务,而且会顺便在本机redis的命令行客户端 redis-cli。

  • 有关如何在你写的大器Web Server中实现SESSION,请参看本项目官网www.d2school.com 相关(免费视频)课程;
  • 有关Redis的学习,请关注www.d2school.com 课程。

3.3 数据库

  • 可以使用 mysql 官方的 MySQL C++ Connector;
  • 新人强烈推荐: 相对传统的C++封装 : MySQL++ (注:欢迎关注《白话 C++》下册,有详细的 MySQL 数据库及 MySQL++使用的章节;
  • 新人推荐: CppDB
  • github上,搜索 “MySQL C++”,你将找到大量国内或国外的MySQL C++连接库;
  • 有经验的C++程序员推荐:sqlpp11

更多课程(视频课程、文字课程),请到 第2学堂查看。 谢谢。

About

a cpp Web Server

Topics

Resources

License

Releases

No releases published

Packages

No packages published

Languages