How to Efficiently Manage TCP Connection Deadlines with the Ping Pong Method in Go

Priyansh SaoPriyansh Sao
4 min read
package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "log"
    "net"
    "time"
)
// creating a default interval
const defInterval = 1 * time.Second

// pinger periodically writes "ping" to the Writer.
// The interval can be changed by sending a new duration into the reset channel.
func pinger(ctx context.Context, w io.Writer, reset chan time.Duration) {
    var interval time.Duration

    // Try to get initial interval from reset channel
    select {
    case <-ctx.Done():
        return
    case interval = <-reset:
    default:
    }

    if interval == 0 {
        interval = defInterval
    }

    timer := time.NewTimer(interval)
    defer func() {
        if !timer.Stop() {
            <-timer.C
        }
    }()

    for {
        select {
        case <-ctx.Done():
            return
        case newInterval := <-reset:
            if !timer.Stop() {
                <-timer.C
            }

            if newInterval >= 0 {
                if newInterval == 0 {
                    interval = defInterval
                } else {
                    interval = newInterval
                }
            }

            _ = timer.Reset(interval)

        case <-timer.C:
            if _, err := w.Write([]byte("ping\n")); err != nil {
                return
            }
            _ = timer.Reset(interval)
        }
    }
}

func main() {
    done := make(chan struct{})

    listener, err := net.Listen("tcp", "127.0.0.1:8080")
    if err != nil {
        log.Fatal("unable to listen on port ", listener.Addr().String())
    }
    fmt.Printf("[server]: started listening on (%s)\n", listener.Addr().String())
    defer listener.Close()

    begin := time.Now()

    go func() {

        defer close(done)

        resetTimer := make(chan time.Duration, 1)
        resetTimer <- time.Second

        for {
            conn, err := listener.Accept()
            if err != nil {
                if errors.Is(err, io.EOF) {
                    fmt.Printf("[server]: failed to connect with client\n")
                }
                return
            }
            defer conn.Close()

            fmt.Printf("[server]: connected to client\n")

            err = conn.SetDeadline(time.Now().Add(15 * time.Second))
            if err != nil {
                fmt.Printf("[server]: unable to set deadline")
            }

            ctx, cancel := context.WithCancel(context.Background())
            defer cancel()

            go pinger(ctx, conn, resetTimer)

            buffer := make([]byte, 1024)
            for {
                n, err := conn.Read(buffer)
                if err != nil {
                    fmt.Printf("[server]: failed to read\n")
                    return
                }

                fmt.Printf("[server]: received %q\n", buffer[:n])


                err = conn.SetDeadline(time.Now().Add(5 * time.Second))
                if err != nil {
                    fmt.Println("[server] SetDeadline error:", err)
                    return
                }
            }

        }

    }()

    conn, err := net.Dial("tcp", listener.Addr().String())
    if err != nil {
        if errors.Is(err, io.EOF) {
            fmt.Printf("[client]: connection failed\n")
        }
        return
    }
    defer conn.Close()

    buffer := make([]byte, 1024)
    for i := 0; i < 4; i++ {
        n, err := conn.Read(buffer)
        if err != nil {
            if errors.Is(err, io.EOF) {
                fmt.Printf("[client]: unable to read\n")
            }
            return
        }

        fmt.Printf("[client]: received %q\n", buffer[:n])
    }

    _, err = conn.Write([]byte("Pong!"))
    if err != nil {
        if errors.Is(err, io.EOF) {
            fmt.Printf("[client]: unable to write\n")
        }
        return
    }

    for i := 0; i < 4; i++ {
        n, err := conn.Read(buffer)
        if err != nil {
            if errors.Is(err, io.EOF) {
                fmt.Printf("[client]: unable to read\n")
            }
            return
        }

        fmt.Printf("[client]: received %q\n", buffer[:n])
    }

    <-done
    fmt.Printf("[main %s] done\n", time.Since(begin).Truncate(time.Second))
}

1. Default setup

  • defInterval = 1s → Default ping interval if none is set.

  • Purpose: Server sends "ping\n" messages to the client periodically.

2. pinger function

  • Takes a context, an io.Writer (connection), and a reset channel for changing the interval.

  • Starts with:

    • Try to get initial interval from reset channel.

    • If none, use defInterval.

  • Uses a time.Timer to send "ping\n" every interval.

  • Can reset interval if a new value is sent via reset channel.

  • Stops if ctx.Done() is signaled.

3. Main server setup

  • Creates a TCP listener on 127.0.0.1:8080.

  • Starts goroutine for server logic.

4. Server goroutine logic

  • Makes resetTimer channel and sets it initially to 1 second.

  • Loops over listener.Accept() to accept new clients.

  • For each client:

    • Sets deadline (timeout) for 15 seconds initially.

    • Starts a pinger goroutine to send "ping" periodically.

    • Reads incoming data in a loop:

      • Prints received data.

      • Resets read deadline to 5 seconds after each message.

5. Client logic

  • Connects to the server.

  • Reads 4 "ping" messages from the server.

  • Sends "Pong!" back to the server.

  • Reads another 4 "ping" messages.

  • Ends when server closes.

6. Key behaviors

  • Server: Sends "ping" every second until the connection closes or context is cancelled.

  • Client: Reads a few "ping" messages, sends "Pong!", and reads more pings.

  • Deadlines: Prevent hangs if one side stops responding.

  • Context cancel: Stops pinger goroutine when connection closes.

0
Subscribe to my newsletter

Read articles from Priyansh Sao directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Priyansh Sao
Priyansh Sao