Streams

Understand how streams are created, closed, and used to transmit data.

Data is transmitted through independent bidirectional or unidirectional streams. Unidirectional streams carry data from the initiator of the stream to the peer. Data can be transmitted in both directions of a bidirectional stream. Multiple streams can be opened at the same time on a multiplexed connection. Streams are identified by an ever increasing 62-bit integer.

Slic stream identifiers are similar to QUIC, see RFC9000. The first least significant bit of a stream identifier specifies if the initiator is the client or the server. The second least significant bit specifies if the stream is bidirectional or unidirectional. The following table summarizes the four stream types:

BitsStream Type
0x00
Client-Initiated, Bidirectional
0x01
Server-Initiated, Bidirectional
0x02
Client-Initiated, Unidirectional
0x03
Server-Initiated, Unidirectional

The following table describes stream-specific frames:

Frame TypeDescription
Stream
Carries application data.
StreamLast
Carries application data and informs the peer that no more data will be sent.
StreamReadsClosed
Informs the peer of the stream reads closure.
StreamWritesClosed
Informs the peer of the stream writes closure.
StreamWindowUpdate
Informs the peer of a stream window update.

Sending a Stream or StreamLast frame with a newly allocated stream identifier creates the stream. Sending another stream frame with a newly allocated stream identifier is a protocol error.

The peer accepts a new stream when it receives a Stream or StreamLast frame with a stream identifier larger than the last accepted stream identifier.

The stream identifier must be the next expected stream identifier. For example, if the last stream accepted by the server is the bidirectional stream with the identifier 0, the identifier of the next accepted bidirectional stream must be 4.

Each side of a stream maintains a reads and writes closed state. When the application is done writing data on a stream, it closes writes on the stream. When it's done reading data, it closes reads.

The update of the closed state triggers the sending of one of the following frames:

  • Slic sends a StreamWritesClosed frame to the peer when the application closes writes on the stream. Upon receiving this frame, the peer stops reading data from the stream and close the stream reads.

  • Slic sends a StreamReadsClosed frame to the peer when the application closes reads on the stream. Upon receiving this frame, the peer stops sending data over the stream and close the stream writes.

A stream is considered closed when both writes and reads are closed.

The Stream and StreamLast frames cary a sequence of bytes provided by the application. Multiple Stream frames can be sent over the Slic connection for a specific stream. They will be received in order by the peer. The StreamLast frame cary the last sequence of bytes delivered to the peer. Upon receiving this frame, the peer can assume that no more data will be sent for this stream.

Sending a Stream frame after a StreamLast frame or multiple StreamLast frames for the same stream is considered a protocol error.

The MaxStreamFrameSize parameter limits the maximum size of a Stream or StreamLast frame. If the application data is larger than this parameter value, the data is sent in chunks with multiple Stream frames. Frames are serialized on the underlying duplex connection so sending a frame delays the sending of other frames. Reducing the maximum stream frame size reduces this delay. It's in particular useful when dealing with slow connections.

A stream has two separate state machines: one for its write-side and one for its read-side.

The state machines also depend on the type of the stream. The type of a stream is defined as follows:

  • a local stream is a stream created by the application
  • a remote stream is a stream accepted by the application

A local stream doesn't have the same read or write state machine as a remote stream. As we will see below, different state machines are required for controlling stream concurrency.

The following state diagram shows the write state machine of a local stream:

The write-side is initially in the Ready state. In this state, the stream is ready to accept data from the application. The write-side enters the Write state when the application starts writing data. When the write-side is in the Write state, Slic can send Stream or StreamLast frames on that stream to carry the application data.

The write-side exits the Write state to enter the WaitForPeerReadsClosed state when the application indicates that no more data will be written. Once it gets this notification from the application, the write-side sends a StreamLast frame to notify the peer.

In the WaitForPeerReadsClosed state, the write-side waits for the peer to consume all the data. This is required to keep track of the number of matching remote streams opened on the peer. The application can't open a new stream if the remote stream count reached MaxBidirectionalStreams or MaxUnidirectionalStreams (these parameters are provided by the peer on connection establishment). The peer sends the StreamReadsClosed frame once it consumed all the data. The write-side enters the Closed state when the stream receives this frame.

If the application closes writes, the write-side enters directly the Closed state and sends the StreamWritesClosed frame to notify the peer of the writes closure.

The following state diagram shows the write state machine of a remote bidirectional stream (a remote unidirectional stream doesn't have a write-side):

The state machine doesn't have the WaitForPeerReadsClosed state because the stream's write-side doesn't need to wait for the peer to consume all the data.

The following state diagram shows the read state machine of a remote stream:

The application accepts a remote stream following the reading of a Stream or StreamLast frame on the connection. The read-side of the stream is initially in the Read state. In this state the stream buffers the data received from the peer.

The read-side enters the WaitForAppConsume state when the peer notifies the stream that no more data will be sent (with the StreamLast frame). In this state, the read-side waits for the application to consume all the buffered data. Once the application consumed all the data, the read-side enters the Closed state and sends the StreamReadsClosed frame.

If the application closes reads, the read-side enters directly the Closed state and sends the StreamReadsClosed frame to notify the peer of the reads closure.

The following state diagram shows the read state machine of a local bidirectional stream (a local unidirectional stream doesn't have a read-side):

The state machine doesn't have the WaitForAppConsume state because the stream's read-side doesn't need to notify the peer that its done reading.

Was this page helpful?