Skip to content

Build a test client

XQUIC serve as a middleware of transport layer, which induces QUIC facilities to socket application with optional HTTP(or HTTP/3) functionality.

To help you quickly familiarize yourself with how to utilize XQUIC in an socket application, we provide a minimum client and server with XQUIC interface and callback operations.

Please be aware that mini_client and mini_server only demonstrate part of the XQUIC interface, and are only suitable for learning and testing purposes.

mini_client basically includes the following modules:

  • Engine: Engine is the core module of XQUIC main process, which must be initialized in the first stage.

  • Connection: Connection module is a submodule of Engine, which is responsible for establishing and managing connections.

  • Request: Request module is a submodule of Connection, which is responsible for data transmission.

Engine

Engine manage the core logic of XQUIC, including connection management, common callback management, incoming and outgoing packet processing, etc. To make sure the correctness of packet processing, both c/s should be initialized with engine.

Here follows the basic steps to build a xquic engine in client:

Create an engine

Before building a xquic engine, make sure to initialize with several configurations as follows:

  • xqc_engine_type_t engine_type: type of engine, 0: server, 1: client

  • xqc_config_t * engine_config: configuration of engine

engine default config can be obtained using xqc_engine_get_default_config:

c
/* Psudo */ 
xqc_engine_get_default_config(&engine_config, engine_type);
  • xqc_engine_ssl_config_t * ssl_config: configuration of ssl

mini_client init ssl config using xqc_mini_cli_init_engine_ssl_config, which defines the following parameters of ssl config:

c
/* Psudo */ 
ssl_config->ciphers = args->quic_cfg.ciphers;   // "TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256"
ssl_config->groups = args->quic_cfg.groups;     // "P-256:X25519:P-384:P-521"
  • xqc_engine_callback_t * engine_callbacks and xqc_transport_callbacks_t * transport_callbacks: engine basic callbacks and transport callbacks.

mini_client using xqc_mini_cli_init_callback to initialize the engine callbacks and transport callbacks with the following self-defined callback functions. It's recommended to refers to callbacks API for more details if you'd like to customize the callbacks:

c
/* Psudo */ 
static xqc_engine_callback_t callback = {
    .set_event_timer = xqc_mini_cli_set_event_timer,
    .log_callbacks = {
        .xqc_log_write_err = xqc_mini_cli_write_log_file,
        .xqc_log_write_stat = xqc_mini_cli_write_log_file,
        .xqc_qlog_event_write = xqc_mini_cli_write_qlog_file
    },
    .keylog_cb = xqc_mini_cli_keylog_cb,
};

static xqc_transport_callbacks_t transport_cbs = {
    .write_socket = xqc_mini_cli_write_socket,
    .write_socket_ex = xqc_mini_cli_write_socket_ex,
    .save_token = xqc_mini_cli_save_token,
    .save_session_cb = xqc_mini_cli_save_session_cb,
    .save_tp_cb = xqc_mini_cli_save_tp_cb,
};
  • void * user_data: application context, will be passed to callback functions for convenient uses.

After that engine can be created using xqc_engine_create:

c
/* Psudo */ 
xqc_engine_t *engine = xqc_engine_create(1, &engine_config, &ssl_config,
                                         &callback, &transport_cbs, user_data);

Register application callbacks

After initialization of engine, there's still some engine context to be completed, one necesarry step is to register ALPN(Application-Layer Protocol Negotiation Extention) context, that is, the callbacks hooks of application layer.

These callback hooks span the entire lifecycle of a quic transmission, including on connection create/close, request create/close, etc. and can enable application layer to customize the behavior on each stage.

In XQUIC, you can use the default application protocol callbacks xqc_app_proto_callbacks_t, or you can customize another self-defined application protocol contexts upon the original application protocol callbacks.

Here we breiefly introduce both ways to register application callbacks.

Init application protocol callbacks

1 Step: Application callbacks should be customized by application layer, and should be set to transport callbacks as the following format.

