...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
Beast offers full support for WebSockets using a synchronous interface. It uses the same style of interfaces found in Boost.Asio: versions that throw exceptions, or versions that return the error code in a reference parameter:
websocketpp |
|
---|---|
template<class DynamicBuffer> void read(DynamicBuffer& dynabuf) |
<not available> |
websocketpp supports multiple transports by utilizing a trait,
the config::transport_type
(asio
transport example) To get an idea of the complexity involved
with implementing a transport, compare the asio transport to
the iostream
transport
(a layer that allows websocket communication over a std::iostream
).
In contrast, Beast abstracts the transport by defining just one
NextLayer
template argument The type requirements for NextLayer
are already
familiar to users as they are documented in Asio: AsyncReadStream, AsyncWriteStream, SyncReadStream, SyncWriteStream.
The type requirements for instantiating beast::websocket::stream
versus websocketpp::connection
with user defined types are vastly reduced (18 functions versus
2). Note that websocketpp connections are passed by shared_ptr
. Beast does not
use shared_ptr
anywhere in its public interface. A beast::websocket::stream
is constructible and movable in a manner identical to a boost::asio::ip::tcp::socket
. Callers can put such
objects in a shared_ptr
if they want to, but there is no requirement to do so.
template<class NextLayer> class stream { NextLayer next_layer_; ... } |
template <typename config> class connection : public config::transport_type::transport_con_type , public config::connection_base { public: typedef lib::shared_ptr<type> ptr; ... } |
websocketpp provides multi-role support through a hierarchy of
different classes. A beast::websocket::stream
is role-agnostic, it offers member functions to perform both
client and server handshakes in the same class. The same types
are used for client and server streams.
Beast |
|
---|---|
<not needed> |
template <typename config> class client : public endpoint<connection<config>,config>; template <typename config> class server : public endpoint<connection<config>,config>; |
websocketpp uses mutexes to protect shared data from concurrent
access. In contrast, Beast does not use mutexes anywhere in its
implementation. Instead, it follows the Asio pattern. Calls to
asynchronous initiation functions use the same method to invoke
intermediate handlers as the method used to invoke the final
handler, through the asio_handler_invoke
mechanism.
The only requirement in Beast is that calls to asynchronous initiation
functions are made from the same implicit or explicit strand.
For example, if the io_context
associated with a beast::websocket::stream
is single threaded, this counts as an implicit strand and no
performance costs associated with mutexes are incurred.
template <class Function> friend void asio_handler_invoke(Function&& f, read_frame_op* op) { return boost_asio_handler_invoke_helpers::invoke(f, op->d_->h); } |
mutex_type m_read_mutex; |
websocketpp requires a one-time call to set the handler for each
event in its interface (for example, upon message receipt). The
handler is represented by a std::function
equivalent. Its important to recognize that the websocketpp interface
performs type-erasure on this handler.
In comparison, Beast handlers are specified in a manner identical
to Boost.Asio. They are function objects which can be copied
or moved but most importantly they are not type erased. The compiler
can see through the type directly to the implementation, permitting
optimization. Furthermore, Beast follows the Asio rules for treatment
of handlers. It respects any allocation, continuation, or invocation
customizations associated with the handler through the use of
argument dependent lookup overloads of functions such as asio_handler_allocate
.
The Beast completion handler is provided at the call site. For each call to an asynchronous initiation function, it is guaranteed that there will be exactly one final call to the handler. This functions exactly the same way as the asynchronous initiation functions found in Boost.Asio, allowing the composition of higher level abstractions.
template< class DynamicBuffer, // Supports user defined types class ReadHandler // Handler is NOT type-erased > typename async_completion< // Return value customization ReadHandler, // supports futures and coroutines void(error_code) >::result_type async_read( DynamicBuffer& dynabuf, ReadHandler&& handler); |
typedef lib::function< void(connection_hdl,message_ptr) > message_handler; void set_message_handler(message_handler h); |
Beast fully supports the Extensible Asynchronous Model developed by Christopher Kohlhoff, author of Boost.Asio (see Section 8).
Beast websocket asynchronous interfaces may be used seamlessly
with std::future
stackful/stackless coroutines,
or user defined customizations.
websocketpp |
|
---|---|
beast::async_completion< ReadHandler, void(error_code)> completion{handler}; read_op< DynamicBuffer, decltype(completion.handler)>{ completion.handler, *this, op, buffer}; return completion.result.get(); // Customization point |
<not available> |
websocketpp defines a message buffer, passed in arguments by
shared_ptr
, and
an associated message manager which permits aggregation and reuse
of memory. The implementation of websocketpp::message
uses a std::string
to hold the payload.
If an incoming message is broken up into multiple frames, the
string may be reallocated for each continuation frame. The std::string
always uses the standard
allocator, it is not possible to customize the choice of allocator.
Beast allows callers to specify the object for receiving the
message or frame data, which is of any type meeting the requirements
of DynamicBuffer
(modeled after boost::asio::streambuf
).
Beast comes with the class basic_multi_buffer
,
an efficient implementation of the DynamicBuffer concept which makes
use of multiple allocated octet arrays. If an incoming message
is broken up into multiple pieces, no reallocation occurs. Instead,
new allocations are appended to the sequence when existing allocations
are filled. Beast does not impose any particular memory management
model on callers. The basic_multi_buffer
provided
by beast supports standard allocators through a template argument.
Use the DynamicBuffer that comes with beast,
customize the allocator if you desire, or provide your own type
that meets the requirements.
template<class DynamicBuffer> read(DynamicBuffer& dynabuf); |
template <template<class> class con_msg_manager> class message { public: typedef lib::shared_ptr<message> ptr; ... std::string m_payload; ... }; |
When sending a message, websocketpp requires that the payload
is packaged in a websocketpp::message
object using std::string
as the storage, or it requires a copy of the caller provided
buffer by constructing a new message object. Messages are placed
onto an outgoing queue. An asynchronous write operation runs
in the background to clear the queue. No user facing handler
can be registered to be notified when messages or frames have
completed sending.
Beast doesn't allocate or make copies of buffers when sending data. The caller's buffers are sent in-place. You can use any object meeting the requirements of __ConstBufferSequence, permitting efficient scatter-gather I/O.
The ConstBufferSequence interface
allows callers to send data from memory-mapped regions (not possible
in websocketpp). Callers can also use the same buffers to send
data to multiple streams, for example broadcasting common subscription
data to many clients at once. For each call to async_write
the completion
handler is called once when the data finishes sending, in a manner
identical to boost::asio::async_write
.
template<class ConstBufferSequence> void write(ConstBufferSequence const& buffers); |
lib::error_code send(std::string const & payload, frame::opcode::value op = frame::opcode::text); ... lib::error_code send(message_ptr msg); |
websocketpp requires that the entire message fit into memory, and that the size is known ahead of time.
Beast allows callers to compose messages in individual frames. This is useful when the size of the data is not known ahead of time or if it is not desired to buffer the entire message in memory at once before sending it. For example, sending periodic output of a database query running on a coroutine. Or sending the contents of a file in pieces, without bringing it all into memory.
websocketpp |
|
---|---|
template<class ConstBufferSequence> void write_some(bool fin, ConstBufferSequence const& buffers); |
<not available> |
The websocketpp read implementation continuously reads asynchronously from the network and buffers message data. To prevent unbounded growth and leverage TCP/IP's flow control mechanism, callers can periodically turn this 'read pump' off and back on.
In contrast a beast::websocket::stream
does not independently begin background activity, nor does it
buffer messages. It receives data only when there is a call to
an asynchronous initiation function (for example beast::websocket::stream::async_read
) with an associated
handler. Applications do not need to implement explicit logic
to regulate the flow of data. Instead, they follow the traditional
model of issuing a read, receiving a read completion, processing
the message, then issuing a new read and repeating the process.
Beast |
|
---|---|
<implicit> |
lib::error_code pause_reading(); lib::error_code resume_reading(); |
websocketpp offers the endpoint
class which can handle binding and listening to a port, and spawning
connection objects.
Beast does not reinvent the wheel here, callers use the interfaces
already in boost::asio
for receiving incoming connections resolving host names, or establishing
outgoing connections. After the socket (or boost::asio::ssl::stream
)
is connected, the beast::websocket::stream
is constructed around it and the WebSocket handshake can be performed.
Beast users are free to implement their own "connection manager", but there is no requirement to do so.
#include <boost/asio.hpp> |
template <typename config> class endpoint : public config::socket_type; |
Callers invoke beast::websocket::accept
to perform the WebSocket handshake, but there is no requirement
to use this function. Advanced users can perform the WebSocket
handshake themselves. Beast WebSocket provides the tools for
composing the request or response, and the Beast HTTP interface
provides the container and algorithms for sending and receiving
HTTP/1 messages including the necessary HTTP Upgrade request
for establishing the WebSocket session.
Beast allows the caller to pass the incoming HTTP Upgrade request for the cases where the caller has already received an HTTP message. This flexibility permits novel and robust implementations. For example, a listening socket that can handshake in multiple protocols on the same port.
Sometimes callers want to read some bytes on the socket before reading the WebSocket HTTP Upgrade request. Beast allows these already-received bytes to be supplied to an overload of the accepting function to permit sophisticated features. For example, a listening socket that can accept both regular WebSocket and Secure WebSocket (SSL) connections.