Skip to Content
Course content

203: Working with net.Conn Directly

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

Imagine you're using an old-school walkie-talkie. When you want to talk to someone, you first have to make sure you're both tuned to the exact same frequency. Once you've established that link, there's no fancy "envelope" or "header" around your words; you just press the button, speak into the mic, and release it. To hear the other person, you just listen to the speaker. You don't know exactly when they'll start talking or how long their sentence will be—you just keep your ear open until you hear something.

That's exactly how net.Conn works in Go. While most of your work probably happens in net/http, that package is actually just a high-level wrapper. Underneath the hood, HTTP is just a specific way of formatting text sent over a net.Conn. When we work with net.Conn directly, we're stripping away the "HTTP" part and dealing with the raw stream of bytes.

Stripping away the HTTP abstraction

In a typical HTTP request, the browser sends a formatted block of text, and the server parses it. But with net.Conn, there is no "request" or "response" object. There is only a stream. You push bytes into one end, and the other person pulls bytes out of their end.

I remember the first time I tried to write a custom TCP protocol; I expected the Read method to wait until the other side finished their entire thought. It doesn't. It returns as soon as any data is available. If the sender sends "Hello World" in two separate packets, your Read might return "Hello " first, and then "World" on the next call. You have to be prepared for that fragmentation.

package main

import (
	"fmt"
	"net"
	"os"
)

func main() {
	// We "Dial" to establish the connection (like tuning the walkie-talkie)
	conn, err := net.Dial("tcp", "localhost:8080")
	if err != nil {
		fmt.Println("Error connecting:", err)
		os.Exit(1)
	}
	defer conn.Close()

	// We send raw bytes. No headers, no methods, just data.
	message := "PING"
	fmt.Fprintf(conn, message)

	// We create a buffer to hold the incoming data.
	// This is like our "ear" waiting for sound.
	buffer := make([]byte, 1024)
	n, err := conn.Read(buffer)
	if err != nil {
		fmt.Println("Error reading:", err)
		return
	}

	fmt.Printf("Received: %s\n", string(buffer[:n]))
}

The Read-Write Loop

Usually, you don't just send one message and quit. You want a conversation. To do this, we typically wrap the Read and Write operations in a loop. Because net.Conn implements the io.Reader and io.Writer interfaces, you can use all the great utility functions in the io package, like io.Copy or bufio.NewScanner.

I highly recommend using bufio.Scanner when you're dealing with text-based protocols. It handles the "fragmentation" problem for you by looking for a specific delimiter (like a newline character) and giving you the full line once it arrives.

// Inside a server's handler function
func handleConnection(conn net.Conn) {
	defer conn.Close()
	
	// Using a scanner simplifies the "listening" process significantly
	scanner := bufio.NewScanner(conn)
	for scanner.Scan() {
		text := scanner.Text()
		fmt.Printf("Client said: %s\n", text)
		
		// Send a response back
		conn.Write([]byte("ACK: " + text + "\n"))
	}
}

The Danger of the Infinite Read

Here is a trap I've fallen into more than once: the blocking read. When you call conn.Read(), your goroutine stops dead in its tracks. It will wait there forever if the other side never sends data and never closes the connection. This is how you end up with "leaked" goroutines that eat up your memory.

To prevent this, you should use SetDeadline. This tells Go: "If I don't hear anything for the next 5 seconds, stop waiting and return an error." It's the software equivalent of saying, "Hello? Are you still there?" before hanging up the phone.

import "time"

// ... inside your connection handler
// Set a deadline for the next Read operation
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, err := conn.Read(buffer)
if err != nil {
	// This will trigger if the deadline is hit
	fmt.Println("Connection timed out!")
}



📋 Practical Task

Build a TCP-based Heartbeat Monitor

Your task is to create a small server and client pair that implements a "Heartbeat" mechanism to ensure a connection is still alive.

  • The Server: Should listen on port 9000. It must expect a message saying "HEARTBEAT" every 3 seconds. If the server doesn't receive a message within 5 seconds (use SetReadDeadline), it should print "Client lost: Timeout" and close the connection.
  • The Client: Should connect to the server and send the string "HEARTBEAT\n" every 3 seconds using a time.Ticker.
  • The Goal: Run both. Verify that the server stays active. Then, manually kill the client process and observe the server detecting the timeout and cleaning up the connection.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.