Skip to content

Commit 11665c2

Browse files
committed
design doc work
1 parent 575f98e commit 11665c2

File tree

3 files changed

+141
-14
lines changed

3 files changed

+141
-14
lines changed

doc/modules/ROOT/nav.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
* xref:sans_io_philosophy.adoc[]
2-
2+
* xref:design.adoc[]
33
* xref:reference:boost/ws_proto.adoc[Reference]

doc/modules/ROOT/pages/design.adoc

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//
2+
// Copyright (c) 2024 Vinnie Falco ([email protected])
3+
//
4+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5+
// file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
6+
//
7+
// Official repository: https://github.com/cppalliance/ws_proto
8+
//
9+
10+
11+
= Design
12+
13+
Websocket data is divided into frames, consisting of a header containing a
14+
length followed by a sequence of bytes called the payload. A message can be
15+
broken up into multiple frames, while a control frame is always delivered in
16+
a single frame. All but the first frame of a message is called a continuation
17+
frame, while the last frame of a message is called a final frame.
18+
19+
The diagram below shows a complete frame followed by an incomplete frame:
20+
21+
[source]
22+
----
23+
| frame | partial...
24+
----
25+
26+
== Reading
27+
28+
When network I/O is performed, ideal results are achieved when the amount of
29+
work achieved per operation is maximized. This is because of the significant
30+
fixed cost for each I/O. The fraction of resources lost to overhead can be
31+
reduced by using the largest practically sized buffer possible in each read
32+
operation.
33+
34+
The `ws_proto::frame_stream` decoder uses a persistent, fixed-size internal
35+
buffer to hold incoming bytes from the peer and decode the frames inside.
36+
Because the buffer does not resize, this can produce zero or more complete
37+
frames and up to two partial frames. At the beginning of a session, the first
38+
input buffer will usually contain zero or more complete frames, and up to one
39+
partial frame:
40+
41+
[source]
42+
----
43+
| frame | frame | partial...
44+
----
45+
46+
A partial frame is created when the last frame in the buffer cannot fit
47+
completely. In this case, the remainder of the frame payload will appear in the
48+
next buffer of data received from the peer:
49+
50+
[source]
51+
----
52+
...partial | frame | frame |
53+
----
54+
55+
Depending on the size of the read buffer and the length of incoming frames, the
56+
decoded contents may not contain any complete frames:
57+
58+
[source]
59+
----
60+
...partial | partial...
61+
----
62+
63+
It is also possible that the current contents of the read buffer do not contain
64+
enough frame data to produce a complete message:
65+
66+
[source]
67+
----
68+
...partial...
69+
----
70+
71+
The maximum payload size for a control frame is 127 bytes. A partial control
72+
frame can appear at the beginning of the read buffer, the end of the read
73+
buffer, or both ends. The library buffers partial control frame data and always
74+
delivers complete control frames to the caller as a single contiguous span of
75+
bytes.
76+
77+
=== Read Results
78+
79+
The default behavior of the library when returning decoded results from a frame
80+
stream is as follows:
81+
82+
* Control frames are returned as a span of bytes
83+
* Complete message frames are returned as a span of bytes
84+
* Partial message frames are returned as if they were complete

doc/modules/ROOT/pages/sans_io_philosophy.adoc

+56-13
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,50 @@
1313

1414
== What is Sans-I/O?
1515

16-
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.
16+
Sans-I/O is a design philosophy for developing network protocol libraries in
17+
which the library itself does not perform any I/O operations or asynchronous
18+
flow control. Instead, it provides interfaces for consuming and producing
19+
buffers of data and events. This simple change not only facilitates the
20+
evelopment of such libraries but also enables their use with any asynchronous
21+
or synchronous I/O runtime.
1722

1823

1924
== Why a Sans-I/O approach?
2025

2126

2227
=== Reusability
2328