c
/* Psudo: register application protocols callbacks */
xqc_app_proto_callbacks_t ap_cbs = {
    .conn_cbs = {
        .conn_create_notify = <customed conn_create_notify function>,
        .conn_close_notify = <customed conn_close_notify function>,
        .conn_handshake_finished = <customed conn_handshake_finished function>,
        .conn_ping_acked = <customed conn_ping_acked function>,
    },
    .stream_cbs = {
        .stream_write_notify = <customed stream_write_notify function>,
        .stream_read_notify = <customed stream_read_notify function>,
        .stream_close_notify = <customed stream_close_notify function>,
    }
};

2 Step: The callbacks can only take effect after being registered to engine, XQUIC provide interface xqc_engine_register_alpn to register application callbacks.

For example:

c
/* Psudo */ 
xqc_engine_register_alpn(engine, <alpn name>, <alpn len>, &ap_cbs, NULL);

Init additional self-defined application protocol (HTTP/3)

In special cases, you may need to customize a new application protocol, and may need to register new callbacks.

Take HTTP/3 as an example, the following steps enables XQUIC to handle requests events using HTTP/3 protocol specifications, which replace the original application protocol callbacks.

1 Step: Encapsulate a new application protocol calbacks xqc_h3_callbacks_t to be implemented by application layer:

c
/* Psudo: init http3 callbacks */
xqc_h3_callbacks_t h3_cbs = {
    .h3c_cbs = {
        .h3_conn_create_notify = <customed h3_conn_create_notify function>,
        .h3_conn_close_notify = <customed h3_conn_close_notify function>,
        .h3_conn_handshake_finished = <customed h3_conn_handshake_finished function>,
    },
    .h3r_cbs = {
        .h3_request_create_notify = <customed h3_request_create_notify function>,
        .h3_request_close_notify = <customed h3_request_close_notify function>,
        .h3_request_read_notify = <customed h3_request_read_notify function>,
        .h3_request_write_notify = <customed h3_request_write_notify function>,
    }
};

2 Step: Redefine the original application protocol callbacks, in which new protocol features can be implemented, HTTP/3 protocols are innerly implemented by default.

for example:

c
/* Psudo */ 
const xqc_conn_callbacks_t h3_conn_callbacks = {
    .conn_create_notify         = <redefined h3_conn_create_notify>,
    .conn_close_notify          = <redefined h3_conn_close_notify>,
    .conn_handshake_finished    = <redefined h3_conn_handshake_finished>,
    .conn_ping_acked            = <redefined h3_conn_ping_acked_notify>,
};

const xqc_stream_callbacks_t h3_stream_callbacks = {
    .stream_create_notify  = <redefined h3_stream_create_notify>,
    .stream_write_notify   = <redefined h3_stream_write_notify>,
    .stream_read_notify    = <redefined h3_stream_read_notify>,
    .stream_close_notify   = <redefined h3_stream_close_notify>,
    .stream_closing_notify = <redefined h3_stream_closing_notify>,
};

3 Step: XQUIC register h3 callbacks to engine using xqc_h3_ctx_init, in which h3 callbacks will be set to original application protocol callbacks.

xqc_h3_ctx_init Psudocode:

c
/* Psudo */ 
// Step 1: create h3 context using h3 callbacks
xqc_h3_ctx_t *h3_ctx = xqc_h3_ctx_create(h3_cbs);

// Step 2: replace application protocols callbacks with h3 callbacks
xqc_app_proto_callbacks_t ap_cbs = {
    .conn_cbs       = h3_conn_callbacks,
    .stream_cbs     = h3_stream_callbacks,
};

// Step 3: register H3 protocol context to engine
xqc_engine_register_alpn(engine, XQC_ALPN_H3, strlen(XQC_ALPN_H3), &ap_cbs, h3_ctx);

Connection

XQUIC can support reliable transmission upon the UDP transport layer based on the Connection module.

Unlike TCP, QUIC connection is more like an abstraction of socket state management, and is negotiate and maintained based on socket transmission.

Initialize a socket and socket callback

The client is typically the initiator of a QUIC connection.

In mini_client, the initialization starts with initializing a socket and registering callback functions for socket events.

Refer to xqc_mini_cli_user_conn_create for more details.

c
/* Psudo */ 
// Step 1: create socket, save fd to user_conn->fd
xqc_mini_cli_init_socket(user_conn);

// Step 2: register socket event callback
user_conn->ev_socket = event_new(ctx->eb, user_conn->fd, EV_READ | EV_PERSIST,
    xqc_mini_cli_socket_event_callback, user_conn);
