The dwnx programmers’ guide

This document describes a basic usage of dwnx library and common pitfalls which programmers might encounter.

Initialization

The dwnx_conn is an opaque object that corresponds to a single QMux connection. If the endpoint is a client, use dwnx_conn_client_new() to initialized it. For a server, use dwnx_conn_server_new() instead.

These functions take the following objects:

The dwnx_callbacks stores the callbacks that are called throughout the lifetime of the connection. The mandatory callbacks are:

  • dwnx_callbacks.rand: This callback is called when the library needs the unpredictable bytes of data.

The remaining callbacks are optional.

The dwnx_settings stores the configuration of the QMux connection. It should be first initialized by dwnx_settings_default(). dwnx_settings.conn_id is the identifier of this connection. This is not a part of QMux protocol, and just used in logging purpose only to identify the particular connection. dwnx_settings.initial_ts is the initial timestamp passed to dwnx_conn. The timestamp is updated by calling the functions that take dwnx_tstamp during the connection lifetime.

The dwnx_transport_params stores the local QUIC transport parameters that are sent to the remote endpoint. It should be initialized by dwnx_transport_params_default(). For HTTP/3 server, at least the following fields should be nonzero to allow the client to create a stream and send data:

For HTTP/3 client, the following fields should be nonzero to allow the server to create a stream and send data:

dwnx_transport_params.max_record_size is basically ignored, and the library always uses 16384.

Read the transport data

To pass the data read from the underlying reliable transport, call dwnx_conn_read() with the current timestamp. The application can pass the byte stream to this function without worrying about the QMux record boundary. The function processes the data in streaming fashion. If it returns the negative error code, close the connection.

After reading data, try writing data by calling dwnx_conn_writev_stream(). See below.

Write a QMux record

To write a QMux record, call dwnx_conn_writev_stream(). It writes a single QMux record to the given buffer. It can accept the stream ID and its stream data to send. The application should handle the following negative error code gracefully:

  • DWNX_ERR_STREAM_DATA_BLOCKED: The stream data cannot be sent due to the flow control limit. Try sending the stream data of the another stream if any.

  • DWNX_ERR_STREAM_SHUT_WR: The send side of the stream has been closed. Try sending the stream data of the another stream if any.

  • DWNX_ERR_WRITE_MORE: The application is allowed to write more stream data if any.

If the one of the above negative error codes are returned, call dwnx_conn_writev_stream() again with the same dest and destlen parameters.

In general, the application must keep calling dwnx_conn_writev_stream() repeatedly until it returns the integer >= 0, or the negative error codes other than the ones listed above. If 0 is returned, no QMux record is produced, which means that there is no data to send. If the integer > 0 is returned, the QMux record is completely written to the given buffer and its length is the returned value. If one of the negative error codes other than the ones listed above is returned, close the connection.

When the stream data is written, the number of data written is assigned to the object pointed by pdatalen. Because dwnx_conn_writev_stream() may close the stream, when the function returns, the stream has already been closed. To make sure that the number of the written data is notified to the application, use dwnx_callbacks.write_stream_data_offset callback.

The buffer size should be 16384 for the optimal performance if the underlying transport is TLS. The application may choose the smaller sized buffer while the congestion window is smaller than the maximum TLS record to avoid the additional round trip for decryption.

Connection close

The application can close the connection simply by closing the underlying transport connection.

It can also send CONNECTION_CLOSE frame with the error code and the reason phrase. dwnx_conn_write_connection_close() writes this final QMux record containing CONNECTION_CLOSE frame in the given buffer. The application can send this final record, and then close the underlying connection.

If the connection is closed because of idle timeout, it is strongly recommended just close the underlying connection without sending CONNECTION_CLOSE frame.

Timeout

To get the next timeout, call dwnx_conn_get_expiry(). It should be called after reading or writing data. It is not necessary to call this function each time after calling dwnx_conn_read() or dwnx_conn_writev_stream(). In typical scenario, call dwnx_conn_read() repeatedly until all data is drained from the socket, and then call dwnx_conn_get_expiry(). Similarly, call dwnx_conn_writev_stream() repeatedly to write the data until the write is blocked by the underlying transport. Then call dwnx_conn_get_expiry().

When the timer has fired, call dwnx_conn_handle_expiry(). If it returns DWNX_ERR_IDLE_CLOSE, the connection has been quiescent too long. The underlying transport should be closed without sending CONNECTION_CLOSE frame.