##How does the server works ?
The server works with a core and different types of module.
- The Connection module manages the network I/O and communicate with the Core through input and output queues.
- The Config Loader module, well, loads the configurations.
- Logger modules does log.
- Handlers take an http request, and if it can handle it, fill an http response.
There is also something called Filter. Thoses are functions that are called at specific stages of the request processing to modify (if needed) the data.
The Core is here to store and manage every modules.
##The Core
It can load or unload a specific module.
virtual IModule *load(std::string const &path);
virtual void unload(IModule *module);
It can set or unset one of the main modules (Config Loader and Connection).
virtual void setConfigLoader(IConfigLoader *module);
virtual void setConnection(IConnection *module);
virtual void unsetConfigLoader(IConfigLoader *module);
virtual void unsetConnection(IConnection *module);
Here is the SlotRegister
, it allows you to add or remove elements in a "list", with positionning priority.
namespace teq
{
enum class Priority
{
VeryLow = 0,
Low = 1,
Normal = 2,
High = 3,
VeryHigh = 4
};
}
namespace teq
{
template <typename T>
class SlotRegister
{
public:
// ...
T &add(IModule const *parent, T const &value, Priority priority = Priority::Normal);
void remove(T const &value);
void clear(IModule const *parent);
// ...
};
}
Note that the SlotRegister
does not allow to access the stored elements.
To access thoses, we use a SlotList
(which is a SlotRegister
). This one allow direct access to the storage.
namespace teq
{
template <typename T>
class SlotList final : public SlotRegister<T>
{
// ...
size_type size() const noexcept;
bool empty() const noexcept;
T const &operator[](size_type index) const;
T &operator[](size_type index);
iterator begin() noexcept;
iterator end() noexcept;
const_iterator begin() const noexcept;
const_iterator end() const noexcept;
};
}
The
begin()
andend()
method will allow you to use this container in range-based for loops.
The Filter.hpp
file also define aliases on those types for uniform usage.
namespace teq
{
namespace filter
{
enum class Type
{
InputData,
Request,
Response,
OutputData
};
template <Type T>
using Register = SlotRegister<...>;
template <Type T>
using List = SlotList<...>;
}
}
Let's move back on the ACore
.
So here are the loggers, handlers and filters getters.
virtual SlotRegister<ILogger *> &loggers();
virtual SlotRegister<IHandler *> &handlers();
template <filter::Type T>
filter::Register<T> &get();
You can add a specific filter this way
core.get<filter::Type::Request>().add(...);
Finally, it has a log
method which will pass the parameters to every registered logger.
virtual void log(LogType type, std::string const &message);
All of these methods are already implemented in the ACore, but you are free to override them as you want.
The ACore
store all of theses modules.
// List of every loaded modules
std::vector<std::pair<std::unique_ptr<GenModule>, std::unique_ptr<IModule>>> m_modules;
// Main modules
IConfigLoader *m_configLoader;
IConnection *m_connection;
// Filter lists
filter::List<filter::Type::InputData> m_inputFilters;
filter::List<filter::Type::Request> m_requestFilters;
filter::List<filter::Type::Response> m_responseFilters;
filter::List<filter::Type::OutputData> m_outputFilters;
SlotList<IHandler *> m_handlers;
SlotList<ILogger *> m_loggers;
nlohmann::json m_config;
The GenModule
stored with each IModule
represents the dynamically loaded library, and need to be
stored as long as the corresponding module exists.
The nlohmann::json m_config
is the global config object,
it will store every module's config.
##The http Request and Response
##IBase
Both of these elements have a lot in common. This is why there is an http::IBase
which provide them some methods.
Firstly, you can get and set the version.
virtual std::pair<std::int32_t, std::int32_t> version() const = 0;
virtual void setVersion(std::int32_t major = 1, std::int32_t minor = 1) = 0;
This would allow this usage
req.setVersion(1, 1); // Setting the http version to 1.1
auto [major, minor] = req.version();
std::cout << "Request version is << major << '.' << minor << std::endl;
Same thing for the body
virtual std::string const &body() const = 0;
virtual void setBody(std::string const &body) = 0;
Then you can access and manipulate the different parameters with those methods.
virtual bool hasParam(std::string const ¶m) const = 0;
virtual std::string &operator[](std::string const ¶m) = 0;
virtual std::string const &at(std::string const ¶m) const = 0;
virtual std::string &at(std::string const ¶m) = 0;
virtual void clearParams() = 0;
virtual size_type paramCount() const noexcept = 0;
virtual iterator begin() noexcept = 0;
virtual iterator end() noexcept = 0;
virtual const_iterator begin() const noexcept = 0;
virtual const_iterator end() const noexcept = 0;
Again, you can note the
begin()
andend()
methods for range-based for loop.
Finally, both Request and Response might need to be displayed as a string.
virtual std::string toString() = 0;
The http::Uri
object will be used by the IRequest
interface.
It can store a uri of this format.
https://www.ludonope.com/path/to/something.html?p=oui&p2=non&p3=ah
You can access the path with those methods
std::string const &path() const;
void setPath(std::string const &path);
You can check if it has a certain parameter
bool hasParam(std::string const ¶m) const;
This method allow you to get the uri as a string
std::string toString() const;
Also, the global operator<<
has been overloaded to allow using http::Uri
directly with an std::ostream
.
std::ostream &operator<<(std::ostream &os, teq::http::Uri const &uri);
Finally, to make the Uri parameters convenient, we made it inherit an std::map
.
namespace teq
{
namespace http
{
class Uri final : public std::map<std::string, std::vector<std::string>>
{
// ...
};
}
}
This way, you can use it like a regular std::map
, with all the good methods which come with it !
auto val = uri["search"][0];
Notice that it's a std::map
of std::string
and a vector<std::string>
. This is because you can have a parameter more than once. For example, this is a valid uri.
https://www.ludonope.com/search.php?query=hello&query=world
##IRequest
The request is quite simple. In addition of the IBase methods, it simply is composed of a method and a Uri.
virtual Method method() const = 0;
virtual void setMethod(Method method) = 0;
virtual Uri const &uri() const = 0;
virtual void setUri(Uri const &uri) = 0;
The http methods are stored in an enum.
namespace teq
{
namespace http
{
enum class Method
{
GET,
HEAD,
POST,
PUT,
DELETE,
CONNECT,
OPTIONS,
TRACE
};
}
}
##IResponse
Similarly, the response is simply made of a status code and a reason (the corresponding message).
virtual StatusCode status() const = 0;
virtual void setStatus(StatusCode status) = 0;
virtual std::string const &reason() const = 0;
virtual void setReason(std::string const &reason) = 0;
The different status code are stored in a enum. You can add custom ones in your implementation.
namespace teq
{
namespace http
{
struct StatusCode
{
enum
{
// Informational
Continue = 100,
SwitchingProtocol = 101,
// Success
OK = 200,
Created = 201,
Accepted = 202,
NonAuthoritativeInformation = 203,
NoContent = 204,
ResetContent = 205,
PartialContent = 206,
// Redirection
MultipleChoices = 300,
MovedPermanently = 301,
Found = 302,
SeeOther = 303,
NotModified = 304,
UseProxy = 305,
TemporaryRedirect = 307,
// Client Error
BadRequest = 400,
Unauthorized = 401,
PaymentRequired = 402,
Forbidden = 403,
NotFound = 404,
MethodNotAllowed = 405,
NotAcceptable = 406,
ProxyAuthentificationRequired = 407,
RequestTimeOut = 408,
Conflict = 409,
Gone = 410,
LengthRequired = 411,
PreconditionFailed = 412,
RequestEntityTooLarge = 413,
RequestURITooLarge = 414,
UnsupportedMediaType = 415,
RequestedRangeNotSatisfiable = 416,
ExpectationFailed = 417,
// Server Error
InternalServerError = 500,
NotImplemented = 501,
BadGateway = 502,
ServiceUnavailable = 503,
GatewayTimeOut = 504,
HTTPVersionNotSupported = 505
};
};
}
}
##The modules
Every module inherit from IModule
. This basic interface provide just what is needed to manipulate each module in the same fashion.
namespace teq
{
class IModule
{
public:
virtual ~IModule() noexcept {}
virtual std::string const &name() const = 0;
virtual std::string const &description() const = 0;
virtual void init(ACore &core, nlohmann::json const &config) = 0;
};
}
The init
method will be called by the core when it just finished loading the module.
This way, the module will be able to register itself in the right slots onto the core.
For example, a logger init
method could look something like this
void Logger::init(teq::ACore &core, nlohmann::json const &)
{
core.loggers().add(this, this);
}
We also defined a bit mode specific module type, which is the IMainModule
, from which inherit both the Connection module and the Configuration Loader module.
namespace teq
{
class IMainModule : public IModule
{
public:
virtual ~IMainModule() noexcept {}
virtual void enable() = 0;
virtual void disable() = 0;
};
}
This might allow us to launch a thread in these modules. This will be needed by the Connection module, but it can also be used for the Connection Loader module, if we want some hot reload configuration features for example.
##The Connection module
The connection module has one major functionnality. It has to launch his own thread to manage network I/O with the client independently.
To manage this, the core will give it two http::Message
queues.
An http::Message
is really simple, it's just a string
with an id
. This is used by the connection module when it has to send a response, to which request it correspond.
namespace teq
{
namespace http
{
struct Message
{
std::string id;
std::string content;
};
}
}
To achieve that, the IConnection
interface provides those methods, in addition to the IMainModule
ones
virtual void setInput(std::queue<http::Message> &queue) = 0;
virtual void setOutput(std::queue<http::Message> &queue) = 0;
##The Configuration Loader module
The IConfigLoader
allow the core to simply load a configuration file. Your implementation will define the file language (JSON, XML, YAML, ...), but the config itself will be stored as a JSON object (from the nlohmann json Library).
It provide a single method in complement of the IMainModule
ones
namespace teq
{
class IConfigLoader : public IMainModule
{
public:
virtual ~IConfigLoader() noexcept {}
virtual void load(std::string const &path, nlohmann::json &config) = 0;
};
}
##The Logger module
The ILogger
interface only add a log
method, which takes a LogType
and a string
message
namespace teq
{
enum class LogType
{
Trace,
Debug,
Info,
Warning,
Error
};
}
virtual void log(LogType type, std::string const &message) = 0;
##The Handler module
The handler module is also quite simple.
The handle
method takes the input request, and the output response to fill, and returns a bool
to indicate if it effectively handled the request.
virtual bool handle(http::IRequest &req, http::IResponse &res) = 0;
For example, if you made a simple php
file handler, it would look something like this
bool PhpHandler::handle(http::IRequest &req, http::IResponse &res)
{
auto path = req.uri().path();
if (path.size() >= 4 && path.substr(path.size() - 4, 4) != ".php")
{
return false;
}
// Handle the request and fill the response
// ...
return true;
}
Please note that this handler is only checking for ".php" extension, which is not the only php valid extension. This is only for code simplicity.
##The Filter module
And finally, the IFilter
interface. This one is a little bit different, in the fact that it doesn't add any method.
It is only used as a Marker Interface, for polymophism and readability.
Why doesn't it have any method ?
Because filters themselves are functions, and not a module.
With the IModule::init(...)
method, you can add callbacks in the differents filter registers.
There are 4 different types of filter
namespace teq
{
namespace filter
{
enum class Type
{
InputData,
Request,
Response,
OutputData
};
}
}
The InputData
filter applies on the input string
which comes directly from the Connection module. This is before the string is parsed as a request. It can for example apply some decryption or decompression to the data.
std::function<void(std::string &)>
The Request
filter applies, as the name suggest, on the request. This step occurs once the request was parsed, but before it is consumed by a handler.
std::function<void(http::IRequest &)>
The Response
filter is really similar to the Request
one, it applies on the reponse right after it was filled by a handler.
std::function<void(http::IResponse &)>
The OutputData
filter is just as the InputData
one, but applies on the output string
. This one can apply encryption or compression for example.
std::function<void(std::string &)>