Search code examples
gonetwork-programmingtcp

How do I make it such that a TCP connection will timeout if the connection doesn't receive a response every second?


I'm trying to create a TCP server that will timeout if the client does not respond within the span of every second.

I tried:

func main() {
    listener, err := net.Listen("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }
    for {
        conn, err := listener.Accept()
        conn.SetDeadline(time.Now().Add(timeout))
        if err != nil {
            log.Print(err)
        }
        go handleConn(conn)
    }

}

where the timeout is a single second but the disconnects immediately, not even waiting for a reply.


Solution

  • What you want can be achieved by setting socket options on your listener. Tweak the values as per your needs

    Note that this is its own KeepAlive and does not depend on incoming/outgoing data by application

    func enableTCPKeepAlive(listener *net.TCPListener) error {
        rawConn, err := listener.SyscallConn()
        if err != nil {
            return err
        }
        cfg := config.TLSServerConfig()
        rawConn.Control(
            func(fdPtr uintptr) {
                // got socket file descriptor. Setting parameters.
                fd := int(fdPtr)
                //Idle time before sending probe.
                err = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPIDLE, cfg.TCPKAIdleTime)
                if err != nil {
                    return err
                }
                //Number of probes.
                err := syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPCNT, cfg.TCPKANumProbes)
                if err != nil {
                    return err
                }
                //Wait time after an unsuccessful probe.
                err = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, syscall.TCP_KEEPINTVL, cfg.TCPKAInterval)
                if err != nil {
                    return err
                }
                // go syscall doesn't have the constant 0x12 (18) for TCP_USER_TIMEOUT.
                // 0x12 value referenced from linux kernel source code header:
                // include/uapi/linux/tcp.h
                err = syscall.SetsockoptInt(fd, syscall.IPPROTO_TCP, 0x12, cfg.TCPKAUserTimeout)
                if err != nil {
                    return err
                }
            })
        return nil
    }
    

    There are more options available than the ones I have mentioned above. Call this function on your listener before the for loop.

    func main() {
        listener, err := net.Listen("tcp", "localhost:8000")
        if err != nil {
            log.Fatal(err)
        }
        err = enableTCPKeepAlive(listener)
        if err != nil {
            log.Fatal(err)
        }
        for {
            conn, err := listener.Accept()
            conn.SetDeadline(time.Now().Add(timeout))
            if err != nil {
                log.Print(err)
            }
            go handleConn(conn)
        }
    
    }