Skip to content

构建一个测试客户端

XQUIC 作为传输层的中间件,通过可选的 HTTP(或 HTTP/3)功能将 QUIC 设施引入套接字应用程序。

为了帮助你快速熟悉如何在套接字应用程序中利用 XQUIC,我们提供了一个带有 XQUIC 接口和回调操作的最小客户端和服务器。

请注意,mini_client 和 mini_server 仅展示了 XQUIC 接口的一部分,并且仅适用于学习和测试目的。

mini_client 基本上包括以下模块:

  • Engine: Engine是 XQUIC 主进程的核心模块,必须在第一阶段初始化。

  • Connection: Connection模块是Engine的子模块,负责建立和管理Connection。

  • Request: Request模块是Connection的子模块,负责数据传输。

Engine

Engine管理 XQUIC 的核心逻辑,包括Connection管理、通用回调管理、进出数据包处理等。为了确保数据包处理的正确性,客户端和服务器都应使用Engine进行初始化。

以下是客户端构建 xquic Engine的基本步骤:

创建Engine

在构建 xquic Engine之前,确保初始化以下几个配置:

  • xqc_engine_type_t engine_type: Engine类型,0: 服务器,1: 客户端

  • xqc_config_t * engine_config: Engine配置

可以使用 xqc_engine_get_default_config 获取Engine默认配置:

c
/* Psudo */ 
xqc_engine_get_default_config(&engine_config, engine_type);
  • xqc_engine_ssl_config_t * ssl_config: SSL 配置

mini_client 使用 xqc_mini_cli_init_engine_ssl_config 初始化 SSL 配置,定义了以下 SSL 配置参数:

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 和 xqc_transport_callbacks_t * transport_callbacks: Engine基本回调和传输回调。

mini_client 使用 xqc_mini_cli_init_callback 初始化Engine回调和传输回调,使用以下自定义回调函数。如果你希望自定义回调,建议参考回调 API 获取更多详细信息:

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: 应用程序上下文,将在回调函数中传递以方便使用。

完成上述步骤之后,可以使用 xqc_engine_create 创建Engine:

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

注册应用层回调

Engine初始化后,还有一些Engine上下文需要完成,其中一个必要的步骤是注册 ALPN(应用层协议协商扩展)上下文,即应用层的回调钩子。

这些回调钩子贯穿了整个 QUIC 传输的生命周期,包括Connection创建/关闭、Request创建/关闭等,并能使应用层在每个阶段自定义行为。

在 XQUIC 中,你可以使用默认的应用协议回调 xqc_app_proto_callbacks_t,也可以在原有应用协议回调的基础上自定义新的应用协议上下文。

这里简要介绍两种注册应用层回调的方法。

初始化应用协议回调

第一步: 应用层回调应由应用层自定义,并应按照以下格式设置为传输回调。

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

第二步: 回调只有在注册到Engine后才能生效,XQUIC 提供了接口 xqc_engine_register_alpn 用于注册应用层回调。例如:

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

初始化额外自定义应用协议(以HTTP/3为例)

在特殊情况下,你可能需要自定义一个新的应用协议,并可能需要注册新的回调。以 HTTP/3 为例,以下步骤使 XQUIC 能够使用 HTTP/3 协议规范处理Request事件,从而替换原有的应用协议回调。

第一步: 封装一个新的应用协议回调 xqc_h3_callbacks_t,由应用层实现:

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>,
    }
};

第二步: 重新定义原始应用协议回调,在其中可以实现新的协议特性。

XQUIC的HTTP/3 协议的回调函数在协议栈中内置实现并覆盖原有的传输回调函数。

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>,
};

第三步: 使用 xqc_h3_ctx_init 将 HTTP/3 回调注册到Engine,在此过程中 HTTP/3 回调将被设置为原始应用协议回调。

c
// 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 可以基于Connection模块在 UDP 传输层之上支持可靠传输。

与 TCP 不同,QUIC Connection更像是对套接字状态管理的抽象,并且基于套接字传输进行协商和维护。

初始化socket和socket回调

客户端通常是 QUIC Connection的发起者。

在 mini_client 中,初始化从初始化一个socket和为socket事件注册回调函数开始。

更多详情请参阅 xqc_mini_cli_user_conn_create

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);

为了接收和处理来自服务器的数据包回复,有必要为 READ EVENT 注册一个回调函数来处理 recvfrom。可以启动一个新线程来处理读取事件,但请确保 XQUIC Engine保持在一个线程中运行。

创建Connection

mini_client 以建立 h3 Connection为例展示创建Connection的过程,客户端依赖以下参数创建Connection:

  • xqc_engine_t * engine: 在前面步骤中构建的 XQUIC Engine

  • xqc_conn_settings_t conn_settings: conn_settings决定了Connection的基本特性,mini_client 使用 xqc_mini_cli_init_conn_settings 初始化 conn_settings

  • xqc_conn_ssl_config_t conn_ssl_config: conn_ssl_config用于记录接口 xqc_mini_cli_init_conn_ssl_config 中获取的上次传输中记录的session ticket和传输参数,从而允许0-RTT包的传输。

对mini_client来说,如果存在session_ticket,transport_parameter,token文件(这些文件应该在Engine中注册的 传输回调 中被写入目标文件),则应读取文件内容并传递给 XQUIC Connection。

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 如果 no_encryption 为 1,则不会对 1-RTT 数据包应用加密传输。

  • const struct sockaddr * peer_addr and socklen_t peer_addrlen 由前面的过程 xqc_mini_cli_user_conn_create 创建。

  • void * user_data 应用程序上下文数据,将在回调函数中使用。

有了上述参数,可以使用 xqc_h3_connect 建立 h3 Connection:

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

通过之前的准备工作,用户数据可以通过Request模块发送,该模块直接链接到传输层的流模块。

在应用层,用户可以使用Request接口创建、关闭、发送和接收数据。

mini_client 以处理 h3 Connection上的 h3 Request为例。

发送Request

mini_client 使用 xqc_mini_cli_send_h3_req 在Connection建立后主动执行发送一个 GET 请求的操作,这基本上使用以下接口。

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);

接收Request

为了处理从服务器接收到的数据,有两个基本步骤:

步骤 1: 注册套接字读取事件回调函数,该回调函数接收来自服务器的原始数据并将其传递给 XQUIC Engine。

例如,mini_client 在 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);

步骤 2: 定义应用层协议以处理由 XQUIC Engine解析的源数据。

例如,mini_client 在 xqc_mini_cli_h3_request_read_notify 中处理 h3 Request数据。

c
// 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);