event_add(user_conn->ev_socket, NULL);

To receive and process the packets replies from the server, it's necessary to register a callback function for READ EVENT to process recvfrom. It's workable to start a new thread to process read events, but make sure the XQUIC engine remains in a single thread.

Build a connection

mini_client using h3 connection establishment as an example.

Client can only establish a h3 connection with the following parameters:

  • xqc_engine_t * engine

XQUIC engine built in previous steps

  • xqc_conn_settings_t conn_settings

It decides the basic features of connection, mini_client using xqc_mini_cli_init_conn_settings to initialize the conn_settings;

  • xqc_conn_ssl_config_t conn_ssl_config

To support 0-RTT transmision, XQUIC use conn_ssl_config to get session ticket and transport parameters recorded in previous transmission using interface xqc_mini_cli_init_conn_ssl_config.

For mini_client, session_ticket, transport_parameter, token, which were written to target file in transport callbacks registered in engine, should be read and passed to connection if exists.

c
/* Pseudo: save session ticket and transport parameter to conn_ssl_config */
conn_ssl_config->session_ticket_data = file_data(session_ticket_name);
conn_ssl_config->session_ticket_len = sizeof(file_data(session_ticket_name));
conn_ssl_config->transport_parameter_data = file_data(transport_parameter_name);
conn_ssl_config->transport_parameter_data_len = sizeof(file_data(transport_parameter_name));
token = file_data(token_name);
token_len = sizeof(token);
  • int no_encryption

If no_encryption is 1, no encryption transmission will be applyed to 1-RTT packets.

  • const struct sockaddr * peer_addr and socklen_t peer_addrlen

created by previous process xqc_mini_cli_user_conn_create.

  • void * user_data

application context data, will be used in callback functions.

With the previous parameters, h3 connection can be established with xqc_h3_connect:

c
const xqc_cid_t *cid = xqc_h3_connect(ctx->engine, &conn_settings, token,
        token_len, args->req_cfg.host, no_encryption, &conn_ssl_config,
        peer_addr, peer_addrlen, user_conn);

Request

With previous preparations, user data can be sent with request module, which is directly linked to stream module in tranport layer.

In application layer, user can create/close/send/receive data with request interfaces.

mini_client using processing h3 request on h3 connection as an example.

Send request

mini_client using xqc_mini_cli_send_h3_req to proactively perform sending a GET request after the connection is established, which basically uses the following interfaces.

c
/* Psudo */
// Step 1: create a request
xqc_h3_request_t *h3_request = xqc_h3_request_create(engine, cid, &stream_settings, user_stream);

// Step 2: format header data
xqc_http_headers_t h3_hdrs;
xqc_mini_cli_format_h3_req(h3_hdrs.headers, request_config);

// Step 3: send header
xqc_h3_request_send_headers(h3_request, &h3_hdrs, fin);

// (Step 4: send body if send POST request)
xqc_h3_request_send_body(h3_request, send_body + send_offset, send_body_len - send_offset, fin);

Receive request

To process data received from server, there's two basic steps:

Step 1: Register callback of socket READ EVENT, which receive raw data from server and pass it to XQUIC engine.

For example, mini_client handle READ EVENT in xqc_mini_cli_socket_event_callback.

c
/* Psudo */
// Step 1: receive raw data into packet_buf
int recv_size = recvfrom(fd, packet_buf, sizeof(packet_buf), 0,
                     peer_addr, &peer_addrlen);

// Step 2: pass raw data to XQUIC engine
xqc_engine_packet_process(ctx->engine, packet_buf, recv_size,
                          user_conn->local_addr, user_conn->local_addrlen,
                          user_conn->peer_addr, user_conn->peer_addrlen,
                          (xqc_usec_t)recv_time, user_conn);

Step 2: Define the application layer protocols to process the source data parsed by XQUIC engine.

For example, mini_client process the h3 request data in xqc_mini_cli_h3_request_read_notify.

c
/* Psudo */
// Step 1: receive h3 headers
xqc_http_headers_t *headers = xqc_h3_request_recv_headers(h3_request, &fin);

// Step 2: receive h3 body
ssize_t read = xqc_h3_request_recv_body(h3_request, recv_buff, recv_buff_size, &fin);