libhv是c++编写HTTP API 服务端/客户端最简单的库,没有之一

具有如下特性:

  • 跨平台(Windows, Linux, Mac)
  • 支持https
  • 支持RESTful API
  • 支持application/json、application/x-www-form-urlencoded、multipart/form-data
  • 内置web service文件服务和indexof service目录服务
  • 可扩展多进程/多线程模型

libhv简介

libhv是一个跨平台的类似libevent、libev、libuv的异步IO事件循环库,但提供了更加简单的API接口和更加丰富的协议(包括http、ftp、smtp、dns、icmp等)。
libhv已广泛实用在公司的IOT平台、http API服务之中,正确性、稳定性、可扩展性、性能都有保证,完全开源,请放心使用。

项目地址
码云镜像
QQ技术交流群:739352073
libhv每日一学博文

入门

环境搭建

使用的是centos7

$ git clone https://github.com/ithewei/libhv.git
$ cd libhv/
$ sudo make
$ sudo make install  # 这和CMakeList.txt里面的内容有关
mkdir -p 2>/dev/null /usr/local/include/hv
cp -r 2>/dev/null include/hv/* /usr/local/include/hv
cp -r 2>/dev/null lib/libhv.a lib/libhv.so /usr/local/lib

ps:
mkdir -p : 递归创建目录
cp -r : 递归复制

第一个工程:一个最简单的restful http服务器

接收post 请求

1、使用CLion创建一个新工程
在这里插入图片描述

2、编写CMakeList.txt

cmake_minimum_required(VERSION 3.16)
project(libhv_test)

set(CMAKE_CXX_STANDARD 11)


#添加头文件搜索路径
include_directories(/usr/local/include/hv)
#添加库文件搜索路径
link_directories(/usr/local/lib)

#用于将当前目录下的所有源文件的名字保存在变量 DIR_SRCS 中
add_executable(libhv_test main.cpp)

#在这里根据名字boost_thread去寻找libboost_thread.a文件
target_link_libraries(libhv_test -lhv)

3、编写程序

#include "HttpServer.h"
int http_api_echo(HttpRequest* req, HttpResponse* res) {
    res->body = req->body;
    return 0;
}
int main() {
    HttpService service;
    service.base_url = "/v1/api";
    service.AddApi("/echo", HTTP_POST, http_api_echo);

    http_server_t server;
    server.port = 8080;
    server.service = &service;
    http_server_run(&server);


    return 0;
}

4、curl测试

# curl -H "Content-Type:application/json" -XPOST  http://localhost:8080/v1/api/echo  -d '{ "id":"3", "provinceId":"3", "cityName":"zhuhai", "description":"live in zhuhai"}'
{ "id":"3", "provinceId":"3", "cityName":"zhuhai", "description":"live in

第二个工程:一个最简单的restful http客户端

发送post请求

1、使用CLion创建一个新工程

2、编写CMakeList.txt

和第一个工程中差不多,只是把工程名改了

3、编写程序

#include "http_client.h"

int main() {
    HttpRequest req;
    req.method = HTTP_POST;
    req.url = "http://localhost:8080/v1/api/echo";
    req.body = "hello world";
    
    HttpResponse res;
    int ret = http_client_send(&req, &res);
    if (ret != 0){
        printf("* Failed:%s:%d\n", http_client_strerror(ret), ret);
    }else{
        printf("%s\n", res.Dump(true, true).c_str());
    }
    
    
    return 0;
}

4、返回结果

HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 11
Content-Type: text/plain
Date: Fri, 17 Jul 2020 07:56:39 GMT
Server: httpd/1.20.7.17

hello world

Process finished with exit code 0

工具库json.hpp使用

这个头文件是对nlohmann的封装,可以单独拿出来使用

using namespace nlohmann;

json作为一级数据结构

如果你想创建一个这样的对象

{
  "pi": 3.141,
  "happy": true,
  "name": "Niels",
  "nothing": null,
  "answer": {
    "everything": 42
  },
  "list": [1, 0, 2],
  "object": {
    "currency": "USD",
    "value": 42.99
  }
}

可以这样写:

// 创建一个空结构(null)
json j;

// 添加一个数值,存为 double
j["pi"] = 3.141;
// 添加一个布尔值,存为 bool 
j["happy"] = true;
// 添加一个字符串,存为 std::string
j["name"] = "Niels";
// 添加另一个空对象,使用 nullptr
j["nothing"] = nullptr;
// 添加一个对象内的对象
j["answer"]["everything"] = 42;
// 添加一个数组,存为 std::vector
j["list"] = {1, 0, 2};
// 添加另一个对象
j["object"] = {{"currency", "USD"}, {"value", 42.99}};

还可以这样写

json j2 = {
  {"pi", 3.141},
  {"happy", true},
  {"name", "Niels"},
  {"nothing", nullptr},
  {"answer", {
    {"everything", 42}
  }},
  {"list", {1, 0, 2}},
  {"object", {
    {"currency", "USD"},
    {"value", 42.99}
  }}
};

json反序列化与反序列化

  1. 将字符串解析为json: 通过为字符串字面量附加_json可以创建一个json对象
    json j = "{\"happy\": true, \"pi\": 3.141}"_json;             // 从字符串字面量创建对象
    auto j2 = R"({"happy": true, "pi": 3.141})"_json;             // 最好是使用原始字符串字面量
    auto j3 = json::parse("{\"happy\": true, \"pi\": 3.141}");  // 显式地分析

    std::cout << j["happy"] <<" " <<j2["happy"] <<" " <<j3["happy"] << "\n";
  1. 将json对象转换为字符串
    json j = "{\"happy\": true, \"pi\": 3.141}"_json;

    // 显式地转换至字符串
    std::string s = j.dump();

    std::cout << s << std::endl; // {"happy":true,"pi":3.141}

// 传入缩进空格数,使用良好的打印形式进行序列化
    std::cout << j.dump(4) << std::endl;
/*
 {
    "happy": true,
    "pi": 3.141
}
 */

