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.