Boost logo

Boost :

Subject: Re: [boost] New Lib "Beast", HTTP + WebSocket protocols
From: Vinnie Falco (vinnie.falco_at_[hidden])
Date: 2016-04-29 12:49:16


On Fri, Apr 29, 2016 at 8:31 AM, Hartmut Kaiser
<hartmut.kaiser_at_[hidden]> wrote:
> How does this compare to www.zaphoyd.com/websocketpp'?
> Why should I use it and not websocketpp?

You should use Beast instead of websocketpp if: Beast has the features
you need (compression and subprotocols planned), and you care about
any of the library differences in the section that follows.

First, websocketpp is a great library; its clear the author paid a lot
of attention to implementing broad support for websocket including
Hixie-76 which is not a feature found in Beast. That library has great
documentation, support, and quite a few years of presence. In this
author's opinion it is one of the best available WebSocket
implementation in C++, and by a wide margin.

Features already in websocketpp but planned for Beast include
per-message compression and utilities for analyzing subprotocols. The
section that follows provides a feature by feature comparison of
websocketpp and Beast, with links to relevant source material and then
exposition.

1. Synchronous Interfaces

websocketpp:
    <not available>

beast:
    template<class Streambuf>
    void
    read(opcode& op, Streambuf& streambuf)

https://github.com/vinniefalco/Beast/blob/6c8b4b2f8dde72b01507e4ac7fde4ffea57ebc99/include/beast/websocket/stream.hpp#L774

Beast offers full support for websocket using a synchronous interface.
It uses the same style of synchronous interfaces found in Boost.Asio:
versions that throw exceptions, or versions that return the error code
in a reference parameter.

2. Connection Model

websocketpp:
    template <typename config>
    class connection
    : public config::transport_type::transport_con_type
    , public config::connection_base
    {
    public:
        typedef lib::shared_ptr<type> ptr;
    ...

https://github.com/zaphoyd/websocketpp/blob/378437aecdcb1dfe62096ffd5d944bf1f640ccc3/websocketpp/connection.hpp#L234

beast:
    template<class NextLayer>
    class stream : public detail::stream_base
    {
       NextLayer next_layer_;
        ...

https://github.com/vinniefalco/Beast/blob/6c8b4b2f8dde72b01507e4ac7fde4ffea57ebc99/include/beast/websocket/stream.hpp

websocketpp supports multiple transports by utilizing a trait, the
config::transport_type. An example of the implementation of this
concept is the asio transport, declared here:
https://github.com/zaphoyd/websocketpp/blob/378437aecdcb1dfe62096ffd5d944bf1f640ccc3/websocketpp/transport/asio/connection.hpp#L60
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):
https://github.com/zaphoyd/websocketpp/blob/378437aecdcb1dfe62096ffd5d944bf1f640ccc3/websocketpp/transport/iostream/connection.hpp#L59

In contrast, beast abstracts the transport by defining just one
template argument, the NextLayer. The type requirements for NextLayer
are already familiar to users as they are documented in Asio:
http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/reference/SyncReadStream.html
http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/reference/SyncWriteStream.html
http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/reference/AsyncReadStream.html
http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/reference/AsyncWriteStream.html

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::socket. Callers can put such objects
in a shared_ptr if they want to, but there is no requirement to do so.

3. Client and Server Roles

websocketpp:
    template <typename config>
    class client : public endpoint<connection<config>,config>;
    template <typename config>
    class server : public endpoint<connection<config>,config> {

https://github.com/zaphoyd/websocketpp/blob/378437aecdcb1dfe62096ffd5d944bf1f640ccc3/websocketpp/roles/server_endpoint.hpp#L39
https://github.com/zaphoyd/websocketpp/blob/378437aecdcb1dfe62096ffd5d944bf1f640ccc3/websocketpp/roles/client_endpoint.hpp#L42

beast:
    <not applicable>

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.

4. Thread Safety

websocketpp:
    mutex_type m_read_mutex;

https://github.com/zaphoyd/websocketpp/blob/378437aecdcb1dfe62096ffd5d944bf1f640ccc3/websocketpp/transport/iostream/connection.hpp#L706

beast:
    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);
    }

https://github.com/vinniefalco/Beast/blob/6c8b4b2f8dde72b01507e4ac7fde4ffea57ebc99/include/beast/websocket/impl/read_frame_op.ipp#L118

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:
http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/reference/asio_handler_invoke.html

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_service associated with a
beast::websocket::stream's value for NextLayer is single threaded,
this counts as an implicit strand and no performance costs associated
with mutexes are incurred.

5. Callback Model

websocketpp:
    typedef lib::function<void(connection_hdl,message_ptr)> message_handler;
    void set_message_handler(message_handler h);

https://github.com/zaphoyd/websocketpp/blob/378437aecdcb1dfe62096ffd5d944bf1f640ccc3/websocketpp/connection.hpp#L281
https://github.com/zaphoyd/websocketpp/blob/378437aecdcb1dfe62096ffd5d944bf1f640ccc3/websocketpp/connection.hpp#L473

