Using the RUST API
This tutorial describes how to use the TQUIC RUST API.
The complete example code for this article can be found in the TQUIC RUST example repository on GitHub:
Endpoint Instantiation
The Endpoint is responsible for managing connections, processing incoming packets, and scheduling outgoing packets.
To create an Endpoint
instance, simply use the new
method provided by the Endpoint
struct. Further explanation of the parameters for this method can be found below.
pub fn new(
config: Box<crate::Config>,
is_server: bool,
handler: Box<dyn TransportHandler>,
sender: Rc<dyn PacketSendHandler>,
) -> Self
It is important to notice that the TQUIC library takes a different approach in receiving and sending packets - it doesn't rely on sockets for this purpose. Instead, it delegates these responsibilities to the callbacks provided by the user. Moreover, the library doesn't impose any specific event loop requirement but rather offers functions that assist users in scheduling events. This flexibility enables enhanced customization and seamless integration across various systems.
Configurations
The Config
maintains various connection parameters. You can use the Config's new
method to create an Config instance, and use various setter methods to further customize the configurations.
// Create default configurations.
let mut config = Config::new()?;
// Set connection idle timeout
config.set_max_idle_timeout(30000);
// Set application level protocol
let mut tls_config = TlsConfig::new()?;
tls_config.set_application_protos(vec![b"h3".to_vec()]);
config.set_tls_config(tls_config);
Refer to the Config documentation for more configuration options.
Work mode
The Endpoint can works in either server or client mode, which can be specified using the is_server
parameter.
If your program requires both QUIC client and server functionality, it is recommended to instantiate two separate endpoints.
TransportHandler
The Endpoint notifies the application layer to handle relevant QUIC connection/stream events by invoking the callback functions listed in TransportHandler
.
pub trait TransportHandler {
// Called when a new connection has been created. This callback is called
// as soon as connection object is created inside the endpoint, but
// before the handshake is done. The connection has progressed enough to
// send early data if possible.
fn on_conn_created(&mut self, conn: &mut Connection);
// Called when the handshake is completed.
fn on_conn_established(&mut self, conn: &mut Connection);
// Called when the connection is closed. The connection is no longer
// accessible after this callback returns. It is a good time to clean up
// the connection context.
fn on_conn_closed(&mut self, conn: &mut Connection);
// Called when the stream is created.
fn on_stream_created(&mut self, conn: &mut Connection, stream_id: u64);
// Called when the stream is readable. This callback is called when either
// there are bytes to be read or an error is ready to be collected.
fn on_stream_readable(&mut self, conn: &mut Connection, stream_id: u64);
// Called when the stream is writable.
fn on_stream_writable(&mut self, conn: &mut Connection, stream_id: u64);
// Called when the stream is closed. The stream is no longer accessible
// after this callback returns. It is a good time to clean up the stream
// context.
fn on_stream_closed(&mut self, conn: &mut Connection, stream_id: u64);
// Called when client receives a token in NEW_TOKEN frame.
fn on_new_token(&mut self, conn: &mut Connection, token: Vec<u8>);
}
PacketSendHandler
The Endpoint utilizes the PacketSendHandler
to send packets. Within the PacketSendHandler, there is a callback function called on_packets_send
, which is responsible for sending a batch of UDP packets to the network.
pub trait PacketSendHandler {
fn on_packets_send(&self, pkts: &[(Vec<u8>, PacketInfo)]) -> Result<usize>;
}
The example implementation of PacketSendHandler can be found further down in this article.
Receiving packets
The application receives UDP packets and delivers them to TQUIC by invoking the Endpoint's recv
method, as demonstrated in the following example.
// Receive an incoming UDP datagram
let (len, remote) = match socket.recv_from(&mut recv_buf) {
Ok(v) => v,
Err(e) => {
if e.kind() == std::io::ErrorKind::WouldBlock {
break;
}
// Error handling
}
};
// Build a PacketInfo
let pkt_buf = &mut recv_buf[..len];
let pkt_info = PacketInfo {
src: remote, // Source address of the packet
dst: socket.local_addr()?, // Destination address of the packet
time: Instant::now(), // Arriaval time of the packet
};
// Delivery the incoming packet to the endpoint
match self.endpoint.recv(pkt_buf, &pkt_info) {
Ok(_) => {}
Err(e) => {
// Error handling
}
};
Sending packets
The on_packets_send
function is responsible for sending a batch of UDP packets to the network. It should return the number of packets that were successfully sent. In case there are any packets that fail to be sent, the Endpoint will handle their retry at a later time.
fn on_packets_send(&self, pkts: &[(Vec<u8>, PacketInfo)]) -> tquic::Result<usize> {
let mut count = 0;
for (pkt, info) in pkts {
if let Err(e) = self.socket.send_to(pkt, info.dst) {
if e.kind() == std::io::ErrorKind::WouldBlock {
return Ok(count);
}
// Error handling
}
count += 1;
}
Ok(count)
}
The simple example demonstrated the use of recv_from
and send_to
to read and send packets one by one. However, it is recommended that applications use more efficient methods available to read and send packets in batches.
Process connections
The process_connections()
function must be called to postprocess connections in the following cases:
- When some incoming packets have been processed by using the
recv
method of Endpoint. - When timeout events have been handled by calling the
on_timeout
method of Endpoint. - When new connections have been initiated by the client through calling the
connect
method of Endpoint.
The code example below utilizes Mio Poll as the event framework:
// Connect to a server (client)
endpoint.connect(local, remote, server_name, session, token)?;
// Do event loop
let mut events = mio::Events::with_capacity(1024);
loop {
endpoint.process_connections()?;
// Get the the earliest timeout on the endpoint
let timeout = endpoint.timeout();
// Wait for timeout or IO events
poll.poll(&mut events, timeout)?;
// Process IO events
for event in events.iter() {
if event.is_readable() {
process_read_event()?;
}
}
// Process timeout events.
// Note: Since `poll()` doesn't clearly tell if there was a timeout when it returns,
// it is up to the endpoint to check for a timeout and deal with it.
endpoint.on_timeout(Instant::now());
}
Client connection initiation
The client utilizes the connect method of the Endpoint to establish a connection with a server.
pub fn connect(
&mut self,
local: SocketAddr,
remote: SocketAddr,
server_name: Option<&str>,
session: Option<&[u8]>,
token: Option<&[u8]>
) -> Result<u64>
The client can provide session
/token
parameters to create a 0RTT connection, which helps reduce the time needed for establishing a new connection. Please refer to the "0RTT Connection" section for instructions on obtaining the session
/token
.
On the server-side, when a connection is created, the Endpoint invokes the on_conn_created
callback function to notify the application.
Using Stream
Creating streams
QUIC allows either endpoint to create streams. The application invokes the stream_new()
function on a connection to create a new stream.
pub fn stream_new(
&mut self,
stream_id: u64,
urgency: u8,
incremental: bool
) -> Result<()>
On the peer side, when a stream is created, the Endpoint invokes the on_stream_created
callback function to notify the application.
Writing to streams
When the stream is writable, the Endpoint invokes the on_stream_writable
callback function to notify the application. The application can send data via the Connection's stream_write
method.
pub fn stream_write(
&mut self,
stream_id: u64,
buf: Bytes,
fin: bool
) -> Result<usize>
If stream_write
returns an Error::Done
error, no more data could be written due to flow control restrictions. The application can continue sending data the next time when the stream becomes writable.
Reading from streams
When the stream is readable or encounters an error, the Endpoint invokes on_stream_readable
callback function to notify the application. The application can use the Connection's stream_read
method to read data or collect error information.
pub fn stream_read(
&mut self,
stream_id: u64,
out: &mut [u8]
) -> Result<(usize, bool)>
If stream_read
returns an Error::Done
error, no more data could be read. The application can continue reading data the next time when the stream becomes readable.
See the TQUIC API documentation for more stream related operations.
0RTT Connection
In order to minimize the time required to establish a new connection, a client that has previously connected to a server may cache certain parameters from that connection and subsequently initiate a 0-RTT connection with the server. This allows the client to send data immediately, without waiting for a handshake to complete.
To be specific, the client needs to maintain session state and address token for previously connections, and utilize those parameters to establish a new connection. An example is as follows:
fn on_conn_closed(&mut self, conn: &mut Connection) {
if let Some(session_file) = &self.session_file {
// Save the session data (including TLS session data
// and QUIC transport parameters)
if let Some(session) = conn.session() {
std::fs::write(session_file, session).ok();
}
}
}
fn on_new_token(&mut self, conn: &mut Connection, token: Vec<u8>);
if let Some(token_file) = &self.token_file {
// Save the address token
std::fs::write(token_file, &token).ok()
}
}
// Client try to create a 0RTT connection.
let c = endpoint.connect(self, local, remote, server_name, session, token)?;
Using multiply cerificates
TQUIC supports selecting different TLSConfig
s based on SNI. An applications can implement the TlsConfigSelector
trait for selecting custom certificates:
pub trait TlsConfigSelector: Send + Sync {
// Get default TLSConfig
fn get_default(&self) -> Option<&TlsConfig>;
// Get the TLSConfig based on SNI
fn select(&self, server_name: &str) -> Option<&TlsConfig>;
}
Then, it calls Config's set_tls_config_selector
method to activate the custom TLSConfig selector.
Packets decryption
TQUIC supports exporting TLS keys in NSS key log format, which allows Wireshark and other tools to decrypt and analyze QUIC packets.
You can specify the output of the keylog file using the Connection's set_keylog
method, as shown below:
fn on_conn_created(&mut self, conn: &mut Connection) {
// Set key log for the connection.
if let Some(keylog_file) = &self.keylog_file {
if let Ok(file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(keylog_file)
{
conn.set_keylog(Box::new(file));
} else {
error!("{} set key log failed", conn.trace_id());
}
}
}