Connection state listener
Since v.0.5.1 RESTinio supports notification about client connection state changes. In v.0.6.0 this support was somewhat changed and extended and this chapter describes features of v.0.6.0.
RESTinio knows moments when a new connection from a client is accepted and when
this connection is closed or upgraded (to websocket connection). By default,
RESTinio doesn’t inform a user about those facts. But since v.0.5.1 a user can
specify own connection state listener object for RESTinio server and RESTinio
will call state_changed
method of that object at the appropriate moments.
How to specify connection state listener?
There are three steps those needs to be done to specify own connection state listener to RESTinio server.
The first step is the creation of class with non-static state_changed
method. This class can look like:
class my_state_listener {
...
public:
...
void state_changed(
const restinio::connection_state::notice_t & notice) {
...
}
};
Please notice that state_changed
gets a single argument of type
restinio::connection_state::notice_t
by const reference and returns void.
It is also important to mention that since v.0.6.0 state_changed
can
throw exceptions (in v.0.5.1 that method should be a noexcept one). If
a connection listener throws an exception on accept or
upgraded_to_websocket event then the new connection will be automatically
closed by RESTinio.
The second step is the definition of my_state_listener
as a type of
connection state listener for RESTinio. A typedef with name
connection_state_listener_t
should be defined inside the server’s traits:
struct my_traits : public restinio::default_traits_t {
using connection_state_listener_t = my_state_listener;
};
The third step is the creation an instance of state listener and passing it to the server’s settings:
restinio::run( restinio::on_this_thread<my_traits>()
.port(8080)
.address("localhost")
.connection_state_listener(
std::make_shared<my_state_listener>(...))
.request_handler(...)
... );
The restinio::connection_state::notice_t class
An instance of restinio::connection_state::notice_t
class passed to
state_changed
method contains all available information about the
connection and its state. This class has the following interface:
class notice_t
{
...
public :
...
//! Get the connection id.
connection_id_t connection_id() const noexcept;
//! Get the remote endpoint for the connection.
endpoint_t remote_endpoint() const noexcept;
//! Get the cause for the notification.
cause_t cause() const noexcept;
};
Where restinio::connection_state::cause_t
is the following variant type:
using cause_t = variant_t< accepted_t, closed_t, upgraded_to_websocket_t >;
Name variant_t
is an alias for std::variant
from C++17 or to a custom
implementation of std::variant
with as-close-as-possible interface
(if fact, a wonderful variant-lite library is used by RESTinio).
To deal with variant
type in C++14 RESTinio provides several backported by
variant-lite
functions: restinio::holds_alternative
,
restinio::get
, restinio::get_if
, restinio::visit
.
Each type from cause_t
variant is related to the appropriate cause and
contains information related to that cause (if that information is available).
Types restinio::connection_state::closed_t
and
restinio::connection_state::upgraded_to_websocket_t
are empty now, but
they can be expanded in the future versions of RESTinio.
Type restinio::connection_state::accepted_t
has the following interface:
class accepted_t final
{
...
public:
[[NODISCARD]] bool is_tls_connection() const noexcept;
template< typename Lambda >
void try_inspect_tls( Lambda && lambda ) const;
template< typename Lambda >
decltype(auto) inspect_tls_or_throw( Lambda && lambda ) const;
template< typename Lambda, typename T >
T inspect_tls_or_default( Lambda && lambda, T && default_value ) const;
};
When and where state listener is called?
RESTinio uses just one instance of state listener object for all connections.
It means that several calls to state_changed
can be performed from
different threads at the same time (if RESTinio is run on a thread pool).
RESTinio doesn’t serialize calls to state listener object if those calls
are related to different connections. But calls for a single connection
will be serialized in the sense that accepted
notification will happen
before closed
or upgraded_to_websocket
notification.
It is important to mention that if RESTinio is run on thread pool then
notifications accepted
and closed
/upgraded_to_websocket
for a
particular connection can be issued on different threads.
Access to TLS-parameters
Since v.0.6.0 RESTinio supports access to TLS-related parameters
for a connection listener. That access is provided via
restinio::connection_state::accepted_t
type when the connection
listener is called after acception of a new connection.
Class restinio::connection_state::accepted_t
allows to check is the
new connection a TLS-connection:
void my_listener::state_changed(
const restinio::connection_state::notice_t & notice)
{
using restinio::connection_state::accepted_t;
if(auto * accepted = restinio::get_if<accepted_t>(¬ice.cause()))
{
if(accepted->is_tls_connection())
{
// This is a TLS-connection and we can handle its parameters.
...
}
}
}
Handling of TLS-parameters is possible in form of calling methods
try_inspect_tls
, inspect_tls_or_throw
and inspect_tls_or_default
of restinio::connection_state::accepted_t
class. All those methods
get a lambda/functor object with the following format:
some_type lambda(const restinio::connection_state::tls_accessor_t &);
The type restinio::connection_state::tls_accessor_t
is defined in
restinio/tls.hpp
header file. That file is not included in
restinio/core.hpp
and should be included into your programm explicitely:
#include <restinio/core.hpp>
#include <restinio/tls.hpp>
Since v.0.6.0 type restinio::connection_state::tls_accessor_t
has the
following public interface:
class tls_accessor_t
{
public:
[[nodiscard]] auto native_handle() const noexcept;
};
Where tls_accessor_t::native_handle()
returns the value of
the asio::ssl::stream::native_handle()
method (the pointer to SSL
struct from OpenSSL library).
Method accepted_t::try_inspect_tls
calls specified lambda only if
the new connection is TLS-connection, otherwise it does nothing. For
example:
class my_cause_visitor_t {
void operator()(const restinio::connection_state::accepted_t & cause) const {
std::optional<std::string> user_name;
cause.try_inspect_tls(
[&](const restinio::connection_state::tls_accessor_t & tls_info) {
// An attempt to extract name of the user from the certificate.
user_name = extract_user_name_from_certificate(
tls_info.native_handle());
});
// If we have user_name...
if(user_name) {
... // ...we can do something with it
}
...
}
...
};
void some_state_listener_t::state_changed(
const restinio::connection_state::notice_t & notice) {
...
restinio::visit(my_cause_visitor_t{...}, notice.cause());
}
Method accepted_t::inspect_tls_or_throw
calls specified lambda if
the new connection is TLS-connection or throws otherwise. If the lambda is
called the value returned by that lambda is returned. For
example:
class my_cause_visitor_t {
void operator()(const restinio::connection_state::accepted_t & cause) const {
// We expect only TLS-connections.
std::string user_name = cause.inspect_tls_or_throw(
[&](const restinio::connection_state::tls_accessor_t & tls_info) {
// An attempt to extract name of the user from the certificate.
return extract_user_name_from_certificate(tls_info.native_handle());
});
// Now we have the user_name and can do something with it.
...
}
...
};
void some_state_listener_t::state_changed(
const restinio::connection_state::notice_t & notice) {
...
restinio::visit(my_cause_visitor_t{...}, notice.cause());
}
Method accepted_t::inspect_tls_or_default
calls specified lambda if the new
connection is TLS-connection or returns the default value passed to
inspect_tls_or_default
. If the lambda is called the value returned by that
lambda is returned. For example:
class my_cause_visitor_t {
void operator()(const restinio::connection_state::accepted_t & cause) const {
// TLS-connection will be handled a specific way.
std::string user_name = cause.inspect_tls_or_default(
[&](const restinio::connection_state::tls_accessor_t & tls_info) {
// An attempt to extract name of the user from the certificate.
return extract_user_name_from_certificate(tls_info.native_handle());
},
// For all other connections that user name will be used.
"anonymous"s);
// Now we have the user_name and can do something with it.
...
}
...
};
void some_state_listener_t::state_changed(
const restinio::connection_state::notice_t & notice) {
...
restinio::visit(my_cause_visitor_t{...}, notice.cause());
}
Limitation of the current RESTinio logic and max_pipelined_requests
The current version of RESTinio has the following logic:
- RESTinio accepts a new connection and reads the first request;
- when the first request received RESTinio calls request_handler and waits while response will be provided. Because RESTinio supports asynchronous processing there can be some time between calling request_handler and receiving a response from request_handler;
- all that time RESTinio doesn’t read new data from the connection.
It means that if a connection is closed by client while request_handler handles the request, RESTinio won’t know about the disconnection. As a result connection state listener won’t be called.
The following small example allows to see that situation:
#include <restinio/core.hpp>
struct my_connection_listener
{
void
state_changed(const restinio::connection_state::notice_t & notice) noexcept
{
using namespace restinio::connection_state;
if(restinio::holds_alternative<accepted_t>(notice.cause()))
std::cout << notice.connection_id() << " -- accepted" << std::endl;
else if(restinio::holds_alternative<closed_t>(notice.cause()))
std::cout << notice.connection_id() << " -- closed" << std::endl;
}
};
int main()
{
struct my_traits : public restinio::default_single_thread_traits_t
{
using logger_t = restinio::single_threaded_ostream_logger_t;
using connection_state_listener_t = my_connection_listener;
};
std::vector< restinio::request_handle_t > requests;
restinio::run(
restinio::on_this_thread< my_traits >()
.port(8080)
.address("localhost")
.handle_request_timeout(std::chrono::hours{24})
.connection_state_listener(
std::make_unique<my_connection_listener>())
// .max_pipelined_requests(4) // (1)
.cleanup_func([&] {
requests.clear();
})
.request_handler([&](auto req) {
requests.push_back(req);
return restinio::request_accepted();
}));
return 0;
}
Just compile and run that example. Then call curl -v http://localhost:8080
and then terminate curl
by Ctrl+C. There won’t be a notification about
disconnection. It is because RESTinio reads one incoming request and stops
reading connection until the request read will be processed. No read operations
mean that RESTinio can’t detect a disconnection.
But RESTinio can read and process more that one request from a connection.
This feature is called pipelined requests. There is setting
max_pipelined_requests
that specified how many requests can be read from a
connection. By default, this setting is equal to 1. But it can be changed.
So if the line (1) in the example above will be uncommented then RESTinio will continue reading the connection after extracting the first incoming request. That allows to detect a disconnection and connection state listener will be invoked when the disconnection is detected.
As the conclusion: if you want to receive notifications about disconnections
while requests are being processing then increase the value of
max_pipelined_requests
settings.
It can look like a flaw in RESTinio and maybe it is. But this is a complex
question and we don’t find an appropriate solution yet. Maybe RESTinio’s logic
will be changed in some future version. But for v.0.6.0 a user has to
deal with max_pipelined_requests
value.
Thread safety of state listener
If RESTinio is run on a thread pool then calls of state_changed
can be made
from several threads. Moreover, several calls to state_changed
can be
performed at the same time.
It’s a user’s task to make a state listener thread-safe.
Performance impact
Usage of a custom state listener can have some performance impact. Its value depends on the logic of listener object, thread-safety mechanisms used in the implementation of a listener object and the contention on the listener object in a multithreaded application. So it’s the user’s responsibility to make state listener object as quick as possible.
If the default value for connection_state_listener_t
is used (e.g.
connection_state_listener_t
in server traits has the default value of
restinio::connection_state::noop_listener_t
) then there won’t be any
performance impact because all listener-related code will be eliminated by C++
compiler (it’s a consequence of specializations inside RESTinio’s code
for noop_listener_t
case).