Home > Back-end >  Closing client server communication in Go
Closing client server communication in Go

Time:11-25

I created this server which by connecting it to port 8080 with the PuTTY client sending data that this server receives. Now I would like to close everything with channels, how do I do it? After writing "exit". All written in Golang.

package main

import (
    "bufio"
    "fmt"
    "net"
)

func main() {

    //Ascolta richiesta
    datastream, err := net.Listen("tcp", ":8080")

    if err != nil {
        fmt.Println(err)
        return
    }
    defer datastream.Close()

    //Accetta richiesta
    for {
        connessione, err := datastream.Accept()
        if err != nil {
            fmt.Println(err)
            return
        }

        go handle(connessione)
    }

}


//Connessione Handle > Thread
func handle(connessione net.Conn) {

    scanner := bufio.NewScanner(connessione)

    for scanner.Scan() {
        data := scanner.Text()
        fmt.Printf("%q\n", data)
        if data == "exit" {
            connessione.Close()
        }

    }

    if err := scanner.Err(); err != nil {
        fmt.Println("error", err)
    }
}

CodePudding user response:

Disclaimer, there might be a better way to do this, but this is what I have come up with.

The main challenge is closing all connections. The reason this is challenging is because we can only check a channel if we are not waiting on a Read of the network connection. By default, reads on network connections will always block until data is sent or they are closed from the other side.

We can use a readDeadline to return for the Read of the network connection at a set time. This allows us to check if the channel is still open every x seconds if we keep extending the deadline each time it is exceeded.

Now we can close the shared channel to terminate all connections. This will take up to the configured timeout. Also note that once a channel is closed you can't reopen it, if you want to accept connections after the "exit" command you have to create a new channel. If you want to do this as part of a graceful shutdown, you don't need to do this and you can ignore the goroutine which resets the exit channel.

We end up with something like:

package main

import (
    "bufio"
    "errors"
    "fmt"
    "io"
    "net"
    "os"
    "time"
)

func main() {

    //Ascolta richiesta
    datastream, err := net.Listen("tcp", ":8080")

    if err != nil {
        fmt.Println(err)
        return
    }
    defer datastream.Close()

    // Make a channel on which we can't send any data, just close it
    exit := make(chan struct{})

    // Replace the closed channel with a new channel so we can accept new connection again. (optional)
    go func() {
        for {
            <-exit
            exit = make(chan struct{})
            fmt.Println("recreate chan")
        }
    }()

    //Accetta richiesta
    for {
        connessione, err := datastream.Accept()
        if err != nil {
            fmt.Println(err)
            return
        }

        // Give the channel to every connection
        go handle(connessione, exit)
    }

}

//Connessione Handle > Thread
func handle(connessione net.Conn, exit chan struct{}) {

    // Set the read timeout, this will cause the connection to return an os.ErrDeadlineExceeded error
    // every 5 seconds if no data read in that time.
    timeoutEvery := 5 * time.Second
    err := connessione.SetReadDeadline(time.Now().Add(timeoutEvery))
    if err != nil {
        fmt.Println("error", err)
    }

    // This pipe will allow us to copy data from the Network connection to the scanner without the scanner
    // via this goroutine.
    r, w := io.Pipe()
    // Close the pipeReader after we are done
    defer func() {
        r.Close()
    }()
    go func() {
        // Close the pipe and network connection when returning
        defer func() {
            fmt.Println("connection closed")
            connessione.Close()
            w.Close()
        }()

        // Allocate a buffer for the copy
        b := make([]byte, 32*1024)
        for {
            select {
            case <-exit:
                // If exit has been closed, we will enter this case
                return
            default:
                // If exit is still open, we enter this case.

                // Copy data from the connection to the pipe writer, use CopyBuffer to avoid temporary
                // buffer allocation(speed improvement)
                _, err := io.CopyBuffer(w, connessione, b)
                if err != nil {
                    // If an error is returned, check if this is due to the read deadline
                    if errors.Is(err, os.ErrDeadlineExceeded) {
                        // If it is, just extend it by our timeout again
                        err := connessione.SetReadDeadline(time.Now().Add(timeoutEvery))
                        if err != nil {
                            fmt.Println("error", err)
                            return
                        }

                        continue
                    }

                    // If there is any other error, close the connection.

                    fmt.Println("error", err)
                    return
                }
            }
        }
    }()

    scanner := bufio.NewScanner(r)

    for scanner.Scan() {
        data := scanner.Text()
        fmt.Printf("%q\n", data)
        if data == "exit" {
            // Close the exit channel, this will cause all goroutines to close the network connections
            // and the handlers to exit.
            close(exit)
        }

    }

    if err := scanner.Err(); err != nil {
        fmt.Println("error", err)
    }

    fmt.Println("handle return")
}
  • Related