Skip to content

Commit

Permalink
design doc work
Browse files Browse the repository at this point in the history
  • Loading branch information
vinniefalco committed Nov 19, 2024
1 parent 575f98e commit 78fb5e9
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 14 deletions.
2 changes: 1 addition & 1 deletion doc/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
* xref:sans_io_philosophy.adoc[]
* xref:design.adoc[]
* xref:reference:boost/ws_proto.adoc[Reference]
84 changes: 84 additions & 0 deletions doc/modules/ROOT/pages/design.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// Copyright (c) 2024 Vinnie Falco ([email protected])
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/cppalliance/ws_proto
//


= Design

Websocket data is divided into frames, consisting of a header containing a
length followed by a sequence of bytes called the payload. A message can be
broken up into multiple frames, while a control frame is always delivered in
a single frame. All but the first frame of a message is called a continuation
frame, while the last frame of a message is called a final frame.

The diagram below shows a complete frame followed by an incomplete frame:

[source]
----
| frame | partial...
----

== Reading

When network I/O is performed, ideal results are achieved when the amount of
work achieved per operation is maximized. This is because of the significant
fixed cost for each I/O. The fraction of resources lost to overhead can be
reduced by using the largest practically sized buffer possible in each read
operation.

The `ws_proto::frame_stream` decoder uses a persistent, fixed-size internal
buffer to hold incoming bytes from the peer and decode the frames inside.
Because the buffer does not resize, this can produce zero or more complete
frames and up to two partial frames. At the beginning of a session, the first
input buffer will usually contain zero or more complete frames, and up to one
partial frame:

[source]
----
| frame | frame | partial...
----

A partial frame is created when the last frame in the buffer cannot fit
completely. In this case, the remainder of the frame payload will appear in the
next buffer of data received from the peer:

[source]
----
...partial | frame | frame |
----

Depending on the size of the read buffer and the length of incoming frames, the
decoded contents may not contain any complete frames:

[source]
----
...partial | partial...
----

It is also possible that the current contents of the read buffer do not contain
enough frame data to produce a complete message:

[source]
----
...partial...
----

The maximum payload size for a control frame is 127 bytes. A partial control
frame can appear at the beginning of the read buffer, the end of the read
buffer, or both ends. The library buffers partial control frame data and always
delivers complete control frames to the caller as a single contiguous span of
bytes.

=== Read Results

The default behavior of the library when returning decoded results from a frame
stream is as follows:

* Control frames are returned as a span of bytes
* Complete message frames are returned as a span of bytes
* Partial message frames are returned as if they were complete
69 changes: 56 additions & 13 deletions doc/modules/ROOT/pages/sans_io_philosophy.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,50 @@

== What is Sans-I/O?

Sans-I/O is a design philosophy for developing network protocol libraries in which the library itself does not perform any I/O operations or asynchronous flow control. Instead, it provides interfaces for consuming and producing buffers of data and events. This simple change not only facilitates the development of such libraries but also enables their use with any asynchronous or synchronous I/O runtime.
Sans-I/O is a design philosophy for developing network protocol libraries in
which the library itself does not perform any I/O operations or asynchronous
flow control. Instead, it provides interfaces for consuming and producing
buffers of data and events. This simple change not only facilitates the
evelopment of such libraries but also enables their use with any asynchronous
or synchronous I/O runtime.


== Why a Sans-I/O approach?


=== Reusability

Developing high-quality network protocol libraries is a challenging and time-consuming task. Furthermore, most of the time, such libraries need extensive usage and feedback from users to refine their optimal API. Consequently, we aim to facilitate the reusability of such valuable code. A sans-I/O design approach eliminates all I/O-related code from the protocol stack. This enables the reuse of protocol stack code for developing I/O-based network libraries on top of different I/O runtimes.
Developing high-quality network protocol libraries is a challenging and
time-consuming task. Furthermore, most of the time, such libraries need
extensive usage and feedback from users to refine their optimal API.
Consequently, we aim to facilitate the reusability of such valuable code. A
sans-I/O design approach eliminates all I/O-related code from the protocol
stack. This enables the reuse of protocol stack code for developing I/O-based
network libraries on top of different I/O runtimes.


=== Testability

Using a sans-I/O approach improves the testability of the library by enabling the creation of simpler, more reliable, and faster tests, as they no longer need to be written against I/O interfaces.
Using a sans-I/O approach improves the testability of the library by enabling
the creation of simpler, more reliable, and faster tests, as they no longer
need to be written against I/O interfaces.

The following outlines how a sans-I/O approach can enhance the testability of a library implementation:
The following outlines how a sans-I/O approach can enhance the testability of
a library implementation:


==== Lower cognitive complexity

One of the limiting factors in writing high-quality tests is the cognitive complexity of the tests themselves. For example, if we have to deal with I/O operations in the test, we have to set up connections, timers, and an I/O scheduler, which increases the cognitive load for both the writer and reader of the tests. Using a sans-I/O design approach eliminates all of these unnecessary setups and teardowns from the tests, making it possible to write simpler and cleaner tests.
One of the limiting factors in writing high-quality tests is the cognitive
complexity of the tests themselves. For example, if we have to deal with I/O
operations in the test, we have to set up connections, timers, and an I/O
scheduler, which increases the cognitive load for both the writer and reader of
the tests. Using a sans-I/O design approach eliminates all of these unnecessary
setups and teardowns from the tests, making it possible to write simpler and
cleaner tests.

