天天看点

手把手教你 用C++实现一个 可持久化 的http_server

前言

本文介绍一个有趣的 通过C++实现的 持久化的http_server demo,这样我们通过http通信之后的数据可以持久化存储,即使server挂了,数据也不会丢失。我们的http_sever 也就能够真正得作为一个后端server了。

本身持久化这个能力是数据库提供的,像通用的http交互数据都会通过SQL server或者MySQL这样的存储系统来存储关系型数据,而这里我只是调度了一个单机存储引擎作为持久化存储。

主要用到的一些技术:

  1. mongoose C语言 网络通信库,在此库基础上实现了一个C++的httpserver
  2. Rocksdb 单机存储引擎,作为持久化通信数据的存储。
  3. 通过模版工厂来优雅得创建http url 和 对应其操作的实现。

代码地址:​​PersistentHttpserver​​

实现过程

1. HTTPSERVER C++封装

这里mongoose的通信库基本都已经实现了http应用层及以下的通信接口的封装,包括接受http协议的数据并解析或者封装成http请求并发送,我这里需要做的仅仅是做一些接口调用使用C++实现就可以了。

基本接口如下:

class HttpServer {
 public:
  HttpServer() {}
  ~HttpServer() {
    Close();
    if (kv_engine_) {
      delete kv_engine_;
      kv_engine_  = nullptr;
    }
  }
  void Init(const std::string &port); // Init some variable
  bool Start(); // Start a http server with a port
  bool Close();
  // Send a message as a http request
  static void SendHttpRsp(mg_connection *connection, std::string rsp);

  static mg_serve_http_opts s_server_option;
  static KVEngine *kv_engine_; // For persistent the data

private:
  // Listen the event on the port
  static void OnHttpEvent(mg_connection *connection, int event_type,
                          void *event_data);
  // Handle the http request with the definite url.
  static void HandleHttpEvent(mg_connection *connection,
                              http_message *http_req);

  std::string m_port_;
  mg_mgr m_mgr_;
};      

2. Rocksdb单机引擎使用

大家需要高级功能可以扩展,这里仅仅是使用了一些基本的接口调度起了rocksdb

#pragma once

#include <iostream>
#include <string>

#include "rocksdb/db.h"
#include "rocksdb/options.h"

class KVEngine {
 public:
  KVEngine(std::string path) : path_(path) {}
  ~KVEngine() {
    if (db_) {
      delete db_;
      db_ = nullptr;
    }
  }
  void Init() {
    opt_.create_if_missing = true;
    if (Open() != "ok") {
      std::cout << "Open db failed " << std::endl;
    }
  }

  std::string Open() {
    auto s = rocksdb::DB::Open(opt_, path_, &db_);
    if (!s.ok()) {
      return s.ToString();
    }
    return "ok";
  }

  std::string Get(const std::string& key) {
    if (nullptr == db_) {
      return "db_ is nullptr, please init it.";
    }

    std::string tmp_val;
    auto s = db_->Get(rocksdb::ReadOptions(), key, &tmp_val);
    if (!s.ok()) {
      return "not ok";
    }

    return tmp_val;
  }

  std::string Put(const std::string& key, const std::string& val) {
    if (nullptr == db_) {
      return "db_ is nullptr, please init it.";
    }

    auto s = db_->Put(rocksdb::WriteOptions(), key, val);
    if (!s.ok()) {
      std::cout << "Put failed " << s.ToString() << std::endl;
      return s.ToString();
    }

    return "ok";
  }

 private:
  std::string path_;
  rocksdb::DB* db_;
  rocksdb::Options opt_;
}      

3. 模版工厂来创建 url 及其 handler

通过如下模版工厂,我们后续增加更多的URL的时候,不需要更改httpserver.cpp源码 ,仅仅需要增加一个扩展类 及其 实现,并将这个映射添加到全局映射表中就可以了。

template <class OperationType_t>
class OperationRegister {
public:
  virtual OperationType_t* CreateOperation(
  const std::string& op_name, mg_connection* conn, http_message* hm) = 0;

protected:
  OperationRegister() = default;
  virtual ~OperationRegister() = default;
};

