I’m working on a simple server that listens on a TCP socket, accepts connections, reads and process some data from the clients. There are nice examples of such servers in the official Go documentation as well as many posts here and there. But most of them does not give any information about how to close listener and cleanup existing connections.
As I understand, correct approach will be: on shutdown, stop accepting new connections; process existing connections and wait for them to close; finally, when all existing connections have closed, stop the server.
I’ve tried to implement this as follows
package main
import (
"fmt"
"net"
"os"
"sync"
"time"
)
type Server struct {
listener net.Listener
conns []net.Conn
mtx sync.Mutex
cancelled bool
}
func NewServer() *Server {
addr, err := net.ResolveTCPAddr("tcp4", ":9999")
if err != nil {
fmt.Println("Failed to resolve address", err.Error())
os.Exit(1)
}
listener, err := net.Listen("tcp", addr.String())
if err != nil {
fmt.Println("Failed to create listener", err.Error())
os.Exit(1)
}
srv := &Server{
listener,
make([]net.Conn, 0, 50),
sync.Mutex{},
false,
}
go srv.Serve()
return srv
}
func (srv *Server) Serve() {
var stop bool
for {
srv.mtx.Lock()
stop = srv.cancelled
srv.mtx.Unlock()
if stop {
break
}
conn, err := srv.listener.Accept()
if err != nil {
fmt.Println("Failed to accept connection:", err.Error())
continue
}
srv.mtx.Lock()
srv.conns = append(srv.conns, conn)
srv.mtx.Unlock()
go srv.handleConnection(conn, len(srv.conns)-1)
}
}
func (srv *Server) handleConnection(conn net.Conn, id int) error {
fmt.Println("Accepted connection from", conn.RemoteAddr())
defer func() {
fmt.Println("Closing connection from", conn.RemoteAddr())
conn.Close()
srv.mtx.Lock()
srv.conns[id] = nil
srv.mtx.Unlock()
}()
buf := make([]byte, 1024)
_, err := conn.Read(buf)
if err != nil {
fmt.Println("Read error", err.Error())
return err
}
return nil
}
func (srv *Server) Stop() {
fmt.Println("Stop requested")
srv.mtx.Lock()
srv.cancelled = true
srv.mtx.Unlock()
srv.listener.Close()
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("Waiting on connections", len(srv.conns))
}
if len(srv.conns) == 0 {
fmt.Println("Stopped")
return
}
}
}
func main() {
s := NewServer()
time.Sleep(2 * time.Second)
s.Stop()
}
Is this correct, Go-ish approach or there is another, better solution?
And second question: when server stopped, there is an error “use of closed network connection”, e.g.
Stop requested
Failed to accept connection: accept tcp [::]:9999: use of closed network connection
Waiting on connections 0
Stopped
Is it possible to hide this error, but still catch all other errors?