QUIC datagrams are a way to send data that doesn’t need to be guaranteed, which sounds like UDP but is actually more sophisticated.
Imagine you’re building a real-time game. You’re sending player positions, and if one update gets lost, it’s not the end of the world; the next update will fix it. You don’t want to wait for acks and retransmissions for every single position update, as that would add latency. That’s where QUIC datagrams shine. They allow you to send these "best effort" messages without the overhead of QUIC’s reliable delivery mechanisms.
Here’s a simplified look at how it might work in practice. Let’s say you have a simple server and client.
Server (Conceptual Python using aioquic)
import asyncio
from aioquic.asyncio.server import serve
from aioquic.quic.events import DatagramFrameReceived, StreamDataReceived
async def handle_quic_connection(host, port, configuration):
async def handle_event(event):
if isinstance(event, DatagramFrameReceived):
print(f"Received datagram: {event.data.decode()}")
# In a real app, you'd process this game state update
# For demonstration, we might echo it back or do nothing
# Echoing back:
# await connection.send_datagram(event.data)
elif isinstance(event, StreamDataReceived):
print(f"Received stream data: {event.data.decode()}")
# Other event types like HandshakeCompleted, ConnectionTerminated, etc.
await serve(host, port, configuration, stream_handler=handle_event)
# ... (Configuration setup for SSL/TLS certificates would go here)
# For this example, assume 'configuration' is properly set up.
# asyncio.run(handle_quic_connection("localhost", 4433, configuration))
Client (Conceptual Python using aioquic)
import asyncio
from aioquic.asyncio.client import connect
from aioquic.quic.events import HandshakeCompleted
async def send_datagrams(host, port, configuration):
async with connect(host, port, configuration=configuration) as connection:
# Wait for handshake to complete to ensure the connection is ready
await connection.run_async(lambda: asyncio.Event()) # This is a placeholder for waiting
# Send some unreliable datagrams
await connection.send_datagram(b"Player 1: x=10, y=20")
await asyncio.sleep(0.1) # Small delay between datagrams
await connection.send_datagram(b"Player 1: x=12, y=21")
await asyncio.sleep(0.1)
await connection.send_datagram(b"Player 2: x=5, y=15")
# ... (Configuration setup for SSL/TLS certificates would go here)
# For this example, assume 'configuration' is properly set up.
# asyncio.run(send_datagrams("localhost", 4433, configuration))
This is a simplified illustration. In a real application, you’d be managing a QuicConnection object, handling its events, and using connection.send_datagram() to send your unreliable data. The aioquic library handles the QUIC protocol details, including framing these datagrams within QUIC packets.
The problem this solves is the "head-of-line blocking" issue inherent in TCP. In TCP, if a packet is lost, all subsequent packets for that connection must wait for the lost one to be retransmitted, even if they belong to a different logical stream. QUIC, with its independent streams, mitigates this. The datagram extension takes it further by allowing entirely separate, unreliable "streams" that don’t even participate in the reliability mechanisms. This is crucial for applications where low latency is paramount and occasional data loss is acceptable.
Internally, when you call connection.send_datagram(data), the aioquic library (or any QUIC implementation) takes this raw data and packages it into a QUIC packet. This packet will have a specific frame type indicating it’s a datagram. Unlike stream data frames, these datagram frames are not assigned a stream ID that’s part of the reliability protocol. They are essentially sent out with the hope they arrive, but without the guarantee of acknowledgments or retransmissions. The QUIC connection itself still handles encryption and handshake, but the delivery of the datagram is best-effort.
The most surprising thing is that while QUIC inherently provides stream multiplexing to avoid head-of-line blocking between reliable streams, the datagram extension provides a mechanism for unreliable multiplexing. This means you can have multiple independent, unreliable flows of data within a single QUIC connection, all without impacting the reliability of other streams or even other datagram streams. It’s like having multiple UDP sockets bundled under one TLS-encrypted QUIC connection.
What most people don’t realize is that the datagram frames themselves are still subject to the congestion control of the underlying QUIC connection. So, while you’re not waiting for ACKs, you’re still participating in the overall network path congestion. If the path is congested, your datagrams will be throttled by the QUIC connection’s congestion control algorithm, preventing you from flooding the network. This is a key difference from raw UDP, which has no built-in congestion control.
The next concept you’ll likely encounter is how to manage multiple independent datagram flows within a single connection, perhaps using different "logical" datagram types or identifiers embedded within the datagram payload itself.