Back to index

QUIC && HTTP3

2022-12-20

HTTP3 is the next generation of HTTP. It is designed to improve upon HTTP2, the current version of the protocol, by providing faster, more reliable, and more secure communication between clients and servers.

One of the main features of HTTP3 is its use of the QUIC. QUIC is a low-latency, multiplexed transport protocol that runs on top of UDP. It is designed to improve upon TCP (Transmission Control Protocol), the traditional transport protocol of the internet, by reducing the number of round trips required for connection establishment and data transfer.

TCP is a transport protocol that establishes a reliable end-to-end connection between two devices before transmitting data. It does this by using a three-way handshake to establish a connection, and by using sequence numbers and acknowledgement messages to ensure that data is delivered in the correct order. This is very slow sometimes since you're basically doing an arduous back-and-forth just to get information from point A to point B across.

QUIC, on the other hand, is a connectionless transport protocol that does not require a three-way handshake to establish a connection. It requires one round-trip or, in some cases, you can even configure it to have zero round-trips (see 0-RTT here).

I'll detail a few cool techniques QUIC uses that gives it many steps ahead of TCP streams:

Stream Multiplexing

TCP uses a single stream of data to transmit information between two devices. This means that if multiple streams of data are transmitted simultaneously, they must be multiplexed together and transmitted in a single stream. This can lead to delays and inefficiencies, especially when transmitting large amounts of data.

QUIC can have multiple streams within a single connection. You don't need to multiplex the streams yourself. This is just out-of-the-box. So your website can request many different resources (e.g., javascript, images, whatever) from the same connection.

In QUIC, those are actually called streams. You, as a developer, can configure a single QUIC connection to have multiple streams, each doing something different.

Formally, streams are independent channels of communication within a single connection. Each stream is assigned a priority, and the client can use this priority to indicate the order in which it would like the streams to be processed. This allows the client to prioritize certain streams over others, which can be useful in situations where some streams are more important than others.

Packet Pacing

TCP uses a "slow start" algorithm to gradually increase the amount of data transmitted over a connection. This is designed to prevent congestion on the network, but it can also lead to delays and inefficiencies when transmitting large amounts of data.

QUIC uses a packet pacing algorithm to more evenly distribute the transmission of data over time. This can result in faster and more efficient data transfer, especially when transmitting large amounts of data.

Cryptography

HTTP3 also includes support for TLS 1.3, the latest version of the Transport Layer Security (TLS) protocol. TLS is a cryptographic protocol that is used to secure communication over the internet.

Example Client && Server

This is taken from a famous QUIC implementation in Go. Run the full example here:

import (
  "io"
  "fmt"
  "github.com/lucas-clemente/quic-go"
)

// Start a server that echos all data on the first stream opened by the
// client
func echoServer() {
  listener, _ := quic.ListenAddr(addr, generateTLSConfig(), nil)
  conn, _ := listener.Accept(context.Background())
  stream, _ := conn.AcceptStream(context.Background())
  // Echo everything back to the same stream
  io.Copy(stream, stream)
}

// Start a client that sends "bunnyfoofoo" to the server.
// It's expected that the server sends back the same msg.
func clientMain() {
  msg := "bunnyfoofoo"
  tlsConf := &tls.Config{
    InsecureSkipVerify: true,
    NextProtos:         []string{"quic-echo-example"},
  }
  conn, _ := quic.DialAddr(addr, tlsConf, nil)
  stream, _ := conn.OpenStreamSync(context.Background())
  stream.Write([]byte(msg))
  buf := make([]byte, len(msg))
  io.ReadFull(stream, buf)
  fmt.Printf("Client: Got '%s'\n", buf)
}