Frontend Labs
⋯

34. Networking deep dive: net.Conn, TCP/UDP, framing your own protocol

34.1 net.Conn: the interface underneath every connection

type Conn interface {
	Read(b []byte) (n int, err error)
	Write(b []byte) (n int, err error)
	Close() error
	SetDeadline(t time.Time) error
	// ... plus local/remote address accessors
}

TCP connections, TLS connections (Chapter 35), and Unix domain sockets all implement net.Conn — protocol-level code written against the interface works over any of them unchanged.

34.2 TCP server skeleton

ln, err := net.Listen("tcp", ":9000")
if err != nil {
	log.Fatal(err)
}
for {
	conn, err := ln.Accept()
	if err != nil {
		continue
	}
	go handle(conn) // one goroutine per connection — cheap, Chapter 15
}

34.3 UDP: connectionless, message-oriented

addr, err := net.ResolveUDPAddr("udp", ":9001")
if err != nil {
	log.Fatal(err)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
	log.Fatal(err)
}
buf := make([]byte, 1500) // stay under typical MTU to avoid IP fragmentation
for {
	n, remote, err := conn.ReadFromUDP(buf)
	if err != nil {
		continue
	}
	packet := append([]byte(nil), buf[:n]...) // copy before reusing buf in the next loop
	go handleDatagram(conn, remote, packet)
}

TCP guarantees ordered, reliable delivery over a byte stream with no built-in message boundaries; UDP delivers discrete datagrams with no ordering or delivery guarantee at all. Choosing between them is choosing whether your application layer needs to reinvent ordering/reliability (UDP-based protocols like QUIC do) or can lean on TCP's.

34.4 TCP has no message boundaries — you must frame your own

A Read on a TCP net.Conn can return a partial message, multiple messages concatenated, or anything in between — TCP only guarantees byte order, not that one Write maps to one Read. Real protocols solve this with either delimiters (bufio.Scanner with \n, as Chapter 26's cache server does) or length-prefixing:

func writeFramed(w io.Writer, payload []byte) error {
	var header [4]byte
	binary.BigEndian.PutUint32(header[:], uint32(len(payload)))
	if _, err := w.Write(header[:]); err != nil {
		return err
	}
	_, err := w.Write(payload)
	return err
}

func readFramed(r io.Reader) ([]byte, error) {
	var header [4]byte
	if _, err := io.ReadFull(r, header[:]); err != nil {
		return nil, err
	}
	size := binary.BigEndian.Uint32(header[:])
	payload := make([]byte, size)
	_, err := io.ReadFull(r, payload)
	return payload, err
}

io.ReadFull is the detail that matters here: a plain Read can return fewer bytes than requested even when more are coming — ReadFull loops until it either fills the buffer or hits an error.

34.5 Real-world example

Redis's RESP protocol and HTTP/1.1's Content-Length header are both solving exactly this framing problem at the application layer, because TCP itself gives you none of it. Chapter 38's chat server uses newline-delimited framing (simple, human-debuggable with nc); a binary protocol handling arbitrary payloads (including payloads containing raw newline bytes) needs length-prefixing instead.

Applied in the series: the TCP/IP guide's Go labs use net.Conn for real protocols: a byte-stream framing demo, a DNS client, raw HTTP/1.1, and the PROXY protocol.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36