beast:
    template<class Streambuf, class ReadHandler>
    typename async_completion<ReadHandler, void(error_code)>::result_type
    async_read(opcode& op, Streambuf& streambuf, ReadHandler&& handler);

https://github.com/vinniefalco/Beast/blob/6c8b4b2f8dde72b01507e4ac7fde4ffea57ebc99/include/beast/websocket/stream.hpp#L834

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 customizations, continuation
customization, or invoke customization 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.

6. Extensible Asynchronous Model

websocketpp:
    <not available>

beast:
    ...
    beast::async_completion<ReadHandler, void(error_code)> completion(handler);
    read_op<Streambuf, decltype(completion.handler)>{
        completion.handler, *this, op, streambuf};
    return completion.result.get();

https://github.com/vinniefalco/Beast/blob/6c8b4b2f8dde72b01507e4ac7fde4ffea57ebc99/include/beast/websocket/impl/stream.ipp#L378

Beast fully supports the Extensible Asynchronous Model developed by
Christopher Kohlhoff, author of Boost.Asio. See Section 8 in
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3896.pdf

This means that Beast websocket asynchronous interface can be used
with std::future, stackful/stackless coroutines, or user defined
customizations.

7. Message Buffering

websocketpp:
    template <template<class> class con_msg_manager>
    class message {
    public:
        typedef lib::shared_ptr<message> ptr;
        ...
        std::string m_payload;

https://github.com/zaphoyd/websocketpp/blob/378437aecdcb1dfe62096ffd5d944bf1f640ccc3/websocketpp/message_buffer/message.hpp#L78

beast:
    template<class Streambuf>

https://github.com/vinniefalco/Beast/blob/6c8b4b2f8dde72b01507e4ac7fde4ffea57ebc99/include/beast/websocket/stream.hpp#L834
http://vinniefalco.github.io/beast/beast/types/Streambuf.html

websocketpp defines a message buffer, passed in arguments by
shared_ptr, and an associated message manager which permits
aggregation and memory 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
Streambuf (modeled after boost::asio::streambuf) and described here:
http://vinniefalco.github.io/beast/beast/types/Streambuf.html

Beast comes with the class beast::basic_streambuf, an efficient
implementation of the Streambuf 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_streambuf provided by beast supports standard allocators through
a template argument. Use the Streambuf that comes with beast,
customize the allocator if you desire, or provide your own type that
meets the requirements:
https://github.com/vinniefalco/Beast/blob/6c8b4b2f8dde72b01507e4ac7fde4ffea57ebc99/include/beast/basic_streambuf.hpp#L21

8. Sending Messages

websocketpp:
    lib::error_code send(std::string const & payload,
        frame::opcode::value op = frame::opcode::text);
    ...
    lib::error_code send(message_ptr msg);

https://github.com/zaphoyd/websocketpp/blob/378437aecdcb1dfe62096ffd5d944bf1f640ccc3/websocketpp/connection.hpp#L672

beast:
    template<class ConstBufferSequence, class WriteHandler>
    typename async_completion<WriteHandler, void(error_code)>::result_type
    async_write(ConstBufferSequence const& buffers, WriteHandler&& handler);

https://github.com/vinniefalco/Beast/blob/6c8b4b2f8dde72b01507e4ac7fde4ffea57ebc99/include/beast/websocket/stream.hpp#L1048

When sending a message, websocketpp requires that the payload is
packaged in a websocketpp::message object using std::string as the
storage, or it makes 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 and copy buffers when sending data. The callers
buffers are sent in-place. You can use any object meeting the
requirements of ConstBufferSequence, permitting efficient
scatter-gather I/O:
http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/reference/ConstBufferSequence.html

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.

9. Streaming Messages

websocketpp:
    <not available>

beast:
    template<class ConstBufferSequence, class WriteHandler>
    typename async_completion<WriteHandler, void(error_code)>::result_type
    async_write_frame(bool fin,
        ConstBufferSequence const& buffers, WriteHandler&& handler);

https://github.com/vinniefalco/Beast/blob/6c8b4b2f8dde72b01507e4ac7fde4ffea57ebc99/include/beast/websocket/stream.hpp#L1151

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.

10. Flow Control

websocketpp:
    lib::error_code pause_reading();

https://github.com/zaphoyd/websocketpp/blob/378437aecdcb1dfe62096ffd5d944bf1f640ccc3/websocketpp/connection.hpp#L728

beast:
    <implicit>

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 off the read pump. 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.

11. Connection Establishment

websocketpp:
    template <typename config>
    class endpoint : public config::socket_type;

https://github.com/zaphoyd/websocketpp/blob/378437aecdcb1dfe62096ffd5d944bf1f640ccc3/websocketpp/transport/asio/endpoint.hpp#L52

beast:
    <boost::asio>

http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/reference/async_connect.html
http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/reference/basic_socket_acceptor/async_accept.html

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.

---
The design choices of Beast.WebSocket were made to give users the
familiar Asio interface while leveraging its strengths, to create a
library that is lean, easy to understand, and doesn't duplicate
functionality already possible in Asio. We hope that we've succeeded
in this goal.

Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk