Add a reverse proxy example (#309)

This commit is contained in:
An Tao 2019-12-08 08:31:56 +08:00 committed by GitHub
parent 6571e55631
commit ea43d8127d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 411 additions and 0 deletions

View File

@ -6,6 +6,8 @@ The following examples can help you understand how to use Drogon:
2. [client_example](https://github.com/an-tao/drogon/tree/master/examples/client_example/main.cc) - A client example.
3. [simple_example](https://github.com/an-tao/drogon/tree/master/examples/simple_example) - A simple example showing how to create a web application using Drogon.
4. [simple_example_test](https://github.com/an-tao/drogon/tree/master/examples/simple_example_test) - Some tests for the `simple_example`.
5. [simple_reverse_proxy](https://github.com/an-tao/drogon/tree/master/examples/simple_reverse_proxy)
- A Example showing how to use drogon as a http reverse proxy with a simple round robin.
### [TechEmpower Framework Benchmarks](https://github.com/TechEmpower/FrameworkBenchmarks) test suite

View File

@ -0,0 +1,36 @@
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
build
cmake-build-debug
.idea

View File

@ -0,0 +1,56 @@
cmake_minimum_required (VERSION 3.5)
project(simple_reverse_proxy CXX)
include(CheckIncludeFileCXX)
check_include_file_cxx(any HAS_ANY)
check_include_file_cxx(string_view HAS_STRING_VIEW)
if(HAS_ANY AND HAS_STRING_VIEW)
set(CMAKE_CXX_STANDARD 17)
else()
set(CMAKE_CXX_STANDARD 14)
endif()
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_executable(simple_reverse_proxy main.cc)
##########
# If you include the drogon source code locally in your project, use this method to add drogon
# add_subdirectory(drogon)
# target_link_libraries(simple_reverse_proxy PRIVATE drogon)
##########
find_package(Drogon CONFIG REQUIRED)
target_link_libraries(simple_reverse_proxy PRIVATE Drogon::Drogon)
if(CMAKE_CXX_STANDARD LESS 17)
#With C++14, use boost to support any and string_view
message(STATUS "use c++14")
find_package(Boost 1.61.0 REQUIRED)
target_include_directories(simple_reverse_proxy PRIVATE ${Boost_INCLUDE_DIRS})
else()
message(STATUS "use c++17")
endif()
aux_source_directory(controllers CTL_SRC)
aux_source_directory(filters FILTER_SRC)
aux_source_directory(plugins PLUGIN_SRC)
aux_source_directory(models MODEL_SRC)
file(GLOB SCP_LIST ${CMAKE_CURRENT_SOURCE_DIR}/views/*.csp)
foreach(cspFile ${SCP_LIST})
message(STATUS "cspFile:" ${cspFile})
EXEC_PROGRAM(basename ARGS "${cspFile} .csp" OUTPUT_VARIABLE classname)
message(STATUS "view classname:" ${classname})
ADD_CUSTOM_COMMAND(OUTPUT ${classname}.h ${classname}.cc
COMMAND drogon_ctl
ARGS create view ${cspFile}
DEPENDS ${cspFile}
VERBATIM )
set(VIEWSRC ${VIEWSRC} ${classname}.cc)
endforeach()
target_include_directories(simple_reverse_proxy PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/models)
target_sources(simple_reverse_proxy PRIVATE ${SRC_DIR} ${CTL_SRC} ${FILTER_SRC} ${VIEWSRC} ${PLUGIN_SRC} ${MODEL_SRC})

View File

@ -0,0 +1,3 @@
### A Example showing how to use drogon as a http reverse proxy with a simple round robin.
This project is created by drogon_ctl command, please compile it after installing drogon.

View File

@ -0,0 +1,154 @@
/* This is a JSON format configuration file
*/
{
/*
//ssl:The global ssl files setting
"ssl": {
"cert": "../../trantor/trantor/tests/server.pem",
"key": "../../trantor/trantor/tests/server.pem"
},*/
"listeners": [{
//address: Ip address,0.0.0.0 by default
"address": "0.0.0.0",
//port: Port number
"port": 8088,
//https: If true, use https for security,false by default
"https": false
}],
"app": {
//threads_num: The number of IO threads, 1 by default, if the value is set to 0, the number of threads
//is the number of CPU cores
"threads_num": 0,
//enable_session: False by default
"enable_session": false,
"session_timeout": 0,
//document_root: Root path of HTTP document, defaut path is ./
"document_root": "./",
//home_page: Set the HTML file of the home page, the default value is "index.html"
//If there isn't any handler registered to the path "/", the home page file in the "document_root" is send to clients as a response
//to the request for "/".
"home_page": "index.html",
//static_file_headers: Headers for static files
/*"static_file_headers": [
{
"name": "field-name",
"value": "field-value"
}
],*/
//upload_path: The path to save the uploaded file. "uploads" by default.
//If the path isn't prefixed with /, ./ or ../,
//it is relative path of document_root path
"upload_path": "uploads",
/* file_types:
* HTTP download file types,The file types supported by drogon
* by default are "html", "js", "css", "xml", "xsl", "txt", "svg",
* "ttf", "otf", "woff2", "woff" , "eot", "png", "jpg", "jpeg",
* "gif", "bmp", "ico", "icns", etc. */
"file_types": [
"gif",
"png",
"jpg",
"js",
"css",
"html",
"ico",
"swf",
"xap",
"apk",
"cur",
"xml"
],
//max_connections: maximum connections number,100000 by default
"max_connections": 100000,
//max_connections_per_ip: maximum connections number per clinet,0 by default which means no limit
"max_connections_per_ip": 0,
//Load_dynamic_views: False by default, when set to true, drogon
//compiles and loads dynamically "CSP View Files" in directories defined
//by "dynamic_views_path"
"load_dynamic_views": false,
//dynamic_views_path: If the path isn't prefixed with /, ./ or ../,
//it is relative path of document_root path
"dynamic_views_path": [
"./views"
],
//log: Set log output, drogon output logs to stdout by default
"log": {
//log_path: Log file path,empty by default,in which case,logs are output to the stdout
//"log_path": "./",
//logfile_base_name: Log file base name,empty by default which means drogon names logfile as
//drogon.log ...
"logfile_base_name": "",
//log_size_limit: 100000000 bytes by default,
//When the log file size reaches "log_size_limit", the log file is switched.
"log_size_limit": 100000000,
//log_level: "DEBUG" by default,options:"TRACE","DEBUG","INFO","WARN"
//The TRACE level is only valid when built in DEBUG mode.
"log_level": "DEBUG"
},
//run_as_daemon: False by default
"run_as_daemon": false,
//relaunch_on_error: False by default, if true, the program will be restart by the parent after exiting;
"relaunch_on_error": false,
//use_sendfile: True by default, if ture, the program
//uses sendfile() system-call to send static files to clients;
"use_sendfile": true,
//use_gzip: True by default, use gzip to compress the response body's content;
"use_gzip": true,
//static_files_cache_time: 5 (seconds) by default, the time in which the static file response is cached,
//0 means cache forever, the negative value means no cache
"static_files_cache_time": 5,
//idle_connection_timeout: Defaults to 60 seconds, the lifetime
//of the connection without read or write
"idle_connection_timeout": 60,
//server_header_field: Set the 'Server' header field in each response sent by drogon,
//empty string by default with which the 'Server' header field is set to "Server: drogon/version string\r\n"
"server_header_field": "",
//enable_server_header: Set true to force drogon to add a 'Server' header to each HTTP response. The default
//value is true.
"enable_server_header": false,
//enable_date_header: Set true to force drogon to add a 'Date' header to each HTTP response. The default
//value is true.
"enable_date_header": false,
//keepalive_requests: Set the maximum number of requests that can be served through one keep-alive connection.
//After the maximum number of requests are made, the connection is closed.
//The default value of 0 means no limit.
"keepalive_requests": 0,
//pipelining_requests: Set the maximum number of unhandled requests that can be cached in pipelining buffer.
//After the maximum number of requests are made, the connection is closed.
//The default value of 0 means no limit.
"pipelining_requests": 0,
//gzip_static: If it is set to true, when the client requests a static file, drogon first finds the compressed
//file with the extension ".gz" in the same path and send the compressed file to the client.
//The default value of gzip_static is true.
"gzip_static": true,
//client_max_body_size: Set the maximum body size of HTTP requests received by drogon. The default value is "1M".
//One can set it to "1024", "1k", "10M", "1G", etc. Setting it to "" means no limit.
"client_max_body_size": "1M",
//max_memory_body_size: Set the maximum body size in memory of HTTP requests received by drogon. The default value is "64K" bytes.
//If the body size of a HTTP request exceeds this limit, the body is stored to a temporary file for processing.
//Setting it to "" means no limit.
"client_max_memory_body_size": "64K",
//client_max_websocket_message_size: Set the maximum size of messages sent by WebSocket client. The default value is "128K".
//One can set it to "1024", "1k", "10M", "1G", etc. Setting it to "" means no limit.
"client_max_websocket_message_size": "128K"
},
//plugins: Define all plugins running in the application
"plugins": [{
//name: The class name of the plugin
"name": "my_plugin::SimpleReverseProxy",
//dependencies: Plugins that the plugin depends on. It can be commented out
"dependencies": [],
//config: The configuration of the plugin. This json object is the parameter to initialize the plugin.
//It can be commented out
"config": {
"pipelining": 16,
"backends": ["http://127.0.0.1:8848", "https://localhost:8849"],
"same_client_to_same_backend": {
"enabled": false,
"cache_timeout": 3600
}
}
}],
//custom_config: custom configuration for users. This object can be get by the app().getCustomConfig() method.
"custom_config": {}
}

View File

@ -0,0 +1,9 @@
#include <drogon/drogon.h>
int main()
{
// Set HTTP listener address and port
drogon::app().loadConfigFile("../config.json");
// Run HTTP framework,the method will block in the internal event loop
drogon::app().run();
return 0;
}

View File

@ -0,0 +1,107 @@
/**
*
* plugin_SimpleReverseProxy.cc
*
*/
#include "SimpleReverseProxy.h"
using namespace drogon;
using namespace my_plugin;
void SimpleReverseProxy::initAndStart(const Json::Value &config)
{
/// Initialize and start the plugin
if (config.isMember("backends") && config["backends"].isArray())
{
for (auto &backend : config["backends"])
{
backendAddrs_.emplace_back(backend.asString());
}
if (backendAddrs_.empty())
{
LOG_ERROR << "You must set at least one backend";
abort();
}
}
else
{
LOG_ERROR << "Error in configuration";
abort();
}
pipeliningDepth_ = config.get("pipelining", 0).asInt();
if (config.isMember("same_client_to_same_backend"))
{
sameClientToSameBackend_ = config["same_client_to_same_backend"]
.get("enabled", false)
.asBool();
cacheTimeout_ = config["same_client_to_same_backend"]
.get("cache_timeout", 0)
.asInt();
}
if (sameClientToSameBackend_)
{
clientMap_ = std::make_unique<drogon::CacheMap<std::string, size_t>>(
app().getLoop());
}
clients_.init(
[this](std::vector<HttpClientPtr> &clients, size_t ioLoopIndex) {
clients.resize(backendAddrs_.size());
});
clientIndex_.init(
[this](size_t &index, size_t ioLoopIndex) { index = ioLoopIndex; });
drogon::app().registerPreRoutingAdvice([this](const HttpRequestPtr &req,
AdviceCallback &&callback,
AdviceChainCallback &&pass) {
preRouting(req, std::move(callback), std::move(pass));
});
}
void SimpleReverseProxy::shutdown()
{
}
void SimpleReverseProxy::preRouting(const HttpRequestPtr &req,
AdviceCallback &&callback,
AdviceChainCallback &&)
{
size_t index;
auto &clientsVector = *clients_;
if (sameClientToSameBackend_)
{
std::lock_guard<std::mutex> lock(mapMutex_);
auto ip = req->peerAddr().toIp();
if (!clientMap_->findAndFetch(ip, index))
{
index = (++*clientIndex_) % clientsVector.size();
clientMap_->insert(ip, index, cacheTimeout_);
}
}
else
{
index = ++(*clientIndex_) % clientsVector.size();
}
auto &clientPtr = clientsVector[index];
if (!clientPtr)
{
auto &addr = backendAddrs_[index];
clientPtr = HttpClient::newHttpClient(
addr, trantor::EventLoop::getEventLoopOfCurrentThread());
clientPtr->setPipeliningDepth(pipeliningDepth_);
}
clientPtr->sendRequest(
req,
[callback = std::move(callback)](ReqResult result,
const HttpResponsePtr &resp) {
if (result == ReqResult::Ok)
{
callback(resp);
}
else
{
auto errResp = HttpResponse::newHttpResponse();
errResp->setStatusCode(k500InternalServerError);
callback(errResp);
}
});
}

View File

@ -0,0 +1,44 @@
/**
*
* plugin_SimpleReverseProxy.h
*
*/
#pragma once
#include <drogon/plugins/Plugin.h>
#include <drogon/drogon.h>
#include <vector>
#include <memory>
namespace my_plugin
{
class SimpleReverseProxy : public drogon::Plugin<SimpleReverseProxy>
{
public:
SimpleReverseProxy()
{
}
/// This method must be called by drogon to initialize and start the plugin.
/// It must be implemented by the user.
virtual void initAndStart(const Json::Value &config) override;
/// This method must be called by drogon to shutdown the plugin.
/// It must be implemented by the user.
virtual void shutdown() override;
private:
// Create a HTTP client for every backend in every IO event loop.
drogon::IOThreadStorage<std::vector<drogon::HttpClientPtr>> clients_;
drogon::IOThreadStorage<size_t> clientIndex_{0};
std::vector<std::string> backendAddrs_;
bool sameClientToSameBackend_{false};
size_t cacheTimeout_{0};
std::mutex mapMutex_;
size_t pipeliningDepth_{0};
void preRouting(const drogon::HttpRequestPtr &,
drogon::AdviceCallback &&,
drogon::AdviceChainCallback &&);
std::unique_ptr<drogon::CacheMap<std::string, size_t>> clientMap_;
};
} // namespace my_plugin