How do I stop a Listening server in Go
Asked Answered
S

4

23

I've been trying to find a way to stop a listening server in Go gracefully. Because listen.Accept blocks it is necessary to close the listening socket to signal the end, but I can't tell that error apart from any other errors as the relevant error isn't exported.

Can I do better than this? See FIXME in the code below in serve()

package main

import (
    "io"
    "log"
    "net"
    "time"
)

// Echo server struct
type EchoServer struct {
    listen net.Listener
    done   chan bool
}

// Respond to incoming connection
//
// Write the address connected to then echo
func (es *EchoServer) respond(remote *net.TCPConn) {
    defer remote.Close()
    _, err := io.Copy(remote, remote)
    if err != nil {
        log.Printf("Error: %s", err)
    }
}

// Listen for incoming connections
func (es *EchoServer) serve() {
    for {
        conn, err := es.listen.Accept()
        // FIXME I'd like to detect "use of closed network connection" here
        // FIXME but it isn't exported from net
        if err != nil {
            log.Printf("Accept failed: %v", err)
            break
        }
        go es.respond(conn.(*net.TCPConn))
    }
    es.done <- true
}

// Stop the server by closing the listening listen
func (es *EchoServer) stop() {
    es.listen.Close()
    <-es.done
}

// Make a new echo server
func NewEchoServer(address string) *EchoServer {
    listen, err := net.Listen("tcp", address)
    if err != nil {
        log.Fatalf("Failed to open listening socket: %s", err)
    }
    es := &EchoServer{
        listen: listen,
        done:   make(chan bool),
    }
    go es.serve()
    return es
}

// Main
func main() {
    log.Println("Starting echo server")
    es := NewEchoServer("127.0.0.1:18081")
    // Run the server for 1 second
    time.Sleep(1 * time.Second)
    // Close the server
    log.Println("Stopping echo server")
    es.stop()
}

This prints

2012/11/16 12:53:35 Starting echo server
2012/11/16 12:53:36 Stopping echo server
2012/11/16 12:53:36 Accept failed: accept tcp 127.0.0.1:18081: use of closed network connection

I'd like to hide the Accept failed message, but obviously I don't want to mask other errors Accept can report. I could of course look in the error test for use of closed network connection but that would be really ugly. I could set a flag saying I'm about to close and ignore errors if that was set I suppose - Is there a better way?

Simply answered 16/11, 2012 at 12:57 Comment(1)
I believe it is a good explanation. forum.golangbridge.org/t/correct-shutdown-of-net-listener/8705Matthaeus
G
8

Check some "is it time to stop" flag in your loop right after the accept() call, then flip it from your main, then connect to your listening port to get server socket "un-stuck". This is very similar to the old "self-pipe trick".

Gentes answered 16/11, 2012 at 13:17 Comment(3)
That is a neat idea! There is a race condition with real connections coming in though which you'd need to work around.Simply
Yes, true. You can try an inverse of that - always have that single internal connection from the start and use it as a control channel.Gentes
Great and simple!Ephialtes
M
18

I would handle this by using es.done to send a signal before it closes the connection. In addition to the following code you'd need to create es.done with make(chan bool, 1) so that we can put a single value in it without blocking.

// Listen for incoming connections
func (es *EchoServer) serve() {
  for {
    conn, err := es.listen.Accept()
    if err != nil {
      select {
      case <-es.done:
        // If we called stop() then there will be a value in es.done, so
        // we'll get here and we can exit without showing the error.
      default:
        log.Printf("Accept failed: %v", err)
      }
      return
    }
    go es.respond(conn.(*net.TCPConn))
  }
}

// Stop the server by closing the listening listen
func (es *EchoServer) stop() {
  es.done <- true   // We can advance past this because we gave it buffer of 1
  es.listen.Close() // Now it the Accept will have an error above
}
Maffick answered 16/11, 2012 at 15:38 Comment(2)
Hmm, nice idea. I think a bool would do just as well as using the channel though which is pretty much the solution I came up with. You still need the channel though to synchronise the stop with serve so you know when it has stopped.Simply
Don't bother buffering the channel or sending a message down the channel. Just close it.Unwatched
G
8

Check some "is it time to stop" flag in your loop right after the accept() call, then flip it from your main, then connect to your listening port to get server socket "un-stuck". This is very similar to the old "self-pipe trick".

Gentes answered 16/11, 2012 at 13:17 Comment(3)
That is a neat idea! There is a race condition with real connections coming in though which you'd need to work around.Simply
Yes, true. You can try an inverse of that - always have that single internal connection from the start and use it as a control channel.Gentes
Great and simple!Ephialtes
E
2

Something among these lines might work in this case, I hope:

// Listen for incoming connections
func (es *EchoServer) serve() {
        for {
                conn, err := es.listen.Accept()
                if err != nil {
                    if x, ok := err.(*net.OpError); ok && x.Op == "accept" { // We're done
                            log.Print("Stoping")
                            break
                    }

                    log.Printf("Accept failed: %v", err)
                    continue
                }
                go es.respond(conn.(*net.TCPConn))
        }
        es.done <- true
}
Elkins answered 16/11, 2012 at 13:17 Comment(3)
A good idea thanks! I'm not sure it would tell apart an official stop from Syscall.Accept() returning an error (a recent example I've seen is the process running out of sockets) would it?Simply
Dunno, I would have to try/experiment with it.Elkins
Great example, but shouldn't the x.Op be compared to close, e.g. && x.Op == "close" If we stop the listener by using listener.Close(), it seems to put close inside the error operation. godoc.org/net#TCPListener.CloseBramble
S
-7

Here's a simple way that's good enough for local development.

http://www.sergiotapia.me/how-to-stop-your-go-http-server/


package main

import (  
    "net/http"
    "os"

    "github.com/bmizerany/pat"
)

var mux = pat.New()

func main() {  
    mux.Get("/kill", http.HandlerFunc(kill))
    http.Handle("/", mux)
    http.ListenAndServe(":8080", nil)
}

func kill(w http.ResponseWriter, r *http.Request) {  
    os.Exit(0)
}
Soidisant answered 15/6, 2014 at 1:5 Comment(1)
Exiting the entire process is not a valid way to close a server!Chela

© 2022 - 2024 — McMap. All rights reserved.