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.