注意: 该库仅支持UTF-8,当你在库中存储不同编码的字符串时,调用dump可能会引发异常

  1. (反)序列化为流
// 反序列化自标准输入
json j;
std::cin >> j;

// 序列化至标准输出
std::cout << j;

// 设置良好打印形式所需的缩进
std::cout << std::setw(4) << j << std::endl;
  1. (反)序列化到文件
// 读入一个 JSON 文件
std::ifstream i("file.json");
json j;
i >> j;

// 将美化的 JSON 写入另一个文件
std::ofstream o("pretty.json");
o << std::setw(4) << j << std:endl;

像容器一样使用json

  1. 这个不知道什么意思
    std::vector<std::uint8_t> v = {'t', 'r', 'u', 'e'};

    json j = json::parse(v.begin(), v.end());
    json j1 = json::parse(v);
  1. 向容器一样访问
// 使用 push_back 创建一个数组
json j;
j.push_back("foo");
j.push_back(1);
j.push_back(true);
// 也可以使用 emplace_back
j.emplace_back(1.78);

// 在数组上进行迭代
for (json::iterator it = j.begin(); it != j.end(); ++it) {
    std::cout << *it << '\n';
}

// 基于范围的 for 循环
for (auto& element : j) {
    std::cout << element << '\n';
}

// 访问器 getter/setter
const std::string tmp = j[0];
j[1] = 42;
bool foo = j.at(2);

// 比较
j == "[\"foo\", 1, true]"_json;  // true

// 其他东东
j.size();     // 3 条
j.empty();    // false
j.type();     // json::value_t::array
j.clear();    // 数组再次为空

// 便利的类型检测器
j.is_null();
j.is_boolean();
j.is_number();
j.is_object();
j.is_array();
j.is_string();

// 创建一个 JSON 对象
json o;
o["foo"] = 23;
o["bar"] = false;
o["baz"] = 3.141;

// 也可以使用 emplace
o.emplace("weather", "sunny");

// 为对象指定迭代成员函数
for (json::iterator it = o.begin(); it != o.end(); ++it) {
  std::cout << it.key() << " : " << it.value() << "\n";
}

// 查找一个条目
if (o.find("foo") != o.end()) {
  // 找到键为 "foo" 的一个条目
}

