Stream Concepts
ReadStream
In a read stream, data keeps coming in chunks. You keep awaiting ever-new
chunks of data. As soon as one arrives into the buffer you provided,
your coroutine is resumed, you can process that chunk, and await the
next one. This single-chunk read is reflected by operation read_some:
capy::task<> test_read_some(capy::ReadStream auto& stream, capy::MutableBufferSequence auto buffer)
{
auto [ec, n] = co_await stream.read_some(buffer);
// decltype(ec) == std::error_code
// decltype(n) == std::size_t
}
You provide a buffer to fill and await the result. The coroutine is resumed as soon as the stream has something to report to you:
-
either read bytes,
-
or a non-nominal status (contingency),
-
or both.
The two returned pieces of data inform you about two things:
-
n— tells you how many bytes were read into the buffer. -
ec— tells you about the status,bool(ec) == falsebeing the nominal status.
When no contingency is reported (!ec), then n > 0. That is, read_some does not resume
a coroutine unless it really has something to communicate. When n is the size of the buffer
(full buffer fill), then bool(ec) is false (no contingency). If a contingency occurred after a
full buffer read, it will be reported in the subsequent call to read_some.[1]
While many values of ec can be returned, Capy allows you to distinguish
the following conditions, defined in enumeration cond, relevant to stream processing.
| name | meaning |
|---|---|
|
The read stream transmitted all the bytes that it intended. Nothing left to read. |
|
Your own program requested the stream to stop reading, and it obeyed. |
|
The transport closed without the secure handshake’s goodbye message, which might indicate a truncation attack. |
|
The read operation exceeded the time allowed for the operation. |
auto [ec, n] = co_await stream.read_some(buffer);
if (ec == capy::cond::eof) (1)
co_return;
| 1 | Check if the value of ec matches to condition cond::eof. |
Due to the chunked nature of the reads from stream, a typical interaction with a read stream involves an iteration:
capy::task<> keep_reading(capy::ReadStream auto& stream, capy::MutableBufferSequence auto buffer)
{
for (;;)
{
auto [ec, n] = co_await stream.read_some(buffer);
// process `n` bytes from `buffer` (even if `bool(ec) == true`)
if (ec)
break;
}
}
Such iteration is hidden between generic algorithms read and read_at_least.
The contract of a read stream is represented by concept ReadStream.
WriteStream
When you need to send your data some place, you use a write stream.
This operation also happens in chunks. Your coroutine is being resumed after each
written chunk. This is implemented with operation write_some:
capy::task<> test_write_some(capy::WriteStream auto& stream, capy::ConstBufferSequence auto buffer)
{
auto [ec, n] = co_await stream.write_some(buffer);
// decltype(ec) == std::error_code
// decltype(n) == std::size_t
}
You pass the bytes to write in buffer. Upon resume, the write stream has written
a chunk (but not necessarily all) of these bytes. The operation result is:
-
ec— status of the operation (or stream, depending on the value ofec), -
n— the number of bytes written.
Any value where bool(ec) == true indicates a contingency (such as broken connection).
When no contingency is reported (!ec), then n > 0. That is, write_some does not resume
a coroutine, unless it has something to communicate. When n is the size of the buffer
(full buffer fill), then bool(ec) is false (no contingency). If a contingency ocurred after a
full buffer write, it will be reported in the subsequent call to write_some.[1]
Note that for a common case where n < buffer_size(buffer) you need to rearrange the buffer,
so that the first n bytes is removed, before requesting another chunked write. Unlike with the read case,
writing a long content, which does not fit into a single buffer sequence, requires a nested loop.
This is why in the Introduction section the "echo" example reads:
for (;;) (1)
{
auto [ec, n] = co_await stream.read_some(capy::make_buffer(buf)); (2)
auto [wec, _] = co_await capy::write(stream, capy::const_buffer(buf, n)); (3)
// ...
}
| 1 | The outer loop: one iteration per one chunked read into the buffer. |
| 2 | The single chunked read via member function read_some. |
| 3 | Algorithm write with embedded inner loop that keeps awaiting
member function write_some until the buffer is fully written from. |
Capy’s generic algorithms involving write streams are write and write_at_least.
The contract of a write stream is represented by concept WriteStream.
Stream
A type that models both ReadStream and WriteStream is a Stream.
A TCP socket is the common case: one object carries traffic in both directions.
Also, an example above clearly uses such a "duplex" Stream.