mirror of
https://github.com/boostorg/beast.git
synced 2025-06-25 03:51:36 +02:00
396 lines
13 KiB
Plaintext
396 lines
13 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 (`asio_handler_invoke`),
|
|
providing execution guarantees on composed operations in a manner
|
|
identical to Boost.Asio. The implementation also uses handler allocation
|
|
hooks (`asio_handler_allocate`) when allocating memory internally for
|
|
composed operations.
|
|
|
|
There is no need for inheritance or virtual members in `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 `websocket::stream`] which wraps a
|
|
"next layer" object. The next layer object must meet the requirements of
|
|
`SyncReadStream` and `SyncWriteStream` if synchronous operations are performed,
|
|
`AsyncReadStream` and `AsyncWriteStream` is 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:
|
|
```
|
|
io_service ios;
|
|
websocket::stream<ip::tcp::socket> ws(ios);
|
|
|
|
ssl::context ctx(ssl::context::sslv23);
|
|
websocket::stream<ssl::stream<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:
|
|
```
|
|
tcp::socket&& sock;
|
|
...
|
|
websocket::stream<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:
|
|
```
|
|
tcp::socket sock;
|
|
...
|
|
websocket::stream<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.
|
|
```
|
|
ssl::context ctx(ssl::context::sslv23);
|
|
websocket::stream<ssl::stream<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";
|
|
io_service ios;
|
|
tcp::resolver r(ios);
|
|
websocket::stream<ip::tcp::socket> ws(ios);
|
|
connect(ws.next_layer(), r.resolve(tcp::resolver::query{host, "ws"}));
|
|
```
|
|
|
|
Accepting an incoming connection:
|
|
```
|
|
void do_accept(tcp::acceptor& acceptor)
|
|
{
|
|
websocket::stream<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. `hanshake` is used to send the
|
|
request with the required host and resource strings.
|
|
```
|
|
websocket::stream<ip::tcp::socket> ws(ios);
|
|
...
|
|
ws.set_option(websocket::keep_alive(true));
|
|
ws.handshake("ws.mywebapp.com:80", "/cgi-bin/bitcoin-prices");
|
|
```
|
|
|
|
The `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:
|
|
```
|
|
websocket::stream<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(tcp::socket& sock)
|
|
{
|
|
boost::asio::streambuf sb;
|
|
read_until(sock, sb, "\r\n\r\n");
|
|
...
|
|
websocket::stream<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(tcp::socket& sock)
|
|
{
|
|
boost::asio::streambuf sb;
|
|
http::request<http::empty_body> request;
|
|
http::read(sock, request);
|
|
if(http::is_upgrade(request))
|
|
{
|
|
websocket::stream<ip::tcp::socket&> ws(sock);
|
|
ws.accept(request);
|
|
...
|
|
}
|
|
}
|
|
```
|
|
|
|
[note Identifiers in the `http` namespace are part of Beast.HTTP. ]
|
|
|
|
[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(websocket::stream<ip::tcp::socket>& ws)
|
|
{
|
|
streambuf sb;
|
|
websocket::opcode::value op;
|
|
ws.read(sb);
|
|
|
|
ws.set_option(websocket::message_type(op));
|
|
websocket::write(ws, sb.data());
|
|
sb.consume(sb.size());
|
|
}
|
|
```
|
|
|
|
[important Calls to `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(websocket::stream<ip::tcp::socket>& ws)
|
|
{
|
|
streambuf sb;
|
|
websocket::frame_info fi;
|
|
for(;;)
|
|
{
|
|
ws.read_frame(fi, sb);
|
|
if(fi.fin)
|
|
break;
|
|
}
|
|
ws.set_option(websocket::message_type(fi.op));
|
|
consuming_buffers<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 noted. The receipt of a close frame
|
|
initiates the WebSocket close procedure, eventually resulting in the
|
|
error code `websocket::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(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
|
|
`websocket::stream::close`:
|
|
```
|
|
ws.close();
|
|
```
|
|
|
|
[note To receive the `websocket::error::closed` error, a read operation
|
|
is required. ]
|
|
|
|
[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 `Streambuf`. This concept is modeled on `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 io_service]
|
|
|
|
The creation and operation of the `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.WSProto itself does not use or require threads.
|
|
|
|
[endsect]
|
|
|
|
|
|
|
|
[section:safety Thread Safety]
|
|
|
|
Like a regular asio socket, a `websocket::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]
|