mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-20 11:05:54 +00:00
* Improve test coverage * tests for invokable in composed ops * Update documentation * Add License badge to README * Target Windows 7 SDK and later * Make role_type private * Remove extra unused masking functions * Allow stream reuse / reconnect after failure * Restructure logic of composed operations * Allow 0 for read_message_max meaning no limit * Respect keep alive when building HTTP responses * Check version in upgrade request * Response with 426 status on unsupported WebSocket version * Remove unnecessary Sec-WebSocket-Key in HTTP responses * Rename to mask_buffer_size * Remove maybe_throw * Add ping, async_ping, async_on_pong * Add ping_op * Add pong_op * Fix crash in accept_op * Fix suspend in close_op * Fix read_frame_op logic * Fix crash in read_op * Fix races in echo sync and async echo servers
438 lines
15 KiB
Plaintext
438 lines
15 KiB
Plaintext
[/
|
|
Copyright (c) 2013-2016 Vinnie Falco (vinnie dot falco at gmail dot com)
|
|
|
|
Distributed under the Boost Software License, Version 1.0. (See accompanying
|
|
file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
|
]
|
|
|
|
[section:websocket WebSocket]
|
|
|
|
The WebSocket Protocol enables two-way communication between a client
|
|
running untrusted code in a controlled environment to a remote host that has
|
|
opted-in to communications from that code. The protocol consists of an opening
|
|
handshake followed by basic message framing, layered over TCP. The goal of
|
|
this technology is to provide a mechanism for browser-based applications that
|
|
need two-way communication with servers that does not rely on opening multiple
|
|
HTTP connections.
|
|
|
|
Beast.WebSocket provides developers with a robust WebSocket implementation
|
|
built on Boost.Asio with a consistent asynchronous model using a modern
|
|
C++ approach.
|
|
|
|
The WebSocket protocol is described fully in
|
|
[@https://tools.ietf.org/html/rfc6455 rfc6455]
|
|
|
|
|
|
|
|
[section:motivation Motivation]
|
|
|
|
Today's web applications increasingly rely on alternatives to standard HTTP
|
|
to achieve performance and/or responsiveness. While WebSocket implementations
|
|
are widely available in common web development languages such as Javascript,
|
|
good implementations in C++ are scarce. A survey of existing C++ WebSocket
|
|
solutions reveals interfaces which lack symmetry, impose performance penalties,
|
|
and needlessly restrict implementation strategies.
|
|
|
|
Beast.WebSocket is built on Boost.Asio, a robust cross platform networking
|
|
framework that is part of Boost and also offered as a standalone library.
|
|
A proposal to add networking functionality to the C++ standard library,
|
|
based on Boost.Asio, is under consideration by the standards committee.
|
|
Since the final approved networking interface for the C++ standard library
|
|
will likely closely resemble the current interface of Boost.Asio, it is
|
|
logical for Beast.WebSocket to use Boost.Asio as its network transport.
|
|
|
|
Beast.WebSocket takes advantage of Boost.Asio's extensible asynchronous
|
|
model, handler allocation, and handler invocation hooks. Calls to
|
|
Beast.WebSocket asynchronous initiation functions allow callers the choice
|
|
of using a completion handler, stackful or stackless coroutines, futures,
|
|
or user defined customizations (for example, Boost.Fiber). The
|
|
implementation uses handler invocation hooks
|
|
([@http://www.boost.org/doc/libs/1_61_0/doc/html/boost_asio/reference/asio_handler_invoke.html `asio_handler_invoke`]),
|
|
providing execution guarantees on composed operations in a manner
|
|
identical to Boost.Asio. The implementation also uses handler allocation hooks
|
|
([@http://www.boost.org/doc/libs/1_61_0/doc/html/boost_asio/reference/asio_handler_allocate.html `asio_handler_allocate`])
|
|
when allocating memory internally for composed operations.
|
|
|
|
There is no need for inheritance or virtual members in a
|
|
[link beast.ref.websocket__stream `beast::websocket::stream`].
|
|
All operations are templated and transparent to the compiler, allowing for
|
|
maximum inlining and optimization.
|
|
|
|
[note The documentation which follows assumes familiarity with
|
|
both Boost.Asio and the WebSocket protocol specification described in
|
|
[@https://tools.ietf.org/html/rfc6455 rfc6455] ]
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[section:creating Creating the socket]
|
|
|
|
The interface to Beast's WebSocket implementation is a single template
|
|
class [link beast.ref.websocket__stream `beast::websocket::stream`] which
|
|
wraps a "next layer" object. The next layer object must meet the requirements
|
|
of [link beast.types.streams.SyncStream [*`SyncReadStream`]] if synchronous
|
|
operations are performed, or
|
|
[link beast.types.streams.AsyncStream [*`AsyncStream`]] if asynchronous
|
|
operations are performed, or both. Arguments supplied during construction are
|
|
passed to next layer's constructor. Here we declare two websockets which have
|
|
ownership of the next layer:
|
|
```
|
|
boost::asio::io_service ios;
|
|
beast::websocket::stream<boost::asio::ip::tcp::socket> ws(ios);
|
|
|
|
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
|
|
beast::websocket::stream<
|
|
boost::asio::ssl::stream<boost::asio::ip::tcp::socket>> wss(ios, ctx);
|
|
```
|
|
|
|
For servers that can handshake in multiple protocols, it may be desired
|
|
to wrap an object that already exists. This socket can be moved in:
|
|
```
|
|
boost::asio::ip::tcp::socket&& sock;
|
|
...
|
|
beast::websocket::stream<
|
|
boost::asio::ip::tcp::socket> ws(std::move(sock));
|
|
```
|
|
|
|
Or, the wrapper can be constructed with a non-owning reference. In
|
|
this case, the caller is responsible for managing the lifetime of the
|
|
underlying socket being wrapped:
|
|
```
|
|
boost::asio::ip::tcp::socket sock;
|
|
...
|
|
beast::websocket::stream<boost::asio::ip::tcp::socket&> ws(sock);
|
|
```
|
|
|
|
The layer being wrapped can be accessed through the websocket's "next layer",
|
|
permitting callers to interact directly with its interface.
|
|
```
|
|
boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
|
|
beast::websocket::stream<
|
|
boost::asio::ssl::stream<boost::asio::ip::tcp::socket>> ws(ios, ctx);
|
|
...
|
|
ws.next_layer().shutdown(); // ssl::stream shutdown
|
|
```
|
|
|
|
[important Initiating read and write operations on the next layer while
|
|
websocket operations are being performed can break invariants, and
|
|
result in undefined behavior. ]
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[section:connecting Making connections]
|
|
|
|
Connections are established by using the interfaces which already exist
|
|
for the next layer. For example, making an outgoing connection:
|
|
```
|
|
std::string const host = "mywebapp.com";
|
|
boost::asio::io_service ios;
|
|
boost::asio::ip::tcp::resolver r(ios);
|
|
beast::websocket::stream<boost::asio::ip::tcp::socket> ws(ios);
|
|
boost::asio::connect(ws.next_layer(),
|
|
r.resolve(boost::asio::ip::tcp::resolver::query{host, "ws"}));
|
|
```
|
|
|
|
Accepting an incoming connection:
|
|
```
|
|
void do_accept(boost::asio::ip::tcp::acceptor& acceptor)
|
|
{
|
|
beast::websocket::stream<boost::asio::ip::tcp::socket> ws(acceptor.get_io_service());
|
|
acceptor.accept(ws.next_layer());
|
|
}
|
|
```
|
|
|
|
[note Examples use synchronous interfaces for clarity of exposition. ]
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[section:handshaking Handshaking]
|
|
|
|
A WebSocket session begins when one side sends the HTTP Upgrade request
|
|
for websocket, and the other side sends an appropriate HTTP response
|
|
indicating that the request was accepted and that the connection has
|
|
been upgraded. The HTTP Upgrade request must include the Host HTTP field,
|
|
and the URI of the resource to request. `handshake` is used to send the
|
|
request with the required host and resource strings.
|
|
```
|
|
beast::websocket::stream<boost::asio::ip::tcp::socket> ws(ios);
|
|
...
|
|
ws.set_option(beast::websocket::keep_alive(true));
|
|
ws.handshake("ws.example.com:80", "/cgi-bin/bitcoin-prices");
|
|
```
|
|
|
|
The [link beast.ref.websocket__stream `beast::websocket::stream`] automatically
|
|
handles receiving and processing the HTTP response to the handshake request.
|
|
The call to handshake is successful if a HTTP response is received with the
|
|
101 "Switching Protocols" status code. On failure, an error is returned or an
|
|
exception is thrown. Depending on the keep alive setting, the socket may remain
|
|
open for a subsequent handshake attempt
|
|
|
|
Performing a handshake for an incoming websocket upgrade request operates
|
|
similarly. If the handshake fails, an error is returned or exception thrown:
|
|
```
|
|
beast::websocket::stream<boost::asio::ip::tcp::socket> ws(ios);
|
|
...
|
|
ws.accept();
|
|
```
|
|
|
|
Servers that can handshake in multiple protocols may have already read data
|
|
on the connection, or might have already received an entire HTTP request
|
|
containing the upgrade request. Overloads of `accept` allow callers to
|
|
pass in this additional buffered handshake data.
|
|
```
|
|
void do_accept(boost::asio::ip::tcp::socket& sock)
|
|
{
|
|
boost::asio::streambuf sb;
|
|
boost::asio::read_until(sock, sb, "\r\n\r\n");
|
|
...
|
|
beast::websocket::stream<boost::asio::ip::tcp::socket&> ws(sock);
|
|
ws.accept(sb.data());
|
|
...
|
|
}
|
|
```
|
|
|
|
Alternatively, the caller can pass an entire HTTP request if it was
|
|
obtained elsewhere:
|
|
```
|
|
void do_accept(boost::asio::ip::tcp::socket& sock)
|
|
{
|
|
boost::asio::streambuf sb;
|
|
beast::http::request<http::empty_body> request;
|
|
beast::http::read(sock, request);
|
|
if(beast::http::is_upgrade(request))
|
|
{
|
|
websocket::stream<ip::tcp::socket&> ws(sock);
|
|
ws.accept(request);
|
|
...
|
|
}
|
|
}
|
|
```
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[section:messages Messages]
|
|
|
|
After the WebSocket handshake is accomplished, callers may send and receive
|
|
messages using the message oriented interface. This interface requires that
|
|
all of the buffers representing the message are known ahead of time:
|
|
```
|
|
void echo(beast::websocket::stream<boost::asio::ip::tcp::socket>& ws)
|
|
{
|
|
beast::streambuf sb;
|
|
beast::websocket::opcode::value op;
|
|
ws.read(sb);
|
|
|
|
ws.set_option(beast::websocket::message_type(op));
|
|
ws.write(sb.data());
|
|
sb.consume(sb.size());
|
|
}
|
|
```
|
|
|
|
[important Calls to [link beast.ref.websocket__stream.set_option `set_option`]
|
|
must be made from the same implicit or explicit strand as that used to perform
|
|
other operations. ]
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[section:frames Frames]
|
|
|
|
Some use-cases make it impractical or impossible to buffer the entire
|
|
message ahead of time:
|
|
|
|
* Streaming multimedia to an endpoint.
|
|
* Sending a message that does not fit in memory at once.
|
|
* Providing incremental results as they become available.
|
|
|
|
For these cases, the frame oriented interface may be used. This
|
|
example reads and echoes a complete message using this interface:
|
|
```
|
|
void echo(beast::websocket::stream<boost::asio::ip::tcp::socket>& ws)
|
|
{
|
|
beast::streambuf sb;
|
|
beast::websocket::frame_info fi;
|
|
for(;;)
|
|
{
|
|
ws.read_frame(fi, sb);
|
|
if(fi.fin)
|
|
break;
|
|
}
|
|
ws.set_option(beast::websocket::message_type(fi.op));
|
|
beast::consuming_buffers<
|
|
beast::streambuf::const_buffers_type> cb(sb.data());
|
|
for(;;)
|
|
{
|
|
using boost::asio::buffer_size;
|
|
std::size_t size = std::min(buffer_size(cb));
|
|
if(size > 512)
|
|
{
|
|
ws.write_frame(false, beast::prepare_buffers(512, cb));
|
|
cb.consume(512);
|
|
}
|
|
else
|
|
{
|
|
ws.write_frame(true, cb);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[section:controlframes Control frames]
|
|
|
|
During read operations, the implementation automatically reads and processes
|
|
WebSocket control frames such as ping, pong, and close. Pings are replied
|
|
to as soon as possible, pongs are delivered to the pong callback. The receipt
|
|
of a close frame initiates the WebSocket close procedure, eventually resulting
|
|
in the error code [link beast.ref.websocket__error `error::closed`] being
|
|
delivered to the caller in a subsequent read operation, assuming no other error
|
|
takes place.
|
|
|
|
To ensure timely delivery of control frames, large messages are broken up
|
|
into smaller sized frames. The implementation chooses the size and number
|
|
of the frames making up the message. The automatic fragment size option
|
|
gives callers control over the size of these frames:
|
|
```
|
|
...
|
|
ws.set_option(beast::websocket::auto_fragment_size(8192));
|
|
```
|
|
|
|
The WebSocket protocol defines a procedure and control message for initiating
|
|
a close of the session. Handling of close initiated by the remote end of the
|
|
connection is performed automatically. To manually initiate a close, use
|
|
[link beast.ref.websocket__stream.close `close`]:
|
|
```
|
|
ws.close();
|
|
```
|
|
|
|
[note To receive the [link beast.ref.websocket__error `error::closed`]
|
|
error, a read operation is required. ]
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[section:pongs Pong messages]
|
|
|
|
To receive pong control frames, callers may register a "pong callback" using
|
|
[link beast.ref.websocket__stream.set_option `set_option`]:
|
|
|
|
the following signature:
|
|
```
|
|
void on_pong(ping_data const& payload);
|
|
...
|
|
ws.set_option(pong_callback{&on_pong});
|
|
```
|
|
|
|
When a pong callback is registered, any pongs received through either
|
|
synchronous read functions or asynchronous read functions will invoke the
|
|
pong callback, passing the payload in the pong message as the argument.
|
|
|
|
Unlike regular completion handlers used in calls to asynchronous initiation
|
|
functions, the pong callback only needs to be set once. The callback is not
|
|
reset when a pong is received. The same callback is used for both synchronous
|
|
and asynchronous reads. The pong callback is passive; in order to receive
|
|
pongs, a synchronous or asynchronous stream read function must be active.
|
|
|
|
[note When an asynchronous read function receives a pong, the the pong callback
|
|
is invoked in the same manner as that used to invoke the final completion
|
|
handler of the corresponding read function.]
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[section:buffers Buffers]
|
|
|
|
Because calls to read data may return a variable amount of bytes, the
|
|
interface to calls that read data require an object that meets the requirements
|
|
of [link beast.types.Streambuf [*`Streambuf`]]. This concept is modeled on
|
|
[@http://www.boost.org/doc/libs/1_61_0/doc/html/boost_asio/reference/basic_streambuf.html `boost::asio::basic_streambuf`].
|
|
|
|
The implementation does not perform queueing or buffering of messages. If
|
|
desired, these features should be provided by callers. The impact of this
|
|
design is that library users are in full control of the allocation strategy
|
|
used to store data and the back-pressure applied on the read and write side
|
|
of the underlying TCP/IP connection.
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[section:async Asynchronous interface]
|
|
|
|
Asynchronous versions are available for all functions:
|
|
```
|
|
websocket::opcode op;
|
|
ws.async_read(op, sb,
|
|
[](boost::system::error_code const& ec)
|
|
{
|
|
...
|
|
});
|
|
```
|
|
|
|
Calls to asynchronous initiation functions support the extensible asynchronous
|
|
model developed by the Boost.Asio author, allowing for traditional completion
|
|
handlers, stackful or stackless coroutines, and even futures:
|
|
```
|
|
void echo(websocket::stream<ip::tcp::socket>& ws,
|
|
boost::asio::yield_context yield)
|
|
{
|
|
ws.async_read(sb, yield);
|
|
std::future<websocket::error_code> fut =
|
|
ws.async_write, sb.data(), boost::use_future);
|
|
...
|
|
}
|
|
```
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[section:io_service The io_service]
|
|
|
|
The creation and operation of the
|
|
[@http://www.boost.org/doc/libs/1_61_0/doc/html/boost_asio/reference/io_service.html `boost::asio::io_service`]
|
|
associated with the underlying stream is left to the callers, permitting any
|
|
implementation strategy including one that does not require threads for
|
|
environments where threads are unavailable. Beast.WebSocket itself does not
|
|
use or require threads.
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[section:safety Thread Safety]
|
|
|
|
Like a regular asio socket, a [link beast.ref.websocket__stream `stream`] is
|
|
not thread safe. Callers are responsible for synchronizing operations on the
|
|
socket using an implicit or explicit strand, as per the Asio documentation.
|
|
The asynchronous interface supports one active read and one active write
|
|
simultaneously. Undefined behavior results if two or more reads or two or
|
|
more writes are attempted concurrently. Caller initiated WebSocket ping, pong,
|
|
and close operations each count as an active write.
|
|
|
|
The implementation uses composed asynchronous operations internally; a high
|
|
level read can cause both reads and writes to take place on the underlying
|
|
stream. This behavior is transparent to callers.
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[endsect]
|
|
|
|
[include quickref.xml]
|