diff --git a/examples/README.md b/examples/README.md index 6e49696c..44bff420 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/simple_reverse_proxy/.gitignore b/examples/simple_reverse_proxy/.gitignore new file mode 100644 index 00000000..49953a91 --- /dev/null +++ b/examples/simple_reverse_proxy/.gitignore @@ -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 diff --git a/examples/simple_reverse_proxy/CMakeLists.txt b/examples/simple_reverse_proxy/CMakeLists.txt new file mode 100644 index 00000000..b78c56cb --- /dev/null +++ b/examples/simple_reverse_proxy/CMakeLists.txt @@ -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}) diff --git a/examples/simple_reverse_proxy/README.md b/examples/simple_reverse_proxy/README.md new file mode 100644 index 00000000..62294b90 --- /dev/null +++ b/examples/simple_reverse_proxy/README.md @@ -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. \ No newline at end of file diff --git a/examples/simple_reverse_proxy/config.json b/examples/simple_reverse_proxy/config.json new file mode 100644 index 00000000..ae4dbd8d --- /dev/null +++ b/examples/simple_reverse_proxy/config.json @@ -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": {} +} \ No newline at end of file diff --git a/examples/simple_reverse_proxy/main.cc b/examples/simple_reverse_proxy/main.cc new file mode 100644 index 00000000..6e02e086 --- /dev/null +++ b/examples/simple_reverse_proxy/main.cc @@ -0,0 +1,9 @@ +#include +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; +} diff --git a/examples/simple_reverse_proxy/plugins/SimpleReverseProxy.cc b/examples/simple_reverse_proxy/plugins/SimpleReverseProxy.cc new file mode 100644 index 00000000..63e0e2b2 --- /dev/null +++ b/examples/simple_reverse_proxy/plugins/SimpleReverseProxy.cc @@ -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>( + app().getLoop()); + } + clients_.init( + [this](std::vector &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 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); + } + }); +} \ No newline at end of file diff --git a/examples/simple_reverse_proxy/plugins/SimpleReverseProxy.h b/examples/simple_reverse_proxy/plugins/SimpleReverseProxy.h new file mode 100644 index 00000000..3d4d4fb7 --- /dev/null +++ b/examples/simple_reverse_proxy/plugins/SimpleReverseProxy.h @@ -0,0 +1,44 @@ +/** + * + * plugin_SimpleReverseProxy.h + * + */ + +#pragma once + +#include +#include +#include +#include + +namespace my_plugin +{ +class SimpleReverseProxy : public drogon::Plugin +{ + 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> clients_; + drogon::IOThreadStorage clientIndex_{0}; + std::vector 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> clientMap_; +}; +} // namespace my_plugin