Frontend Labs
⋯

Day 56 — Connection Multiplexing Concepts

Learning Objectives

  • Understand HTTP/2 multiplexing vs HTTP/1.1 connection-per-request
  • Build a minimal stream multiplexer over a single TCP connection
  • Know why H2 needs fewer backend connections than H1

Exercise: Simple Stream Multiplexer

package main

import (
    "encoding/binary"
    "fmt"
    "io"
    "net"
    "sync"
)

// Simple multiplexer: multiple logical streams over one TCP connection
// Frame: [4B stream-id][2B frame-type][4B payload-len][payload]
const (
    FrameData  uint16 = 0x01
    FrameClose uint16 = 0x02
)

type Frame struct {
    StreamID uint32
    Type     uint16
    Payload  []byte
}

func WriteFrame(w io.Writer, f Frame) error {
    hdr := make([]byte, 10)
    binary.BigEndian.PutUint32(hdr[0:], f.StreamID)
    binary.BigEndian.PutUint16(hdr[4:], f.Type)
    binary.BigEndian.PutUint32(hdr[6:], uint32(len(f.Payload)))
    if _, err := w.Write(hdr); err != nil { return err }
    _, err := w.Write(f.Payload)
    return err
}

func ReadFrame(r io.Reader) (Frame, error) {
    hdr := make([]byte, 10)
    if _, err := io.ReadFull(r, hdr); err != nil {
        return Frame{}, err
    }
    f := Frame{
        StreamID: binary.BigEndian.Uint32(hdr[0:]),
        Type:     binary.BigEndian.Uint16(hdr[4:]),
    }
    length := binary.BigEndian.Uint32(hdr[6:])
    if length > 0 {
        f.Payload = make([]byte, length)
        if _, err := io.ReadFull(r, f.Payload); err != nil {
            return Frame{}, err
        }
    }
    return f, nil
}

type Mux struct {
    conn    net.Conn
    streams map[uint32]chan []byte
    mu      sync.Mutex
    wmu     sync.Mutex
}

func NewMux(conn net.Conn) *Mux {
    m := &Mux{conn: conn, streams: make(map[uint32]chan []byte)}
    go m.demux()
    return m
}

func (m *Mux) demux() {
    for {
        frame, err := ReadFrame(m.conn)
        if err != nil { return }
        m.mu.Lock()
        ch, ok := m.streams[frame.StreamID]
        m.mu.Unlock()
        if ok {
            ch <- frame.Payload
        }
    }
}

func (m *Mux) OpenStream(id uint32) chan []byte {
    ch := make(chan []byte, 10)
    m.mu.Lock()
    m.streams[id] = ch
    m.mu.Unlock()
    return ch
}

func (m *Mux) Send(streamID uint32, data []byte) error {
    m.wmu.Lock()
    defer m.wmu.Unlock()
    return WriteFrame(m.conn, Frame{StreamID: streamID, Type: FrameData, Payload: data})
}

func main() {
    server, client := net.Pipe()

    // Server: echo all streams
    serverMux := NewMux(server)
    go func() {
        for id := uint32(1); id <= 3; id++ {
            ch := serverMux.OpenStream(id)
            go func(streamID uint32, recv chan []byte) {
                for data := range recv {
                    serverMux.Send(streamID, []byte("ECHO:"+string(data)))
                }
            }(id, ch)
        }
    }()

    // Client: 3 concurrent streams over 1 connection
    clientMux := NewMux(client)
    var wg sync.WaitGroup
    for id := uint32(1); id <= 3; id++ {
        wg.Add(1)
        ch := clientMux.OpenStream(id)
        go func(streamID uint32, recv chan []byte) {
            defer wg.Done()
            clientMux.Send(streamID, []byte(fmt.Sprintf("hello from stream %d", streamID)))
            response := <-recv
            fmt.Printf("[stream %d] %s\n", streamID, response)
        }(id, ch)
    }
    wg.Wait()
    server.Close()
    client.Close()
}

Real-World Context

HTTP/2 multiplexes 100+ requests over 1 TCP connection. Your L4 load balancer must NOT use round-robin per-connection for H2 — all 100 requests would go to the same backend. Your L7 proxy (Week 9) uses httputil.ReverseProxy which correctly handles H2 multiplexing.


Generated from week8.md · 8 Oct 2026 16:36