Back to Blog

HTTP/2, TLS, and WebSockets: Modern Application Protocols

How HTTP/2 multiplexes streams over a single connection, how TLS secures communication with encryption and authentication, and how WebSockets enable real-time bidirectional messaging.

2021-03-10
Share
Computer Engineeringnetworkinghttptls

Terminology

  • HTTP (HyperText Transfer Protocol): an application-layer protocol for transferring hypermedia documents; the foundation of data communication on the web, using a request-response model
  • HTTP/1.1: the widely deployed version of HTTP that introduced persistent connections and chunked transfer encoding, but suffers from head-of-line blocking
  • HTTP/2: a major revision of HTTP that introduces binary framing, multiplexed streams, header compression, and server push over a single TCP connection
  • Stream: in HTTP/2, an independent, bidirectional sequence of frames exchanged between client and server within a single connection; each request-response pair uses its own stream
  • Frame: the smallest unit of communication in HTTP/2; each frame has a type (HEADERS, DATA, SETTINGS, etc.) and belongs to a specific stream
  • Multiplexing: the ability to send multiple requests and responses simultaneously over a single TCP connection, interleaving frames from different streams
  • Head-of-line (HOL) blocking: a condition where a slow or blocked request prevents subsequent requests from being processed; in HTTP/1.1, this occurs because requests on a connection are processed sequentially
  • HPACK: the header compression algorithm used by HTTP/2 that reduces overhead by encoding headers as indices into a shared dynamic table
  • TLS (Transport Layer Security): a cryptographic protocol that provides privacy and data integrity between two communicating applications through encryption, authentication, and message integrity checks
  • Handshake: the initial negotiation between client and server to establish a secure TLS connection, including cipher suite selection, certificate exchange, and key agreement
  • Certificate: a digital document issued by a Certificate Authority (CA) that binds a public key to a domain name, allowing clients to verify the server's identity
  • Cipher suite: a combination of algorithms used for key exchange, encryption, and message authentication in a TLS connection
  • Symmetric encryption: encryption where the same key is used for both encryption and decryption; fast and used for bulk data transfer (AES, ChaCha20)
  • Asymmetric encryption: encryption using a key pair (public and private); slower but enables key exchange and digital signatures (RSA, ECDSA)
  • Perfect forward secrecy (PFS): a property where compromise of long-term keys does not compromise past session keys; achieved by using ephemeral key exchange (ECDHE)
  • WebSocket: a protocol that provides full-duplex, bidirectional communication over a single TCP connection, initiated via an HTTP upgrade handshake
  • Full-duplex: communication where both parties can send and receive data simultaneously, without taking turns
  • Server push: an HTTP/2 feature where the server proactively sends resources to the client before the client requests them, based on the server's knowledge of what the client will need
  • ALPN (Application-Layer Protocol Negotiation): a TLS extension that allows the client and server to negotiate which application protocol (HTTP/1.1, HTTP/2) to use during the TLS handshake
  • What & Why

    HTTP/1.1 served the web well for over 15 years, but its design has fundamental limitations. Each TCP connection processes requests sequentially: the second request cannot be sent until the first response is complete (head-of-line blocking). Browsers work around this by opening 6-8 parallel TCP connections per domain, but each connection requires its own TCP handshake and TLS negotiation, wasting time and resources.

    HTTP/2 solves this by multiplexing multiple request-response pairs as independent streams over a single TCP connection. Frames from different streams are interleaved, so a slow response on one stream does not block others. Header compression (HPACK) reduces the repetitive overhead of sending similar headers on every request. The result is faster page loads, especially on high-latency connections.

    TLS secures the communication channel. Without TLS, anyone on the network path can read and modify HTTP traffic. TLS provides three guarantees: confidentiality (encryption prevents eavesdropping), integrity (message authentication codes detect tampering), and authentication (certificates verify the server's identity). TLS 1.3 streamlined the handshake to just one round trip, making HTTPS nearly as fast as plain HTTP.

    WebSockets fill a gap that HTTP's request-response model cannot: real-time, bidirectional communication. With HTTP, the server can only send data in response to a client request. WebSockets upgrade an HTTP connection to a persistent, full-duplex channel where either side can send messages at any time. This is essential for chat applications, live dashboards, collaborative editing, and multiplayer games.

    How It Works

    HTTP/2 Binary Framing

    HTTP/2 replaces HTTP/1.1's text-based protocol with a binary framing layer. All communication is split into frames, each tagged with a stream ID. The connection carries multiple concurrent streams.

    Client Server HEADERS frame (stream 1) HEADERS frame (stream 3) HEADERS frame (stream 5) HEADERS+DATA (stream 1) DATA frame (stream 5) DATA frame (stream 3) DATA frame (stream 1) Frames from different streams interleaved on one TCP connection

    Key HTTP/2 features:

    Multiplexing: Multiple requests and responses share a single TCP connection. Each stream is independent, so a slow response on stream 1 does not block streams 3 and 5. This eliminates the need for multiple TCP connections.

    Header compression (HPACK): HTTP headers are repetitive (same Host, User-Agent, Accept on every request). HPACK maintains a dynamic table of previously sent headers and encodes repeated headers as small indices. First request might send 800 bytes of headers; subsequent requests on the same connection might send 20 bytes.

    Stream prioritization: Clients can assign priorities and dependencies to streams, telling the server which resources are most important. The server uses this to allocate bandwidth to critical resources (CSS, JavaScript) before less important ones (images).

    Server push: The server can proactively send resources it knows the client will need. When the client requests an HTML page, the server can push the associated CSS and JavaScript files without waiting for the client to parse the HTML and request them.

    TLS 1.3 Handshake

    TLS 1.3 reduced the handshake from two round trips (TLS 1.2) to one:

    1. Client Hello: The client sends supported cipher suites and key shares (ephemeral public keys for key exchange) in a single message
    2. Server Hello + Encrypted Extensions + Certificate + Finished: The server selects a cipher suite, sends its key share, certificate, and finishes the handshake, all in one flight
    3. Client Finished: The client verifies the certificate, computes the shared secret, and sends its Finished message

    After step 2, the server can already send encrypted application data. After step 3, the client can send encrypted data. Total: 1 RTT for the handshake (compared to 2 RTTs in TLS 1.2).

    TLS 1.3 also supports 0-RTT resumption: if the client has connected to the server before, it can send encrypted application data in the very first message, achieving zero additional latency for the TLS handshake. The trade-off is that 0-RTT data is vulnerable to replay attacks, so it should only be used for idempotent requests.

    Key exchange: TLS 1.3 exclusively uses ephemeral Diffie-Hellman (ECDHE), providing perfect forward secrecy. Even if the server's long-term private key is compromised, past sessions cannot be decrypted because each session used a unique ephemeral key.

    WebSocket Protocol

    WebSockets start as an HTTP request with an Upgrade header:

    1. Client sends an HTTP/1.1 request with Upgrade: websocket and a random key
    2. Server responds with 101 Switching Protocols and a derived accept key
    3. The TCP connection is now a WebSocket connection: both sides can send frames at any time

    WebSocket frames are lightweight: a 2-byte header for small messages (up to 125 bytes payload), 4 bytes for medium messages (up to 65535 bytes), and 10 bytes for large messages. Client-to-server frames are masked with a 4-byte key to prevent cache poisoning attacks on intermediary proxies.

    Unlike HTTP's request-response model, WebSocket messages are independent. The server can push data to the client at any time without the client asking. The client can send messages at any time without waiting for a response. Either side can close the connection with a close frame.

    Complexity Analysis

    Metric HTTP/1.1 HTTP/2
    Connections per domain $6$-$8$ (browser limit) $1$
    HOL blocking Yes (per connection) No (at HTTP level)
    Header overhead (repeated) $\sim 800$ bytes/request $\sim 20$ bytes (HPACK)
    Handshake cost (with TLS) $1 + 2 = 3$ RTTs (TCP + TLS 1.2) $1 + 1 = 2$ RTTs (TCP + TLS 1.3)

    Time to first byte for a new HTTPS connection:

    $t_{\text{TTFB}} = t_{\text{DNS}} + t_{\text{TCP}} + t_{\text{TLS}} + t_{\text{request}} + t_{\text{server}}$

    With TLS 1.3:

    $t_{\text{TTFB}} = t_{\text{DNS}} + 1\text{RTT}_{\text{TCP}} + 1\text{RTT}_{\text{TLS}} + 1\text{RTT}_{\text{HTTP}} + t_{\text{server}}$

    With TLS 1.3 0-RTT resumption, the TLS and HTTP request can overlap:

    $t_{\text{TTFB}} = t_{\text{DNS}} + 1\text{RTT}_{\text{TCP}} + 1\text{RTT}_{\text{TLS+HTTP}} + t_{\text{server}}$

    HPACK compression ratio for repeated headers:

    $\text{Compression ratio} = 1 - \frac{\text{compressed size}}{\text{original size}} \approx 95\%\text{-}98\%$

    WebSocket frame overhead per message:

    $\text{Overhead} = \begin{cases} 2 \text{ bytes} & \text{if payload} \leq 125 \text{ bytes} \\ 4 \text{ bytes} & \text{if payload} \leq 65535 \text{ bytes} \\ 10 \text{ bytes} & \text{if payload} > 65535 \text{ bytes} \end{cases}$

    Client-to-server frames add 4 bytes for the masking key. Compare this to HTTP/1.1 where each request carries hundreds of bytes of headers.

    Implementation

    ALGORITHM HTTP2Multiplexer(connection, pendingRequests)
    INPUT: connection: single TCP connection, pendingRequests: list of HTTP requests
    OUTPUT: responses for all requests
    BEGIN
      nextStreamID <- 1  // client-initiated streams use odd IDs
      activeStreams <- empty map
      responses <- empty map
    
      // Send all requests as HEADERS frames
      FOR EACH request IN pendingRequests DO
        streamID <- nextStreamID
        nextStreamID <- nextStreamID + 2
    
        headersFrame <- CREATE_FRAME(
          type: HEADERS,
          streamID: streamID,
          flags: END_HEADERS,
          payload: HPACK_ENCODE(request.headers)
        )
        SEND_FRAME(connection, headersFrame)
    
        IF request.body is not empty THEN
          dataFrame <- CREATE_FRAME(
            type: DATA,
            streamID: streamID,
            flags: END_STREAM,
            payload: request.body
          )
          SEND_FRAME(connection, dataFrame)
        END IF
    
        activeStreams[streamID] <- request
      END FOR
    
      // Receive response frames (interleaved from different streams)
      WHILE LENGTH(responses) < LENGTH(pendingRequests) DO
        frame <- RECEIVE_FRAME(connection)
    
        IF frame.type = HEADERS THEN
          responses[frame.streamID] <- NEW_RESPONSE()
          responses[frame.streamID].headers <- HPACK_DECODE(frame.payload)
        ELSE IF frame.type = DATA THEN
          APPEND frame.payload TO responses[frame.streamID].body
        END IF
    
        IF frame.flags CONTAINS END_STREAM THEN
          MARK_COMPLETE(responses[frame.streamID])
          REMOVE activeStreams[frame.streamID]
        END IF
      END WHILE
    
      RETURN responses
    END
    
    ALGORITHM TLS13Handshake(client, server)
    INPUT: client: TLS client, server: TLS server
    OUTPUT: encrypted connection with shared secret
    BEGIN
      // Client Hello (1 message)
      clientRandom <- GENERATE_RANDOM(32)
      clientKeyShare <- GENERATE_ECDHE_KEYPAIR()
      clientHello <- CREATE_MESSAGE(
        type: CLIENT_HELLO,
        random: clientRandom,
        cipherSuites: [TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256],
        keyShares: [clientKeyShare.publicKey],
        supportedVersions: [TLS_1_3]
      )
      SEND(client, server, clientHello)
    
      // Server Hello + encrypted data (1 flight)
      serverRandom <- GENERATE_RANDOM(32)
      serverKeyShare <- GENERATE_ECDHE_KEYPAIR()
    
      // Compute shared secret using ECDHE
      sharedSecret <- ECDHE(serverKeyShare.privateKey, clientKeyShare.publicKey)
      handshakeKeys <- DERIVE_KEYS(sharedSecret, clientRandom, serverRandom)
    
      serverHello <- CREATE_MESSAGE(
        type: SERVER_HELLO,
        random: serverRandom,
        cipherSuite: TLS_AES_256_GCM_SHA384,
        keyShare: serverKeyShare.publicKey
      )
      SEND(server, client, serverHello)
    
      // Encrypted with handshake keys
      SEND_ENCRYPTED(server, client, handshakeKeys,
        encryptedExtensions: server.extensions,
        certificate: server.certificate,
        certificateVerify: SIGN(server.privateKey, handshakeTranscript),
        finished: HMAC(handshakeKeys.serverFinished, handshakeTranscript)
      )
    
      // Client verifies and finishes
      VERIFY_CERTIFICATE(server.certificate, trustedCAs)
      VERIFY_SIGNATURE(server.certificate.publicKey, certificateVerify, handshakeTranscript)
    
      clientSharedSecret <- ECDHE(clientKeyShare.privateKey, serverKeyShare.publicKey)
      applicationKeys <- DERIVE_APPLICATION_KEYS(clientSharedSecret)
    
      SEND_ENCRYPTED(client, server, handshakeKeys,
        finished: HMAC(handshakeKeys.clientFinished, handshakeTranscript)
      )
    
      // Connection established with application keys
      RETURN ENCRYPTED_CONNECTION(applicationKeys)
    END
    
    ALGORITHM WebSocketHandshakeAndCommunicate(client, server, path)
    INPUT: client: WebSocket client, server: target server, path: endpoint path
    OUTPUT: bidirectional message channel
    BEGIN
      // Step 1: HTTP Upgrade request
      wsKey <- BASE64_ENCODE(GENERATE_RANDOM(16))
      upgradeRequest <- CREATE_HTTP_REQUEST(
        method: GET,
        path: path,
        headers: {
          "Upgrade": "websocket",
          "Connection": "Upgrade",
          "Sec-WebSocket-Key": wsKey,
          "Sec-WebSocket-Version": "13"
        }
      )
      SEND_HTTP(client, server, upgradeRequest)
    
      // Step 2: Server responds with 101
      response <- RECEIVE_HTTP(server)
      IF response.status != 101 THEN
        RETURN ERROR("WebSocket upgrade failed")
      END IF
    
      expectedAccept <- BASE64_ENCODE(SHA1(wsKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
      IF response.headers["Sec-WebSocket-Accept"] != expectedAccept THEN
        RETURN ERROR("Invalid accept key")
      END IF
    
      // Step 3: Connection is now WebSocket, full-duplex messaging
      channel <- NEW_WEBSOCKET_CHANNEL(client, server)
    
      // Either side can send at any time
      RETURN channel
    END
    
    ALGORITHM SendWebSocketFrame(channel, message, isClient)
    INPUT: channel: WebSocket connection, message: data to send, isClient: boolean
    OUTPUT: frame sent
    BEGIN
      frame <- NEW_FRAME()
      frame.fin <- true  // final fragment
      frame.opcode <- IF IS_TEXT(message) THEN 0x1 ELSE 0x2  // text or binary
    
      IF LENGTH(message) <= 125 THEN
        frame.payloadLength <- LENGTH(message)
      ELSE IF LENGTH(message) <= 65535 THEN
        frame.payloadLength <- 126
        frame.extendedLength <- LENGTH(message)  // 2 bytes
      ELSE
        frame.payloadLength <- 127
        frame.extendedLength <- LENGTH(message)  // 8 bytes
      END IF
    
      IF isClient THEN
        frame.mask <- true
        frame.maskingKey <- GENERATE_RANDOM(4)
        frame.payload <- XOR_MASK(message, frame.maskingKey)
      ELSE
        frame.mask <- false
        frame.payload <- message
      END IF
    
      SEND_BYTES(channel, SERIALIZE(frame))
    END
    

    Real-World Applications

    • Modern web applications: virtually all major websites use HTTP/2 over TLS; the combination of multiplexing and header compression reduces page load times by 15-50% compared to HTTP/1.1, especially on high-latency mobile connections
    • Real-time collaboration: tools like Google Docs, Figma, and Notion use WebSockets to synchronize edits between users in real time; each keystroke or cursor movement is sent as a small WebSocket message
    • Financial trading platforms: stock tickers and trading interfaces use WebSockets to push price updates to clients with minimal latency; the persistent connection avoids the overhead of repeated HTTP requests
    • Chat and messaging: Slack, Discord, and WhatsApp Web use WebSockets for instant message delivery; the server pushes new messages to connected clients without polling
    • API gateways: services like Cloudflare, AWS API Gateway, and Nginx terminate TLS at the edge, handling certificate management and encryption offloading so backend services can communicate over plain HTTP internally
    • gRPC: Google's RPC framework runs over HTTP/2, using its multiplexing and binary framing to support efficient bidirectional streaming between microservices
    • Certificate transparency: the web PKI ecosystem uses Certificate Transparency logs to detect misissued certificates, with browsers requiring certificates to include Signed Certificate Timestamps (SCTs) from multiple logs

    Key Takeaways

    • HTTP/2 multiplexes multiple request-response streams over a single TCP connection, eliminating HTTP-level head-of-line blocking and reducing the overhead of multiple connections
    • HPACK header compression reduces repeated header overhead by 95-98%, encoding common headers as small indices into a shared dynamic table
    • TLS 1.3 completes the handshake in 1 RTT (down from 2 in TLS 1.2) and supports 0-RTT resumption for returning clients; it exclusively uses ephemeral key exchange (ECDHE) for perfect forward secrecy
    • WebSockets provide full-duplex, bidirectional communication with minimal per-message overhead (2-10 bytes), enabling real-time applications that HTTP's request-response model cannot efficiently support
    • The total time to first byte for a new HTTPS connection is $t_{\text{DNS}} + \text{RTT}_{\text{TCP}} + \text{RTT}_{\text{TLS}} + \text{RTT}_{\text{HTTP}} + t_{\text{server}}$, making connection reuse and 0-RTT resumption critical for performance
    • HTTP/2 still suffers from TCP-level head-of-line blocking (a lost TCP packet stalls all streams); HTTP/3 (QUIC) solves this by running over UDP with per-stream loss recovery