// 使用计数 count()
int foo_present = o.count("foo");  // 1
int fob_present = o.count("fob");  // 0

// 删除条目
o.erase("foo");
  1. 任何序列化容器,包括std::arraystd::vectorstd::dequestd::forward_liststd::list,都可以用来构建JSON数组。同样,关联化容器,包括std::setstd::multisetstd::unordered_setstd::unordered_multiset,也可以用来构建JSON数组。
std::vector<int> c_vector {1, 2, 3, 4};
json j_vec(c_vector);

std::deque<double> c_deque {1.2, 2.3, 3.4, 5.6};
json j_deque(c_deque);

std::list<bool> c_list {true, true, false, true};
json j_list(c_list);

std::forward_list<int64_t> c_flist {12345678909876, 23456789098765, 34567890987654, 45678909876543};
json j_flist(c_flist);

std::array<unsigned long, 4> c_array {{1, 2, 3, 4}};
json j_array(c_array);

std::set<std::string> c_set {"one", "two", "three", "four", "one"};
json j_set(c_set);

std::unordered_set<std::string> c_uset {"one", "two", "three", "four", "one"};
json j_uset(c_uset);

std::multiset<std::string> c_mset {"one", "two", "one", "four"};
json j_mset(c_mset);

std::unordered_multiset<std::string> c_umset {"one", "two", "one", "four"};
json j_umset(c_umset);
  1. 关联键值型容器,也可以用来创建json
std::map<std::string, int> c_map {{"one", 1}, {"two", 2}, {"three", 3}};
json j_map(c_map);

std::unordered_map<const char*, double> c_umap {{"one", 1.2}, {"two", 2.3}, {"three", 3.4}};
json j_umap(c_umap);

std::multimap<std::string, bool> c_mmap {{"one", true}, {"two", true}, {"three", false}, {"three", true}};
json j_mmap(c_mmap); 

std::unordered_multimap<std::string, bool> c_ummap {{"one", true}, {"two", true}, {"three", false}, {"three", true}};
json j_ummap(c_ummap); 

隐式转换

json对象的类型是由表达式所决定的,所存储的值隐式的进行转换。比如:

// 字符串
std::string s1 = "Hello, world!";
json js = s1;
std::string s2 = js;

// 布尔值
bool b1 = true;
json jb = b1;
bool b2 = jb;

// 数值
int i = 42;
json jn = i;
double f = jn;

当然,也可以显示的请求数据

std::string vs = js.get<std::string>();
bool vb = jb.get<bool>();
int vi = jn.get<int>();

任意类型转换。

不仅仅是STL容器和标量类型,所有类型都可以序列化到JSON。比如,创建一个ns::person结果

namespace ns {
    // 一个简单的结构,为 person 建模
    struct person {
        std::string name;
        std::string address;
        int age;
    };
}

相互转换:

ns::person p = {"Ned Flanders", "744 Evergreen Terrace", 60};

// 转换至 JSON:将每个值拷贝至 JSON 对象
json j;
j["name"] = p.name;
j["address"] = p.address;
j["age"] = p.age;

// 转换自 JSON:从 JSON 对象中拷贝每个值
ns::person p {
    j["name"].get<std::string>(),
    j["address"].get<std::string>(),
    j["age"].get<int>()
};

当然,还有更好的方法:

// 创建 person 结构
ns::person p {"Ned Flanders", "744 Evergreen Terrace", 60};

// 转换:person → json
json j = p;

std::cout << j << std::endl;

// 转换:json → person
ns::person p2 = j;

要让那么的代码工作,只需要提供两个函数

namespace ns {
    void to_json(json& j, const person& p) {
        j = json{{"name", p.name}, {"address", p.address}, {"age", p.age}};
    }

    void from_json(const json& j, person& p) {
        p.name = j.at("name").get<std::string>();
        p.name = j.at("address").get<std::string>();
        p.name = j.at("age").get<int>();
    }
}

二进制格式

如果想要进行网络数据交换,必须转换为二进制格式,因此,提供了CBORMessagePack功能,使得可以将JSON数据编码成字节向量,会在将字节向量解码成JSON数组

