Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
A
Actor Framework
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Metrics
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
cpp-libs
Actor Framework
Commits
d9255378
Commit
d9255378
authored
Mar 21, 2023
by
Dominik Charousset
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Document the protocol layering API
parent
09e62f81
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
73 additions
and
276 deletions
+73
-276
libcaf_net/ProtocolLayering.rst
libcaf_net/ProtocolLayering.rst
+0
-253
manual/net/Overview.rst
manual/net/Overview.rst
+73
-23
No files found.
libcaf_net/ProtocolLayering.rst
deleted
100644 → 0
View file @
09e62f81
Protocol Layering
=================
Layering plays an important role in the design of the ``caf.net`` module. When
implementing a new protocol for ``caf.net``, this protocol should integrate
naturally with any number of other protocols. To enable this key property,
``caf.net`` establishes a set of conventions and abstract interfaces.
Concepts
--------
A *protocol layer* is a class that implements a single processing step in a
communication pipeline. Multiple layers are organized in a *protocol stack*.
Each layer may only communicate with its direct predecessor or successor in the
stack.
At the bottom of the protocol stack is usually a *transport layer*. For example,
a ``stream_transport`` manages a stream socket and provides access to input and
output buffers to the upper layer.
At the top of the protocol is an *application* that utilizes the lower layers
for communication via the network. Applications should only rely on the
*abstract interface type* when communicating with their lower layer. For
example, an application that processes a data stream should not implement
against a TCP socket interface directly. By programming against the abstract
interface types of ``caf.net``, users can instantiate an application with any
compatible protocol stack of their choosing. For example, a user may add extra
security by using application-level data encryption or combine a custom datagram
transport with protocol layers that establish ordering and reliability to
emulate a stream.
Abstract Interface Types
------------------------
By default, ``caf.net`` distinguishes between these abstract interface types:
* *stream*: A stream interface represents a sequence of Bytes, transmitted
reliable and in order.
* *datagram*: A datagram interface provides access to some basic transfer units
that may arrive out of order or not at all.
* *message*: A message interface provides access to high-level, structured data.
Messages consist of a header and a payload. A single message may span multiple
datagrams, for example.
Note that each interface type also depends on the *direction*, i.e., whether
talking to the upper or lower level. Incoming data always travels the protocol
stack *up*. Outgoing data always travels the protocol stack *down*.
..code-block:: C++
interface base [role: upper layer] {
/// Called whenever the underlying transport is ready to send, allowing the
/// upper layers to produce additional output data or use the event to read
/// from event queues, etc.
/// @returns `true` if the lower layers may proceed, `false` otherwise
/// (aborts execution).
template <class LowerLayerPtr>
bool prepare_send(LowerLayerPtr down);
/// Called whenever the underlying transport finished writing all buffered
/// data for output to query whether an upper layer still has pending events
/// or may produce output data on the next call to `prepare_send`.
/// @returns `true` if the underlying socket may get removed from the I/O
/// event loop, `false` otherwise.
template <class LowerLayerPtr>
bool done_sending(LowerLayerPtr down);
/// When provided, the underlying transport calls this member function
/// before leaving `handle_read_event`. The primary use case for this
/// callback is flushing buffers.
template <class LowerLayerPtr>
[[optional]] void after_reading(LowerLayerPtr down);
}
interface base [role: lower layer] {
/// Returns whether the layer has output slots available.
bool can_send_more() const noexcept;
/// Returns the managed socket.
auto handle() const noexcept;
}
interface stream_oriented [role: upper layer] {
/// Called by the lower layer for cleaning up any state in case of an error.
template <class LowerLayerPtr>
void abort(LowerLayerPtr down, const error& reason);
/// Consumes bytes from the lower layer.
/// @param down Reference to the lower layer that received the data.
/// @param buffer Available bytes to read.
/// @param delta Bytes that arrived since last calling this function.
/// @returns The number of consumed bytes. May be zero if waiting for more
/// input or negative to signal an error.
/// @note When returning a negative value, clients should also call
/// `down.abort_reason(...)` with an appropriate error code.
template <class LowerLayerPtr>
ptrdiff_t consume(LowerLayerPtr down, byte_span buffer, byte_span delta);
}
interface stream_oriented [role: lower layer] {
/// Configures threshold for the next receive operations. Policies remain
/// active until calling this function again.
/// @warning Calling this function in `consume` invalidates both byte spans.
void configure_read(read_policy policy);
/// Prepares the layer for outgoing traffic, e.g., by allocating an output
/// buffer as necessary.
void begin_output();
/// Returns a reference to the output buffer. Users may only call this
/// function and write to the buffer between calling `begin_output()` and
/// `end_output()`.
byte_buffer& output_buffer();
/// Prepares written data for transfer, e.g., by flushing buffers or
/// registering sockets for write events.
void end_output();
/// Propagates an abort reason to the lower layers. After processing the
/// current read or write event, the lowest layer will call `abort` on its
/// upper layer.
void abort_reason(error reason);
/// Returns the last recent abort reason or `none` if no error occurred.
const error& abort_reason();
}
interface datagram_oriented [role: upper layer] {
/// Consumes a datagram from the lower layer.
/// @param down Reference to the lower layer that received the datagram.
/// @param buffer Payload of the received datagram.
/// @returns The number of consumed bytes or a negative value to signal an
/// error.
/// @note Discarded data is lost permanently.
/// @note When returning a negative value for the number of consumed bytes,
/// clients must also call `down.set_read_error(...)` with an
/// appropriate error code.
template <class LowerLayerPtr>
ptrdiff_t consume(LowerLayerPtr down, byte_span buffer);
}
interface datagram_oriented [role: lower layer] {
/// Prepares the layer for an outgoing datagram, e.g., by allocating an
/// output buffer as necessary.
void begin_datagram();
/// Returns a reference to the buffer for assembling the current datagram.
/// Users may only call this function and write to the buffer between
/// calling `begin_datagram()` and `end_datagram()`.
/// @note Lower layers may pre-fill the buffer, e.g., to prefix custom
/// headers.
byte_buffer& datagram_buffer();
/// Seals and prepares a datagram for transfer.
void end_datagram();
}
interface message_oriented [role: upper layer] {
/// Consumes a message from the lower layer.
/// @param down Reference to the lower layer that received the message.
/// @param buffer Payload of the received message.
/// @returns The number of consumed bytes or a negative value to signal an
/// error.
/// @note Discarded data is lost permanently.
/// @note When returning a negative value for the number of consumed bytes,
/// clients must also call `down.set_read_error(...)` with an
/// appropriate error code.
template <class LowerLayerPtr>
ptrdiff_t consume(LowerLayerPtr down, byte_span buffer);
}
interface message_oriented [role: lower layer] {
/// Prepares the layer for an outgoing message, e.g., by allocating an
/// output buffer as necessary.
void begin_message();
/// Returns a reference to the buffer for assembling the current message.
/// Users may only call this function and write to the buffer between
/// calling `begin_message()` and `end_message()`.
/// @note Lower layers may pre-fill the buffer, e.g., to prefix custom
/// headers.
byte_buffer& message_buffer();
/// Seals and prepares a message for transfer.
/// @note When returning `false`, clients must also call
/// `down.set_read_error(...)` with an appropriate error code.
template <class LowerLayerPtr>
bool end_message();
}
interface mixed_message_oriented [role: upper layer] {
/// Consumes a binary message from the lower layer.
/// @param down Reference to the lower layer that received the message.
/// @param buffer Payload of the received message.
/// @returns The number of consumed bytes or a negative value to signal an
/// error.
/// @note Discarded data is lost permanently.
/// @note When returning a negative value for the number of consumed bytes,
/// clients must also call `down.set_read_error(...)` with an
/// appropriate error code.
template <class LowerLayerPtr>
ptrdiff_t consume_binary(LowerLayerPtr down, byte_span buffer);
/// Consumes a text message from the lower layer.
/// @param down Reference to the lower layer that received the message.
/// @param text Payload of the received message. The encoding depends on the
/// application.
/// @returns The number of consumed characters or a negative value to signal
/// an error.
/// @note Discarded data is lost permanently.
/// @note When returning a negative value for the number of consumed
/// characters, clients must also call `down.set_read_error(...)` with
/// an appropriate error code.
template <class LowerLayerPtr>
ptrdiff_t consume_text(LowerLayerPtr down, string_view text);
}
interface mixed_message_oriented [role: lower layer] {
/// Prepares the layer for an outgoing binary message, e.g., by allocating
/// an output buffer as necessary.
void begin_binary_message();
/// Returns a reference to the buffer for assembling the current message.
/// Users may only call this function and write to the buffer between
/// calling `begin_binary_message()` and `end_binary_message()`.
/// @note Lower layers may pre-fill the buffer, e.g., to prefix custom
/// headers.
byte_buffer& binary_message_buffer();
/// Seals and prepares a binary message for transfer.
/// @note When returning `false`, clients must also call
/// `down.set_read_error(...)` with an appropriate error code.
template <class LowerLayerPtr>
bool end_binary_message();
/// Prepares the layer for an outgoing text message, e.g., by allocating
/// an output buffer as necessary.
void begin_text_message();
/// Returns a reference to the buffer for assembling the current message.
/// Users may only call this function and write to the buffer between
/// calling `begin_text_message()` and `end_text_message()`.
/// @note Lower layers may pre-fill the buffer, e.g., to prefix custom
/// headers.
std::vector<char>& text_message_buffer();
/// Seals and prepares a text message for transfer.
/// @note When returning `false`, clients must also call
/// `down.set_read_error(...)` with an appropriate error code.
template <class LowerLayerPtr>
bool end_text_message();
}
manual/net/Overview.rst
View file @
d9255378
...
...
@@ -21,10 +21,6 @@ bottom up, usually starting at the actor system. For example:
namespace ws = caf::net::web_socket;
auto conn = ws::with(sys)
.connect("localhost", 8080)
.do_on_error([](const caf::error& err) {
std::cerr << "*** failed to connect: " << to_string(err)
<< std::endl;
})
.start([&sys](auto pull, auto push) {
sys.spawn(my_actor, pull, push);
});
...
...
@@ -46,19 +42,13 @@ back to the sender could look like this:
self->make_observable().from_resource(pull).subscribe(push);
}
In general,
connecting
to a server follows the pattern:
In general,
starting a client that connects
to a server follows the pattern:
.. code-block:: C++
PROTOCOL::with(CONTEXT).connect(WHERE).start(ON_CONNECT);
To establish the connection asynchronously:
.. code-block:: C++
PROTOCOL::with(CONTEXT).async_connect(WHERE).start(ON_CONNECT);
And for accepting incoming connections:
Starting a server uses ``accept`` instead:
.. code-block:: C++
...
...
@@ -72,17 +62,77 @@ the background such as a Prometheus endpoint. These services take no callback in
prometheus::with(sys).accept(8008).start();
In all cases, CAF returns a ``disposable`` that allows users to cancel the
activity at any time. Note: when canceling a server, it only disposes the accept
socket itself. Previously established connections remain unaffected.
In all cases, CAF returns ``expected<disposable>``. The ``disposble`` allows
users to cancel the activity at any time. Note: when canceling a server, it only
disposes the accept socket itself. Previously established connections remain
unaffected.
For error reporting, most factories allow setting a callback with
``do_on_error``.
For error reporting, most factories also allow setting a callback with
``do_on_error``. This can be useful for ignoring the result value but still
reporting errors.
Many protocols also accept additional configuration parameters. For example, the
factory for establishing WebSocket connections allows users to tweak parameters
for the handshake such as protocols or extensions fields. These options are
listed at the documentation for the individual protocols.
Layering
--------
``connect`` API also allows to configure multiple connection attempts.
Protocol Layering
-----------------
Layering plays an important role in the design of the ``caf.net`` module. When
implementing a new protocol for ``caf.net``, this protocol should integrate
naturally with any number of other protocols. To enable this key property,
``caf.net`` establishes a set of conventions and abstract interfaces.
Please note that the protocol layering is meant for adding new networking
capabilities to CAF, but we do not recommend using the protocol stacks directly
in application code. The protocol stacks are meant as building blocks for
higher-level the declarative, high-level APIs.
A *protocol layer* is a class that implements a single processing step in a
communication pipeline. Multiple layers are organized in a *protocol stack*.
Each layer may only communicate with its direct predecessor or successor in the
stack.
At the bottom of the protocol stack is usually a *transport layer*. For example,
an ``octet_stream::transport`` that manages a stream socket and provides access
to input and output buffers to the upper layer.
At the top of the protocol is an *application* that utilizes the lower layers
for communication via the network. Applications should only rely on the
*abstract interface type* when communicating with their lower layer. For
example, an application that processes a data stream should not implement
against a TCP socket interface directly. By programming against the abstract
interface types of ``caf.net``, users can instantiate an application with any
compatible protocol stack of their choosing. For example, a user may add extra
security by using application-level data encryption or combine a custom datagram
transport with protocol layers that establish ordering and reliability to
emulate a stream.
By default, ``caf.net`` distinguishes between these abstract interface types:
* *datagram*: A datagram interface provides access to some basic transfer units
that may arrive out of order or not at all.
* *stream*: An octet stream interface represents a sequence of Bytes,
transmitted reliable and in order.
* *message*: A message interface provides access to high-level, structured data.
Messages usually consist of a header and a payload. A single message may span
multiple datagrams.
Note that each interface type also depends on the *direction*, i.e., whether
talking to the upper or lower level. Incoming data always travels the protocol
stack *up*. Outgoing data always travels the protocol stack *down*.
A protocol stack always lives in a ``socket_manager``. The deepest layer in the
stack is always a ``socket_event_layer`` that simply turns events on sockets
(e.g., ready-to-read) into function calls. Only transport layers will implement
this layer.
A transport layer then responds to socket events by reading and writing to the
socket. The transport acts as the lower layer for the next layer in the
processing chain. For example, the ``octet_stream::transport`` is an
``octet_stream::lower_layer``. To interface with an octet stream, user-defined
classes implement ``octet_stream::upper_layer``.
When instantiating a protocol stack, each layer is represented by a concrete
object and we build the pipeline from top to bottom, i.e., we create the highest
layer first and then pass the last layer to the next lower layer until arriving
at the socket manager.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment