天天看點

手把手教你 用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之前的資料。