Skip to content

C++11实现的基于Boost.Beast、高性能且跨平台的异步HTTP服务器。High performace and cross-platform HTTP Server library.

License

Notifications You must be signed in to change notification settings

Leopard-C/HttpServer

Repository files navigation

Http Server

C++11 Boost Windows& Linux MIT-License

基于 Boost.Beast 实现的跨平台、高性能的HTTP服务器。

1. 文档

2. 依赖

3. 功能要点

  • 支持WindowsLinux平台
  • 支持GET,HEAD,POST,PUT,DELETE,CONNECT,OPTIONS,TRACE,PATCH方法
  • 请求拦截器
  • 响应拦截器
  • 路由管理(静态路由,正则路由)
  • 工作线程数量设置(设置一个范围,根据实时请求数量自动调整线程数量)
  • 多地址监听
  • Keep-Alive超时时间设置
  • body内容大小限制
  • (大)文件响应
  • 支持Set-Cookie
  • 自动解析以下3种类型的body
    • application/x-www-form-urlencoded
    • application/json
    • multipart/form-data

通过server-assistant/server-assistant.py脚本,配合include/server/helper/...,可以实现以下扩展功能:

  • 控制器模式:通过特定的doxygen格式注释,实现自动收集路由定义。
  • 数据传输对象DTO:自动生成序列化、反序列化代码。
  • 参数检查和取值。

4. 简单使用

// main.cpp
#include <server/http_server.h>
#include <server/router.h>
#include <server/request.h>
#include <server/response.h>

using namespace ic::server;

int main() {
    // 1. HTTP服务器配置
    HttpServerConfig config;
    config.add_endpoint("0.0.0.0", 8099, true);  // 监听地址,支持多个地址
    config.set_min_num_threads(2);            // 最少2个工作线程
    config.set_max_num_threads(8);            // 最多8个工作线程
    config.set_log_access(true);              // 打印请求日志
    config.set_log_access_verbose(false);     // 不打印详细日志(调试时可开启)
    config.set_tcp_stream_timeout_ms(15000);  // 超时时间15s
    config.set_body_limit(11 * 1024 * 1024);  // 请求内容大小限制11MB
    // 或者从配置文件读取,参考 config/server.json 文件
    // HttpServerConfig config;
    // if (!config.ReadFromFile("config/server.json")) {
    //     return false;
    // }

    // 2. 创建HTTP服务器
    HttpServer svr(config);

    // 3. 注册路由(可以使用普通函数、类的静态函数、lambda函数)
    auto router = svr.router();
    // 3.1 GET请求
    router->AddStaticRoute("/echo", HttpMethod::kGET, [](Request& req, Response& res){
        std::string text = req.GetUrlParam("text");
        res.SetStringBody(text, "text/plain");
    });
    // 3.2 POST和OPTIONS请求
    router->AddStaticRoute("/user/register", HttpMethod::kPOST | HttpMethod::kOPTIONS, [](Request& req, Response& res){
        if (req.method() == HttpMethod::kOPTIONS) {
            res.SetHeader("Access-Control-Allow-Origin", "*");
            res.SetStringBody(204);
            return;
        }
        std::string username = req.GetBodyParam("username");
        std::string password = req.GetBodyParam("password");
        // ...
        res.SetStringBody("OK", "text/plain");
    });
    // 3.3 正则路由,响应文件流
    router->AddRegexRoute("/img/(.*)", HttpMethod::kGET, [](Request& req, Response& res){
        std::string uri = req.GetRouteRegexMatch(0);
        std::string filename = HttpServer::GetBinDirUtf8() + "../data/web/img/" + uri;
        res.SetFileBody(filename);
    });
    // 3.4 响应application/json
    router->AddStaticRoute("/server/stop", HttpMethod::kGET, [](Request& req, Json::Value& res){
        req.svr()->StopAsync();
        res["code"] = 0;
        res["msg"] = "OK";
        res["data"]["time"] = time(NULL);
    });

    // 4. 启动服务器
    svr.Start();  // 阻塞
    // 异步启动
    // svr.StartAsync();
    // ...
    // svr.WaitForStop();

    return 0;
}

5. 控制器模式

// user_controller.h
#pragma once
#include <server/request.h>
#include <server/response.h>

using Request = ic::server::Request;
using Response = ic::server::Response;

class UserController {
public:
    /**
     * @brief 登录.
     * @route  /api/User/Login
     * @method POST
     * @config Authorization(0)
     */
    static void Login(Request& req, Response& res);

    /**
     * @brief 上传用户头像.
     * @route  /api/User/UploadAvatar
     * @method POST
     * @config Authorization(1)
     */
    static void UploadAvatar(Request& req, Response& res);