// 创建一个 JSON 数据
json j = R"({"compact": true, "schema": 0})"_json;

// 序列化至 CBOR
std::vector<std::uint8_t> v_cbor = json::to_cbor(j);  
// v_cbor => 0xa2, 0x67, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0xf5, 0x66, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x00

// 从 CBOR 中返回 JSON
json j_from_cbor = json::from_cbor(v_cbor);

// 序列化至 MessagePack
std::vector<std::uint8_t> v_msgpack = json::to_msgpack(j);  
// v_msgpack => 0x82, 0xa7, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x63, 0x74, 0xc3, 0xa6, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x00

// 从 MessagePack 中返回 JSON
json j_from_msgpack = json::from_msgpack(v_msgpack);

参考

源码阅读

examples/hmain_test.cpp

0、作用

  • 解析命令行【后台运行。。。】
  • 解析配置文件
  • 日志
  • 多线程

1、使用CLion创建一个新工程

2、编写CMakeList.txt

和第一个工程中差不多,只是把工程名改了

3、编写程序
hmain_test.cpp

  
#include "hv.h"
#include "hmain.h"
#include "iniparser.h"

typedef struct conf_ctx_s {
    IniParser* parser;
    int loglevel;
    int worker_processes;
    int worker_threads;
    int port;
} conf_ctx_t;
conf_ctx_t g_conf_ctx;

inline void conf_ctx_init(conf_ctx_t* ctx) {
    ctx->parser = new IniParser;
    ctx->loglevel = LOG_LEVEL_DEBUG;
    ctx->worker_processes = 0;
    ctx->worker_threads = 0;
    ctx->port = 0;
}

static void print_version();
static void print_help();

static int  parse_confile(const char* confile);
static void worker_fn(void* userdata);

// short options
static const char options[] = "hvc:ts:dp:";
// long options
static const option_t long_options[] = {
    {'h', "help",       NO_ARGUMENT},
    {'v', "version",    NO_ARGUMENT},
    {'c', "confile",    REQUIRED_ARGUMENT},
    {'t', "test",       NO_ARGUMENT},
    {'s', "signal",     REQUIRED_ARGUMENT},
    {'d', "daemon",     NO_ARGUMENT},
    {'p', "port",       REQUIRED_ARGUMENT}
};
static const char detail_options[] = R"(
  -h|--help                 Print this information
  -v|--version              Print version
  -c|--confile <confile>    Set configure file, default etc/{program}.conf
  -t|--test                 Test Configure file and exit
  -s|--signal <signal>      Send <signal> to process,
                            <signal>=[start,stop,restart,status,reload]
  -d|--daemon               Daemonize
  -p|--port <port>          Set listen port
)";

void print_version() {
    printf("%s version %s\n", g_main_ctx.program_name, hv_compile_version());
}

void print_help() {
    printf("Usage: %s [%s]\n", g_main_ctx.program_name, options);
    printf("Options:\n%s\n", detail_options);
}

