Introduction to asynchronous networking programming with go

Introduction

On traditional servers when you need to handle more than one request concurrently they usually use thread. Threads are light (at least more light than a process) and usually uses from 2 to 4 megabytes. If you need to scale to handle 10.000 requests, threads will not work. Some services to handle 10k requests uses what is called an event loop. Event loops are usually single thread (but there could be more than one) and it uses a queue of work. The idea behind event loop is to avoid blocking code (networking, I/O, etc). With go you don’t have threads, you have goroutines which are very cheap in terms of stack and scheduling. Go has a scheduler which works like an event loop, but it allows the developer to forget about events, queues, saving state, etc.

The net package

We need to first understand how the net package works. The net package has a core interface called Conn, which is a connection which is connected to something else. The following methods are in the Conn interface:

  • Read: Implements the IO reader
  • Write: Implements the IO writers
  • Close
  • SetDeadline, SetReadDeadline, SetWriteDeadline

Another very important interface which is the Listener interface and it will be used on services. This interface has a very important method called Accept. Accept will take a new connection coming from the wire and it will block until the connections come in and when that connection comes in it will return it.

Writing a listener with go

Our code will use the net package. The first step will be to start a listener on localhost at port 9000. If the listener didn’t return any error, out accept loop will continuously process the new connections.

package main

import (
“net”
)

func main() {
           // listener initialization
    l, err := net.Listen(“tcp”, “localhost:9000”)
           if err != nil {
        log.Fatal(err)
           } 
           // The accept loop
           for {
        conn, err := l.Accept()
        if err != nil {
            // here you need to check if the err implements
            // the net error interface, since the error could 
                        // be temporary. for simplification, we just log the erro
            log.Fatal(err)
        }

            }
}

The accept loop

Now we are ready to handle our connections. Let’s modify our accept loop to use the data that comes from the connection.

// The accept loop
for {
conn, err := l.Accept()
    if err != nil {
        // here you need to check if the err implements
        // the net error interface, since the error could 
                // be temporary. for simplification, we just log the erro
        log.Fatal(err)
    }
    n, err := io.Copy(os.Stderr, conn)
    if err != nil {
        log.Printf(“Connection finished. Error %v”, err)
    }
    log.Printf(“Received %d bytes”, n)
}

We are ready to execute the service. Open a terminal and execute the service with:

go build .
./service

Using goroutines

If you try to connect two or more clients to this service you will notice that until the first closes the connection, the second client message will never be printed. The problem comes from the line

n, err := io.Copy(os.Stderr, conn)

Now we will move the blocking code to a new function and we will start a new goroutine using the “go” keyword.

package main

import (
“net”
)

func main() {
           // listener initialization
    l, err := net.Listen(“tcp”, “localhost:9000”)
           if err != nil {
        log.Fatal(err)
           } 
           // The accept loop
           for {
        conn, err := l.Accept()
        if err != nil {
            log.Fatal(err)
        }
        go serviceConn(conn) // here we call the go routine
            }
}

func serviceConn(conn net.Conn) {
n, err := io.Copy(os.Stderr, conn)
    if err != nil {
        log.Printf(“Connection finished. Error %v”, err)
    }
    log.Printf(“Received %d bytes”, n)

}

Now if you try to connect more than one client you will see both messages printed. The go scheduler will resolve how to switch on blocking calls.

Adding timeouts to close inactive clients

The client might send the request and then stop doing anything, not sending any reset or closing the connection. This could be by a network issue or even a malicious user. Usually when you see the error message “No file descriptors available” then you need to add timeouts.

We are going to use the method SetReadDeadline to implement the timeout on the serviceConn function:

func serviceConn(conn net.Conn) {
    defer conn.Close()
for {
    conn.SetReadDeadline(time.Now().Add(5 * time.Second))
    var buf [128]byte
    n, err := conn.Read(buf[:])
    if err != nill {
            log.Printf(“Connection finished. Error %v”, err)
            // the next return will close the connection
            return
        }
        os.Stderr.Write(buf[:n])
    }
}