Skip to content

Instantly share code, notes, and snippets.

@incfly
Created March 12, 2025 22:49
Show Gist options
  • Save incfly/32000d50ead93ee3edc5dc120848d2a4 to your computer and use it in GitHub Desktop.
Save incfly/32000d50ead93ee3edc5dc120848d2a4 to your computer and use it in GitHub Desktop.
What would happen for net.Conn read when conn is closed.

Demo

# Terminal 1
./tcp-server --port 8080 --close graceful --delay 3

# Terminal 2
./tcp-client --port 8080 --mode read
[EOF] Server closed the connection gracefully
Client terminating

# Terminal 1
./tcp-server --port 8080 --close immediate --delay 3

# Terminal 2
./tcp-client --port 8080 --mode read
[RST] Connection reset by peer (abrupt close)
Client terminating

## Half Open Connection

> is it possible that something happened on the client (process/node crashes) that a fin/rst packet is never sent to one side and conn.Read would hang?

Yes, absolutely. If a client crashes abruptly (like from a process kill -9, power failure, network disconnection, etc.), it's very possible that no FIN or RST packet would be sent to the server. In this scenario, conn.Read() on the server side would indeed hang indefinitely.
This is a common issue in networked applications and is known as a "half-open" connection problem. The server thinks the connection is still valid because it never received an explicit termination notification, but the client is gone.
To handle this situation, TCP implementations use keepalive mechanisms, but these are often not enabled by default or have very long timeouts. In Go, you have several options to protect against this:

```golang
tcpConn := conn.(*net.TCPConn)
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)

Another option is the idle timeout to read which we already used.

package main
import (
"bufio"
"flag"
"fmt"
"io"
"net"
"os"
"strings"
"time"
)
func main() {
// Parse command-line flags
host := flag.String("host", "localhost", "Server host to connect to")
port := flag.Int("port", 8080, "Server port to connect to")
mode := flag.String("mode", "read", "Client mode: 'read', 'write', or 'interactive'")
flag.Parse()
addr := fmt.Sprintf("%s:%d", *host, *port)
fmt.Printf("Connecting to %s...\n", addr)
// Connect to server
conn, err := net.Dial("tcp", addr)
if err != nil {
fmt.Printf("Failed to connect to %s: %v\n", addr, err)
os.Exit(1)
}
defer conn.Close()
fmt.Printf("Connected to %s\n", addr)
// Create a channel to signal when reading is done
done := make(chan struct{})
// Always start a goroutine to read from the connection
go func() {
defer close(done)
buffer := make([]byte, 1024)
for {
// Read from the connection
n, err := conn.Read(buffer)
if err != nil {
if err == io.EOF {
fmt.Println("\n[EOF] Server closed the connection gracefully")
} else if strings.Contains(err.Error(), "reset by peer") {
fmt.Println("\n[RST] Connection reset by peer (abrupt close)")
} else if strings.Contains(err.Error(), "use of closed network connection") {
fmt.Println("\n[CLOSED] Local side closed the connection")
} else {
fmt.Printf("\n[ERROR] Read error: %v\n", err)
}
return
}
// Print the received data
fmt.Printf("Received %d bytes: %s", n, string(buffer[:n]))
}
}()
// Handle different client modes
switch *mode {
case "read":
// Just read from server until connection closes
fmt.Println("Read mode: Just reading data from server until connection closes")
<-done // Wait for the read goroutine to complete
case "write":
// Keep writing data to the server
fmt.Println("Write mode: Sending data to server every second until connection closes")
count := 0
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
count++
msg := fmt.Sprintf("Client message #%d at %s\n", count, time.Now().Format(time.RFC3339))
_, err := conn.Write([]byte(msg))
if err != nil {
fmt.Printf("Write error: %v\n", err)
return
}
fmt.Printf("Sent message #%d\n", count)
case <-done:
fmt.Println("Server connection closed, stopping write loop")
return
}
}
case "interactive":
// Let the user type messages to send
fmt.Println("Interactive mode: Type messages to send, empty line to quit")
// Start a goroutine to handle user input
go func() {
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("Enter message (empty line to quit): ")
if !scanner.Scan() {
break
}
message := scanner.Text()
if message == "" {
fmt.Println("Empty line detected, exiting...")
conn.Close()
return
}
// Send the message
_, err := conn.Write([]byte(message + "\n"))
if err != nil {
fmt.Printf("Error sending message: %v\n", err)
return
}
}
}()
// Wait for the connection to be closed by the server
<-done
}
fmt.Println("Client terminating")
}
package main
import (
"flag"
"fmt"
"net"
"os"
"strconv"
"time"
)
func main() {
// Parse command-line flags
port := flag.Int("port", 8080, "Port to listen on")
closeType := flag.String("close", "graceful", "Type of close: 'graceful', 'immediate', or 'half'")
delayBeforeClose := flag.Int("delay", 3, "Seconds to wait before closing connection")
flag.Parse()
// Create listener
addr := ":" + strconv.Itoa(*port)
listener, err := net.Listen("tcp", addr)
if err != nil {
fmt.Printf("Failed to listen on %s: %v\n", addr, err)
os.Exit(1)
}
defer listener.Close()
fmt.Printf("Server listening on %s\n", addr)
fmt.Printf("Close type: %s with %d seconds delay\n", *closeType, *delayBeforeClose)
for {
// Wait for a connection
conn, err := listener.Accept()
if err != nil {
fmt.Printf("Error accepting connection: %v\n", err)
continue
}
// Handle connection in a new goroutine
go handleConnection(conn, *closeType, *delayBeforeClose)
}
}
func handleConnection(conn net.Conn, closeType string, delaySeconds int) {
defer func() {
fmt.Println("Connection handler completed")
}()
// Log client connection
remoteAddr := conn.RemoteAddr().String()
fmt.Printf("New connection from: %s\n", remoteAddr)
// Write initial message
initialMsg := "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\nStarting data transmission...\n"
_, err := conn.Write([]byte(initialMsg))
if err != nil {
fmt.Printf("Error sending initial message: %v\n", err)
conn.Close()
return
}
// Start a goroutine to read from the connection
go func() {
buffer := make([]byte, 1024)
for {
n, err := conn.Read(buffer)
if err != nil {
fmt.Printf("Read from client error: %v\n", err)
return
}
if n > 0 {
fmt.Printf("Received %d bytes from client: %s\n", n, string(buffer[:n]))
}
}
}()
// Send some data periodically
dataCount := 0
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
// Set timer for connection close
timer := time.NewTimer(time.Duration(delaySeconds) * time.Second)
defer timer.Stop()
for {
select {
case <-ticker.C:
dataCount++
data := fmt.Sprintf("Data packet #%d sent at %s\n", dataCount, time.Now().Format(time.RFC3339))
_, err := conn.Write([]byte(data))
if err != nil {
fmt.Printf("Error sending data: %v\n", err)
return
}
fmt.Printf("Sent packet #%d to %s\n", dataCount, remoteAddr)
case <-timer.C:
fmt.Printf("Time to close connection to %s using %s method\n", remoteAddr, closeType)
switch closeType {
case "graceful":
// Graceful close - close the connection normally
fmt.Println("Performing graceful close")
conn.Close()
return
case "immediate":
// Immediate close - set linger to 0 and close
fmt.Println("Performing immediate close (with RST)")
tcpConn := conn.(*net.TCPConn)
tcpConn.SetLinger(0) // Send RST instead of FIN
conn.Close()
return
case "half":
// Half-close - shutdown write side only
fmt.Println("Performing half-close (shutdown write side)")
tcpConn := conn.(*net.TCPConn)
tcpConn.CloseWrite()
// Keep reading from client after half-close
fmt.Println("Server is now only reading (half-closed state)")
buffer := make([]byte, 1024)
for {
n, err := conn.Read(buffer)
if err != nil {
fmt.Printf("Read after half-close error: %v\n", err)
conn.Close() // Finally close the connection completely
return
}
fmt.Printf("After half-close, received %d bytes: %s\n", n, string(buffer[:n]))
}
default:
fmt.Printf("Unknown close type: %s\n", closeType)
conn.Close()
return
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment