Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to measure RTT/latency through TCP clients (created in GoLang) from a TCP server created in GoLang?

so I am hosting a TCP server through GoLang and then I want to connect to my TCP server using multiple TCP clients and measure the RTT every time a new client is connected. I haven't found anything that allows me to measure RTT to connect to this server in Golang (like do I connect to localhost, it doesn't work) Below is my code for the TCP server.

package main

import (
    "bufio"
    "fmt"
    "log"
    "math/rand"
    "net"
    "os"
    "strconv"
    "strings"
    "time"
)

var counter int

const MIN = 1
const MAX = 100

func random() int {
    return rand.Intn(MAX-MIN) + MIN
}

func verifyPortNo(portNo string) bool {

    conn, err := net.Listen("tcp", portNo)
    if err != nil {
        log.Println("Connection error: ", err)
        log.Println("Cannot verify port")
        return false
    }

    log.Println("Available")
    conn.Close()
    return true
}

func handleConnection(con net.Conn, counter int) {
    fmt.Printf("Client %d: %s\n", counter, con.LocalAddr().String())
    defer con.Close()
    for {
        clientRequest, err := bufio.NewReader(con).ReadString('\n')
        if err != nil {
            fmt.Println(err)
            return
        }

        stop := strings.TrimSpace(clientRequest)
        if stop == "STOP" {
            break
        }
        result := strconv.Itoa(random()) + "\n"
        con.Write([]byte(string(result)))
    }
}

func main() {
    arguments := os.Args //first element of the argument array is the program name
    if len(arguments) == 1 {
        fmt.Println("Please provide a port number")
        return
    }

    PortNo := "localhost:" + arguments[1]
    fmt.Println(PortNo)
    if !verifyPortNo(PortNo) {
        return
    }
    n, err := net.Listen("tcp", PortNo)
    if err != nil {
        fmt.Println(err)
        return
    }

    //close the listener when the application closes
    defer n.Close()

    rand.Seed(time.Now().Unix())

    for {
        //while loop for TCP server to accept connections
        conn, err := n.Accept()
        if err != nil {
            fmt.Println(err)
            return
        }
        counter++
        go handleConnection(conn, counter)
    }

}

Below is my code for the TCP clients.

package main

import (
    "bufio"
    "log"
    "net"
    "os"
    "strings"
    "time"
)

var counter int

func main() {
    for {
        go createTCPClient()
        time.Sleep(1 * time.Second)
    }

    // log.Println("Available")
    //netstat -anp TCP | grep 9999
}

func createTCPClient() {
    PortNo := "localhost:" + os.Args[1]

    conn, err := net.Dial("tcp", PortNo)
    if err != nil {
        log.Println("Connection error: ", err)
        log.Println("Cannot verify port")
        return
    }
    defer conn.Close()

    serverReader := bufio.NewReader(conn)
    for {
        reply, err := serverReader.ReadString('\n')
        if err != nil {
            println("Write to server failed:", err.Error())
            os.Exit(1)
        }
        println("reply from server=", strings.TrimSpace(reply))
    }

}

The code works (see figure below) but I cannot wrap my head around measuring the RTT for each TCP client and displaying it.

enter image description here

like image 336
zahid kamil Avatar asked Sep 01 '25 20:09

zahid kamil


1 Answers

The only portable solution is using/designing an application protocol that lets you determine the RTT. Eg, time the difference between a request/response.

Alternatively, OS kernels often record the TCP connection latency. However:

  • there isn't a portable way to retrieve TCP RTT
  • TCP RTT isn't available on all platforms.

This cut-down example demonstrates reading the TCPInfo containing the TCP RTT under Linux:

//go:build linux

package main

import (
    "fmt"
    "net"
    "time"

    "golang.org/x/sys/unix"
)

func main() {
    listener, err := net.Listen("tcp", ":0")
    check(err)

    fmt.Println("Listening on", listener.Addr())

    for {
        conn, err := listener.Accept()
        check(err)
        go func(conn *net.TCPConn) {
            defer conn.Close()
            info, err := tcpInfo(conn)
            check(err)
            rtt := time.Duration(info.Rtt) * time.Microsecond
            fmt.Println(rtt)
        }(conn.(*net.TCPConn))
    }
}

func tcpInfo(conn *net.TCPConn) (*unix.TCPInfo, error) {
    raw, err := conn.SyscallConn()
    if err != nil {
        return nil, err
    }

    var info *unix.TCPInfo
    ctrlErr := raw.Control(func(fd uintptr) {
        info, err = unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
    })
    switch {
    case ctrlErr != nil:
        return nil, ctrlErr
    case err != nil:
        return nil, err
    }
    return info, nil
}

func check(err error) {
    if err != nil {
        panic(err)
    }
}

Example output for connections over localhost:

$ ./tcpinfo 
Listening on [::]:34761
97µs
69µs
103µs
60µs
92µs
like image 88
mpx Avatar answered Sep 03 '25 17:09

mpx