构建一个测试客户端
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默认配置:
/* 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 配置参数:
/* 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 获取更多详细信息:
/* 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:
/* 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
,也可以在原有应用协议回调的基础上自定义新的应用协议上下文。
这里简要介绍两种注册应用层回调的方法。
初始化应用协议回调
第一步: 应用层回调应由应用层自定义,并应按照以下格式设置为传输回调。
/* 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 用于注册应用层回调。例如:
/* 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
,由应用层实现:
/* 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 协议的回调函数在协议栈中内置实现并覆盖原有的传输回调函数。
/* 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 回调将被设置为原始应用协议回调。
// 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
。
/* 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 Enginexqc_conn_settings_t
conn_settings: conn_settings决定了Connection的基本特性,mini_client 使用xqc_mini_cli_init_conn_settings
初始化 conn_settingsxqc_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。
/* 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 andsocklen_t
peer_addrlen 由前面的过程 xqc_mini_cli_user_conn_create 创建。void *
user_data 应用程序上下文数据,将在回调函数中使用。
有了上述参数,可以使用 xqc_h3_connect 建立 h3 Connection:
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
请求的操作,这基本上使用以下接口。
/* 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
中处理读取事件。
/* 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数据。
// 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);