// Factory class template
template <class OperationType_t>
class OperationFactory {
public:
  // Single pattern of the factory
  static OperationFactory<OperationType_t>& Instance() {
    static OperationFactory<OperationType_t> instance;
    return instance;
  }

  void RegisterOperation(const std::string& op_name,
             mg_connection* conn, http_message* hm,
             OperationRegister<OperationType_t>* reg) {
    operationRegister[op_name] = reg;
  }

  OperationType_t* GetOperation(
  const std::string& op_name,
  mg_connection* conn, http_message* hm) {
    if (operationRegister.find(op_name) != operationRegister.end()) {
      return operationRegister[op_name]->CreateOperation(op_name, conn, hm);
    }

    return nullptr;
  }

private:
  // We don't allow to constructor, copy constructor and align constructor
  OperationFactory() = default;
  ~OperationFactory() = default;

  OperationFactory(const OperationFactory&) = delete;
  const OperationFactory& operator= (const OperationFactory&) = delete;

  std::map<std::string, OperationRegister<OperationType_t>* > operationRegister;
};

// An template class to create the detail Operation
template <class OperationType_t, class OperationImpl_t>
class OperationImplRegister : public OperationRegister<OperationType_t> {
public:
  explicit OperationImplRegister(
  const std::string& op_name, mg_connection* conn, http_message* hm) {
    OperationFactory<OperationType_t>::Instance().RegisterOperation(op_name, conn, hm, this);
  }

  OperationType_t* CreateOperation(
  const std::string& op_name, mg_connection* conn, http_message* hm) {
    return new OperationImpl_t(op_name, conn, hm);
  }
};      

后续仅仅需要将对应的URL 字符串 及其实现类添加到如下映射表中就可以了。

// Register all the http request's input string and their Class pointer.
void InitializeAllOp(mg_connection* conn, http_message* hm) {
  static bool initialize = false;
  if (!initialize) {
    static OperationImplRegister<Url, GetValue>
    getValue("/test/getvalue", conn, hm);
    static OperationImplRegister<Url, SetValueUrl>
    setValue("/test/setvalue", conn, hm);
    static OperationImplRegister<Url, RouteUrl>
    routeUrl("/", conn, hm);
    initialize = true;
  }
}      

我们在实际​

​HandleHttpEvent​

​逻辑中就不需要做任何更改,十分友好得提升了代码得可扩展性。

void HttpServer::HandleHttpEvent(mg_connection *connection, http_message *http_req) {
  std::string req_str = std::string(http_req->message.p, http_req->message.len);
  std::string url = std::string(http_req->uri.p, http_req->uri.len);
  InitializeAllOp(connection, http_req);

  // Register the operation for the url
  auto *judge = new JudgeOperation(connection, http_req);
  auto res = judge->Judge(url);
  if (res != "ok") {
    SendHttpRsp(connection, res);
  }
}      

关于模版工厂的细节可以参考:​​C++ 通过模版工厂实现 简单反射机制​​

编译及使用

1. 编译

编译之前需要确保测试环境已经成功安装了rocksdb。

git clone https://github.com/BaronStack/PersistentHttpserver.git
cd PersistentHttpserver
make httpserver      

rocksdb的on mac安装:​

​brew install rocksdb​

rocksdb的on linux安装:​​rocksdb-Install​​

2. 使用

  • 第一个console : ​

    ​./httpserver​

  • 第二个console:
╰─$ curl -d "value=firstvalue" 127.0.0.1:7999/test/setvalue
{ "result": ok }      

设置了一个数值之后可以看到httpserver运行的目录处 生成了一个db目录:

db
|-- 000010.sst
|-- 000013.sst
|-- 000016.sst
|-- 000019.sst
|-- 000025.sst
|-- 000030.log
|-- CURRENT
|-- IDENTITY
|-- LOCK
|-- LOG
|-- MANIFEST-000029
|-- OPTIONS-000029
`-- OPTIONS-000032      

停止第一个./httpserver 进程,重新运行,在第二个终端再此输入获取数据的请求命令

╰─$ curl -d "value=firstvalue" 127.0.0.1:7999/test/getvalue
{ "result": firstvalue }      

可以看到能够获取到重启server之前的数据。