Skip to Content
Course content

201: TCP and UDP with the net Package

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you a snippet of code that looks perfectly fine at first glance. A lot of developers coming to Go from higher-level languages make this exact mistake when they first touch the net package. They treat a TCP connection like a series of discrete messages, when in reality, it's just a continuous stream of bytes.

// A simple TCP server that reads a message and prints it
listener, _ := net.Listen("tcp", ":8080")
conn, _ := listener.Accept()
defer conn.Close()

buf := make([]byte, 1024)
n, err := conn.Read(buf) // The mistake is right here
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Received: %s\n", string(buf[:n]))

The 'Missing Data' Mystery

The bug here isn't a syntax error—it's a conceptual one. If you run this and send a small string like "Hello", it works. But if you send a large JSON payload or a long string, you might find that your server only prints the first few hundred bytes and then stops. Or, if the network is jittery, the data might arrive in two separate chunks, and your server will exit after the first chunk, leaving the rest of the message hanging in the buffer.

TCP does not have "messages." It has a stream. When you call conn.Read(buf), you are essentially saying, "Give me whatever is available in the buffer right now, up to 1024 bytes." You have no guarantee that one Read call equals one Write call from the client.

Treating TCP as a Stream with bufio

To fix this, you need a way to tell the server when a message actually ends. This is called "framing." The easiest way to do this in Go is by using a delimiter (like a newline character) and the bufio package, which handles the buffering for you so you don't have to manage byte slices manually.

reader := bufio.NewReader(conn)
// Keep reading until we hit a newline character
message, err := reader.ReadString('\n')
if err != nil {
    log.Printf("Error reading: %v", err)
}
fmt.Printf("Received full message: %s", message)

By using ReadString('\n'), Go will keep calling Read under the hood until it encounters that specific byte. Now, it doesn't matter if the network splits your message into ten pieces; your code will wait until the complete "frame" has arrived. In a real production system, you'd probably use a length-prefix (sending the number of bytes first), but for most tools, delimiters are the way to go.

The Connectionless Nature of UDP

Now, UDP is a completely different animal. While TCP is like a phone call (you establish a connection, you know the other person is there, and data arrives in order), UDP is like sending postcards. You just throw the data at an address and hope for the best.

In Go, you don't "Accept" a UDP connection because there is no connection. You just listen for packets. I've found that people often get confused trying to use net.Dial with UDP. While you can do it to "lock" a socket to a specific address, the more idiomatic way for a server is ListenUDP.

addr, _ := net.ResolveUDPAddr("udp", ":8081")
conn, _ := net.ListenUDP("udp", addr)
defer conn.Close()

buf := make([]byte, 1024)
// ReadFromUDP gives us the data AND the address of who sent it
n, remoteAddr, err := conn.ReadFromUDP(buf)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Received %d bytes from %s: %s\n", n, remoteAddr, string(buf[:n]))

Notice the difference: ReadFromUDP gives you the remoteAddr. Since there's no persistent connection, you need that address if you want to send a response back. Also, notice that for UDP, the "packet" is preserved. If the client sends 50 bytes, ReadFromUDP will generally give you those 50 bytes in one go, or nothing at all.

  • Use TCP when you need reliability, ordering, and a persistent session (e.g., a database connection, an API).
  • Use UDP when speed is more important than 100% delivery, or when you're dealing with many small, independent updates (e.g., gaming state, DNS, heartbeats).



📋 Practical Task

Build a UDP Heartbeat Monitor

Your task is to create a simple UDP-based monitoring system. You will need to write two small programs: a Heartbeat Client and a Monitor Server.

The Heartbeat Client:

  • Should send a UDP packet containing the string "ALIVE" to localhost:9000 every 2 seconds.
  • It should run in an infinite loop.

The Monitor Server:

  • Should listen on UDP port 9000.
  • Every time it receives a packet, it should print the sender's IP address and the current timestamp to the console.
  • It must be able to handle packets from multiple different clients simultaneously.

Requirements:

  • Use net.ListenUDP for the server.
  • Use net.DialUDP or net.WriteToUDP for the client.
  • Ensure the server doesn't crash if it receives a packet larger than its buffer.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.