Skip to content

wfrest框架文档

wfrest是一个快速🚀, 高效⌛️, 简单易用的💥 c++ 异步web框架.

wfrest基于C++ Workflow开发. C++ Workflow 是一个设计轻盈优雅的企业级程序引擎.

简介

你可以用来:

  • 快速搭建http服务器:
cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;

    svr.GET("/hello", [](const HttpReq *req, HttpResp *resp)
    {
        resp->String("world\n");
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

使用中有疑问?

可以先查看Discussionsissues列表,看看是否能找到答案。

非常欢迎将您使用中遇到的问题发送到issues。

也可以通过QQ群:884394197 联系我们。

编译 && 安装

需求

  • workflow, 版本大于等于 v0.9.9
  • Linux , 比如ubuntu 18.04 或者更新版本
  • Cmake
  • zlib1g-dev
  • libssl-dev
  • libgtest-dev
  • gcc 和 g++ 或者 llvm + clang

如果你在ubuntu 20.04,你可以用以下命令安装

bash
apt-get install build-essential cmake zlib1g-dev libssl-dev libgtest-dev -y

cmake

git clone --recursive https://github.com/wfrest/wfrest
cd wfrest
make
sudo make install

编译例子:

make example

测试:

make check

Docker

docker build -t wfrest ./docker/ubuntu/

如果你用podman

podman build -t wfrest ./docker/ubuntu/

你也可以从dockerhub中拖拉镜像

docker pull wfrest/wfrest

API示例

路由中的参数

路径中的参数

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;

    // 此处理程序将匹配 /user/chanchan 但不会匹配 /user/ 或 /user
    // curl -v "ip:port/user/chanchan/"
    svr.GET("/user/{name}", [](const HttpReq *req, HttpResp *resp)
    {
        // 引用:不复制
        const std::string& name = req->param("name");
        // resp->set_status(HttpStatusOK); // 自动设置
        resp->String("你好 " + name + "\n");
    });

    // wildcast/chanchan/action... (前缀)
    svr.GET("/wildcast/{name}/action*", [](const HttpReq *req, HttpResp *resp)
    {
        const std::string& name = req->param("name");
        const std::string& match_path = req->match_path();

        resp->String("[name : " + name + "] [match path : " + match_path + "]\n");
    });

    // 请求将保存路由定义
    svr.GET("/user/{name}/match*", [](const HttpReq *req, HttpResp *resp)
    {
        // 如果 /user/chanchan/match1234
        // full_path : /user/{name}/match*
        // current_path : /user/chanchan/match1234
        // match_path : match1234
        const std::string &full_path = req->full_path();
        const std::string &current_path = req->current_path();
        std::string res;
        if (full_path == "/user/{name}/match*")
        {
            res = full_path + " 匹配 : " + current_path;
        } else
        {
            res = full_path + " 不匹配";
        }
        resp->String(res);
    });

    // 此处理程序将为 /user/groups 添加一个新路由。
    // 无论定义的顺序如何,精确路由都会在参数路由之前解析。
    // 以 /user/groups 开头的路由永远不会被解释为 /user/{name}/... 路由
    svr.GET("/user/groups", [](const HttpReq *req, HttpResp *resp)
    {
        resp->String(req->full_path());
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

url请求参数

查询字符串参数

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;

    // 请求响应匹配的URL:/query_list?username=chanchann&password=yyy
    svr.GET("/query_list", [](const HttpReq *req, HttpResp *resp)
    {
        const std::map<std::string, std::string>& query_list = req->query_list();
        for (auto &query: query_list)
        {
            fprintf(stderr, "%s : %s\n", query.first.c_str(), query.second.c_str());
        }
    });

    // 请求响应匹配的URL:/query?username=chanchann&password=yyy
    svr.GET("/query", [](const HttpReq *req, HttpResp *resp)
    {
        const std::string& user_name = req->query("username");
        const std::string& password = req->query("password");
        const std::string& info = req->query("info"); // 没有这个字段
        const std::string& address = req->default_query("address", "china");
        resp->String(user_name + " " + password + " " + info + " " + address + "\n");
    });

    // 请求响应匹配的URL:/query_has?username=chanchann&password=
    // 判断参数是否存在的逻辑是,如果参数值为空,则认为参数存在
    // 只有参数未提交时才认为参数不存在
    svr.GET("/query_has", [](const HttpReq *req, HttpResp *resp)
    {
        if (req->has_query("password"))
        {
            fprintf(stderr, "存在password查询参数\n");
        }
        if (req->has_query("info"))
        {
            fprintf(stderr, "存在info查询参数\n");
        }
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

表单数据

表单提交

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;

    // URL编码表单
    // curl -v http://ip:port/post \
    // -H "body-type:application/x-www-form-urlencoded" \
    // -d 'user=admin&pswd=123456'
    svr.POST("/post", [](const HttpReq *req, HttpResp *resp)
    {
        if (req->content_type() != APPLICATION_URLENCODED)
        {
            resp->set_status(HttpStatusBadRequest);
            return;
        }
        std::map<std::string, std::string> &form_kv = req->form_kv();
        for (auto &kv: form_kv)
        {
            fprintf(stderr, "键 %s : 值 %s\n", kv.first.c_str(), kv.second.c_str());
        }
    });

    // curl -X POST http://ip:port/form \
    // -F "file=@/path/file" \
    // -H "Content-Type: multipart/form-data"
    svr.POST("/form", [](const HttpReq *req, HttpResp *resp)
    {
        if (req->content_type() != MULTIPART_FORM_DATA)
        {
            resp->set_status(HttpStatusBadRequest);
            return;
        }
        /*
            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST
            // <name ,<filename, body>>
            using Form = std::map<std::string, std::pair<std::string, std::string>>;
        */
        const Form &form = req->form();
        for (auto &it: form)
        {
            auto &name = it.first;
            auto &file_info = it.second;
            fprintf(stderr, "%s : %s = %s",
                    name.c_str(),
                    file_info.first.c_str(),
                    file_info.second.c_str());
        }
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

多部分表单编码器

使用MultiPartEncoder来编码多部分数据格式内容并发送。

cpp
svr.GET("/form_send", [](const HttpReq *req, HttpResp *resp)
{
    MultiPartEncoder encoder;
    encoder.add_param("Filename", "1.jpg");
    encoder.add_file("test_1.txt", "./www/test_1.txt");
    encoder.add_file("test_2.txt", "./www/test_2.txt");
    resp->String(std::move(encoder));
});

HTTP头部字段

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;

    svr.POST("/post", [](const HttpReq *req, HttpResp *resp)
    {
        const std::string& host = req->header("Host");
        const std::string& content_type = req->header("Content-Type");
        if (req->has_header("User-Agent"))
        {
            fprintf(stderr, "存在User-Agent...");
        }
        resp->String(host + " " + content_type + "\n");
    });


    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

发送文件

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;

    // 单个文件
    svr.GET("/file1", [](const HttpReq *req, HttpResp *resp)
    {
        resp->File("todo.txt");
    });

    svr.GET("/file2", [](const HttpReq *req, HttpResp *resp)
    {
        resp->File("html/index.html");
    });

    svr.GET("/file3", [](const HttpReq *req, HttpResp *resp)
    {
        resp->File("/html/index.html");
    });

    svr.GET("/file4", [](const HttpReq *req, HttpResp *resp)
    {
        resp->File("todo.txt", 0);
    });

    svr.GET("/file5", [](const HttpReq *req, HttpResp *resp)
    {
        resp->File("todo.txt", 0, 10);
    });

    svr.GET("/file6", [](const HttpReq *req, HttpResp *resp)
    {
        resp->File("todo.txt", 5, 10);
    });

    svr.GET("/file7", [](const HttpReq *req, HttpResp *resp)
    {
        resp->File("todo.txt", 5, -1);
    });

    svr.GET("/file8", [](const HttpReq *req, HttpResp *resp)
    {
        resp->File("todo.txt", -5, -1);
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

保存文件

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;

    // curl -v -X POST "ip:port/file_write1" -F "file=@filename" -H "Content-Type: multipart/form-data"
    svr.POST("/file_write1", [](const HttpReq *req, HttpResp *resp)
    {
        std::string& body = req->body();   // multipart/form - body有边界
        resp->Save("test.txt", std::move(body));
    });

    svr.GET("/file_write2", [](const HttpReq *req, HttpResp *resp)
    {
        std::string content = "1234567890987654321";

        resp->Save("test1.txt", std::move(content));
    });

    // 您可以指定保存文件成功时返回给客户端的消息
    // 默认是 "Save File success\n"
    svr.GET("/file_write3", [](const HttpReq *req, HttpResp *resp)
    {
        std::string content = "1234567890987654321";

        resp->Save("test2.txt", std::move(content), "测试通知测试成功");
    });
    
    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

上传文件

cpp
#include "wfrest/HttpServer.h"
#include "wfrest/PathUtil.h"
using namespace wfrest;

int main()
{
    HttpServer svr;

    // 将文件上传到父目录是非常危险的:
    // curl -v -X POST "ip:port/upload" -F "file=@demo.txt; filename=../demo.txt" -H "Content-Type: multipart/form-data"
    // 然后你会发现文件被存储在父目录中,这是危险的
    svr.POST("/upload", [](const HttpReq *req, HttpResp *resp)
    {
        Form &form = req->form();

        if (form.empty())
        {
            resp->set_status(HttpStatusBadRequest);
        } else
        {
            for(auto& part : form)
            {
                const std::string& name = part.first;
                // 文件名 : 文件内容
                std::pair<std::string, std::string>& fileinfo = part.second;
                // file->filename 不应该被信任。参见MDN上的Content-Disposition
                // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#directives
                // 文件名始终是可选的,不应该被应用程序盲目使用:
                // 应该去除路径信息,并转换为服务器文件系统规则。
                if(fileinfo.first.empty())
                {
                    continue;
                }
                fprintf(stderr, "文件名 : %s\n", fileinfo.first.c_str());

                resp->Save(PathUtil::base(fileinfo.first), std::move(fileinfo.second));
            }
        }
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

内置json (wfrest::Json) 用法

我们提供内置的json,强烈推荐使用。

如果您想了解更多json API用法,可以查看 json api

cpp
#include "wfrest/HttpServer.h"
#include "wfrest/Json.h"

using namespace wfrest;

int main()
{
    HttpServer svr;

    // curl -v http://ip:port/json1
    svr.GET("/json1", [](const HttpReq *req, HttpResp *resp)
    {
        Json json;
        json["test"] = 123;
        json["json"] = "test json";
        resp->Json(json);
    });

    // 接收json
    //   curl -X POST http://ip:port/json2
    //   -H 'Content-Type: application/json'
    //   -d '{"login":"my_login","password":"my_password"}'
    svr.POST("/json2", [](const HttpReq *req, HttpResp *resp)
    {
        if (req->content_type() != APPLICATION_JSON)
        {
            resp->String("不是APPLICATION_JSON");
            return;
        }
        fprintf(stderr, "Json : %s", req->json().dump(4).c_str());
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

计算型Handler

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

void Fibonacci(int n, HttpResp *resp)
{
    unsigned long long x = 0, y = 1;
    if (n <= 0 || n > 94)
    {
        fprintf(stderr, "invalid parameter");
        return;
    }
    for (int i = 2; i < n; i++)
    {
        y = x + y;
        x = y - x;
    }
    if (n == 1)
        y = 0;
    resp->String("fib(" + std::to_string(n) + ") is : " + std::to_string(y) + "\n");
}

int main()
{
    HttpServer svr;
    // 第二个参数表示此计算队列ID为1
    // 然后这个处理器成为一个计算任务
    // curl -v http://ip:port/compute_task?num=20
    svr.GET("/compute_task", 1, [](const HttpReq *req, HttpResp *resp)
    {
        int num = std::stoi(req->query("num"));
        Fibonacci(num, resp);
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

计算任务接口

如果您想启动一个计算任务,只需使用resp->Compute()

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

void Factorial(int n, HttpResp *resp)
{
    unsigned long long factorial = 1;
    for(int i = 1; i <= n; i++)
    {
        factorial *= i;
    }
    resp->String("fac(" + std::to_string(n) + ") = " + std::to_string(factorial));
}

int main()
{
    HttpServer svr;

    svr.GET("/compute", [](const HttpReq *req, HttpResp *resp)
    {
        // 第一个参数是队列ID
        resp->Compute(1, Factorial, 10, resp);
    });

    if (svr.track().start(8888) == 0)
    {
        svr.list_routes();
        wait_group.wait();
        getchar();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

系列型Handler

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;

    svr.GET("/series", [](const HttpReq *req, HttpResp *resp, SeriesWork* series)
    {
        auto *timer = WFTaskFactory::create_timer_task(5000000, [](WFTimerTask *) {
            printf("定时器任务完成(5秒)。\n");
        });

        series->push_back(timer);
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

压缩算法

cpp
// 服务器
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;
    
    // 1. 您可以使用 `./13_compess_client` 
    // 2. 或者使用Python脚本 `python3 13_compress_client.py`
    // 3. echo '{"testgzip": "gzip compress data"}' | gzip |  \
    // curl -v -i --data-binary @- -H "Content-Encoding: gzip" http://ip:port/gzip
    svr.POST("/gzip", [](const HttpReq *req, HttpResp *resp)
    {
        // 我们自动解压从客户端发送的压缩数据
        // 目前仅支持gzip和br格式
        std::string& data = req->body();
        fprintf(stderr, "ungzip data : %s\n", data.c_str());
        resp->set_compress(Compress::GZIP);
        resp->String("测试服务器发送gzip数据\n");
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}
cpp
// 客户端
#include "workflow/WFTaskFactory.h"
#include "wfrest/Compress.h"
#include "wfrest/ErrorCode.h"

using namespace protocol;
using namespace wfrest;

struct CompessContext
{
    std::string data;
};

void http_callback(WFHttpTask *task)
{
    const void *body;
    size_t body_len;
    task->get_resp()->get_parsed_body(&body, &body_len);
    std::string decompress_data;
    int ret = Compressor::ungzip(static_cast<const char *>(body), body_len, &decompress_data);
    fprintf(stderr, "解压数据 : %s", decompress_data.c_str());
    delete static_cast<CompessContext *>(task->user_data);
}

int main()
{
    signal(SIGINT, sig_handler);

    std::string url = "http://127.0.0.1:8888";

    WFHttpTask *task = WFTaskFactory::create_http_task(url + "/gzip",
                                                       4,
                                                       2,
                                                       http_callback);
    std::string content = "客户端发送测试Gzip";
    auto *ctx = new CompessContext;
    int ret = Compressor::gzip(&content, &ctx->data);
    if(ret != StatusOK)
    {
        ctx->data = std::move(content);
    }
    task->user_data = ctx;
    task->get_req()->set_method("POST");
    task->get_req()->add_header_pair("Content-Encoding", "gzip");
    task->get_req()->append_output_body_nocopy(ctx->data.c_str(), ctx->data.size());
    task->start();
    getchar();
}

关于如何编写客户端,您可以参考workflow,它目前支持HTTP、Redis、MySQL和Kafka协议。您可以使用它来编写高效的异步客户端。

蓝图(BluePrint)

wfrest支持Flask风格的蓝图。

您可以参考 Flask蓝图到底是什么?

蓝图是一个有限制的wfrest服务器。它不能处理网络通信,但可以处理路由。

对于更大的项目,您的所有代码不应该在同一个文件中。相反,您可以将较大的代码分割或拆分到不同的文件中,这使您的项目更具模块化。

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

// 您可以将不同的业务逻辑拆分到不同的文件/模块中
void set_admin_bp(BluePrint &bp)
{
    bp.GET("/page/new/", [](const HttpReq *req, HttpResp *resp)
    {
        fprintf(stderr, "New page\n");
    });

    bp.GET("/page/edit/", [](const HttpReq *req, HttpResp *resp)
    {
        fprintf(stderr, "Edit page\n");
    });
}

int main()
{
    HttpServer svr;
    
    svr.POST("/page/{uri}", [](const HttpReq *req, HttpResp *resp)
    {
        fprintf(stderr, "Blog Page\n");
    });

    BluePrint admin_bp;
    set_admin_bp(admin_bp);

    svr.register_blueprint(admin_bp, "/admin");

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

静态文件服务

cpp
#include "wfrest/HttpServer.h"

using namespace wfrest;

int main()
{
    HttpServer svr;
    svr.Static("/static", "./www/static");

    svr.Static("/public", "./www");

    svr.Static("/", "./www/index.html");
    
    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

以下是设置和获取cookie的示例。

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;

    // curl --cookie "name=chanchan, passwd=123" "http://ip:port/cookie"
    svr.GET("/cookie", [](const HttpReq *req, HttpResp *resp)
    {
        const std::map<std::string, std::string> &cookie = req->cookies();
        if(cookie.empty())  // 没有cookie
        {
            HttpCookie cookie;
            // 您可以设置的内容:
            // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
            cookie.set_path("/").set_http_only(true);
            resp->add_cookie(std::move(cookie));
            resp->String("设置Cookie\n");
        }
        fprintf(stderr, "cookie :\n");
        for(auto &c : cookie)
        {
            fprintf(stderr, "%s : %s\n", c.first.c_str(), c.second.c_str());
        }
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

以下是一个更具体的示例,您可以查看教程

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;

    svr.GET("/login", [](const HttpReq *req, HttpResp *resp)
    {
        HttpCookie cookie;
        cookie.set_key("user")
                .set_value("chanchan")
                .set_path("/")
                .set_domain("localhost")
                .set_http_only(true);

        resp->add_cookie(std::move(cookie));
        resp->String("登录成功");
    });

    svr.GET("/home", [](const HttpReq *req, HttpResp *resp)
    {
        const std::string &cookie_val = req->cookie("user");
        if(cookie_val != "chanchan")
        {
            resp->set_status(HttpStatusUnauthorized);
            std::string err = R"(
            {
                "error": "未授权"
            }
            )";
            resp->Json(err);
        } else
        {
            std::string success = R"(
            {
                "home": "data"
            }
            )";
            resp->Json(success);
        }
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

如何向用户代理设置多个cookie?

参考:https://www.rfc-editor.org/rfc/rfc6265#section-5.4

如下一个例子所示,服务器可以在用户代理上存储多个cookie。 例如,服务器可以通过返回两个Set-Cookie头字段来存储会话标识符以及用户的首选语言。 请注意,服务器使用Secure和HttpOnly属性为更敏感的会话标识符提供额外的安全保护(参见第4.1.2节)。

== 服务器 -> 用户代理 ==

Set-Cookie: SID=31d4d96e407aad42; Path=/; Secure; HttpOnly Set-Cookie: lang=en-US; Path=/; Domain=example.com

你可以这样编写代码:

cpp
svr.GET("/multi", [](const HttpReq *req, HttpResp *resp)
{
    // 设置cookie
    HttpCookie cookie;

    cookie.set_key("user")
            .set_value("chanchan")
            .set_path("/")
            .set_domain("localhost")
            .set_http_only(true);

    resp->add_cookie(std::move(cookie));

    HttpCookie cookie2;
    cookie2.set_key("animal")
            .set_value("panda");
    resp->add_cookie(std::move(cookie2));

    resp->set_status(HttpStatusOK);
    resp->String("登录成功");
});

自定义服务器配置

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;
    
    svr.GET("/config", [](const HttpReq *req, HttpResp *resp)
    {
        resp->String("config");
    });

    if (svr.max_connections(4000)
            .peer_response_timeout(20 * 1000)
            .keep_alive_timeout(30 * 1000)
            .track()
            .start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

配置项

配置项如下:

cpp
struct WFServerParams
{
	size_t max_connections;
	int peer_response_timeout;	/* 每个读写操作的超时时间 */
	int receive_timeout;	/* 接收整个消息的超时时间 */
	int keep_alive_timeout;
	size_t request_size_limit;
	int ssl_accept_timeout;	/* 如果不是ssl,这将被忽略 */
};

默认值为:

cpp
static constexpr struct WFServerParams SERVER_PARAMS_DEFAULT =
{
	.max_connections		=	2000,
	.peer_response_timeout	=	10 * 1000,
	.receive_timeout		=	-1,
	.keep_alive_timeout		=	60 * 1000,
	.request_size_limit		=	(size_t)-1,
	.ssl_accept_timeout		=	10 * 1000,
};

跟踪

您可以打开跟踪日志。

格式:

[WFREST] 2022-01-13 18:00:04 | 200 | 127.0.0.1 | GET | "/data" | -- 
[WFREST] 2022-01-13 18:00:08 | 200 | 127.0.0.1 | GET | "/hello" | -- 
[WFREST] 2022-01-13 18:00:17 | 404 | 127.0.0.1 | GET | "/hello1" | --

您还可以设置自己的跟踪记录器:

cpp
svr.track([](HttpTask *server_task){
    spdlog::info(...);
    // BOOST_LOG_TRIVIAL(info) << "Status : ";
    // LOG(ERROR) << "time : " << time;
  ...
});

面向切面编程

在计算机领域,面向切面编程(AOP)是一种旨在通过允许分离横切关注点来增加模块化的编程范式。

有关更多信息,您可以查看 什么是AOP

cpp
#include "wfrest/HttpServer.h"
#include "wfrest/Aspect.h"
using namespace wfrest;

// 日志切面
struct LogAop : public Aspect
{
    bool before(const HttpReq *req, HttpResp *resp) override 
    {
        fprintf(stderr, "before \n");
        return true;
    }

    // 'after()' 应该在回复后被调用
    bool after(const HttpReq *req, HttpResp *resp) override
    {
        fprintf(stderr, "After log\n");
        return true;
    }
};

int main()
{
    HttpServer svr;

    svr.GET("/aop", [](const HttpReq *req, HttpResp *resp)
    {
        resp->String("aop");
    }, LogAop());

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

从切面传输数据到HTTP处理器:

cpp
#include "wfrest/HttpServer.h"
#include "wfrest/Aspect.h"
using namespace wfrest;

struct TransferAop : public Aspect
{
    bool before(const HttpReq *req, HttpResp *resp) override 
    {
        auto *content = new std::string("transfer data");
        resp->user_data = content;
        return true;
    }

    // 如果需要删除resp的'user_data',在'after()'中删除它。
    bool after(const HttpReq *req, HttpResp *resp) override
    { 
        fprintf(stderr, "state : %d\terror : %d\n", 
                resp->get_state(), resp->get_error());
        delete static_cast<std::string *>(resp->user_data);
        return true;
    }
};

int main()
{
    HttpServer svr;

    svr.GET("/aop", [](const HttpReq *req, HttpResp *resp)
    {
        auto *content = static_cast<std::string *>(resp->user_data);
        resp->String(std::move(*content));
    }, TransferAop());

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

全局切面

注册全局切面的方式如下:

cpp
svr.Use(FirstAop());
svr.Use(SecondAop(), ThirdAop());

Https 服务器

设置Https服务器非常简单。

http和https之间的唯一区别是https需要您提供SSL密钥的路径和SSL证书的路径。

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main(int argc, char **argv)
{
    // 在cert文件中 
    // 执行sudo ./gen.sh 生成crt / key文件
    if (argc != 3)
    {
        fprintf(stderr, "%s [cert file] [key file]\n",
                argv[0]);
        exit(1);
    }

    HttpServer svr;

    svr.GET("/https", [](const HttpReq *req, HttpResp *resp)
    {
        resp->String("测试Https\n");
    });

    if (svr.start(8888, argv[1], argv[2]) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server\n");
        exit(1);
    }
    return 0;
}

代理服务

cpp
#include "wfrest/HttpServer.h"
using namespace wfrest;

int main()
{
    HttpServer svr;

    // curl -v http://ip:port/proxy
    svr.GET("/proxy", [](const HttpReq *req, HttpResp *resp)
    {
        resp->Http("http://www.baidu.com");
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server");
        exit(1);
    }
    return 0;
}

MySQL

cpp
#include "workflow/MySQLResult.h"
#include "wfrest/HttpServer.h"

using namespace wfrest;
using namespace protocol;

int main(int argc, char **argv)
{
    HttpServer svr;
 
    svr.GET("/mysql00", [](const HttpReq *req, HttpResp *resp)
    {
        resp->MySQL("mysql://root:111111@localhost", "SHOW DATABASES");

    });

    svr.GET("/mysql01", [](const HttpReq *req, HttpResp *resp)
    {
        std::string url = "mysql://root:111111@localhost";
        resp->MySQL(url, "SHOW DATABASES", [resp](Json *json) 
        {
            Json js;
            js["rows"] = (*json)["result_set"][0]["rows"];
            resp->String(js.dump());
        });
    });

    svr.GET("/mysql02", [](const HttpReq *req, HttpResp *resp)
    {
        std::string url = "mysql://root:111111@localhost";

        resp->MySQL(url, "SHOW DATABASES", [resp](MySQLResultCursor *cursor) 
        {
            std::string res;
            std::vector<MySQLCell> arr;
            while (cursor->fetch_row(arr))
            {
                res.append(arr[0].as_string());
                res.append("\n");
            }
            resp->String(std::move(res));
        });

    });

    svr.GET("/multi", [](const HttpReq *req, HttpResp *resp)
    {
        resp->MySQL("mysql://root:111111@localhost/wfrest_test", "SHOW DATABASES; SELECT * FROM wfrest");
    });

    svr.POST("/client", [](const HttpReq *req, HttpResp *resp)
    {
        std::string &sql = req->body();
        resp->MySQL("mysql://root:111111@localhost/wfrest_test", std::move(sql));
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server\n");
        exit(1);
    }
    return 0;
}

Redis

cpp
#include "wfrest/HttpServer.h"

using namespace wfrest;

int main(int argc, char **argv)
{
    HttpServer svr;

    svr.GET("/redis0", [](const HttpReq *req, HttpResp *resp)
    {
        resp->Redis("redis://127.0.0.1/", "SET", {"test_val", "test_key"});
    });

    svr.GET("/redis1", [](const HttpReq *req, HttpResp *resp)
    {
        resp->Redis("redis://127.0.0.1/", "GET", {"test_val"});
    });

    if (svr.start(8888) == 0)
    {
        getchar();
        svr.stop();
    } else
    {
        fprintf(stderr, "Cannot start server\n");
        exit(1);
    }
    return 0;
}

默认返回格式为json。

Redis URL格式

redis://:password@host:port/dbnum?query#fragment

如果使用SSL,请使用:

rediss://:password@host:port/dbnum?query#fragment

密码是可选的。默认端口是6379;默认dbnum是0,其范围是0到15。 query和fragment在工厂中不使用,您可以自己定义它们。例如,如果您想使用上游选择,可以定义自己的query和fragment。有关详细信息,请参阅上游文档。 Redis URL示例:

redis://127.0.0.1/

redis://:12345678@redis.some-host.com/1

json api

To create a json object by reading a JSON file:

You can use c++ file stream:

cpp
#include "Json.h"
#include <fstream>
using namespace wfrest;

std::ifstream f("example.json");
Json data = Json::parse(f);

Or you can use FILE*

cpp
FILE* fp = fopen("example.json", "r");
Json data = Json::parse(fp);
fclose(fp);

Creating json objects from string

Using (raw) string literals and json::parse

cpp
Json data = Json::parse(R"(
{
  "pi": 3.141,
  "happy": true
}
)");

Creating json objects by initializer list

cpp
Json data = Json::Object{
    {"null", nullptr},
    {"integer", 1},
    {"float", 1.3},
    {"boolean", true},
    {"string", "something"},
    {"array", Json::Array{1, 2}},
    {"object",
     Json::Object{
         {"key", "value"},
         {"key2", "value2"},
     }},
};

Create simple json value

cpp
Json null1 = nullptr;

Json num1 = 1;

Json num2 = 1.0;

Json bool1 = true;

Json bool2 = false;

Json str1 = "string";

Json obj1 = Json::Object();

Json arr1 = Json::Array();

Explict declare Json type

If you want to be explicit or express the json type is array or object, the functions Json::Array and Json::Object will help:

cpp
Json empty_object_explicit = Json::Object();
Json empty_array_explicit = Json::Array();

Parse and dump

cpp
Json js = Json::parse(R"({"test1":false})");
cpp
Json data;
data["key"]["chanchan"] = 1;

// default compact json string
{"key":{"chanchan":1}}

// using member function
std::string dump_str = data.dump();

// streams
std::ostringstream os;
os << data;
std::cout << os.str() << std::endl;

// For pretty stringification, there is the option to choose the identation size in number of spaces:
{
  "key": {
    "chanchan": 1
  }
}
std::string dump_str_pretty = data.dump(2);

STL-like access

Example JSON arrays

  • Create an array using push_back
cpp
Json data;
data.push_back(3.141);
data.push_back(true);
data.push_back("chanchan");
data.push_back(nullptr);
Json arr;
arr.push_back(42);
arr.push_back("answer");
data.push_back(arr);
  • iterate the array
cpp
for (Json::iterator it = data.begin(); it != data.end(); it++)
{
    std::cout << it->value() << std::endl;
}

for (auto it = data.begin(); it != data.end(); ++it)
{
    std::cout << *it << std::endl;
}

for (const auto& it : data)
{
    std::cout << it << std::endl;
}

-iterate the array reversely

cpp
for (Json::reverse_iterator it = data.rbegin(); it != data.rend(); it++)
{
  std::cout << it->value() << std::endl;
}
  • access by using operator[index]
cpp
Json data;
data.push_back(1);        // 0
data.push_back(2.1);      // 1
data.push_back(nullptr);  // 2
data.push_back("string"); // 3
data.push_back(true);     // 4
data.push_back(false);    // 5

// You can user index to access the array element
int num_int = data[0].get<int>();
double num_double = data[1].get<double>();
std::nullptr_t null = data[2].get<std::nullptr_t>();
std::string str = data[3].get<std::string>();
bool bool_true = data[4].get<bool>();
bool bool_false = data[5].get<bool>();

// implicit conversion
int num_int = data[0];
double num_double = data[1];
std::nullptr_t null = data[2];
std::string str = data[3];
bool bool_true = data[4];
bool bool_false = data[5];

// Object
Json::Object obj;
obj["123"] = 12;
obj["123"]["1"] = "test";
data.push_back(obj); // 6
Json::Object obj1 = data[6].get<Json::Object>();
// implicit conversion
Json::Object obj2 = data[6];

// Array
Json::Array arr;
arr.push_back(1);
arr.push_back(nullptr);
data.push_back(arr);

Json::Array arr1 = data[7].get<Json::Array>();
// implicit conversion
Json::Array arr2 = data[7];
  • Update element
cpp
arr[1] = true;  // update method
  • Erase element
cpp
arr.erase(1);

Example JSON objects

  • use push_back
cpp
Json data;

data.push_back("pi", 3.141);
data.push_back("happy", true);
data.push_back("name", "chanchan");
data.push_back("nothing", nullptr);
Json answer;
answer.push_back("everything", 42);
data.push_back("answer", answer);
  • use operator[]

The JSON values can be constructed (comfortably) by using standard index operators:

Use operator[] to assign values to JSON objects:

cpp
// create an empty structure (null)
Json data;
std::cout << "empty structure is " << data << std::endl;

// add a number that is stored as double (note the implicit conversion of j
// to an object)
data["pi"] = 3.141;

// add a Boolean that is stored as bool
data["happy"] = true;

// add a string that is stored as std::string
data["name"] = "chanchan";

// add another null object by passing nullptr
data["nothing"] = nullptr;

// add an object inside the object
data["answer"]["everything"] = 42;
  • Update element
cpp
data["key"] = 1;
data["key"] = 2.0;
  • Erase element
cpp
data.erase("key");
  • iterate object elements
cpp
for (Json::iterator it = data.begin(); it != data.end(); it++)
{
    std::cout << it->key() << " : " << it->value() << std::endl;
}
for (auto it = data.begin(); it != data.end(); it++)
{
    // equal to it->value()
    std::cout << *it << std::endl;
}
for (const auto& it : data)
{
    std::cout << it.key() << " : " << it.value() << std::endl;
}
  • iterate object elements reversely
cpp
for (Json::reverse_iterator it = data.rbegin(); it != data.rend(); it++)
{
  std::cout << it->key() << " : " << it->value() << std::endl;
}