24-
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.
29+
Developing high-quality network protocol libraries is a challenging and
30+
time-consuming task. Furthermore, most of the time, such libraries need
31+
extensive usage and feedback from users to refine their optimal API.
32+
Consequently, we aim to facilitate the reusability of such valuable code. A
33+
sans-I/O design approach eliminates all I/O-related code from the protocol
34+
stack. This enables the reuse of protocol stack code for developing I/O-based
35+
network libraries on top of different I/O runtimes.
2536

2637

2738
=== Testability
2839

29-
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.
40+
Using a sans-I/O approach improves the testability of the library by enabling
41+
the creation of simpler, more reliable, and faster tests, as they no longer
42+
need to be written against I/O interfaces.
3043

31-
The following outlines how a sans-I/O approach can enhance the testability of a library implementation:
44+
The following outlines how a sans-I/O approach can enhance the testability of
45+
a library implementation:
3246

3347

3448
==== Lower cognitive complexity
3549

36-
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.
50+
One of the limiting factors in writing high-quality tests is the cognitive
51+
complexity of the tests themselves. For example, if we have to deal with I/O
52+
operations in the test, we have to set up connections, timers, and an I/O
53+
scheduler, which increases the cognitive load for both the writer and reader of
54+
the tests. Using a sans-I/O design approach eliminates all of these unnecessary
55+
setups and teardowns from the tests, making it possible to write simpler and
56+
cleaner tests.
3757

38-
The following example demonstrates the additional setup required in an I/O-coupled design:
58+
The following example demonstrates the additional setup required in an
59+
I/O-coupled design:
3960

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

4970
==== Higher code coverage
5071

51-
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.
72+
Covering all the corner cases in network-facing libraries is limited by how we
73+
can actually reproduce them in the tests. We might not be able to reproduce some
74+
error conditions or create conditions for interleaving events to cover all the
75+
possibilities. A sans-I/O design eliminates the coupling to I/O interfaces and
76+
instead provides us with synchronous interfaces that we can use to test all
77+
possible combinations of events and data arrivals.
5278

5379

5480
==== Faster execution times
5581

56-
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.
82+
Most of the time, I/O operations and system calls are the slowest part of a
83+
test. A sans-I/O design allows for writing tests with virtually no I/O
84+
operations or system calls at all. Consequently, we can execute thousands of
85+
tests within seconds, instead of minutes or even hours if we have to deal with
86+
real socket or pipe operations.
5787

58-
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.
88+
Moreover, testing time-sensitive logic with an I/O-coupled library requires
89+
realistic delays to cover all possibilities, which can quickly add up to a
90+
significant number (even if they are in orders of milliseconds individually).
91+
By using a sans-I/O approach, we can cover all of these combinations using
92+
synchronous interfaces, which require no delay at all.
5993

60-
The following example demonstrates the necessity of adding delays to tests for I/O-coupled functionalities:
94+
The following example demonstrates the necessity of adding delays to tests for
95+
I/O-coupled functionalities:
6196

6297
[source,cpp]
6398
----
@@ -70,9 +105,17 @@ BEAST_EXPECT(got_timeout);
70105

71106
==== Deterministic test results
72107

73-
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.
74-
75-
The following example demonstrates the ease of testing within a sans-I/O design, without any reliance on I/O operations or system calls:
108+
Relying on I/O operations and system calls can lead to flaky tests since we
109+
depend on resources and conditions controlled by the operating system. This
110+
issue becomes more pronounced when incorporating time-sensitive test cases, as
111+
they may fail due to the operating system scheduling other processes between
112+
tests or delaying network operations. A sans-I/O designed library can be
113+
thoroughly tested without any I/O operation, relying solely on simple function
114+
calls. In fact, a sans-I/O implementation resembles a giant state machine,
115+
allowing for deterministic and self-contained testing.
116+
117+
The following example demonstrates the ease of testing within a sans-I/O
118+
design, without any reliance on I/O operations or system calls:
76119

77120
[source,cpp]
78121
----

0 commit comments

Comments
 (0)