复现已有算法
This commit is contained in:
+72
@@ -0,0 +1,72 @@
|
||||
link_libraries(${PROJECT_NAME})
|
||||
|
||||
set(benchmark_sources benchmark/BenchmarkCtrl.cc benchmark/JsonCtrl.cc
|
||||
benchmark/main.cc)
|
||||
|
||||
add_executable(client client_example/main.cc)
|
||||
add_executable(websocket_client websocket_client/WebSocketClient.cc)
|
||||
add_executable(websocket_server websocket_server/WebSocketServer.cc)
|
||||
add_executable(benchmark ${benchmark_sources})
|
||||
add_executable(helloworld helloworld/main.cc
|
||||
helloworld/HelloController.cc
|
||||
helloworld/HelloViewController.cc)
|
||||
drogon_create_views(helloworld
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/helloworld
|
||||
${CMAKE_CURRENT_BINARY_DIR})
|
||||
add_executable(file_upload file_upload/file_upload.cc)
|
||||
drogon_create_views(file_upload
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/file_upload
|
||||
${CMAKE_CURRENT_BINARY_DIR})
|
||||
add_executable(login_session login_session/main.cc)
|
||||
drogon_create_views(login_session
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/login_session
|
||||
${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
add_executable(jsonstore jsonstore/main.cc)
|
||||
|
||||
add_executable(redis_simple redis/main.cc
|
||||
redis/controllers/Client.cc
|
||||
redis/controllers/WsClient.cc)
|
||||
|
||||
add_executable(redis_chat redis_chat/main.cc
|
||||
redis_chat/controllers/Chat.cc)
|
||||
|
||||
add_executable(async_stream async_stream/main.cc
|
||||
async_stream/RequestStreamExampleCtrl.cc)
|
||||
|
||||
add_executable(cors cors/main.cc)
|
||||
|
||||
set(example_targets
|
||||
benchmark
|
||||
client
|
||||
websocket_client
|
||||
websocket_server
|
||||
helloworld
|
||||
file_upload
|
||||
login_session
|
||||
jsonstore
|
||||
redis_simple
|
||||
redis_chat
|
||||
async_stream
|
||||
cors)
|
||||
|
||||
foreach(target ${example_targets})
|
||||
set_target_properties(${target} PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin)
|
||||
endforeach()
|
||||
|
||||
install(TARGETS ${example_targets}
|
||||
RUNTIME DESTINATION ${INSTALL_BIN_DIR})
|
||||
|
||||
# Add warnings for our example targets--some warnings (such as -Wunused-parameter) only appear
|
||||
# when the templated functions are instantiated at their point of use.
|
||||
if(NOT ${CMAKE_PLATFORM_NAME} STREQUAL "Windows" AND CMAKE_CXX_COMPILER_ID MATCHES GNU)
|
||||
foreach(target ${example_targets})
|
||||
target_compile_options(${target} PRIVATE -Wall -Wextra -Werror)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
set_property(TARGET ${example_targets}
|
||||
PROPERTY CXX_STANDARD ${DROGON_CXX_STANDARD})
|
||||
set_property(TARGET ${example_targets} PROPERTY CXX_STANDARD_REQUIRED ON)
|
||||
set_property(TARGET ${example_targets} PROPERTY CXX_EXTENSIONS OFF)
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Drogon Examples
|
||||
|
||||
The following examples can help you understand how to use Drogon:
|
||||
|
||||
1. [helloworld](https://github.com/drogonframework/drogon/tree/master/examples/helloworld) - The multiple ways of "Hello, World!"
|
||||
2. [client_example](https://github.com/drogonframework/drogon/tree/master/examples/client_example/main.cc) - A client example
|
||||
3. [websocket_client](https://github.com/drogonframework/drogon/tree/master/examples/websocket_client/WebSocketClient.cc) - An example on how to use the WebSocket client
|
||||
4. [login_session](https://github.com/drogonframework/drogon/tree/master/examples/login_session) - How to use the built-in session system to handle login and out
|
||||
5. [file_upload](https://github.com/drogonframework/drogon/tree/master/examples/file_upload) - How to handle file uploads in Drogon
|
||||
6. [simple_reverse_proxy](https://github.com/drogonframework/drogon/tree/master/examples/simple_reverse_proxy) - An example showing how to use Drogon as a HTTP reverse
|
||||
proxy with a simple round robin
|
||||
7. [benchmark](https://github.com/drogonframework/drogon/tree/master/examples/benchmark) - Basic benchmark(https://github.com/drogonframework/drogon/wiki/13-Benchmarks) example
|
||||
8. [jsonstore](https://github.com/drogonframework/drogon/tree/master/examples/jsonstore) - Implementation of a [jsonstore](https://github.com/bluzi/jsonstore)-like storage service that is concurrent and stores in memory. Serving as a showcase on how to build a minimally useful RESTful APIs in Drogon
|
||||
9. [redis](https://github.com/drogonframework/drogon/tree/master/examples/redis) - A simple example of Redis
|
||||
10. [websocket_server](https://github.com/drogonframework/drogon/tree/master/examples/websocket_server) - A example websocket chat room server
|
||||
11. [redis_cache](https://github.com/drogonframework/drogon/tree/master/examples/redis_cache) - An example for using coroutines of Redis clients
|
||||
12. [redis_chat](https://github.com/drogonframework/drogon/tree/master/examples/redis_chat) - A chatroom server built with websocket and Redis pub/sub service
|
||||
13. [prometheus_example](https://github.com/drogonframework/drogon/tree/master/examples/prometheus_example) - An example of how to use the Prometheus exporter in Drogon
|
||||
14. [cors](https://github.com/drogonframework/drogon/tree/master/examples/cors) - An example demonstrating how to implement CORS (Cross-Origin Resource Sharing) support in Drogon
|
||||
|
||||
### [TechEmpower Framework Benchmarks](https://github.com/TechEmpower/FrameworkBenchmarks) test suite
|
||||
|
||||
I created a benchmark suite for the `tfb`, see [here](https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/frameworks/C%2B%2B/drogon) for details.
|
||||
|
||||
### Another test suite
|
||||
|
||||
I also created a test suite for another web frameworks benchmark repository, see [here](https://github.com/the-benchmarker/web-frameworks/tree/master/cpp/drogon). In this project, Drogon is used as a sub-module (locally include in the project).
|
||||
@@ -0,0 +1,167 @@
|
||||
#include <drogon/drogon.h>
|
||||
#include <drogon/HttpController.h>
|
||||
#include <drogon/HttpRequest.h>
|
||||
#include <fstream>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class StreamEchoReader : public RequestStreamReader
|
||||
{
|
||||
public:
|
||||
StreamEchoReader(ResponseStreamPtr respStream)
|
||||
: respStream_(std::move(respStream))
|
||||
{
|
||||
}
|
||||
|
||||
void onStreamData(const char *data, size_t length) override
|
||||
{
|
||||
LOG_INFO << "onStreamData[" << length << "]";
|
||||
respStream_->send({data, length});
|
||||
}
|
||||
|
||||
void onStreamFinish(std::exception_ptr ptr) override
|
||||
{
|
||||
if (ptr)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::rethrow_exception(ptr);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
LOG_ERROR << "onStreamError: " << e.what();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_INFO << "onStreamFinish";
|
||||
}
|
||||
respStream_->close();
|
||||
}
|
||||
|
||||
private:
|
||||
ResponseStreamPtr respStream_;
|
||||
};
|
||||
|
||||
class RequestStreamExampleCtrl : public HttpController<RequestStreamExampleCtrl>
|
||||
{
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
ADD_METHOD_TO(RequestStreamExampleCtrl::stream_echo, "/stream_echo", Post);
|
||||
ADD_METHOD_TO(RequestStreamExampleCtrl::stream_upload,
|
||||
"/stream_upload",
|
||||
Post);
|
||||
METHOD_LIST_END
|
||||
|
||||
void stream_echo(
|
||||
const HttpRequestPtr &,
|
||||
RequestStreamPtr &&stream,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) const
|
||||
{
|
||||
auto resp = drogon::HttpResponse::newAsyncStreamResponse(
|
||||
[stream](ResponseStreamPtr respStream) {
|
||||
stream->setStreamReader(
|
||||
std::make_shared<StreamEchoReader>(std::move(respStream)));
|
||||
});
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void stream_upload(
|
||||
const HttpRequestPtr &req,
|
||||
RequestStreamPtr &&stream,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) const
|
||||
{
|
||||
struct Entry
|
||||
{
|
||||
MultipartHeader header;
|
||||
std::string tmpName;
|
||||
std::ofstream file;
|
||||
};
|
||||
|
||||
auto files = std::make_shared<std::vector<Entry>>();
|
||||
auto reader = RequestStreamReader::newMultipartReader(
|
||||
req,
|
||||
[files](MultipartHeader &&header) {
|
||||
LOG_INFO << "Multipart name: " << header.name
|
||||
<< ", filename:" << header.filename
|
||||
<< ", contentType:" << header.contentType;
|
||||
|
||||
files->push_back({std::move(header)});
|
||||
auto tmpName = drogon::utils::genRandomString(40);
|
||||
if (!files->back().header.filename.empty())
|
||||
{
|
||||
files->back().tmpName = tmpName;
|
||||
files->back().file.open("uploads/" + tmpName,
|
||||
std::ios::trunc);
|
||||
}
|
||||
},
|
||||
[files](const char *data, size_t length) {
|
||||
if (files->back().tmpName.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto ¤tFile = files->back().file;
|
||||
if (length == 0)
|
||||
{
|
||||
LOG_INFO << "file finish";
|
||||
if (currentFile.is_open())
|
||||
{
|
||||
currentFile.flush();
|
||||
currentFile.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
LOG_INFO << "data[" << length << "]: ";
|
||||
if (currentFile.is_open())
|
||||
{
|
||||
LOG_INFO << "write file";
|
||||
currentFile.write(data, length);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_ERROR << "file not open";
|
||||
}
|
||||
},
|
||||
[files, callback = std::move(callback)](std::exception_ptr ex) {
|
||||
if (ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::rethrow_exception(std::move(ex));
|
||||
}
|
||||
catch (const StreamError &e)
|
||||
{
|
||||
LOG_ERROR << "stream error: " << e.what();
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
LOG_ERROR << "multipart error: " << e.what();
|
||||
}
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
resp->setBody("error\n");
|
||||
callback(resp);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_INFO << "stream finish, received " << files->size()
|
||||
<< " files";
|
||||
Json::Value respJson;
|
||||
for (const auto &item : *files)
|
||||
{
|
||||
if (item.tmpName.empty())
|
||||
continue;
|
||||
Json::Value entry;
|
||||
entry["name"] = item.header.name;
|
||||
entry["filename"] = item.header.filename;
|
||||
entry["tmpName"] = item.tmpName;
|
||||
respJson.append(entry);
|
||||
}
|
||||
auto resp = HttpResponse::newHttpJsonResponse(respJson);
|
||||
callback(resp);
|
||||
}
|
||||
});
|
||||
|
||||
stream->setStreamReader(std::move(reader));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
#include <drogon/drogon.h>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <trantor/utils/Logger.h>
|
||||
#include <trantor/net/callbacks.h>
|
||||
#include <trantor/net/TcpConnection.h>
|
||||
|
||||
using namespace drogon;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
std::mutex mutex;
|
||||
std::unordered_map<trantor::TcpConnectionPtr, std::function<void()>>
|
||||
connMapping;
|
||||
|
||||
int main()
|
||||
{
|
||||
app().registerHandler(
|
||||
"/stream",
|
||||
[](const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
const auto &weakConnPtr = req->getConnectionPtr();
|
||||
if (auto connPtr = weakConnPtr.lock())
|
||||
{
|
||||
std::lock_guard lk(mutex);
|
||||
connMapping.emplace(std::move(connPtr), [] {
|
||||
LOG_INFO << "call stop or other options!!!!";
|
||||
});
|
||||
}
|
||||
auto resp = drogon::HttpResponse::newAsyncStreamResponse(
|
||||
[](drogon::ResponseStreamPtr stream) {
|
||||
std::thread([stream =
|
||||
std::shared_ptr<drogon::ResponseStream>{
|
||||
std::move(stream)}]() mutable {
|
||||
std::cout << std::boolalpha << stream->send("hello ")
|
||||
<< std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
std::cout << std::boolalpha << stream->send("world");
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
stream->close();
|
||||
}).detach();
|
||||
});
|
||||
resp->setContentTypeCodeAndCustomString(
|
||||
ContentType::CT_APPLICATION_JSON, "application/json");
|
||||
callback(resp);
|
||||
});
|
||||
|
||||
// Example: register a stream-mode function handler
|
||||
app().registerHandler(
|
||||
"/stream_req",
|
||||
[](const HttpRequestPtr &req,
|
||||
RequestStreamPtr &&stream,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
if (!stream)
|
||||
{
|
||||
LOG_INFO << "stream mode is not enabled";
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
resp->setBody("no stream");
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto reader = RequestStreamReader::newReader(
|
||||
[](const char *data, size_t length) {
|
||||
LOG_INFO << "piece[" << length
|
||||
<< "]: " << std::string_view{data, length};
|
||||
},
|
||||
[callback = std::move(callback)](std::exception_ptr ex) {
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
if (ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::rethrow_exception(std::move(ex));
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
LOG_ERROR << "stream error: " << e.what();
|
||||
}
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
resp->setBody("error\n");
|
||||
callback(resp);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_INFO << "stream finish";
|
||||
resp->setBody("success\n");
|
||||
callback(resp);
|
||||
}
|
||||
});
|
||||
|
||||
stream->setStreamReader(std::move(reader));
|
||||
},
|
||||
{Post});
|
||||
|
||||
LOG_INFO << "Server running on 127.0.0.1:8848";
|
||||
app().enableRequestStream(); // This is for request stream.
|
||||
app().setConnectionCallback([](const trantor::TcpConnectionPtr &conn) {
|
||||
if (conn->disconnected())
|
||||
{
|
||||
std::lock_guard lk(mutex);
|
||||
if (auto it = connMapping.find(conn); it != connMapping.end())
|
||||
{
|
||||
LOG_INFO << "disconnect";
|
||||
connMapping[conn]();
|
||||
connMapping.erase(conn);
|
||||
}
|
||||
}
|
||||
});
|
||||
app().addListener("127.0.0.1", 8848).run();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#include "BenchmarkCtrl.h"
|
||||
|
||||
void BenchmarkCtrl::asyncHandleHttpRequest(
|
||||
const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||
{
|
||||
// write your application logic here
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody("<p>Hello, world!</p>");
|
||||
resp->setExpiredTime(0);
|
||||
callback(resp);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include <drogon/HttpSimpleController.h>
|
||||
using namespace drogon;
|
||||
|
||||
class BenchmarkCtrl : public drogon::HttpSimpleController<BenchmarkCtrl>
|
||||
{
|
||||
public:
|
||||
void asyncHandleHttpRequest(
|
||||
const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) override;
|
||||
PATH_LIST_BEGIN
|
||||
PATH_ADD("/benchmark", Get);
|
||||
PATH_LIST_END
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "JsonCtrl.h"
|
||||
|
||||
void JsonCtrl::asyncHandleHttpRequest(
|
||||
const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||
{
|
||||
Json::Value ret;
|
||||
ret["message"] = "Hello, World!";
|
||||
auto resp = HttpResponse::newHttpJsonResponse(std::move(ret));
|
||||
callback(resp);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <drogon/HttpSimpleController.h>
|
||||
using namespace drogon;
|
||||
|
||||
class JsonCtrl : public drogon::HttpSimpleController<JsonCtrl>
|
||||
{
|
||||
public:
|
||||
void asyncHandleHttpRequest(
|
||||
const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) override;
|
||||
PATH_LIST_BEGIN
|
||||
// list path definitions here;
|
||||
PATH_ADD("/json", Get);
|
||||
PATH_LIST_END
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
int main()
|
||||
{
|
||||
app()
|
||||
.setLogPath("./")
|
||||
.setLogLevel(trantor::Logger::kWarn)
|
||||
.addListener("0.0.0.0", 7770)
|
||||
.setThreadNum(0)
|
||||
.registerSyncAdvice([](const HttpRequestPtr &req) -> HttpResponsePtr {
|
||||
const auto &path = req->path();
|
||||
if (path.length() == 1 && path[0] == '/')
|
||||
{
|
||||
auto response = HttpResponse::newHttpResponse();
|
||||
response->setBody("<p>Hello, world!</p>");
|
||||
return response;
|
||||
}
|
||||
return nullptr;
|
||||
})
|
||||
.run();
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/tcp.h>
|
||||
#endif
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
int nth_resp = 0;
|
||||
|
||||
int main()
|
||||
{
|
||||
trantor::Logger::setLogLevel(trantor::Logger::kTrace);
|
||||
{
|
||||
auto client = HttpClient::newHttpClient("http://www.baidu.com");
|
||||
client->setSockOptCallback([](int fd) {
|
||||
std::cout << "setSockOptCallback:" << fd << std::endl;
|
||||
#ifdef __linux__
|
||||
int optval = 10;
|
||||
::setsockopt(fd,
|
||||
SOL_TCP,
|
||||
TCP_KEEPCNT,
|
||||
&optval,
|
||||
static_cast<socklen_t>(sizeof optval));
|
||||
::setsockopt(fd,
|
||||
SOL_TCP,
|
||||
TCP_KEEPIDLE,
|
||||
&optval,
|
||||
static_cast<socklen_t>(sizeof optval));
|
||||
::setsockopt(fd,
|
||||
SOL_TCP,
|
||||
TCP_KEEPINTVL,
|
||||
&optval,
|
||||
static_cast<socklen_t>(sizeof optval));
|
||||
#endif
|
||||
});
|
||||
|
||||
auto req = HttpRequest::newHttpRequest();
|
||||
req->setMethod(drogon::Get);
|
||||
req->setPath("/s");
|
||||
req->setParameter("wd", "wx");
|
||||
req->setParameter("oq", "wx");
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
client->sendRequest(
|
||||
req, [](ReqResult result, const HttpResponsePtr &response) {
|
||||
if (result != ReqResult::Ok)
|
||||
{
|
||||
std::cout
|
||||
<< "error while sending request to server! result: "
|
||||
<< result << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "receive response!" << std::endl;
|
||||
// auto headers=response.
|
||||
++nth_resp;
|
||||
std::cout << response->getBody() << std::endl;
|
||||
auto cookies = response->cookies();
|
||||
for (auto const &cookie : cookies)
|
||||
{
|
||||
std::cout << cookie.first << "="
|
||||
<< cookie.second.value()
|
||||
<< ":domain=" << cookie.second.domain()
|
||||
<< std::endl;
|
||||
}
|
||||
std::cout << "count=" << nth_resp << std::endl;
|
||||
});
|
||||
}
|
||||
std::cout << "requestsBufferSize:" << client->requestsBufferSize()
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
app().run();
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
#include <drogon/HttpAppFramework.h>
|
||||
#include <drogon/HttpResponse.h>
|
||||
#include <drogon/drogon.h>
|
||||
#include "trantor/utils/Logger.h"
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
/// Configure Cross-Origin Resource Sharing (CORS) support.
|
||||
///
|
||||
/// This function registers both synchronous pre-processing advice for handling
|
||||
/// OPTIONS preflight requests and post-handling advice to inject CORS headers
|
||||
/// into all responses dynamically based on the incoming request headers.
|
||||
void setupCors()
|
||||
{
|
||||
// Register sync advice to handle CORS preflight (OPTIONS) requests
|
||||
drogon::app().registerSyncAdvice([](const drogon::HttpRequestPtr &req)
|
||||
-> drogon::HttpResponsePtr {
|
||||
if (req->method() == drogon::HttpMethod::Options)
|
||||
{
|
||||
auto resp = drogon::HttpResponse::newHttpResponse();
|
||||
|
||||
// Set Access-Control-Allow-Origin header based on the Origin
|
||||
// request header
|
||||
const auto &origin = req->getHeader("Origin");
|
||||
if (!origin.empty())
|
||||
{
|
||||
resp->addHeader("Access-Control-Allow-Origin", origin);
|
||||
}
|
||||
|
||||
// Set Access-Control-Allow-Methods based on the requested method
|
||||
const auto &requestMethod =
|
||||
req->getHeader("Access-Control-Request-Method");
|
||||
if (!requestMethod.empty())
|
||||
{
|
||||
resp->addHeader("Access-Control-Allow-Methods", requestMethod);
|
||||
}
|
||||
|
||||
// Allow credentials to be included in cross-origin requests
|
||||
resp->addHeader("Access-Control-Allow-Credentials", "true");
|
||||
|
||||
// Set allowed headers from the Access-Control-Request-Headers
|
||||
// header
|
||||
const auto &requestHeaders =
|
||||
req->getHeader("Access-Control-Request-Headers");
|
||||
if (!requestHeaders.empty())
|
||||
{
|
||||
resp->addHeader("Access-Control-Allow-Headers", requestHeaders);
|
||||
}
|
||||
|
||||
return std::move(resp);
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
// Register post-handling advice to add CORS headers to all responses
|
||||
drogon::app().registerPostHandlingAdvice(
|
||||
[](const drogon::HttpRequestPtr &req,
|
||||
const drogon::HttpResponsePtr &resp) -> void {
|
||||
// Set Access-Control-Allow-Origin based on the Origin request
|
||||
// header
|
||||
const auto &origin = req->getHeader("Origin");
|
||||
if (!origin.empty())
|
||||
{
|
||||
resp->addHeader("Access-Control-Allow-Origin", origin);
|
||||
}
|
||||
|
||||
// Reflect the requested Access-Control-Request-Method back in the
|
||||
// response
|
||||
const auto &requestMethod =
|
||||
req->getHeader("Access-Control-Request-Method");
|
||||
if (!requestMethod.empty())
|
||||
{
|
||||
resp->addHeader("Access-Control-Allow-Methods", requestMethod);
|
||||
}
|
||||
|
||||
// Allow credentials to be included in cross-origin requests
|
||||
resp->addHeader("Access-Control-Allow-Credentials", "true");
|
||||
|
||||
// Reflect the requested Access-Control-Request-Headers back
|
||||
const auto &requestHeaders =
|
||||
req->getHeader("Access-Control-Request-Headers");
|
||||
if (!requestHeaders.empty())
|
||||
{
|
||||
resp->addHeader("Access-Control-Allow-Headers", requestHeaders);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to start the Drogon application with CORS-enabled routes.
|
||||
* This example includes:
|
||||
* - A simple GET endpoint `/hello` that returns a greeting message.
|
||||
* - A POST endpoint `/echo` that echoes back the request body.
|
||||
* You can test with curl to test the CORS support:
|
||||
*
|
||||
```
|
||||
curl -i -X OPTIONS http://localhost:8000/echo \
|
||||
-H "Origin: http://localhost:3000" \
|
||||
-H "Access-Control-Request-Method: POST" \
|
||||
-H "Access-Control-Request-Headers: Content-Type"
|
||||
```
|
||||
or
|
||||
```
|
||||
curl -i -X POST http://localhost:8000/echo \
|
||||
-H "Origin: http://localhost:3000" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"key":"value"}'
|
||||
```
|
||||
*/
|
||||
int main()
|
||||
{
|
||||
// Listen on port 8000 for all interfaces
|
||||
app().addListener("0.0.0.0", 8000);
|
||||
|
||||
// Setup CORS support
|
||||
setupCors();
|
||||
|
||||
// Register /hello route for GET and OPTIONS methods
|
||||
app().registerHandler(
|
||||
"/hello",
|
||||
[](const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody("Hello from Drogon!");
|
||||
|
||||
// Log client IP address
|
||||
LOG_INFO << "Request to /hello from " << req->getPeerAddr().toIp();
|
||||
|
||||
callback(resp);
|
||||
},
|
||||
{Get, Options});
|
||||
|
||||
// Register /echo route for POST and OPTIONS methods
|
||||
app().registerHandler(
|
||||
"/echo",
|
||||
[](const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody(std::string("Echo: ").append(req->getBody()));
|
||||
|
||||
// Log client IP and request body
|
||||
LOG_INFO << "Request to /echo from " << req->getPeerAddr().toIp();
|
||||
LOG_INFO << "Echo content: " << req->getBody();
|
||||
|
||||
callback(resp);
|
||||
},
|
||||
{Post, Options});
|
||||
|
||||
// Start the application main loop
|
||||
app().run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>File upload</title>
|
||||
<script type="text/javascript">
|
||||
var xhr;
|
||||
//File uploading method
|
||||
function UpladFile() {
|
||||
var fileObj = document.getElementById("file").files[0]; // js get file object
|
||||
var url = "/upload_endpoint";
|
||||
|
||||
var form = new FormData(); // FormData object
|
||||
form.append("file", fileObj); // File object
|
||||
|
||||
xhr = new XMLHttpRequest(); // XMLHttpRequest object
|
||||
xhr.open("post", url, true); //post
|
||||
xhr.onload = uploadComplete;
|
||||
xhr.onerror = uploadFailed;
|
||||
|
||||
xhr.upload.onprogress = progressFunction;
|
||||
xhr.upload.onloadstart = function(){
|
||||
ot = new Date().getTime();
|
||||
oloaded = 0;
|
||||
};
|
||||
|
||||
xhr.send(form);
|
||||
}
|
||||
|
||||
function uploadComplete(evt) {
|
||||
var data = evt.target.responseText;
|
||||
alert("File has been uploaded.\n" + data);
|
||||
}
|
||||
|
||||
function uploadFailed(evt) {
|
||||
alert("Upload failed!");
|
||||
}
|
||||
|
||||
function cancleUploadFile(){
|
||||
xhr.abort();
|
||||
}
|
||||
|
||||
function progressFunction(evt) {
|
||||
var progressBar = document.getElementById("progressBar");
|
||||
var percentageDiv = document.getElementById("percentage");
|
||||
if (evt.lengthComputable) {//
|
||||
progressBar.max = evt.total;
|
||||
progressBar.value = evt.loaded;
|
||||
percentageDiv.innerHTML = Math.round(evt.loaded / evt.total * 100) + "%";
|
||||
}
|
||||
var time = document.getElementById("time");
|
||||
var nt = new Date().getTime();
|
||||
var pertime = (nt-ot)/1000;
|
||||
ot = new Date().getTime();
|
||||
var perload = evt.loaded - oloaded;
|
||||
oloaded = evt.loaded;
|
||||
var speed = perload/pertime;
|
||||
var bspeed = speed;
|
||||
var units = 'b/s';
|
||||
if(speed/1024>1){
|
||||
speed = speed/1024;
|
||||
units = 'k/s';
|
||||
}
|
||||
if(speed/1024>1){
|
||||
speed = speed/1024;
|
||||
units = 'M/s';
|
||||
}
|
||||
speed = speed.toFixed(1);
|
||||
var resttime = ((evt.total-evt.loaded)/bspeed).toFixed(1);
|
||||
time.innerHTML = ',Speed: '+speed+units+', the remaining time: '+resttime+'s';
|
||||
if(bspeed==0) time.innerHTML = 'Upload cancelled';
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<progress id="progressBar" value="0" max="100" style="width: 300px;"></progress>
|
||||
<span id="percentage"></span><span id="time"></span>
|
||||
<br /><br />
|
||||
<input type="file" id="file" name="myfile" />
|
||||
<input type="button" onclick="UpladFile()" value="Upload" />
|
||||
<input type="button" onclick="cancleUploadFile()" value="Cancel" />
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
#include <drogon/drogon.h>
|
||||
using namespace drogon;
|
||||
|
||||
int main()
|
||||
{
|
||||
app().registerHandler(
|
||||
"/",
|
||||
[](const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
auto resp = HttpResponse::newHttpViewResponse("FileUpload");
|
||||
callback(resp);
|
||||
});
|
||||
|
||||
app().registerHandler(
|
||||
"/upload_endpoint",
|
||||
[](const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
MultiPartParser fileUpload;
|
||||
if (fileUpload.parse(req) != 0 || fileUpload.getFiles().size() != 1)
|
||||
{
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody("Must only be one file");
|
||||
resp->setStatusCode(k403Forbidden);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
auto &file = fileUpload.getFiles()[0];
|
||||
auto md5 = file.getMd5();
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody(
|
||||
"The server has calculated the file's MD5 hash to be " + md5);
|
||||
file.save();
|
||||
LOG_INFO << "The uploaded file has been saved to the ./uploads "
|
||||
"directory";
|
||||
callback(resp);
|
||||
},
|
||||
{Post});
|
||||
|
||||
LOG_INFO << "Server running on 127.0.0.1:8848";
|
||||
app()
|
||||
.setClientMaxBodySize(20 * 2000 * 2000)
|
||||
.setUploadPath("./uploads")
|
||||
.addListener("127.0.0.1", 8848)
|
||||
.run();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#include <drogon/HttpController.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
// HttpControllers are automatically added to Drogon upon Drogon initializing.
|
||||
class SayHello : public HttpController<SayHello>
|
||||
{
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
// Drogon automatically appends the namespace and name of the controller to
|
||||
// the handlers of the controller. In this example, although we are adding
|
||||
// a handler to /. But because it is a part of the SayHello controller. It
|
||||
// ends up in path /SayHello/ (IMPORTANT! It is /SayHello/ not /SayHello
|
||||
// as they are different paths).
|
||||
METHOD_ADD(SayHello::genericHello, "/", Get);
|
||||
// Same for /hello. It ends up at /SayHello/hello
|
||||
METHOD_ADD(SayHello::personalizedHello, "/hello", Get);
|
||||
METHOD_LIST_END
|
||||
|
||||
protected:
|
||||
void genericHello(const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||
{
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody(
|
||||
"Hello, this is a generic hello message from the SayHello "
|
||||
"controller");
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void personalizedHello(
|
||||
const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||
{
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody(
|
||||
"Hi there, this is another hello from the SayHello Controller");
|
||||
callback(resp);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<%c++
|
||||
auto name=@@.get<std::string>("name");
|
||||
bool nameIsEmpty = name == "";
|
||||
if (nameIsEmpty)
|
||||
name = "anonymous";
|
||||
auto message = "Hello, " + name + " from a CSP template";
|
||||
%>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>[[ name ]]</title>
|
||||
</head>
|
||||
<body>
|
||||
<%c++ $$<<message; %>
|
||||
<%c++
|
||||
if (nameIsEmpty)
|
||||
{
|
||||
$$ << "<br>"
|
||||
<< "You can revisit the same page and append ?name=<i>your_name</i> to change the name";
|
||||
}
|
||||
%>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
#include <drogon/HttpSimpleController.h>
|
||||
#include <drogon/HttpResponse.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
// HttpSimpleController does not allow registration of multiple handlers.
|
||||
// Instead, it has one handler - asyncHandleHttpRequest. The
|
||||
// HttpSimpleController is a lightweight class designed to handle really simple
|
||||
// cases.
|
||||
class HelloViewController : public HttpSimpleController<HelloViewController>
|
||||
{
|
||||
public:
|
||||
PATH_LIST_BEGIN
|
||||
// Also unlike HttpContoller, HttpSimpleController does not automatically
|
||||
// prepend the namespace and class name to the path. Thus the path of this
|
||||
// controller is at "/view".
|
||||
PATH_ADD("/view");
|
||||
PATH_LIST_END
|
||||
|
||||
void asyncHandleHttpRequest(
|
||||
const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) override
|
||||
{
|
||||
HttpViewData data;
|
||||
data["name"] = req->getParameter("name");
|
||||
auto resp = HttpResponse::newHttpViewResponse("HelloView", data);
|
||||
callback(resp);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
#include <trantor/utils/Logger.h>
|
||||
#ifdef _WIN32
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
|
||||
#include <drogon/drogon.h>
|
||||
using namespace drogon;
|
||||
|
||||
int main()
|
||||
{
|
||||
// `registerHandler()` adds a handler to the desired path. The handler is
|
||||
// responsible for generating a HTTP response upon an HTTP request being
|
||||
// sent to Drogon
|
||||
app().registerHandler(
|
||||
"/",
|
||||
[](const HttpRequestPtr &request,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
LOG_INFO << "connected:"
|
||||
<< (request->connected() ? "true" : "false");
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody("Hello, World!");
|
||||
callback(resp);
|
||||
},
|
||||
{Get});
|
||||
|
||||
// `registerHandler()` also supports parsing and passing the path as
|
||||
// parameters to the handler. Parameters are specified using {}. The text
|
||||
// inside the {} does not correspond to the index of parameter passed to the
|
||||
// handler (nor it has any meaning). Instead, it is only to make it easier
|
||||
// for users to recognize the function of each parameter.
|
||||
app().registerHandler(
|
||||
"/user/{user-name}",
|
||||
[](const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &name) {
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody("Hello, " + name + "!");
|
||||
callback(resp);
|
||||
},
|
||||
{Get});
|
||||
|
||||
// You can also specify that the parameter is in the query section of the
|
||||
// URL!
|
||||
app().registerHandler(
|
||||
"/hello?user={user-name}",
|
||||
[](const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &name) {
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody("Hello, " + name + "!");
|
||||
callback(resp);
|
||||
},
|
||||
{Get});
|
||||
|
||||
// Or, if you want to, instead of asking drogon to parse it for you. You can
|
||||
// parse the request yourselves.
|
||||
app().registerHandler(
|
||||
"/hello_user",
|
||||
[](const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
auto name = req->getOptionalParameter<std::string>("user");
|
||||
if (!name)
|
||||
resp->setBody("Please tell me your name");
|
||||
else
|
||||
resp->setBody("Hello, " + name.value() + "!");
|
||||
callback(resp);
|
||||
},
|
||||
{Get});
|
||||
|
||||
app()
|
||||
.setBeforeListenSockOptCallback([](int fd) {
|
||||
LOG_INFO << "setBeforeListenSockOptCallback:" << fd;
|
||||
#ifdef _WIN32
|
||||
#elif __linux__
|
||||
int enable = 1;
|
||||
if (setsockopt(
|
||||
fd, IPPROTO_TCP, TCP_FASTOPEN, &enable, sizeof(enable)) ==
|
||||
-1)
|
||||
{
|
||||
LOG_INFO << "setsockopt TCP_FASTOPEN failed";
|
||||
}
|
||||
#else
|
||||
#endif
|
||||
})
|
||||
.setAfterAcceptSockOptCallback([](int) {});
|
||||
|
||||
// Ask Drogon to listen on 127.0.0.1 port 8848. Drogon supports listening
|
||||
// on multiple IP addresses by adding multiple listeners. For example, if
|
||||
// you want the server also listen on 127.0.0.1 port 5555. Just add another
|
||||
// line of addListener("127.0.0.1", 5555)
|
||||
LOG_INFO << "Server running on 127.0.0.1:8848";
|
||||
app().addListener("127.0.0.1", 8848).run();
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
This folder contains a [jsonstore](https://github.com/bluzi/jsonstore)-like storage service. But is multi-threaded and stores everything in memory. Serving as a showcase on how to build a minimally useful RESTful APIs in Drogon.
|
||||
|
||||
## API
|
||||
|
||||
#### /get-token
|
||||
Generate a token that the user can use to create, read, modify and delete records.
|
||||
|
||||
* **method**: GET
|
||||
* **URL params**: None
|
||||
* **Body**: None
|
||||
* **Success response**
|
||||
* **Code**: 200
|
||||
* **Content**: `{"token":"3a322920d42ef0763152a6efff2ed51985530aedd45370f92fd0f0b8dcc30220"}`
|
||||
* **Sample call**
|
||||
|
||||
```bash
|
||||
❯ curl -XGET http://localhost:8848/get-token
|
||||
{"token":"3a322920d42ef0763152a6efff2ed51985530aedd45370f92fd0f0b8dcc30220"}
|
||||
```
|
||||
|
||||
#### /{token}
|
||||
|
||||
Create a new JSON object associated with the token
|
||||
|
||||
* **method**: POST
|
||||
* **URL params**: None
|
||||
* **Body**: The initial JSON object to store
|
||||
* **Success response**
|
||||
* **Code**: 200
|
||||
* **Content**: `{"ok":true}`
|
||||
* **Failed response**:
|
||||
* **Code**: 500
|
||||
* **Why**: Either the token already is associated with the data or request body/content-type is not JSON.
|
||||
* **Content**: `{"ok":false}`
|
||||
* **Sample call**
|
||||
|
||||
```bash
|
||||
❯ curl -XPOST http://localhost:8848/3a322920d42ef0763152a6efff2ed51985530aedd45370f92fd0f0b8dcc30220 \
|
||||
-H 'content-type: application/json' -d '{"foo":{"bar":42}}'
|
||||
{"ok":true}
|
||||
```
|
||||
|
||||
Delete the JSON object associated with the token
|
||||
|
||||
* **method**: DELETE
|
||||
* **URL params**: None
|
||||
* **Body**: None
|
||||
* **Success response**
|
||||
* **Code**: 200
|
||||
* **Content**: `{"ok":true}`
|
||||
* **Sample call**
|
||||
|
||||
```bash
|
||||
❯ curl -XDELETE http://localhost:8848/3a322920d42ef0763152a6efff2ed51985530aedd45370f92fd0f0b8dcc30220
|
||||
{"ok":true}
|
||||
```
|
||||
|
||||
#### /{token}/{some/path/to/data}
|
||||
|
||||
Retrieve data at and below the specified path
|
||||
* **method**: GET
|
||||
* **URL params**: None
|
||||
* **Body**: None
|
||||
* **Success response**
|
||||
* **Code**: 200
|
||||
* **Content**: `{"foo":{"bar":42}}`
|
||||
* **Failed response**:
|
||||
* **Code**: 500
|
||||
* **Why**: No data associated with the provided token, or it does not exist in the JSON object.
|
||||
* **Content**: `{"ok":false}`
|
||||
* **Sample call**
|
||||
|
||||
```bash
|
||||
❯ curl -XGET http://localhost:8848/3a322920d42ef0763152a6efff2ed51985530aedd45370f92fd0f0b8dcc30220/
|
||||
{"foo":{"bar":42}}
|
||||
```
|
||||
|
||||
Update data at the specified path
|
||||
* **method**: PUT
|
||||
* **URL params**: None
|
||||
* **Body**: The JSON object you wish to replace to
|
||||
* **Success response**
|
||||
* **Code**: 200
|
||||
* **Content**: `{"ok":true}`
|
||||
* **Failed response**:
|
||||
* **Code**: 500
|
||||
* **Why**: No data associated with the provided token or it does not exist in the JSON object.
|
||||
* **Content**: `{"ok":false}`
|
||||
* **Sample call**
|
||||
|
||||
```bash
|
||||
❯ curl -XPUT http://localhost:8848/3a322920d42ef0763152a6efff2ed51985530aedd45370f92fd0f0b8dcc30220/foo \
|
||||
-H 'content-type: application/json' -d '{"fruit":"apple"}'
|
||||
{"ok":true}
|
||||
```
|
||||
|
||||
## Example use
|
||||
|
||||
```bash
|
||||
export URL="http://localhost:8848"
|
||||
export TOKEN=`curl $URL/get-token -s | sed 's/.*"\([0-9a-f]*\)".*/\1/'`
|
||||
printf "Token is: $TOKEN\n"
|
||||
|
||||
printf 'Creating new data \n> '
|
||||
curl -XPOST $URL/$TOKEN -H 'content-type: application/json' -d '{"foo":{"bar":42}}'
|
||||
printf '\nRetrieving value of data["foo"]["bar"] \n> '
|
||||
curl $URL/$TOKEN/foo/bar
|
||||
printf '\nModifing data \n> '
|
||||
curl -XPUT $URL/$TOKEN/foo -H 'content-type: application/json' -d '{"zoo":"zebra"}'
|
||||
printf '\nNow data["foo"]["bar"] no longer exists \n> '
|
||||
curl $URL/$TOKEN/foo/bar
|
||||
printf '\nDelete data \n> '
|
||||
curl -XDELETE $URL/$TOKEN
|
||||
echo
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Token is: 5e73ba044b45e68b4856925faea268391091f39fc62ab8c58955cf20957018fa
|
||||
Creating new data
|
||||
> {"ok":true}
|
||||
Retrieving value of data["foo"]["bar"]
|
||||
> 42
|
||||
Modifying data
|
||||
> {"ok":true}
|
||||
Now data["foo"]["bar"] no longer exists
|
||||
> {"ok":false}
|
||||
Delete data
|
||||
> {"ok":true}
|
||||
```
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
HttpResponsePtr makeFailedResponse()
|
||||
{
|
||||
Json::Value json;
|
||||
json["ok"] = false;
|
||||
auto resp = HttpResponse::newHttpJsonResponse(json);
|
||||
resp->setStatusCode(k500InternalServerError);
|
||||
return resp;
|
||||
}
|
||||
|
||||
HttpResponsePtr makeSuccessResponse()
|
||||
{
|
||||
Json::Value json;
|
||||
json["ok"] = true;
|
||||
auto resp = HttpResponse::newHttpJsonResponse(json);
|
||||
return resp;
|
||||
}
|
||||
|
||||
std::string getRandomString(size_t n)
|
||||
{
|
||||
std::vector<unsigned char> random(n);
|
||||
utils::secureRandomBytes(random.data(), random.size());
|
||||
|
||||
// This is cryptographically safe as 256 mod 16 == 0
|
||||
static const std::string alphabets = "0123456789abcdef";
|
||||
assert(256 % alphabets.size() == 0);
|
||||
std::string randomString(n, '\0');
|
||||
for (size_t i = 0; i < n; i++)
|
||||
randomString[i] = alphabets[random[i] % alphabets.size()];
|
||||
return randomString;
|
||||
}
|
||||
|
||||
struct DataItem
|
||||
{
|
||||
Json::Value item;
|
||||
std::mutex mtx;
|
||||
};
|
||||
|
||||
class JsonStore : public HttpController<JsonStore>
|
||||
{
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
ADD_METHOD_TO(JsonStore::getToken, "/get-token", Get);
|
||||
ADD_METHOD_VIA_REGEX(JsonStore::getItem, "/([a-f0-9]{64})/(.*)", Get);
|
||||
ADD_METHOD_VIA_REGEX(JsonStore::createItem, "/([a-f0-9]{64})", Post);
|
||||
ADD_METHOD_VIA_REGEX(JsonStore::deleteItem, "/([a-f0-9]{64})", Delete);
|
||||
ADD_METHOD_VIA_REGEX(JsonStore::updateItem, "/([a-f0-9]{64})/(.*)", Put);
|
||||
METHOD_LIST_END
|
||||
|
||||
void getToken(const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||
{
|
||||
std::string randomString = getRandomString(64);
|
||||
Json::Value res;
|
||||
res["token"] = randomString;
|
||||
|
||||
callback(HttpResponse::newHttpJsonResponse(std::move(res)));
|
||||
}
|
||||
|
||||
void getItem(const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &token,
|
||||
const std::string &path)
|
||||
{
|
||||
auto itemPtr = [this, &token]() -> std::shared_ptr<DataItem> {
|
||||
// It is possible that the item is being removed while another
|
||||
// thread tries to look it up. The mutex here prevents that from
|
||||
// happening.
|
||||
std::lock_guard<std::mutex> lock(storageMtx_);
|
||||
auto it = dataStore_.find(token);
|
||||
if (it == dataStore_.end())
|
||||
return nullptr;
|
||||
return it->second;
|
||||
}();
|
||||
if (itemPtr == nullptr)
|
||||
{
|
||||
callback(makeFailedResponse());
|
||||
return;
|
||||
}
|
||||
|
||||
auto &item = *itemPtr;
|
||||
// Prevents another thread from writing to the same item while this
|
||||
// thread reads. Could cause blockage if multiple clients are asking to
|
||||
// read the same object. But that should be rare.
|
||||
std::lock_guard<std::mutex> lock(item.mtx);
|
||||
Json::Value *valuePtr = walkJson(item.item, path);
|
||||
|
||||
if (valuePtr == nullptr)
|
||||
{
|
||||
callback(makeFailedResponse());
|
||||
return;
|
||||
}
|
||||
|
||||
auto resp = HttpResponse::newHttpJsonResponse(*valuePtr);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
void updateItem(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &token,
|
||||
const std::string &path)
|
||||
{
|
||||
auto jsonPtr = req->jsonObject();
|
||||
auto itemPtr = [this, &token]() -> std::shared_ptr<DataItem> {
|
||||
// It is possible that the item is being removed while another
|
||||
// thread tries to look it up. The mutex here prevents that from
|
||||
// happening.
|
||||
std::lock_guard<std::mutex> lock(storageMtx_);
|
||||
auto it = dataStore_.find(token);
|
||||
if (it == dataStore_.end())
|
||||
return nullptr;
|
||||
return it->second;
|
||||
}();
|
||||
|
||||
if (itemPtr == nullptr || jsonPtr == nullptr)
|
||||
{
|
||||
callback(makeFailedResponse());
|
||||
return;
|
||||
}
|
||||
|
||||
auto &item = *itemPtr;
|
||||
std::lock_guard<std::mutex> lock(item.mtx);
|
||||
Json::Value *valuePtr = walkJson(item.item, path, 1);
|
||||
|
||||
if (valuePtr == nullptr)
|
||||
{
|
||||
callback(makeFailedResponse());
|
||||
return;
|
||||
}
|
||||
|
||||
std::string key = utils::splitString(path, "/").back();
|
||||
(*valuePtr)[key] = *jsonPtr;
|
||||
|
||||
callback(makeSuccessResponse());
|
||||
}
|
||||
|
||||
void createItem(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &token)
|
||||
{
|
||||
auto jsonPtr = req->jsonObject();
|
||||
if (jsonPtr == nullptr)
|
||||
{
|
||||
callback(makeFailedResponse());
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(storageMtx_);
|
||||
if (dataStore_.find(token) == dataStore_.end())
|
||||
{
|
||||
auto item = std::make_shared<DataItem>();
|
||||
item->item = std::move(*jsonPtr);
|
||||
dataStore_.insert({token, std::move(item)});
|
||||
|
||||
callback(makeSuccessResponse());
|
||||
}
|
||||
else
|
||||
{
|
||||
callback(makeFailedResponse());
|
||||
}
|
||||
}
|
||||
|
||||
void deleteItem(const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &token)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(storageMtx_);
|
||||
dataStore_.erase(token);
|
||||
|
||||
callback(makeSuccessResponse());
|
||||
}
|
||||
|
||||
protected:
|
||||
static Json::Value *walkJson(Json::Value &json,
|
||||
const std::string &path,
|
||||
size_t ignore_back = 0)
|
||||
{
|
||||
auto pathElem = utils::splitString(path, "/", false);
|
||||
if (pathElem.size() >= ignore_back)
|
||||
pathElem.resize(pathElem.size() - ignore_back);
|
||||
Json::Value *valuePtr = &json;
|
||||
for (const auto &elem : pathElem)
|
||||
{
|
||||
if (valuePtr->isArray())
|
||||
{
|
||||
Json::Value &value = (*valuePtr)[std::stoi(elem)];
|
||||
if (value.isNull())
|
||||
return nullptr;
|
||||
|
||||
valuePtr = &value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Json::Value &value = (*valuePtr)[elem];
|
||||
if (value.isNull())
|
||||
return nullptr;
|
||||
|
||||
valuePtr = &value;
|
||||
}
|
||||
}
|
||||
return valuePtr;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, std::shared_ptr<DataItem>> dataStore_;
|
||||
std::mutex storageMtx_;
|
||||
};
|
||||
|
||||
int main()
|
||||
{
|
||||
app().addListener("127.0.0.1", 8848).run();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>session example</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/login" method="post">
|
||||
<div class="container">
|
||||
<label for="user"><b>Username</b></label>
|
||||
<input type="text" placeholder="Enter Username" name="user" required>
|
||||
|
||||
<label for="passwd"><b>Password</b></label>
|
||||
<input type="password" placeholder="Enter Password" name="passwd" required>
|
||||
|
||||
<button type="submit">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p>Please login with the credentials <br>Username: user<br>Password: password123</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>session example</title>
|
||||
</head>
|
||||
<body>
|
||||
<form action="/logout" method="post">
|
||||
<div class="container">
|
||||
<button type="submit">Logout</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p>You can logout now</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
#include <drogon/drogon.h>
|
||||
#include <chrono>
|
||||
using namespace drogon;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
int main()
|
||||
{
|
||||
app().registerHandler(
|
||||
"/",
|
||||
[](const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
bool loggedIn =
|
||||
req->session()->getOptional<bool>("loggedIn").value_or(false);
|
||||
HttpResponsePtr resp;
|
||||
if (loggedIn == false)
|
||||
resp = HttpResponse::newHttpViewResponse("LoginPage");
|
||||
else
|
||||
resp = HttpResponse::newHttpViewResponse("LogoutPage");
|
||||
callback(resp);
|
||||
});
|
||||
|
||||
app().registerHandler(
|
||||
"/logout",
|
||||
[](const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
HttpResponsePtr resp = HttpResponse::newHttpResponse();
|
||||
req->session()->erase("loggedIn");
|
||||
resp->setBody("<script>window.location.href = \"/\";</script>");
|
||||
callback(resp);
|
||||
},
|
||||
{Post});
|
||||
|
||||
app().registerHandler(
|
||||
"/login",
|
||||
[](const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
HttpResponsePtr resp = HttpResponse::newHttpResponse();
|
||||
std::string user = req->getParameter("user");
|
||||
std::string passwd = req->getParameter("passwd");
|
||||
|
||||
// NOTE: Do not use MD5 for the password hash under any
|
||||
// circumstances. We only use it because Drogon is not a
|
||||
// cryptography library, so it does not include a better hash
|
||||
// algorithm. Use Argon2 or BCrypt in a real product. username:
|
||||
// user, password: password123
|
||||
if (user == "user" && utils::getMd5("jadsjhdsajkh" + passwd) ==
|
||||
"5B5299CF4CEAE2D523315694B82573C9")
|
||||
{
|
||||
req->session()->insert("loggedIn", true);
|
||||
resp->setBody("<script>window.location.href = \"/\";</script>");
|
||||
callback(resp);
|
||||
}
|
||||
else
|
||||
{
|
||||
resp->setStatusCode(k401Unauthorized);
|
||||
resp->setBody("<script>window.location.href = \"/\";</script>");
|
||||
callback(resp);
|
||||
}
|
||||
},
|
||||
{Post});
|
||||
|
||||
LOG_INFO << "Server running on 127.0.0.1:8848";
|
||||
app()
|
||||
// All sessions are stored for 24 Hours
|
||||
.enableSession(24h)
|
||||
.addListener("127.0.0.1", 8848)
|
||||
.run();
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,visualstudio,visualstudiocode,cmake,c,c++
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,visualstudio,visualstudiocode,cmake,c,c++
|
||||
|
||||
### C ###
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Linker output
|
||||
*.ilk
|
||||
*.map
|
||||
*.exp
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# Debug files
|
||||
*.dSYM/
|
||||
*.su
|
||||
*.idb
|
||||
*.pdb
|
||||
|
||||
# Kernel Module Compile Results
|
||||
*.mod*
|
||||
*.cmd
|
||||
.tmp_versions/
|
||||
modules.order
|
||||
Module.symvers
|
||||
Mkfile.old
|
||||
dkms.conf
|
||||
|
||||
### C++ ###
|
||||
# Prerequisites
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
|
||||
# Precompiled Headers
|
||||
|
||||
# Linker files
|
||||
|
||||
# Debugger Files
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
|
||||
# Executables
|
||||
|
||||
### CMake ###
|
||||
CMakeLists.txt.user
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
CMakeScripts
|
||||
Testing
|
||||
Makefile
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
CTestTestfile.cmake
|
||||
_deps
|
||||
CMakeUserPresets.json
|
||||
|
||||
### CMake Patch ###
|
||||
# External projects
|
||||
*-prefix/
|
||||
|
||||
### Intellij+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Intellij+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
*.code-workspace
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
### VisualStudio ###
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.meta
|
||||
*.iobj
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*[.json, .xml, .info]
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
### VisualStudio Patch ###
|
||||
# Additional files built by Visual Studio
|
||||
*.tlog
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/intellij+all,visualstudio,visualstudiocode,cmake,c,c++
|
||||
@@ -0,0 +1,75 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
project(prometheus_example CXX)
|
||||
|
||||
include(CheckIncludeFileCXX)
|
||||
|
||||
check_include_file_cxx(any HAS_ANY)
|
||||
check_include_file_cxx(string_view HAS_STRING_VIEW)
|
||||
check_include_file_cxx(coroutine HAS_COROUTINE)
|
||||
if (NOT "${CMAKE_CXX_STANDARD}" STREQUAL "")
|
||||
# Do nothing
|
||||
elseif (HAS_ANY AND HAS_STRING_VIEW AND HAS_COROUTINE)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
elseif (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(${PROJECT_NAME} 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(${PROJECT_NAME} PRIVATE drogon)
|
||||
#
|
||||
# and comment out the following lines
|
||||
find_package(Drogon CONFIG REQUIRED)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon)
|
||||
|
||||
# ##############################################################################
|
||||
|
||||
if (CMAKE_CXX_STANDARD LESS 17)
|
||||
message(FATAL_ERROR "c++17 or higher is required")
|
||||
elseif (CMAKE_CXX_STANDARD LESS 20)
|
||||
message(STATUS "use c++17")
|
||||
else ()
|
||||
message(STATUS "use c++20")
|
||||
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)
|
||||
|
||||
drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views
|
||||
${CMAKE_CURRENT_BINARY_DIR})
|
||||
# use the following line to create views with namespaces.
|
||||
# drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views
|
||||
# ${CMAKE_CURRENT_BINARY_DIR} TRUE)
|
||||
# use the following line to create views with namespace CHANGE_ME prefixed
|
||||
# and path namespaces.
|
||||
# drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views
|
||||
# ${CMAKE_CURRENT_BINARY_DIR} TRUE CHANGE_ME)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/models)
|
||||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${SRC_DIR}
|
||||
${CTL_SRC}
|
||||
${FILTER_SRC}
|
||||
${PLUGIN_SRC}
|
||||
${MODEL_SRC})
|
||||
# ##############################################################################
|
||||
# uncomment the following line for dynamically loading views
|
||||
# set_property(TARGET ${PROJECT_NAME} PROPERTY ENABLE_EXPORTS ON)
|
||||
|
||||
# ##############################################################################
|
||||
|
||||
add_subdirectory(test)
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
|
||||
"listeners": [
|
||||
{
|
||||
"address": "0.0.0.0",
|
||||
"port": 5555,
|
||||
"https": false
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "drogon::plugin::PromExporter",
|
||||
"dependencies": [],
|
||||
"config": {
|
||||
"path": "/metrics",
|
||||
"collectors":[
|
||||
{
|
||||
"name": "http_requests_total",
|
||||
"help": "The total number of http requests",
|
||||
"type": "counter",
|
||||
"labels": ["method", "path"]
|
||||
},
|
||||
{
|
||||
"name": "http_request_duration_seconds",
|
||||
"help": "The processing time of http requests, in seconds",
|
||||
"type": "histogram",
|
||||
"labels": ["method", "path"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
#include "PromTestCtrl.h"
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
void PromTestCtrl::fast(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback)
|
||||
{
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody("Hello, world!");
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
drogon::AsyncTask PromTestCtrl::slow(
|
||||
const HttpRequestPtr req,
|
||||
std::function<void(const HttpResponsePtr &)> callback)
|
||||
{
|
||||
// sleep for a random time between 1 and 3 seconds
|
||||
static std::once_flag flag;
|
||||
std::call_once(flag, []() { srand(time(nullptr)); });
|
||||
auto duration = 1 + (rand() % 3);
|
||||
auto loop = trantor::EventLoop::getEventLoopOfCurrentThread();
|
||||
co_await drogon::sleepCoro(loop, std::chrono::seconds(duration));
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody("Hello, world!");
|
||||
callback(resp);
|
||||
co_return;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <drogon/HttpController.h>
|
||||
#include <drogon/utils/coroutine.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class PromTestCtrl : public drogon::HttpController<PromTestCtrl>
|
||||
{
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
ADD_METHOD_TO(PromTestCtrl::fast, "/fast", "PromStat");
|
||||
ADD_METHOD_TO(PromTestCtrl::slow, "/slow", "PromStat");
|
||||
METHOD_LIST_END
|
||||
|
||||
void fast(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
drogon::AsyncTask slow(
|
||||
const HttpRequestPtr req,
|
||||
std::function<void(const HttpResponsePtr &)> callback);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
*
|
||||
* PromStat.cc
|
||||
*
|
||||
*/
|
||||
|
||||
#include "PromStat.h"
|
||||
#include <drogon/plugins/PromExporter.h>
|
||||
#include <drogon/utils/monitoring/Counter.h>
|
||||
#include <drogon/utils/monitoring/Histogram.h>
|
||||
#include <drogon/HttpAppFramework.h>
|
||||
#include <chrono>
|
||||
|
||||
using namespace std::literals::chrono_literals;
|
||||
using namespace drogon;
|
||||
|
||||
Task<HttpResponsePtr> PromStat::invoke(const HttpRequestPtr &req,
|
||||
MiddlewareNextAwaiter &&next)
|
||||
{
|
||||
std::string path{req->matchedPathPattern()};
|
||||
auto method = req->methodString();
|
||||
auto promExporter = app().getPlugin<drogon::plugin::PromExporter>();
|
||||
if (promExporter)
|
||||
{
|
||||
auto collector =
|
||||
promExporter->getCollector<drogon::monitoring::Counter>(
|
||||
"http_requests_total");
|
||||
if (collector)
|
||||
{
|
||||
collector->metric({method, path})->increment();
|
||||
}
|
||||
}
|
||||
auto start = trantor::Date::date();
|
||||
auto resp = co_await next;
|
||||
if (promExporter)
|
||||
{
|
||||
auto collector =
|
||||
promExporter->getCollector<drogon::monitoring::Histogram>(
|
||||
"http_request_duration_seconds");
|
||||
if (collector)
|
||||
{
|
||||
static const std::vector<double> boundaries{
|
||||
0.0001, 0.001, 0.01, 0.1, 0.5, 1, 2, 3};
|
||||
auto end = trantor::Date::date();
|
||||
auto duration =
|
||||
end.microSecondsSinceEpoch() - start.microSecondsSinceEpoch();
|
||||
collector->metric({method, path}, boundaries, 1h, 6)
|
||||
->observe((double)duration / 1000000);
|
||||
}
|
||||
}
|
||||
co_return resp;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
*
|
||||
* PromStat.h
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <drogon/HttpMiddleware.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class PromStat : public HttpCoroMiddleware<PromStat>
|
||||
{
|
||||
public:
|
||||
PromStat()
|
||||
{
|
||||
void(0);
|
||||
}
|
||||
|
||||
virtual ~PromStat()
|
||||
{
|
||||
}
|
||||
|
||||
Task<HttpResponsePtr> invoke(const HttpRequestPtr &req,
|
||||
MiddlewareNextAwaiter &&next) override;
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
drogon::app().loadConfigFile("../config.json").run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
project(prometheus_example_test CXX)
|
||||
|
||||
add_executable(${PROJECT_NAME} test_main.cc)
|
||||
|
||||
# ##############################################################################
|
||||
# If you include the drogon source code locally in your project, use this method
|
||||
# to add drogon
|
||||
# target_link_libraries(${PROJECT_NAME} PRIVATE drogon)
|
||||
#
|
||||
# and comment out the following lines
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon)
|
||||
|
||||
ParseAndAddDrogonTests(${PROJECT_NAME})
|
||||
@@ -0,0 +1,32 @@
|
||||
#define DROGON_TEST_MAIN
|
||||
#include <drogon/drogon_test.h>
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
DROGON_TEST(BasicTest)
|
||||
{
|
||||
// Add your tests here
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
using namespace drogon;
|
||||
|
||||
std::promise<void> p1;
|
||||
std::future<void> f1 = p1.get_future();
|
||||
|
||||
// Start the main loop on another thread
|
||||
std::thread thr([&]() {
|
||||
// Queues the promise to be fulfilled after starting the loop
|
||||
app().getLoop()->queueInLoop([&p1]() { p1.set_value(); });
|
||||
app().run();
|
||||
});
|
||||
|
||||
// The future is only satisfied after the event loop started
|
||||
f1.get();
|
||||
int status = test::run(argc, argv);
|
||||
|
||||
// Ask the event loop to shutdown and wait
|
||||
app().getLoop()->queueInLoop([]() { app().quit(); });
|
||||
thr.join();
|
||||
return status;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
project(redis CXX)
|
||||
|
||||
include(CheckIncludeFileCXX)
|
||||
|
||||
check_include_file_cxx(any HAS_ANY)
|
||||
check_include_file_cxx(string_view HAS_STRING_VIEW)
|
||||
check_include_file_cxx(coroutine HAS_COROUTINE)
|
||||
if (HAS_ANY AND HAS_STRING_VIEW AND HAS_COROUTINE)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
elseif (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(${PROJECT_NAME} 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(${PROJECT_NAME} PRIVATE drogon)
|
||||
#
|
||||
# and comment out the following lines
|
||||
find_package(Drogon CONFIG REQUIRED)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon)
|
||||
|
||||
# ##############################################################################
|
||||
|
||||
if (CMAKE_CXX_STANDARD LESS 17)
|
||||
# With C++14, use boost to support any and std::string_view
|
||||
message(STATUS "use c++14")
|
||||
find_package(Boost 1.61.0 REQUIRED)
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${Boost_INCLUDE_DIRS})
|
||||
elseif (CMAKE_CXX_STANDARD LESS 20)
|
||||
message(STATUS "use c++17")
|
||||
else ()
|
||||
message(STATUS "use c++20")
|
||||
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)
|
||||
|
||||
drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views
|
||||
${CMAKE_CURRENT_BINARY_DIR})
|
||||
# use the following line to create views with namespaces.
|
||||
# drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views
|
||||
# ${CMAKE_CURRENT_BINARY_DIR} TRUE)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/models)
|
||||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${SRC_DIR}
|
||||
${CTL_SRC}
|
||||
${FILTER_SRC}
|
||||
${PLUGIN_SRC}
|
||||
${MODEL_SRC})
|
||||
# ##############################################################################
|
||||
# uncomment the following line for dynamically loading views
|
||||
# set_property(TARGET ${PROJECT_NAME} PROPERTY ENABLE_EXPORTS ON)
|
||||
@@ -0,0 +1,41 @@
|
||||
# Redis example
|
||||
|
||||
A simple redis example
|
||||
|
||||
## Usage
|
||||
|
||||
First of all you need redis running on the port 6379
|
||||
|
||||
### Post
|
||||
|
||||
```
|
||||
curl --location --request POST 'localhost:8080/client/foo' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"value": "bar"
|
||||
}'
|
||||
```
|
||||
|
||||
### Get
|
||||
|
||||
```
|
||||
curl --location --request GET 'localhost:8080/client/foo'
|
||||
```
|
||||
|
||||
## Subscribe and Publish
|
||||
|
||||
Go to a websocket test website, such as https://wstool.js.org
|
||||
|
||||
### Subscribe
|
||||
|
||||
Connect to ws://localhost:8080/sub
|
||||
|
||||
To subscribe to a channel, send channel name: `mychannel`
|
||||
|
||||
To unsubscribe from a channel, send 'unsub ' + channel name: `unsub mychannel`
|
||||
|
||||
### Publish
|
||||
|
||||
Connect to ws://localhost:8080/pub
|
||||
|
||||
To publish message, send channel name and message: `mychannel anything follows will be the message.`
|
||||
@@ -0,0 +1,87 @@
|
||||
#include "Client.h"
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
void Client::get(const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
std::string key)
|
||||
{
|
||||
nosql::RedisClientPtr redisClient = app().getRedisClient();
|
||||
|
||||
redisClient->execCommandAsync(
|
||||
[callback](const nosql::RedisResult &r) {
|
||||
if (r.type() == nosql::RedisResultType::kNil)
|
||||
{
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setStatusCode(k404NotFound);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string redisResponse = r.asString();
|
||||
Json::Value response;
|
||||
|
||||
Json::CharReaderBuilder builder;
|
||||
Json::CharReader *reader = builder.newCharReader();
|
||||
|
||||
Json::Value json;
|
||||
std::string errors;
|
||||
|
||||
bool parsingSuccessful =
|
||||
reader->parse(redisResponse.c_str(),
|
||||
redisResponse.c_str() + redisResponse.size(),
|
||||
&json,
|
||||
&errors);
|
||||
|
||||
delete reader;
|
||||
|
||||
response["response"] = redisResponse;
|
||||
if (parsingSuccessful)
|
||||
{
|
||||
response["response"] = json;
|
||||
}
|
||||
|
||||
auto resp = HttpResponse::newHttpJsonResponse(response);
|
||||
|
||||
callback(resp);
|
||||
},
|
||||
[](const std::exception &err) {
|
||||
LOG_ERROR << "something failed!!! " << err.what();
|
||||
},
|
||||
"get %s",
|
||||
key.c_str());
|
||||
}
|
||||
|
||||
void Client::post(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
std::string key)
|
||||
{
|
||||
nosql::RedisClientPtr redisClient = app().getRedisClient();
|
||||
auto json = req->getJsonObject();
|
||||
|
||||
if (!json)
|
||||
{
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
resp->setBody("missing 'value' in body");
|
||||
resp->setStatusCode(k400BadRequest);
|
||||
callback(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string value = (*json)["value"].asString();
|
||||
|
||||
redisClient->execCommandAsync(
|
||||
[callback](const nosql::RedisResult &) {
|
||||
auto resp = HttpResponse::newHttpResponse();
|
||||
|
||||
resp->setStatusCode(k201Created);
|
||||
|
||||
callback(resp);
|
||||
},
|
||||
[](const std::exception &err) {
|
||||
LOG_ERROR << "something failed!!! " << err.what();
|
||||
},
|
||||
"set %s %s",
|
||||
key.c_str(),
|
||||
value.c_str());
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include <drogon/HttpController.h>
|
||||
using namespace drogon;
|
||||
|
||||
class Client : public drogon::HttpController<Client>
|
||||
{
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
METHOD_ADD(Client::get, "{1}", Get);
|
||||
METHOD_ADD(Client::post, "{1}", Post);
|
||||
METHOD_LIST_END
|
||||
|
||||
void get(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
std::string key);
|
||||
void post(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
std::string key);
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
#include "WsClient.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
|
||||
struct ClientContext
|
||||
{
|
||||
std::unordered_set<std::string> channels_;
|
||||
std::shared_ptr<nosql::RedisSubscriber> subscriber_;
|
||||
};
|
||||
|
||||
void WsClient::handleNewMessage(const WebSocketConnectionPtr &wsConnPtr,
|
||||
std::string &&message,
|
||||
const WebSocketMessageType &type)
|
||||
{
|
||||
if (type == WebSocketMessageType::Ping ||
|
||||
type == WebSocketMessageType::Pong ||
|
||||
type == WebSocketMessageType::Close)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (type != WebSocketMessageType::Text)
|
||||
{
|
||||
LOG_ERROR << "Unsupported message type " << (int)type;
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG << "WsClient new message from "
|
||||
<< wsConnPtr->peerAddr().toIpPort();
|
||||
|
||||
auto context = wsConnPtr->getContext<ClientContext>();
|
||||
if (!context)
|
||||
{
|
||||
auto pos = message.find(' ');
|
||||
if (pos == std::string::npos)
|
||||
{
|
||||
wsConnPtr->send("Invalid publish message.");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string channel = message.substr(0, pos);
|
||||
std::string msg = message.substr(pos + 1);
|
||||
LOG_INFO << "PUBLISH " << channel << " " << msg;
|
||||
|
||||
// Publisher
|
||||
drogon::app().getRedisClient()->execCommandAsync(
|
||||
[wsConnPtr](const nosql::RedisResult &result) {
|
||||
std::string nSubs = std::to_string(result.asInteger());
|
||||
LOG_INFO << "PUBLISH success to " << nSubs << " subscribers.";
|
||||
wsConnPtr->send("PUBLISH success to " + nSubs +
|
||||
" subscribers.");
|
||||
},
|
||||
[wsConnPtr](const nosql::RedisException &ex) {
|
||||
LOG_INFO << "PUBLISH failed, " << ex.what();
|
||||
wsConnPtr->send(std::string("PUBLISH failed: ") + ex.what());
|
||||
},
|
||||
"PUBLISH %s %s",
|
||||
channel.c_str(),
|
||||
msg.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
std::string channel = std::move(message);
|
||||
if (channel.empty())
|
||||
{
|
||||
wsConnPtr->send("Channel not provided");
|
||||
return;
|
||||
}
|
||||
|
||||
bool subscribe = true;
|
||||
if (channel.compare(0, 6, "unsub ") == 0)
|
||||
{
|
||||
channel = channel.substr(6);
|
||||
subscribe = false;
|
||||
}
|
||||
|
||||
if (subscribe)
|
||||
{
|
||||
if (context->channels_.find(channel) != context->channels_.end())
|
||||
{
|
||||
wsConnPtr->send("Already subscribed to channel " + channel);
|
||||
return;
|
||||
}
|
||||
|
||||
context->subscriber_->subscribe(
|
||||
channel,
|
||||
[channel, wsConnPtr](const std::string &subChannel,
|
||||
const std::string &subMessage) {
|
||||
assert(subChannel == channel);
|
||||
LOG_INFO << "Receive channel message " << subMessage;
|
||||
std::string resp = "{\"channel\":\"" + subChannel +
|
||||
"\",\"message\":\"" + subMessage + "\"}";
|
||||
wsConnPtr->send(resp);
|
||||
});
|
||||
|
||||
context->channels_.insert(channel);
|
||||
wsConnPtr->send("Subscribe to channel: " + channel);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (context->channels_.find(channel) == context->channels_.end())
|
||||
{
|
||||
wsConnPtr->send("Channel not subscribed.");
|
||||
return;
|
||||
}
|
||||
context->channels_.erase(channel);
|
||||
context->subscriber_->unsubscribe(channel);
|
||||
wsConnPtr->send("Unsubscribe from channel: " + channel);
|
||||
}
|
||||
}
|
||||
|
||||
void WsClient::handleNewConnection(const HttpRequestPtr &req,
|
||||
const WebSocketConnectionPtr &wsConnPtr)
|
||||
{
|
||||
if (req->getPath() == "/sub")
|
||||
{
|
||||
LOG_DEBUG << "WsClient new subscriber connection from "
|
||||
<< wsConnPtr->peerAddr().toIpPort();
|
||||
std::shared_ptr<ClientContext> context =
|
||||
std::make_shared<ClientContext>();
|
||||
context->subscriber_ = drogon::app().getRedisClient()->newSubscriber();
|
||||
wsConnPtr->setContext(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_DEBUG << "WsClient new publisher connection from "
|
||||
<< wsConnPtr->peerAddr().toIpPort();
|
||||
}
|
||||
}
|
||||
|
||||
void WsClient::handleConnectionClosed(const WebSocketConnectionPtr &wsConnPtr)
|
||||
{
|
||||
LOG_DEBUG << "WsClient close connection from "
|
||||
<< wsConnPtr->peerAddr().toIpPort();
|
||||
// Channels will be auto unsubscribed when subscriber destructed.
|
||||
// auto context = wsConnPtr->getContext<ClientContext>();
|
||||
// for (auto& channel : context->channels_)
|
||||
// {
|
||||
// context->subscriber_->unsubscribe(channel);
|
||||
// }
|
||||
wsConnPtr->clearContext();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <drogon/WebSocketController.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class WsClient : public drogon::WebSocketController<WsClient>
|
||||
{
|
||||
public:
|
||||
void handleNewMessage(const WebSocketConnectionPtr &,
|
||||
std::string &&,
|
||||
const WebSocketMessageType &) override;
|
||||
void handleNewConnection(const HttpRequestPtr &,
|
||||
const WebSocketConnectionPtr &) override;
|
||||
void handleConnectionClosed(const WebSocketConnectionPtr &) override;
|
||||
WS_PATH_LIST_BEGIN
|
||||
WS_PATH_ADD("/sub");
|
||||
WS_PATH_ADD("/pub");
|
||||
WS_PATH_LIST_END
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
// Set HTTP listener address and port
|
||||
drogon::app().addListener("0.0.0.0", 8080);
|
||||
// Run HTTP framework,the method will block in the internal event loop
|
||||
drogon::app().createRedisClient("127.0.0.1", 6379);
|
||||
drogon::app().run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,visualstudio,visualstudiocode,cmake,c,c++
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,visualstudio,visualstudiocode,cmake,c,c++
|
||||
|
||||
### C ###
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
# Object files
|
||||
*.o
|
||||
*.ko
|
||||
*.obj
|
||||
*.elf
|
||||
|
||||
# Linker output
|
||||
*.ilk
|
||||
*.map
|
||||
*.exp
|
||||
|
||||
# Precompiled Headers
|
||||
*.gch
|
||||
*.pch
|
||||
|
||||
# Libraries
|
||||
*.lib
|
||||
*.a
|
||||
*.la
|
||||
*.lo
|
||||
|
||||
# Shared objects (inc. Windows DLLs)
|
||||
*.dll
|
||||
*.so
|
||||
*.so.*
|
||||
*.dylib
|
||||
|
||||
# Executables
|
||||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
*.i*86
|
||||
*.x86_64
|
||||
*.hex
|
||||
|
||||
# Debug files
|
||||
*.dSYM/
|
||||
*.su
|
||||
*.idb
|
||||
*.pdb
|
||||
|
||||
# Kernel Module Compile Results
|
||||
*.mod*
|
||||
*.cmd
|
||||
.tmp_versions/
|
||||
modules.order
|
||||
Module.symvers
|
||||
Mkfile.old
|
||||
dkms.conf
|
||||
|
||||
### C++ ###
|
||||
# Prerequisites
|
||||
|
||||
# Compiled Object files
|
||||
*.slo
|
||||
|
||||
# Precompiled Headers
|
||||
|
||||
# Linker files
|
||||
|
||||
# Debugger Files
|
||||
|
||||
# Compiled Dynamic libraries
|
||||
|
||||
# Fortran module files
|
||||
*.mod
|
||||
*.smod
|
||||
|
||||
# Compiled Static libraries
|
||||
*.lai
|
||||
|
||||
# Executables
|
||||
|
||||
### CMake ###
|
||||
CMakeLists.txt.user
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
CMakeScripts
|
||||
Testing
|
||||
Makefile
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
CTestTestfile.cmake
|
||||
_deps
|
||||
CMakeUserPresets.json
|
||||
|
||||
### CMake Patch ###
|
||||
# External projects
|
||||
*-prefix/
|
||||
|
||||
### Intellij+all ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Intellij+all Patch ###
|
||||
# Ignores the whole .idea folder and all .iml files
|
||||
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
|
||||
|
||||
.idea/
|
||||
|
||||
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
|
||||
|
||||
*.iml
|
||||
modules.xml
|
||||
.idea/misc.xml
|
||||
*.ipr
|
||||
|
||||
# Sonarlint plugin
|
||||
.idea/sonarlint
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
*.code-workspace
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
### VisualStudio ###
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.meta
|
||||
*.iobj
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*[.json, .xml, .info]
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
### VisualStudio Patch ###
|
||||
# Additional files built by Visual Studio
|
||||
*.tlog
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/intellij+all,visualstudio,visualstudiocode,cmake,c,c++
|
||||
@@ -0,0 +1,26 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
project(redis_cache CXX)
|
||||
|
||||
include(CheckIncludeFileCXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
add_executable(${PROJECT_NAME} main.cc)
|
||||
|
||||
find_package(Drogon CONFIG REQUIRED)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon)
|
||||
|
||||
aux_source_directory(controllers CTL_SRC)
|
||||
aux_source_directory(filters FILTER_SRC)
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${SRC_DIR}
|
||||
${CTL_SRC}
|
||||
${FILTER_SRC})
|
||||
|
||||
add_subdirectory(test)
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include "RedisCache.h"
|
||||
|
||||
template <>
|
||||
inline trantor::Date fromString<trantor::Date>(const std::string &str)
|
||||
{
|
||||
return trantor::Date(std::atoll(str.data()));
|
||||
}
|
||||
|
||||
template <>
|
||||
inline std::string toString<trantor::Date>(const trantor::Date &date)
|
||||
{
|
||||
return std::to_string(date.microSecondsSinceEpoch());
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Redis example
|
||||
|
||||
An example for using coroutines of redis clients. This example is a redis implementation of
|
||||
the [local sessions example](https://github.com/drogonframework/drogon/wiki/ENG-07-Session#examples-of-sessions) on wiki.
|
||||
|
||||
## Usage
|
||||
|
||||
First of all you need redis running on the port 6379;
|
||||
|
||||
Please use new compilers that support coroutines. Configure the project with the following
|
||||
command:
|
||||
|
||||
```shell
|
||||
cmake .. -DCMAKE_CXX_FLAGS="-std=c++20 -fcoroutines" #for GCC 10
|
||||
cmake .. -DCMAKE_CXX_FLAGS="-std=c++20" #for GCC >= 11
|
||||
cmake .. -DCMAKE_CXX_FLAGS="/std:c++20" #for MSVC >= 16.25
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <drogon/utils/coroutine.h>
|
||||
#include <drogon/nosql/RedisClient.h>
|
||||
|
||||
template <typename T>
|
||||
T fromString(const std::string &);
|
||||
|
||||
template <typename T>
|
||||
std::string toString(const T &);
|
||||
|
||||
template <typename T>
|
||||
drogon::Task<T> getFromCache(
|
||||
const std::string &key,
|
||||
const drogon::nosql::RedisClientPtr &client) noexcept(false)
|
||||
{
|
||||
auto value = co_await client->execCommandCoro("get %s", key.data());
|
||||
co_return fromString<T>(value.asString());
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
drogon::Task<> updateCache(
|
||||
const std::string &key,
|
||||
T &&value,
|
||||
const drogon::nosql::RedisClientPtr &client) noexcept(false)
|
||||
{
|
||||
std::string vstr = toString(std::forward<T>(value));
|
||||
co_await client->execCommandCoro("set %s %s", key.data(), vstr.data());
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/* This is a JSON format configuration file
|
||||
*/
|
||||
{
|
||||
"listeners": [
|
||||
{
|
||||
//address: Ip address,0.0.0.0 by default
|
||||
"address": "0.0.0.0",
|
||||
//port: Port number
|
||||
"port": 8848,
|
||||
//https: If true, use https for security,false by default
|
||||
"https": false
|
||||
}
|
||||
],
|
||||
"redis_clients": [
|
||||
{
|
||||
//name: Name of the client,'default' by default
|
||||
//"name":"",
|
||||
//host: Server IP, 127.0.0.1 by default
|
||||
"host": "127.0.0.1",
|
||||
//port: Server port, 6379 by default
|
||||
"port": 6379,
|
||||
//passwd: '' by default
|
||||
"passwd": "",
|
||||
//db index: 0 by default
|
||||
"db": 0,
|
||||
//is_fast: false by default, if it is true, the client is faster but user can't call
|
||||
//any synchronous interface of it.
|
||||
"is_fast": true,
|
||||
//number_of_connections: 1 by default, if the 'is_fast' is true, the number is the number of
|
||||
//connections per IO thread, otherwise it is the total number of all connections.
|
||||
"number_of_connections": 1,
|
||||
//timeout: -1.0 by default, in seconds, the timeout for executing a command.
|
||||
//zero or negative value means no timeout.
|
||||
"timeout": -1.0
|
||||
}
|
||||
],
|
||||
"app": {
|
||||
//number_of_threads: 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
|
||||
"number_of_threads": 1,
|
||||
//enable_session: False by default
|
||||
"enable_session": false,
|
||||
"session_timeout": 0,
|
||||
//session_cookie_key: The cookie key of the session, "JSESSIONID" by default
|
||||
"session_cookie_key": "JSESSIONID",
|
||||
//session_max_age: The max age of the session cookie, -1 by default
|
||||
"session_max_age": -1,
|
||||
//document_root: Root path of HTTP document, default 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",
|
||||
//use_implicit_page: enable implicit pages if true, true by default
|
||||
"use_implicit_page": true,
|
||||
//implicit_page: Set the file which would the server access in a directory that a user accessed.
|
||||
//For example, by default, http://localhost/a-directory resolves to http://localhost/a-directory/index.html.
|
||||
"implicit_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"
|
||||
],
|
||||
//locations: An array of locations of static files for GET requests.
|
||||
"locations": [
|
||||
{
|
||||
//uri_prefix: The URI prefix of the location prefixed with "/", the default value is "" that disables the location.
|
||||
//"uri_prefix": "/.well-known/acme-challenge/",
|
||||
//default_content_type: The default content type of the static files without
|
||||
//an extension. empty string by default.
|
||||
"default_content_type": "text/plain",
|
||||
//alias: The location in file system, if it is prefixed with "/", it
|
||||
//presents an absolute path, otherwise it presents a relative path to
|
||||
//the document_root path.
|
||||
//The default value is "" which means use the document root path as the location base path.
|
||||
"alias": "",
|
||||
//is_case_sensitive: indicates whether the URI prefix is case sensitive.
|
||||
"is_case_sensitive": false,
|
||||
//allow_all: true by default. If it is set to false, only static files with a valid extension can be accessed.
|
||||
"allow_all": true,
|
||||
//is_recursive: true by default. If it is set to false, files in sub directories can't be accessed.
|
||||
"is_recursive": true,
|
||||
//filters: string array, the filters applied to the location.
|
||||
"filters": []
|
||||
}
|
||||
],
|
||||
//max_connections: maximum number of connections, 100000 by default
|
||||
"max_connections": 100000,
|
||||
//max_connections_per_ip: maximum number of connections per client, 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"
|
||||
],
|
||||
//dynamic_views_output_path: Default by an empty string which means the output path of source
|
||||
//files is the path where the csp files locate. If the path isn't prefixed with /, it is relative
|
||||
//path of the current working directory.
|
||||
"dynamic_views_output_path": "",
|
||||
//enable_unicode_escaping_in_json: true by default, enable unicode escaping in json.
|
||||
"enable_unicode_escaping_in_json": true,
|
||||
//float_precision_in_json: set precision of float number in json.
|
||||
"float_precision_in_json": {
|
||||
//precision: 0 by default, 0 means use the default precision of the jsoncpp lib.
|
||||
"precision": 0,
|
||||
//precision_type: must be "significant" or "decimal", defaults to "significant" that means
|
||||
//setting max number of significant digits in string, "decimal" means setting max number of
|
||||
//digits after "." in string
|
||||
"precision_type": "significant"
|
||||
},
|
||||
//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,
|
||||
//handle_sig_term: True by default
|
||||
"handle_sig_term": true,
|
||||
//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 true, 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,
|
||||
//use_brotli: False by default, use brotli to compress the response body's content;
|
||||
"use_brotli": false,
|
||||
//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,
|
||||
//simple_controllers_map: Used to configure mapping from path to simple controller
|
||||
"simple_controllers_map": [
|
||||
],
|
||||
//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": true,
|
||||
//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": true,
|
||||
//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,
|
||||
//br_static: If it is set to true, when the client requests a static file, drogon first finds the compressed
|
||||
//file with the extension ".br" in the same path and send the compressed file to the client.
|
||||
//The default value of br_static is true.
|
||||
"br_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",
|
||||
//reuse_port: Defaults to false, users can run multiple processes listening on the same port at the same time.
|
||||
"reuse_port": false
|
||||
},
|
||||
//plugins: Define all plugins running in the application
|
||||
"plugins": [
|
||||
{
|
||||
//name: The class name of the plugin
|
||||
//"name": "drogon::plugin::SecureSSLRedirector",
|
||||
//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": {
|
||||
"ssl_redirect_exempt": [
|
||||
".*\\.jpg"
|
||||
],
|
||||
"secure_ssl_host": "localhost:8849"
|
||||
}
|
||||
}
|
||||
],
|
||||
//custom_config: custom configuration for users. This object can be get by the app().getCustomConfig() method.
|
||||
"custom_config": {}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "SlowCtrl.h"
|
||||
#include "RedisCache.h"
|
||||
#include "DateFuncs.h"
|
||||
#include <drogon/drogon.h>
|
||||
#define VDate "visitDate"
|
||||
|
||||
void SlowCtrl::hello(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
std::string &&userid)
|
||||
{
|
||||
auto resp = drogon::HttpResponse::newHttpResponse();
|
||||
resp->setBody("hello, " + userid);
|
||||
callback(resp);
|
||||
}
|
||||
|
||||
drogon::AsyncTask SlowCtrl::observe(
|
||||
HttpRequestPtr req,
|
||||
std::function<void(const HttpResponsePtr &)> callback,
|
||||
std::string userid)
|
||||
{
|
||||
auto key = userid + "." + VDate;
|
||||
auto redisPtr = drogon::app().getFastRedisClient();
|
||||
try
|
||||
{
|
||||
auto date = co_await getFromCache<trantor::Date>(key, redisPtr);
|
||||
auto resp = drogon::HttpResponse::newHttpResponse();
|
||||
std::string body{"your last visit time: "};
|
||||
body.append(date.toFormattedStringLocal(false));
|
||||
resp->setBody(std::move(body));
|
||||
callback(resp);
|
||||
}
|
||||
catch (const std::exception &err)
|
||||
{
|
||||
LOG_ERROR << err.what();
|
||||
callback(drogon::HttpResponse::newNotFoundResponse());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <drogon/HttpController.h>
|
||||
using namespace drogon;
|
||||
|
||||
class SlowCtrl : public drogon::HttpController<SlowCtrl>
|
||||
{
|
||||
public:
|
||||
METHOD_LIST_BEGIN
|
||||
ADD_METHOD_TO(SlowCtrl::hello, "/slow?userid={userid}", Get, "TimeFilter");
|
||||
ADD_METHOD_TO(SlowCtrl::observe, "/observe?userid={userid}", Get);
|
||||
METHOD_LIST_END
|
||||
|
||||
void hello(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
std::string &&userid);
|
||||
drogon::AsyncTask observe(
|
||||
HttpRequestPtr req,
|
||||
std::function<void(const HttpResponsePtr &)> callback,
|
||||
std::string userid);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// Created by antao on 2018/5/22.
|
||||
//
|
||||
|
||||
#include "TimeFilter.h"
|
||||
#include <drogon/utils/coroutine.h>
|
||||
#include <drogon/drogon.h>
|
||||
#include <string>
|
||||
#include "RedisCache.h"
|
||||
#include "DateFuncs.h"
|
||||
#define VDate "visitDate"
|
||||
|
||||
void TimeFilter::doFilter(const HttpRequestPtr &req,
|
||||
FilterCallback &&cb,
|
||||
FilterChainCallback &&ccb)
|
||||
{
|
||||
drogon::async_run([req,
|
||||
cb = std::move(cb),
|
||||
ccb = std::move(ccb)]() -> drogon::Task<> {
|
||||
auto userid = req->getParameter("userid");
|
||||
if (!userid.empty())
|
||||
{
|
||||
auto key = userid + "." + VDate;
|
||||
const trantor::Date now = trantor::Date::date();
|
||||
auto redisClient = drogon::app().getFastRedisClient();
|
||||
try
|
||||
{
|
||||
auto lastDate =
|
||||
co_await getFromCache<trantor::Date>(key, redisClient);
|
||||
LOG_TRACE << "last:" << lastDate.toFormattedString(false);
|
||||
co_await updateCache(key, now, redisClient);
|
||||
if (now > lastDate.after(10))
|
||||
{
|
||||
// 10 sec later can visit again;
|
||||
ccb();
|
||||
co_return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Json::Value json;
|
||||
json["result"] = "error";
|
||||
json["message"] =
|
||||
"Access interval should be at least 10 seconds";
|
||||
auto res = HttpResponse::newHttpJsonResponse(json);
|
||||
cb(res);
|
||||
co_return;
|
||||
}
|
||||
}
|
||||
catch (const std::exception &err)
|
||||
{
|
||||
LOG_TRACE << "first visit,insert visitDate";
|
||||
try
|
||||
{
|
||||
co_await updateCache(userid + "." VDate, now, redisClient);
|
||||
}
|
||||
catch (const std::exception &err)
|
||||
{
|
||||
LOG_ERROR << err.what();
|
||||
cb(HttpResponse::newHttpJsonResponse(err.what()));
|
||||
co_return;
|
||||
}
|
||||
ccb();
|
||||
co_return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto resp = HttpResponse::newNotFoundResponse();
|
||||
cb(resp);
|
||||
co_return;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Created by antao on 2018/5/22.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <drogon/HttpFilter.h>
|
||||
using namespace drogon;
|
||||
|
||||
class TimeFilter : public drogon::HttpFilter<TimeFilter>
|
||||
{
|
||||
public:
|
||||
void doFilter(const HttpRequestPtr &req,
|
||||
FilterCallback &&cb,
|
||||
FilterChainCallback &&ccb) override;
|
||||
|
||||
TimeFilter()
|
||||
{
|
||||
LOG_DEBUG << "TimeFilter constructor";
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
drogon::app().loadConfigFile("../config.json");
|
||||
drogon::app().run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
project(redis_cache_test CXX)
|
||||
|
||||
add_executable(${PROJECT_NAME} test_main.cc)
|
||||
|
||||
# ##############################################################################
|
||||
# If you include the drogon source code locally in your project, use this method
|
||||
# to add drogon
|
||||
# target_link_libraries(${PROJECT_NAME}_test PRIVATE drogon)
|
||||
#
|
||||
# and comment out the following lines
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon)
|
||||
|
||||
ParseAndAddDrogonTests(${PROJECT_NAME})
|
||||
@@ -0,0 +1,32 @@
|
||||
#define DROGON_TEST_MAIN
|
||||
#include <drogon/drogon_test.h>
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
DROGON_TEST(BasicTest)
|
||||
{
|
||||
// Add your tests here
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
using namespace drogon;
|
||||
|
||||
std::promise<void> p1;
|
||||
std::future<void> f1 = p1.get_future();
|
||||
|
||||
// Start the main loop on another thread
|
||||
std::thread thr([&]() {
|
||||
// Queues the promise to be fulfilled after starting the loop
|
||||
app().getLoop()->queueInLoop([&p1]() { p1.set_value(); });
|
||||
app().run();
|
||||
});
|
||||
|
||||
// The future is only satisfied after the event loop started
|
||||
f1.get();
|
||||
int status = test::run(argc, argv);
|
||||
|
||||
// Ask the event loop to shutdown and wait
|
||||
app().getLoop()->queueInLoop([]() { app().quit(); });
|
||||
thr.join();
|
||||
return status;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
project(redis_chat CXX)
|
||||
|
||||
include(CheckIncludeFileCXX)
|
||||
|
||||
check_include_file_cxx(any HAS_ANY)
|
||||
check_include_file_cxx(string_view HAS_STRING_VIEW)
|
||||
check_include_file_cxx(coroutine HAS_COROUTINE)
|
||||
if (HAS_ANY AND HAS_STRING_VIEW AND HAS_COROUTINE)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
elseif (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(${PROJECT_NAME} 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(${PROJECT_NAME} PRIVATE drogon)
|
||||
#
|
||||
# and comment out the following lines
|
||||
find_package(Drogon CONFIG REQUIRED)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon)
|
||||
|
||||
# ##############################################################################
|
||||
|
||||
if (CMAKE_CXX_STANDARD LESS 17)
|
||||
# With C++14, use boost to support any and std::string_view
|
||||
message(STATUS "use c++14")
|
||||
find_package(Boost 1.61.0 REQUIRED)
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${Boost_INCLUDE_DIRS})
|
||||
elseif (CMAKE_CXX_STANDARD LESS 20)
|
||||
message(STATUS "use c++17")
|
||||
else ()
|
||||
message(STATUS "use c++20")
|
||||
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)
|
||||
|
||||
drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views
|
||||
${CMAKE_CURRENT_BINARY_DIR})
|
||||
# use the following line to create views with namespaces.
|
||||
# drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views
|
||||
# ${CMAKE_CURRENT_BINARY_DIR} TRUE)
|
||||
|
||||
target_include_directories(${PROJECT_NAME}
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/models)
|
||||
target_sources(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
${SRC_DIR}
|
||||
${CTL_SRC}
|
||||
${FILTER_SRC}
|
||||
${PLUGIN_SRC}
|
||||
${MODEL_SRC})
|
||||
# ##############################################################################
|
||||
# uncomment the following line for dynamically loading views
|
||||
# set_property(TARGET ${PROJECT_NAME} PROPERTY ENABLE_EXPORTS ON)
|
||||
@@ -0,0 +1,25 @@
|
||||
# Redis example
|
||||
|
||||
A simple redis chat server
|
||||
|
||||
## Usage
|
||||
|
||||
First you need redis running on the port 6379.
|
||||
|
||||
### Connect with username
|
||||
|
||||
ws://localhost:8080/chat?name=<your-name>
|
||||
|
||||
### Enter room
|
||||
|
||||
Send message `ENTER <roomNo>` to enter a room.
|
||||
|
||||
room number should be 0 - 99.
|
||||
|
||||
### Quit room
|
||||
|
||||
Send message `QUIT` to quit room.
|
||||
|
||||
### Send message
|
||||
|
||||
Send whatever else will be a message.
|
||||
@@ -0,0 +1,258 @@
|
||||
#include "Chat.h"
|
||||
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
|
||||
static void redisLogin(std::function<void(int)> &&callback,
|
||||
const std::string &loginKey,
|
||||
unsigned int timeout);
|
||||
static void redisLogout(std::function<void(int)> &&callback,
|
||||
const std::string &loginKey);
|
||||
|
||||
struct ClientContext
|
||||
{
|
||||
std::string name_;
|
||||
std::string loginKey_;
|
||||
std::string room_;
|
||||
std::shared_ptr<nosql::RedisSubscriber> subscriber_;
|
||||
};
|
||||
|
||||
static bool checkRoomNumber(const std::string &room)
|
||||
{
|
||||
if (room.empty() || room.size() > 2 || (room.size() == 2 && room[0] == '0'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (char c : room)
|
||||
{
|
||||
if (c < '0' || c > '9')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Chat::handleNewMessage(const WebSocketConnectionPtr &wsConnPtr,
|
||||
std::string &&message,
|
||||
const WebSocketMessageType &type)
|
||||
{
|
||||
if (type == WebSocketMessageType::Close ||
|
||||
type == WebSocketMessageType::Ping)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (type != WebSocketMessageType::Text &&
|
||||
type != WebSocketMessageType::Pong)
|
||||
{
|
||||
LOG_ERROR << "Unsupported message type " << (int)type;
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG << "WsClient new message from "
|
||||
<< wsConnPtr->peerAddr().toIpPort();
|
||||
|
||||
auto context = wsConnPtr->getContext<ClientContext>();
|
||||
if (!context || context->name_.empty())
|
||||
{
|
||||
wsConnPtr->send("ERROR: You are not logged in.");
|
||||
wsConnPtr->forceClose();
|
||||
return;
|
||||
}
|
||||
|
||||
auto redisClient = drogon::app().getRedisClient();
|
||||
if (type == WebSocketMessageType::Pong)
|
||||
{
|
||||
redisClient->execCommandAsync(
|
||||
[wsConnPtr](const nosql::RedisResult &) {
|
||||
// Do nothing
|
||||
},
|
||||
[wsConnPtr](const nosql::RedisException &ex) {
|
||||
LOG_ERROR << "Update user status failed: " << ex.what();
|
||||
wsConnPtr->send("ERROR: Service unavailable.");
|
||||
wsConnPtr->forceClose();
|
||||
},
|
||||
"SET %s 1 EX 120",
|
||||
context->loginKey_.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
int operation = 0;
|
||||
std::string room;
|
||||
if (message.compare(0, 6, "ENTER ") == 0)
|
||||
{
|
||||
room = message.substr(6, message.find_last_not_of(" \n") - 5);
|
||||
if (!checkRoomNumber(room))
|
||||
{
|
||||
wsConnPtr->send("ERROR: Invalid room number, should be [0-99].");
|
||||
return;
|
||||
}
|
||||
operation = 1;
|
||||
}
|
||||
else if (message == "QUIT")
|
||||
{
|
||||
operation = 2;
|
||||
}
|
||||
|
||||
switch (operation)
|
||||
{
|
||||
case 0: // Message
|
||||
{
|
||||
if (context->room_.empty())
|
||||
{
|
||||
wsConnPtr->send(
|
||||
"ERROR: Not in room. Send 'ENTER roomNo' to enter a "
|
||||
"room first.");
|
||||
return;
|
||||
}
|
||||
// Publish message
|
||||
std::string msg =
|
||||
"[" + context->room_ + "][" + context->name_ + "] " + message;
|
||||
|
||||
// NOTICE: Dangerous to concat username into redis command!!!
|
||||
// Do not use in production.
|
||||
redisClient->execCommandAsync(
|
||||
[](const nosql::RedisResult &) {},
|
||||
[wsConnPtr](const nosql::RedisException &ex) {
|
||||
wsConnPtr->send(std::string("ERROR: ") + ex.what());
|
||||
},
|
||||
"publish %s %s",
|
||||
context->room_.c_str(),
|
||||
msg.c_str());
|
||||
break;
|
||||
}
|
||||
case 1: // Enter room
|
||||
{
|
||||
if (context->room_ == room)
|
||||
{
|
||||
wsConnPtr->send("ERROR: Already in room " + context->room_);
|
||||
return;
|
||||
}
|
||||
if (!context->room_.empty())
|
||||
{
|
||||
context->subscriber_->unsubscribe(context->room_);
|
||||
}
|
||||
wsConnPtr->send("INFO: Enter room " + room);
|
||||
context->subscriber_->subscribe(
|
||||
room, [wsConnPtr](const std::string &, const std::string &msg) {
|
||||
wsConnPtr->send(msg);
|
||||
});
|
||||
context->room_ = room;
|
||||
break;
|
||||
}
|
||||
case 2: // Quit room
|
||||
{
|
||||
if (!context->room_.empty())
|
||||
{
|
||||
context->subscriber_->unsubscribe(context->room_);
|
||||
wsConnPtr->send("INFO: Quit room " + context->room_);
|
||||
context->room_.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
wsConnPtr->send(
|
||||
"ERROR: Not in room. Send 'ENTER roomNo' to enter a "
|
||||
"room first.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Chat::handleNewConnection(const HttpRequestPtr &req,
|
||||
const WebSocketConnectionPtr &wsConnPtr)
|
||||
{
|
||||
LOG_DEBUG << "WsClient new connection from "
|
||||
<< wsConnPtr->peerAddr().toIpPort();
|
||||
const std::string name = req->getParameter("name");
|
||||
if (name.empty())
|
||||
{
|
||||
wsConnPtr->send("Please give your name in parameters.");
|
||||
wsConnPtr->forceClose();
|
||||
}
|
||||
|
||||
std::string loginKey = "redis_chat:user:" + drogon::utils::getMd5(name);
|
||||
redisLogin(
|
||||
[wsConnPtr, name, loginKey](int status) {
|
||||
if (status < 0)
|
||||
{
|
||||
wsConnPtr->send("ERROR: Service unavailable.");
|
||||
wsConnPtr->shutdown();
|
||||
return;
|
||||
}
|
||||
if (status == 0)
|
||||
{
|
||||
wsConnPtr->send("ERROR: User [" + name +
|
||||
"] already logged in.");
|
||||
wsConnPtr->shutdown();
|
||||
return;
|
||||
}
|
||||
std::shared_ptr<ClientContext> context =
|
||||
std::make_shared<ClientContext>();
|
||||
context->subscriber_ =
|
||||
drogon::app().getRedisClient()->newSubscriber();
|
||||
context->name_ = name;
|
||||
context->loginKey_ = loginKey;
|
||||
wsConnPtr->setContext(context);
|
||||
wsConnPtr->send("Hello, " + name + "!");
|
||||
},
|
||||
loginKey,
|
||||
120);
|
||||
}
|
||||
|
||||
void Chat::handleConnectionClosed(const WebSocketConnectionPtr &wsConnPtr)
|
||||
{
|
||||
LOG_DEBUG << "WsClient close connection from "
|
||||
<< wsConnPtr->peerAddr().toIpPort();
|
||||
auto context = wsConnPtr->getContext<ClientContext>();
|
||||
// Channels will be auto unsubscribed when subscriber destructed.
|
||||
// for (auto& channel : context->channels_)
|
||||
// {
|
||||
// context->subscriber_->unsubscribe(channel);
|
||||
// }
|
||||
if (context)
|
||||
{
|
||||
redisLogout([](int) {}, context->loginKey_);
|
||||
}
|
||||
}
|
||||
|
||||
static void redisLogin(std::function<void(int)> &&callback,
|
||||
const std::string &loginKey,
|
||||
unsigned int timeout)
|
||||
{
|
||||
static const char script[] = R"(
|
||||
local exists = redis.call('GET', KEYS[1]);
|
||||
if exists then return 0 end;
|
||||
redis.call('SET', KEYS[1], 1, 'EX', ARGV[1]);
|
||||
return 1;
|
||||
)";
|
||||
|
||||
drogon::app().getRedisClient()->execCommandAsync(
|
||||
[callback](const nosql::RedisResult &result) {
|
||||
callback((int)result.asInteger());
|
||||
},
|
||||
[callback](const nosql::RedisException &ex) {
|
||||
LOG_ERROR << "Login error: " << ex.what();
|
||||
callback(-1);
|
||||
},
|
||||
"EVAL %s 1 %s %u",
|
||||
script,
|
||||
loginKey.c_str(),
|
||||
timeout);
|
||||
}
|
||||
|
||||
static void redisLogout(std::function<void(int)> &&callback,
|
||||
const std::string &loginKey)
|
||||
{
|
||||
drogon::app().getRedisClient()->execCommandAsync(
|
||||
[callback](const nosql::RedisResult &result) {
|
||||
callback((int)result.asInteger());
|
||||
},
|
||||
[callback](const nosql::RedisException &ex) {
|
||||
LOG_ERROR << "Logout error: " << ex.what();
|
||||
callback(-1);
|
||||
},
|
||||
"DEL %s",
|
||||
loginKey.c_str());
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <drogon/WebSocketController.h>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class Chat : public drogon::WebSocketController<Chat>
|
||||
{
|
||||
public:
|
||||
void handleNewMessage(const WebSocketConnectionPtr &,
|
||||
std::string &&,
|
||||
const WebSocketMessageType &) override;
|
||||
void handleNewConnection(const HttpRequestPtr &,
|
||||
const WebSocketConnectionPtr &) override;
|
||||
void handleConnectionClosed(const WebSocketConnectionPtr &) override;
|
||||
WS_PATH_LIST_BEGIN
|
||||
WS_PATH_ADD("/chat");
|
||||
WS_PATH_LIST_END
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
#include <drogon/drogon.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
drogon::app().addListener("0.0.0.0", 8080);
|
||||
drogon::app().createRedisClient("127.0.0.1", 6379);
|
||||
drogon::app().run();
|
||||
return 0;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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})
|
||||
@@ -0,0 +1,3 @@
|
||||
### An example that shows how to use drogon as an http reverse proxy with a simple round robin.
|
||||
|
||||
This project is created with the drogon_ctl command, please compile it after installing drogon.
|
||||
@@ -0,0 +1,160 @@
|
||||
/* This is a JSON format configuration file
|
||||
*/
|
||||
{
|
||||
/*
|
||||
//ssl: The global ssl files setting
|
||||
"ssl": {
|
||||
"cert": "../../trantor/trantor/tests/server.crt",
|
||||
"key": "../../trantor/trantor/tests/server.key"
|
||||
},*/
|
||||
"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": 2,
|
||||
//enable_session: False by default
|
||||
"enable_session": false,
|
||||
"session_timeout": 0,
|
||||
//session_cookie_key: The cookie key of the session, "JSESSIONID" by default
|
||||
"session_cookie_key": "JSESSIONID",
|
||||
//session_max_age: The max age of the session cookie, -1 by default
|
||||
"session_max_age": -1,
|
||||
//document_root: Root path of HTTP document, default 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 client,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,
|
||||
//handle_sig_term: True by default
|
||||
"handle_sig_term": true,
|
||||
//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 true, 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": {
|
||||
// The pipelining depth of HTTP clients.
|
||||
"pipelining": 16,
|
||||
"backends": ["http://127.0.0.1:8848"],
|
||||
"same_client_to_same_backend": false,
|
||||
//The number of connections created by proxy for each backend in very event loop (IO thread).
|
||||
"connection_factor": 1
|
||||
}
|
||||
}],
|
||||
//custom_config: custom configuration for users. This object can be get by the app().getCustomConfig() method.
|
||||
"custom_config": {}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#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;
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
*
|
||||
* @file 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();
|
||||
sameClientToSameBackend_ =
|
||||
config.get("same_client_to_same_backend", false).asBool();
|
||||
connectionFactor_ = config.get("connection_factor", 1).asInt();
|
||||
if (connectionFactor_ == 0 || connectionFactor_ > 100)
|
||||
{
|
||||
LOG_ERROR << "invalid number of connection factor";
|
||||
abort();
|
||||
}
|
||||
clients_.init(
|
||||
[this](std::vector<HttpClientPtr> &clients, size_t ioLoopIndex) {
|
||||
clients.resize(backendAddrs_.size() * connectionFactor_);
|
||||
});
|
||||
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_)
|
||||
{
|
||||
index = std::hash<uint32_t>{}(req->getPeerAddr().ipNetEndian()) %
|
||||
clientsVector.size();
|
||||
index = (index + (++(*clientIndex_)) * backendAddrs_.size()) %
|
||||
clientsVector.size();
|
||||
}
|
||||
else
|
||||
{
|
||||
index = ++(*clientIndex_) % clientsVector.size();
|
||||
}
|
||||
auto &clientPtr = clientsVector[index];
|
||||
if (!clientPtr)
|
||||
{
|
||||
auto &addr = backendAddrs_[index % backendAddrs_.size()];
|
||||
clientPtr = HttpClient::newHttpClient(
|
||||
addr, trantor::EventLoop::getEventLoopOfCurrentThread());
|
||||
clientPtr->setPipeliningDepth(pipeliningDepth_);
|
||||
}
|
||||
req->setPassThrough(true);
|
||||
clientPtr->sendRequest(
|
||||
req,
|
||||
[callback = std::move(callback)](ReqResult result,
|
||||
const HttpResponsePtr &resp) {
|
||||
if (result == ReqResult::Ok)
|
||||
{
|
||||
resp->setPassThrough(true);
|
||||
callback(resp);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto errResp = HttpResponse::newHttpResponse();
|
||||
errResp->setStatusCode(k500InternalServerError);
|
||||
callback(errResp);
|
||||
}
|
||||
});
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
*
|
||||
* @file 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.
|
||||
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.
|
||||
void shutdown() override;
|
||||
|
||||
private:
|
||||
// Create 'connectionFactor_' HTTP clients 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 pipeliningDepth_{0};
|
||||
void preRouting(const drogon::HttpRequestPtr &,
|
||||
drogon::AdviceCallback &&,
|
||||
drogon::AdviceChainCallback &&);
|
||||
size_t connectionFactor_{1};
|
||||
};
|
||||
} // namespace my_plugin
|
||||
@@ -0,0 +1,82 @@
|
||||
#include <drogon/WebSocketClient.h>
|
||||
#include <drogon/HttpAppFramework.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
using namespace drogon;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
std::string server;
|
||||
std::string path;
|
||||
std::optional<uint16_t> port;
|
||||
// Connect to a public echo server
|
||||
if (argc > 1 && std::string(argv[1]) == "-p")
|
||||
{
|
||||
server = "wss://echo.websocket.events/.ws";
|
||||
path = "/";
|
||||
}
|
||||
else
|
||||
{
|
||||
server = "ws://127.0.0.1";
|
||||
port = 8848;
|
||||
path = "/chat";
|
||||
}
|
||||
|
||||
std::string serverString;
|
||||
if (port.value_or(0) != 0)
|
||||
serverString = server + ":" + std::to_string(port.value());
|
||||
else
|
||||
serverString = server;
|
||||
auto wsPtr = WebSocketClient::newWebSocketClient(serverString);
|
||||
auto req = HttpRequest::newHttpRequest();
|
||||
req->setPath(path);
|
||||
|
||||
wsPtr->setMessageHandler([](const std::string &message,
|
||||
const WebSocketClientPtr &,
|
||||
const WebSocketMessageType &type) {
|
||||
std::string messageType = "Unknown";
|
||||
if (type == WebSocketMessageType::Text)
|
||||
messageType = "text";
|
||||
else if (type == WebSocketMessageType::Pong)
|
||||
messageType = "pong";
|
||||
else if (type == WebSocketMessageType::Ping)
|
||||
messageType = "ping";
|
||||
else if (type == WebSocketMessageType::Binary)
|
||||
messageType = "binary";
|
||||
else if (type == WebSocketMessageType::Close)
|
||||
messageType = "Close";
|
||||
|
||||
LOG_INFO << "new message (" << messageType << "): " << message;
|
||||
});
|
||||
|
||||
wsPtr->setConnectionClosedHandler([](const WebSocketClientPtr &) {
|
||||
LOG_INFO << "WebSocket connection closed!";
|
||||
});
|
||||
|
||||
LOG_INFO << "Connecting to WebSocket at " << server;
|
||||
wsPtr->connectToServer(
|
||||
req,
|
||||
[](ReqResult r,
|
||||
const HttpResponsePtr &,
|
||||
const WebSocketClientPtr &wsPtr) {
|
||||
if (r != ReqResult::Ok)
|
||||
{
|
||||
LOG_ERROR << "Failed to establish WebSocket connection!";
|
||||
wsPtr->stop();
|
||||
return;
|
||||
}
|
||||
LOG_INFO << "WebSocket connected!";
|
||||
wsPtr->getConnection()->setPingMessage("", 2s);
|
||||
wsPtr->getConnection()->send("hello!");
|
||||
});
|
||||
|
||||
// Quit the application after 15 seconds
|
||||
app().getLoop()->runAfter(15, []() { app().quit(); });
|
||||
|
||||
app().setLogLevel(trantor::Logger::kDebug);
|
||||
app().run();
|
||||
LOG_INFO << "bye!";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#include <drogon/WebSocketController.h>
|
||||
#include <drogon/PubSubService.h>
|
||||
#include <drogon/HttpAppFramework.h>
|
||||
using namespace drogon;
|
||||
|
||||
class WebSocketChat : public drogon::WebSocketController<WebSocketChat>
|
||||
{
|
||||
public:
|
||||
void handleNewMessage(const WebSocketConnectionPtr &,
|
||||
std::string &&,
|
||||
const WebSocketMessageType &) override;
|
||||
void handleConnectionClosed(const WebSocketConnectionPtr &) override;
|
||||
void handleNewConnection(const HttpRequestPtr &,
|
||||
const WebSocketConnectionPtr &) override;
|
||||
WS_PATH_LIST_BEGIN
|
||||
WS_PATH_ADD("/chat", Get);
|
||||
WS_ADD_PATH_VIA_REGEX("/[^/]*", Get);
|
||||
WS_PATH_LIST_END
|
||||
private:
|
||||
PubSubService<std::string> chatRooms_;
|
||||
};
|
||||
|
||||
struct Subscriber
|
||||
{
|
||||
std::string chatRoomName_;
|
||||
drogon::SubscriberID id_;
|
||||
};
|
||||
|
||||
void WebSocketChat::handleNewMessage(const WebSocketConnectionPtr &wsConnPtr,
|
||||
std::string &&message,
|
||||
const WebSocketMessageType &type)
|
||||
{
|
||||
// write your application logic here
|
||||
LOG_DEBUG << "new websocket message:" << message;
|
||||
if (type == WebSocketMessageType::Ping)
|
||||
{
|
||||
LOG_DEBUG << "recv a ping";
|
||||
}
|
||||
else if (type == WebSocketMessageType::Text)
|
||||
{
|
||||
auto &s = wsConnPtr->getContextRef<Subscriber>();
|
||||
chatRooms_.publish(s.chatRoomName_, message);
|
||||
}
|
||||
}
|
||||
|
||||
void WebSocketChat::handleConnectionClosed(const WebSocketConnectionPtr &conn)
|
||||
{
|
||||
LOG_DEBUG << "websocket closed!";
|
||||
auto &s = conn->getContextRef<Subscriber>();
|
||||
chatRooms_.unsubscribe(s.chatRoomName_, s.id_);
|
||||
}
|
||||
|
||||
void WebSocketChat::handleNewConnection(const HttpRequestPtr &req,
|
||||
const WebSocketConnectionPtr &conn)
|
||||
{
|
||||
LOG_DEBUG << "new websocket connection!";
|
||||
conn->send("haha!!!");
|
||||
Subscriber s;
|
||||
s.chatRoomName_ = req->getParameter("room_name");
|
||||
s.id_ = chatRooms_.subscribe(s.chatRoomName_,
|
||||
[conn](const std::string &topic,
|
||||
const std::string &message) {
|
||||
// Suppress unused variable warning
|
||||
(void)topic;
|
||||
conn->send(message);
|
||||
});
|
||||
conn->setContext(std::make_shared<Subscriber>(std::move(s)));
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
app().addListener("127.0.0.1", 8848).run();
|
||||
}
|
||||
Reference in New Issue
Block a user