A C++ HTTP server library mimicking ASP.NET Core
Copyright Β© 2024 Tony's Studio
minet core is a C++ HTTP server library that mimics the design of ASP.NET Core. It is designed to be a lightweight, high-performance, and easy-to-use library for building web applications in C++.
Important
Currently, this library only targets Linux platforms. It is not the library you are looking for if you want cross-platform support.
Although minet core is simple, it indeed has some cool features.π
- Easy to Use: It is super easy to create a server with just a few lines of code.
- Flexible Configuration: You can configure the server with a JSON file.
- Dependency Injection: It uses IoC to manage components, and you can replace them with your own.
- Multiple Servers: It provides multiple server implementations, good for study and comparison.
- Extensible: It is designed to be extensible, and you can build your framework on top of it.
The main purpose for minet core is to learn the mechanism behind an HTTP server. The key is to understand how the server accepts and handles requests, and how it sends responses back. It is also a good practice to learn how to design a library that is easy to use and flexible to extend.
The most important part of minet core is the design of the server. The server is responsible for accepting requests. As my knowledge expands, I have implemented three different servers.
The first is Basic
server, accepting requests in one thread. It is simple and easy to understand, but it is not efficient.
The second is Threaded
server, which handles requests in multiple threads with a thread pool.
It is way more efficient than the Basic
server, but it is not the best.
The third is Mayhem
server, based on Threaded
server, but also utilizes epoll for asynchronous I/O. It is the most efficient server in minet core.
Note
Before you move on, ensure you have CMake and your C++ compiler supports C++ 17.π«‘
Warning
If you build minet-core with TSan (Thread Sanitizer) on Ubuntu 20.04, you may get linker error saying missing libtsan_preinit.o
. Check (TSAN) /usr/bin/ld can't find libtsan_preinit.o for solutions.
minet-core relies on some third-party libraries, you can clone it with the following command.
git clone https://github.com/Lord-Turmoil/minet-core.git --recurse-submodules
Or, you can clone it first, and then update the submodules.
git clone https://github.com/Lord-Turmoil/minet-core.git
cd minet-core
git submodule update --init --recursive
If you want to update the submodules, you can run the following command, or ./script/update.sh
.
git submodule update --recursive
Note
By default, the sanitizer options are OFF. You can enable address sanitizer or thread sanitizer by enabling MINET_ASAN
or MINET_TSAN
option respectively. Since these two sanitizers are not compatible with each other, TSan will be ignored if ASan is enabled.
Before you decide to use minet-core in your project, you can build the demo server to see how it works. The following commands will build libray and the demo server. You can also jump to the following section to see the bundled demo.
./script/build.sh debug # build the debug version
./script/build.sh release # build the release version
To use minet core in your project, simply add it as a subdirectory in your CMake project. Then link your target with minetcore
.
add_subdirectory("minet-core")
target_link_libraries(your-app-name PRIVATE minetcore)
It is super easy to create a server with minet-core, just a few lines of code and you are ready to go.π
#include <minet/minet.h>
using namespace minet;
int main()
{
WebHostBuilder::Create()
->UseAppSettings()
->Get("/ping", RequestHandler::Bind(
[](const TextRequest& request, TextResponse& response) {
response.Text().assign("pong");
}))
->Build()
->Run();
return 0;
}
Caution
For now, there is a known issue in this calling chain. You have to call UseAppSettings()
(with or without parameter) before you call any other functions. It loads settings and initializes necessary components.π’
The example above might be a little too simple. For a more comprehensive demonstration, you can see the demo server under demo/
. To see it live, use the following commands. It will run the demo server, and send some dummy requests to it. There are two server options available. And the client will launch 4 processes to send requests at the same time.
# build and run the demo server
./script/demo.sh server # run with Basic server
./script/demo.sh server mayhem # run with Mayhem server
# in another terminal
./script/demo.sh client # 4 processes, each sending 10 requests
./script/demo.sh client N # 4 processes, each sending N requests
./script/demo.sh client N M # 2^M processes, each sending N requests
Basic server handles requests in one thread, so you'll see the client return one response at a time. Mayhem server handles requests asynchronously, so you'll see a significant speedup in the client.
The configuration files are also provided in demo/
, you can modify them to see the effects.
Tip
By default, the server launch at http://localhost:5000
, so make sure this port isn't blocked or occupied. And the demo client uses curl
to send requests.
To relieve you from the tedious work of serializing and deserializing requests and responses, minet-core provides a wrapper for them. You can access the request and response via TextRequest
and TextResponse
for text-based content, or JsonRequest
and JsonResponse
for JSON content.
TextRequest
/TextResponse
provides Text()
method to access the plain text body. JsonRequest
/JsonResponse
provides Json()
method to access the JSON body. You can also use Headers()
to access the headers, and Params()
to access the query parameters.
Request handler is where you write the server logic. It takes the request and response as parameters as shown below. TRequest
and TResponse
are template types for the wrapper.
using RequestHandlerFn = std::function<void(const TRequest&, TResponse&)>;
minet-core provides a convenient way to create request handlers by using predefined templates. For plain text request and response, you can use RequestHandler
directly. For JSON request and response, you can use RestfulHandler
. If request and response types are different, you can use CustomHandler
instead. Below are their definitions.
using RequestHandler = RequestHandlerImpl<TextRequest, TextResponse>;
using RestfulHandler = RequestHandlerImpl<JsonRequest, JsonResponse>;
template <typename TRequest, typename TResponse>
using CustomHandler = RequestHandlerImpl<TRequest, TResponse>;
void text(const TextRequest& request, TextResponse& response);
void json(const JsonRequest& request, JsonResponse& response);
void custom(const TextRequest& request, JsonResponse& response);
// create WebHostBuilder
builder
->Get("/text", RequestHandler::Bind(text))
->Post("/json", RestfulHandler::Bind(json))
->Post("/custom", CustomHandler::Bind(custom));
See, isn't it easy?π
minet-core provides flexible configurations for your server. By default, it will load configurations from appsettings.json
in the working directory. You can change it by passing custom path to UseAppSeettings()
function, just like what you see in the demo server.
The configuration file should be in JSON format, and comments are allowed. All settings in the configuration file are optional, so it can be as simple as {}
. There are two built-in settings for minet-core, server
and logging
, and you can add extra settings.
The server settings include the name
of the server, and the port
to listen on.
{
"server": {
"name": "Basic",
"threads": 4,
"capacity": 1024,
"port": 5000
}
}
Notably, name
specifies which server to use. Currently, minet-core has three server implementation.
Basic
: Default option, a blocking server that handles requests synchronously.Threaded
: A server that handles requests in multiple threads with a thread pool.Mayhem
: An experimental server that handles requests using both epoll and thread pool.
If you choose to use Basic
server, then threads
and capacity
are ignored. For Threaded
and Mayhem
server, threads
is the number of worker threads, and capacity
is the maximum requests queued on each worker thread.
minet-core usesspdlog
for logging, and you can configure it in the logging
section. The format of logging settings is as follows.
{
"logging": {
"level": "Debug",
"pattern": "",
"sinks": [
{
"file": "stdout"
},
{
"pattern": "[%Y-%m-%d %H:%M:%S] %8l [%6n]: %v",
"file": "server.log"
}
],
"loggers": {
"Demo": {
"level": "Debug",
"pattern": "%^[%Y-%m-%d %H:%M:%S] %l [%6n]: %v%$",
"sinks": [
{
"file": "stdout"
},
{
"file": "demo.log"
}
]
}
}
}
}
In the top level of logging
are default configurations. And specific configurations for individual loggers are under loggers
. Each logger has its own level
, pattern
, and sinks
. If one of these are missing, it will inherit from the default settings.
level
is the logging level, and it can be one of the following: All
, Fine
, Debug
, Info
, Warning
, Error
, Critical
, Disabled
.
pattern
is the format of log messages. See spdlog/Custom formatting for more information. By default, it is empty, using the default pattern of spdlog
. If it is missing, will inherit from the default one.
sinks
is the output destination of the log. For file
field, you can use stdout
or stderr
for console output, or specify a file path for file output. You can also specify pattern
here. If pattern is missing, it will use the pattern
in the current logger.
Of course, you can add other settings in the configuration file. In this case, you have to manually handle them in your code. To get the original configuration JSON, use WebHostBuilder::GetAppSettings()
.
Note
minet-core uses MinIoC as the IoC container for dependency injection. See that repository for more information on how to use the container.
Behind the settings, minet-core uses dependency injection to manage all the pluggable components. You can get the IoC container via WebHost::GetServiceContainer()
. It uses lazy initialization for singletons. Also, configurations are also injected into the container, you can get them by resolving ServerConfig
and LoggerConfig
.
If you want to add custom components, or replace the built-in ones, you can then register whatever you want after UseAppSettings()
. The existing components will be replaced if you register the same interface again.
Note
This section contains some personal design preferences.
You may notice that, in minet-core, some classes have private constructors, so that one can only create them in heap using the provided factory function. Why? Because I personally prefer using ->
in chained calls, and I don't want to mix .
and ->
up. It eliminates the use of raw pointers, but also robs the freedom of choosing stack or heap.
minet-core uses some open source libraries, thanks to their great work!π₯°
- nlohmann/json v3.11.3
- gabime/spdlog v1.15.0
- Lord-Turmoil/MinIoC v1.3.0