Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I stop a Listening server in Go

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?

like image 440
Nick Craig-Wood Avatar asked Nov 16 '12 12:11

Nick Craig-Wood


3 Answers

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
}
like image 169
Running Wild Avatar answered Nov 12 '22 03:11

Running Wild


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".

like image 8
Nikolai Fetissov Avatar answered Nov 12 '22 04:11

Nikolai Fetissov


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
}
like image 2
zzzz Avatar answered Nov 12 '22 03:11

zzzz