    /**
     * @brief 获取头像,返回二进制图片文件.
     * @route  ~ /api/User/avatar/([a-z0-9]{2})/([a-z0-9]{32}\.(jpg|jpeg|png|gif))
     * @method GET
     * @config Authorization(1)
     * @priority 99
     */
    static void GetAvatarImage(Request& req, Response& res);
};

该头文件,经过脚本server-assistant/server-assistant.py解析后,即可生成路由注册的代码。(可批量处理控制器目录下的所有头文件)

// Generated by script server-assistant.py
#include <server/router.h>
#include "controller/user/user_controller.h"

bool register_routes(std::shared_ptr<ic::server::Router> router) {
    using namespace ic::server;
    bool ret = true;

    // controller/user/user_controller.h
    ret &= router->AddStaticRoute("/api/User/Login", HttpMethod::kPOST, UserController::Login, "登录.", {{"Authorization", "0"}});
    ret &= router->AddStaticRoute("/api/User/UploadAvatar", HttpMethod::kPOST, UserController::UploadAvatar, "上传用户头像.", {{"Authorization", "1"}});
    ret &= router->AddRegexRoute("/api/User/avatar/([a-z0-9]{2})/([a-z0-9]{32}\\.(jpg|jpeg|png|gif))", HttpMethod::kGET, UserController::GetAvatarImage, "获取头像,返回二进制图片文件.", {{"Authorization", "1"}}, 99);

    return ret;
}

详情请参考脚本说明文档 server-assistant/README.md,以及 example/README.md

6. 检查并获取请求参数(URL参数、Body参数等)

主要有三种方式:

  • 【基础】通过Request类的GetUrlParam(), GetBodyParam(), GetJsonParam()方法获取参数值,并手动进行类型转换。
  • 【推荐】通过宏CHECK_URL_PARAM_INT(param1, param2, ...), CHECK_BODY_PARAM_xxx(...)获取参数值,会自动进行类型转换
    • 支持std::string, bool, int32_t, uint32_t, int64_t, uint64_t, double类型。
  • 【推荐】通过DTO (Data Transfer Object)方式获取参数值,只需在结构体或类中引入引入宏DTO_INDTO_OUT,即可通过脚本自动生成序列化、反序列化代码。

6.1 方式一:获取参数值,并手动类型转换

该库的ic::server::Request类提供如下方法,用于获取请求参数。

最为灵活,运行效率最高,但是需要手动进行类型检查和转换,代码量可能较大。

// URL参数
// 例如:/xxx?value1=large&value2=small
bool exist;
const std::string& value1 = req.GetUrlParam("value1", &exist);

// Path参数(正则表达式路由)
// 例如路由:/user/([a-zA-Z0-9_]+)/([0-9]+)
const std::string& name = req.GetRouteRegexMatch(0);     // ([a-zA-Z0-9_]+) 匹配用户名
int page = std::atoi(req.GetRouteRegexMatch(1).c_str()); // ([0-9]+) 匹配页码

// Body参数(application/x-www-form-urlencoded)
// 通过 req.content_type().IsApplicationXWwwFormUrlEncoded() 判断是否该内容类型
bool exist;
const std::string& value1 = req.GetBodyParam("value1", &exist);

// Body参数(application/json)
// 通过 req.content_type().IsApplicationJson() 判断该是否该内容类型
const Json::Value& value1 = req.GetJsonParam("value1");
// value1.isNull() 判断参数是否存在

// Body参数(multipart/form-data)
// 通过 req.content_type().IsMultipartFormData() 判断该是否该内容类型
const FormParam* value1 = req.GetFormParam("value1");
// 如果参数不存在则value1为空指针
value1->is_file()
value1->filename();
value1->content_type();
value1->content();

6.2 方式二:使用宏 CHECK_PARAM_xxxGET_PARAM_xxx

通过以下3个头文件中定义的宏,可以轻松实现参数检查和取值。

// 检查URL中是否有名称为 param1, param2, ..., paramN 的参数 (一条语句最多检查10个参数)
// 如果没有,则直接响应 `参数缺失` 的错误
// 如果有,但不是int类型,则直接响应 `参数无效` 的错误
// 如果有,且为int类型,将定义 int 类型的变量 param1, param2, ... paramN,并赋予相应的参数值
CHECK_URL_PARAM_INT(param1, param2, ..., paramN);    // int32_t
CHECK_URL_PARAM_UINT(param1, param2, ..., paramN);   // uint32_t
CHECK_URL_PARAM_INT64(param1, param2, ..., paramN);  // int64_t
CHECK_URL_PARAM_UINT64(param1, param2, ..., paramN); // uint64_t
CHECK_URL_PARAM_BOOL(param1, param2, ..., paramN);   // bool
CHECK_URL_PARAM_DOUBLE(param1, param2, ..., paramN); // double
CHECK_URL_PARAM_STR(param1, param2, ..., paramN);    // std::string