The following example demonstrates the additional setup required in an I/O-coupled design:
The following example demonstrates the additional setup required in an
I/O-coupled design:

[source,cpp]
----
Expand All @@ -48,16 +69,30 @@ ws.handshake("localhost", "/");

==== Higher code coverage

Covering all the corner cases in network-facing libraries is limited by how we can actually reproduce them in the tests. We might not be able to reproduce some error conditions or create conditions for interleaving events to cover all the possibilities. A sans-I/O design eliminates the coupling to I/O interfaces and instead provides us with synchronous interfaces that we can use to test all possible combinations of events and data arrivals.
Covering all the corner cases in network-facing libraries is limited by how we
can actually reproduce them in the tests. We might not be able to reproduce some
error conditions or create conditions for interleaving events to cover all the
possibilities. A sans-I/O design eliminates the coupling to I/O interfaces and
instead provides us with synchronous interfaces that we can use to test all
possible combinations of events and data arrivals.


==== Faster execution times

Most of the time, I/O operations and system calls are the slowest part of a test. A sans-I/O design allows for writing tests with virtually no I/O operations or system calls at all. Consequently, we can execute thousands of tests within seconds, instead of minutes or even hours if we have to deal with real socket or pipe operations.
Most of the time, I/O operations and system calls are the slowest part of a
test. A sans-I/O design allows for writing tests with virtually no I/O
operations or system calls at all. Consequently, we can execute thousands of
tests within seconds, instead of minutes or even hours if we have to deal with
real socket or pipe operations.

Moreover, testing time-sensitive logic with an I/O-coupled library requires realistic delays to cover all possibilities, which can quickly add up to a significant number (even if they are in orders of milliseconds individually). By using a sans-I/O approach, we can cover all of these combinations using synchronous interfaces, which require no delay at all.
Moreover, testing time-sensitive logic with an I/O-coupled library requires
realistic delays to cover all possibilities, which can quickly add up to a
significant number (even if they are in orders of milliseconds individually).
By using a sans-I/O approach, we can cover all of these combinations using
synchronous interfaces, which require no delay at all.

The following example demonstrates the necessity of adding delays to tests for I/O-coupled functionalities:
The following example demonstrates the necessity of adding delays to tests for
I/O-coupled functionalities:

[source,cpp]
----
Expand All @@ -70,9 +105,17 @@ BEAST_EXPECT(got_timeout);

==== Deterministic test results

Relying on I/O operations and system calls can lead to flaky tests since we depend on resources and conditions controlled by the operating system. This issue becomes more pronounced when incorporating time-sensitive test cases, as they may fail due to the operating system scheduling other processes between tests or delaying network operations. A sans-I/O designed library can be thoroughly tested without any I/O operation, relying solely on simple function calls. In fact, a sans-I/O implementation resembles a giant state machine, allowing for deterministic and self-contained testing.

The following example demonstrates the ease of testing within a sans-I/O design, without any reliance on I/O operations or system calls:
Relying on I/O operations and system calls can lead to flaky tests since we
depend on resources and conditions controlled by the operating system. This
issue becomes more pronounced when incorporating time-sensitive test cases, as
they may fail due to the operating system scheduling other processes between
tests or delaying network operations. A sans-I/O designed library can be
thoroughly tested without any I/O operation, relying solely on simple function
calls. In fact, a sans-I/O implementation resembles a giant state machine,
allowing for deterministic and self-contained testing.

The following example demonstrates the ease of testing within a sans-I/O
design, without any reliance on I/O operations or system calls:

[source,cpp]
----
Expand Down
49 changes: 49 additions & 0 deletions include/boost/ws_proto/stream.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// Copyright (c) 2024 Vinnie Falco ([email protected])
//
// 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)
//
// Official repository: https://github.com/cppalliance/ws_proto
//

#ifndef BOOST_WS_PROTO_STREAM_HPP
#define BOOST_WS_PROTO_STREAM_HPP

#include <boost/ws_proto/detail/config.hpp>
#include <cstdlib>

namespace boost {
namespace ws_proto {

/** A stream for sending and receiving WebSocket messages.
*/
class BOOST_SYMBOL_VISIBLE
stream
{
public:
/** Destructor.
*/
BOOST_WS_PROTO_DECL
~stream();

/** Constructor.
@param buffer_size The number of bytes to
allocate for the internal buffer.
*/
BOOST_WS_PROTO_DECL
explicit
stream(
std::size_t buffer_size);

private:
class impl;

impl* impl_ = nullptr;
};

} // ws_proto
} // boost

#endif
39 changes: 39 additions & 0 deletions src/stream.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// Copyright (c) 2024 Vinnie Falco ([email protected])
//
// 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)
//
// Official repository: https://github.com/cppalliance/ws_proto
//

#include <boost/ws_proto/stream.hpp>

namespace boost {
namespace ws_proto {

//------------------------------------------------

class stream::impl
{
public:

};

//------------------------------------------------

stream::
~stream()
{
if(impl_)
delete impl_;
}

stream::
stream(
std::size_t buffer_size)
{
}

} // ws_proto
} // boost

0 comments on commit 78fb5e9

Please sign in to comment.