int parse_confile(const char* confile) {
    int ret = g_conf_ctx.parser->LoadFromFile(confile);
    if (ret != 0) {
        printf("Load confile [%s] failed: %d\n", confile, ret);
        exit(-40);
    }

    // logfile
    string str = g_conf_ctx.parser->GetValue("logfile");
    if (!str.empty()) {
        strncpy(g_main_ctx.logfile, str.c_str(), sizeof(g_main_ctx.logfile));
    }
    hlog_set_file(g_main_ctx.logfile);
    // loglevel
    const char* szLoglevel = g_conf_ctx.parser->GetValue("loglevel").c_str();
    int loglevel = LOG_LEVEL_INFO;
    if (stricmp(szLoglevel, "VERBOSE") == 0) {
        loglevel = LOG_LEVEL_VERBOSE;
    } else if (stricmp(szLoglevel, "DEBUG") == 0) {
        loglevel = LOG_LEVEL_DEBUG;
    } else if (stricmp(szLoglevel, "INFO") == 0) {
        loglevel = LOG_LEVEL_INFO;
    } else if (stricmp(szLoglevel, "WARN") == 0) {
        loglevel = LOG_LEVEL_WARN;
    } else if (stricmp(szLoglevel, "ERROR") == 0) {
        loglevel = LOG_LEVEL_ERROR;
    } else if (stricmp(szLoglevel, "FATAL") == 0) {
        loglevel = LOG_LEVEL_FATAL;
    } else if (stricmp(szLoglevel, "SILENT") == 0) {
        loglevel = LOG_LEVEL_SILENT;
    } else {
        loglevel = LOG_LEVEL_INFO;
    }
    g_conf_ctx.loglevel = loglevel;
    hlog_set_level(loglevel);
    // log_filesize
    str = g_conf_ctx.parser->GetValue("log_filesize");
    if (!str.empty()) {
        int num = atoi(str.c_str());
        if (num > 0) {
            // 16 16M 16MB
            const char* p = str.c_str() + str.size() - 1;
            char unit;
            if (*p >= '0' && *p <= '9') unit = 'M';
            else if (*p == 'B')         unit = *(p-1);
            else                        unit = *p;
            unsigned long long filesize = num;
            switch (unit) {
            case 'K': filesize <<= 10; break;
            case 'M': filesize <<= 20; break;
            case 'G': filesize <<= 30; break;
            default:  filesize <<= 20; break;
            }
            hlog_set_max_filesize(filesize);
        }
    }
    // log_remain_days
    str = g_conf_ctx.parser->GetValue("log_remain_days");
    if (!str.empty()) {
        hlog_set_remain_days(atoi(str.c_str()));
    }
    // log_fsync
    str = g_conf_ctx.parser->GetValue("log_fsync");
    if (!str.empty()) {
        logger_enable_fsync(hlog, getboolean(str.c_str()));
    }
    // first log here
    hlogi("%s version: %s", g_main_ctx.program_name, hv_compile_version());
    hlog_fsync();

    // worker_processes
    int worker_processes = 0;
    str = g_conf_ctx.parser->GetValue("worker_processes");
    if (str.size() != 0) {
        if (strcmp(str.c_str(), "auto") == 0) {
            worker_processes = get_ncpu();
            hlogd("worker_processes=ncpu=%d", worker_processes);
        }
        else {
            worker_processes = atoi(str.c_str());
        }
    }
    g_conf_ctx.worker_processes = LIMIT(0, worker_processes, MAXNUM_WORKER_PROCESSES);
    // worker_threads
    int worker_threads = g_conf_ctx.parser->Get<int>("worker_threads");
    g_conf_ctx.worker_threads = LIMIT(0, worker_threads, 16);

    // port
    int port = 0;
    const char* szPort = get_arg("p");
    if (szPort) {
        port = atoi(szPort);
    }
    if (port == 0) {
        port = g_conf_ctx.parser->Get<int>("port");
    }
    if (port == 0) {
        printf("Please config listen port!\n");
        exit(-10);
    }
    g_conf_ctx.port = port;

    hlogi("parse_confile('%s') OK", confile);
    return 0;
}

void master_init(void* userdata) {
#ifdef OS_UNIX
    char proctitle[256] = {0};
    snprintf(proctitle, sizeof(proctitle), "%s: master process", g_main_ctx.program_name);
    setproctitle(proctitle);
    signal(SIGNAL_RELOAD, signal_handler);
#endif
}

void worker_init(void* userdata) {
#ifdef OS_UNIX
    char proctitle[256] = {0};
    snprintf(proctitle, sizeof(proctitle), "%s: worker process", g_main_ctx.program_name);
    setproctitle(proctitle);
    signal(SIGNAL_RELOAD, signal_handler);
#endif
}

static void on_reload(void* userdata) {
    hlogi("reload confile [%s]", g_main_ctx.confile);
    parse_confile(g_main_ctx.confile);
}

int main(int argc, char** argv) {
    // g_main_ctx
    main_ctx_init(argc, argv);
    if (argc == 1) {
        print_help();
        exit(10);
    }
    // int ret = parse_opt(argc, argv, options);
    int ret = parse_opt_long(argc, argv, long_options, ARRAY_SIZE(long_options));
    if (ret != 0) {
        print_help();
        exit(ret);
    }

    printf("---------------arg------------------------------\n");
    printf("%s\n", g_main_ctx.cmdline);
    for (auto& pair : g_main_ctx.arg_kv) {
        printf("%s=%s\n", pair.first.c_str(), pair.second.c_str());
    }
    for (auto& item : g_main_ctx.arg_list) {
        printf("%s\n", item.c_str());
    }
    printf("================================================\n");

    printf("---------------env------------------------------\n");
    for (auto& pair : g_main_ctx.env_kv) {
        printf("%s=%s\n", pair.first.c_str(), pair.second.c_str());
    }
    printf("================================================\n");

    // help
    if (get_arg("h")) {
        print_help();
        exit(0);
    }

    // version
    if (get_arg("v")) {
        print_version();
        exit(0);
    }

    // g_conf_ctx
    conf_ctx_init(&g_conf_ctx);
    const char* confile = get_arg("c");
    if (confile) {
        strncpy(g_main_ctx.confile, confile, sizeof(g_main_ctx.confile));
    }
    parse_confile(g_main_ctx.confile);

    // test
    if (get_arg("t")) {
        printf("Test confile [%s] OK!\n", g_main_ctx.confile);
        exit(0);
    }

    // signal
    signal_init(on_reload);
    const char* signal = get_arg("s");
    if (signal) {
        signal_handle(signal);
    }

#ifdef OS_UNIX
    // daemon
    if (get_arg("d")) {
        // nochdir, noclose
        int ret = daemon(1, 1);
        if (ret != 0) {
            printf("daemon error: %d\n", ret);
            exit(-10);
        }
        // parent process exit after daemon, so pid changed.
        g_main_ctx.pid = getpid();
    }
#endif

    // pidfile
    create_pidfile();

    master_workers_run(worker_fn, (void*)(intptr_t)100L, g_conf_ctx.worker_processes, g_conf_ctx.worker_threads);

    return 0;
}

void worker_fn(void* userdata) {
    long num = (long)(intptr_t)(userdata);
    while (1) {
        printf("num=%ld pid=%ld tid=%ld\n", num, hv_getpid(), hv_gettid());
        hv_delay(10000);
    }
}

4、编写配置文件
libhv_test.conf

logfile=log
#worker_threads=4
worker_processes=auto
log_remain_days=30

5、编译并且命令行运行

$ ./libhv_test -c libhv_test.conf  -p 8081

多线程

#include <iostream>
#include "hthread.h"
#include "hmutex.h"
#include "htime.h"

hmutex_t mutex;
hcondvar_t cv;

int i = 1; //global

HTHREAD_ROUTINE(test_condvar1) {
    while (i <= 9){
        hmutex_lock(&mutex);
        printf("call test_condvar1, id = %lu , i = %d\n",  pthread_self(), i);
        if(i %3 != 0){ // 3的倍数,prinft
            hcondvar_wait(&cv, &mutex);  // 不是3的倍数,那就等待,直到通知是3的倍数
            std::cout << "test_condvar1: " << i << "\n";
        }
        hmutex_unlock(&mutex);
        sleep(1);
    }
}
HTHREAD_ROUTINE(test_condvar2) {
    for(;i <= 9; i++){
        hmutex_lock(&mutex);
        printf("call test_condvar2, id = %lu , i = %d\n",  pthread_self(), i);
        if(i %3 == 0){
            hcondvar_signal(&cv);  // 当前线程并没有休眠/阻塞,还会执行下面的
        }else{
            std::cout << "test_condvar2: " << i << "\n";
        }
        hmutex_unlock(&mutex);
        sleep(1);
    }
}

int main(int argc, char* argv[]) {
    hmutex_init(&mutex);
    hcondvar_init(&cv);


    printf("call main id = %lu \n",  pthread_self());

    hthread_t thread_condvar1 = hthread_create(test_condvar1, NULL);
    hthread_t thread_condvar2 = hthread_create(test_condvar2, NULL);


    hthread_join(thread_condvar2);

    hmutex_destroy(&mutex);
    hcondvar_destroy(&cv);

    return 0;
}
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