// 检查URL中是否有名称为 param1, param2, ..., paramN 的参数
// 如果没有,则使用指定的默认值
// 如果有,但不是int类型,则直接响应 `参数无效` 的错误
// 如果有,且为int类型,将定义 int 类型的变量 param1, param2, ... paramN,并赋予相应的参数值。
GET_URL_PARAM_INT(default_value, param1, param2, ..., paramN);    // int32_t
GET_URL_PARAM_xxx(default_value, param1, param2, ..., paramN);

// 相应的,还有从body参数中取值的宏
CHECK_BODY_PARAM_xxx()  // (1) ContentType: application/x-www-form-urlencoded
GET_BODY_PARAM_xxx()

CHECK_JSON_PARAM_xxx()  // (2) ContentType: application/json
GET_JSON_PARAM_xxx()

CHECK_FORM_PARAM_xxx()  // (3) ContentType: multipart/form-data
GET_FORM_PARAM_xxx()

CHECK_BODY_PARAM_xxx_EX()  // 自动判断上述3种内容类型 (1)(2)(3)
GET_BODY_PARAM_xxx_EX()

例如某用户登录接口,需要传递uid, password两个字符串参数和一个captcha整形参数,如果登录成功,返回token,否则返回登录失败信息。

/**
 * @brief 登录.
 * 
 * @route  /api/User/Login
 * @method POST
 * @config Authorization(Skip)
 */
void Login(Request& req, Response& res) {
    API_INIT();   // from <server/helper/helper.h>,固定写法,放在函数开头

    // 下面这一行代码,将定义两个 std::string 类型的变量,uid 和 password
    // 检查请求的参数中是否有 uid 和 password,如果不存在将返回“参数缺失”的错误信息
    // 如果存在,则将参数值赋值给 uid 和 password 变量。
    CHECK_JSON_PARAM_STR(uid, password);  // from <server/helper/param_check.h>
    // 同上,这里定义的是 int32_t 类型的变量 captcha
    CHECK_JSON_PARAM_INT(captcha);

    // 检验用户名和密码是否正确 (仅供参考)
    std::string token;
    bool success = CheckLogin(uid, password, captcha, &token);
    if (success) {
        data["token"] = token;   // data的类型是Json::Value,在第一行 API_INIT() 宏函数中定义
        RETURN_OK();
    }
    else {
        RETURN_CODE_MSG(status::kWrongUserOrPassword, "用户名或密码错误");
    }
}

如果登录成功,将返回如下格式的响应:

{
    "code": 0,
    "msg": "OK",
    "data": {
        "token": "xxxxxxx...."
    }
}

6.3 方式三:通过DTO (Data Transfer Object)方式获取参数值

包含头文件<server/helper/dto.h>

通过如下方式定义DTO结构体或类。

struct LoginDto {
    DOT_IN;   // 必选 DTO_IN, DTO_OUT, DTO_IN_OUT 中的一种

    /**
     * @brief 账号ID.
     * @required
     */
    std::string uid;

    /**
     * @brief 密码(md5哈希值).
     * @alias password
     * @required
     */
    std::string password_md5;

    /**
     * @brief 用户角色.
     * @ignore
     */
    std::string role;

    /**
     * @brief 时间戳.
     * @optional
     */
    time_t timestamp = 0;
};

需要借助 server-assistant/server-assistant.py 脚本,批量处理所有DTO头文件,生成序列化、反序列化方法的实现代码。

  • DTO_IN: 用于接收请求数据,将生成 bool Deserialize(const Json::Value&) 反序列化方法。
  • DTO_OUT: 作为接口响应数据,将生成 Json::Value Serialize() const 序列化方法。
  • DTO_IN_OUT: 以上两个的融合。

接口参数扩展属性:

  • @required: 该参数必选。(默认行为)
  • @optional: 该参数可选,在定义的地方指定默认值。
  • @ignore: 序列化、反序列化时忽略该参数。
  • @alias {alias}: 指定在JSON对象中的别名。

支持的成员变量类型:

  • 基础数据类型(u)int32_t, (u)int64_t, bool, float, double, std::string
  • 其他DTO类型,如 RoleDto
  • STL容器类型,如vector<int>, set<uint64_t>, vector<RoleDto>
    • STL容器:vector, deque, list, set, unordered_set
    • 元素类型:基础数据类型其他DTO

7. 其他

关于请求拦截器、响应拦截器、路由管理、服务器配置等功能,请参考 example 示例程序代码、项目Wiki: Http Server Wiki

END

Author: Leopard-C

GitHub: Leopard-C/HttpServer

Email: leopard.c@outlook.com

About

C++11实现的基于Boost.Beast、高性能且跨平台的异步HTTP服务器。High performace and cross-platform HTTP Server library.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published