复现已有算法

This commit is contained in:
cloud
2026-07-14 15:43:18 +08:00
parent abebd2a683
commit 50b8111fd9
860 changed files with 182250 additions and 18 deletions
+72
View File
@@ -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
View File
@@ -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 &currentFile = 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));
}
};
+113
View File
@@ -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
};
+11
View File
@@ -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);
}
+15
View File
@@ -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
};
+23
View File
@@ -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();
}
+80
View File
@@ -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
View File
@@ -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);
}
};
+97
View File
@@ -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();
}
+130
View File
@@ -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
View File
@@ -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>
+68
View File
@@ -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"]
}
]
}
}
]
}
@@ -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;
}
@@ -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;
}
+68
View File
@@ -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)
+41
View File
@@ -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
View File
@@ -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());
}
+17
View File
@@ -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());
}
+233
View File
@@ -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";
}
};
+8
View File
@@ -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)
+25
View File
@@ -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
};
+9
View File
@@ -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;
}
@@ -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);
}
});
}
@@ -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();
}