Build a concurrent TCP server in Go

Develop a concurrent TCP server that generates random numbers using only about 65 lines of Go code.
350 readers like this.
gopher illustrations

Renee French. CC BY 3.0

This article is part of a Go series by Mihalis Tsoukalos. Read part 1: Creating random, secure passwords in Go.

TCP and UDP servers are everywhere serving network clients over TCP/IP networks. In this article, I will explain how to develop a concurrent TCP server, in the Go programming language, that returns random numbers. For each incoming connection from a TCP client, the TCP server will start a new goroutine to handle that request.

You can find this project, concTCP.go, on GitHub.

Handling TCP connections

The logic of the program can be found in the Go code of the handleConnection() function, which is implemented as follows:

func handleConnection(c net.Conn) {
        fmt.Printf("Serving %s\n", c.RemoteAddr().String())
        for {
                netData, err := bufio.NewReader(c).ReadString('\n')
                if err != nil {
                        fmt.Println(err)
                        return
                }

                temp := strings.TrimSpace(string(netData))
                if temp == "STOP" {
                        break
                }

                result := strconv.Itoa(random()) + "\n"
                c.Write([]byte(string(result)))
        }
        c.Close()
}

If the TCP client sends the "STOP" string, then the goroutine that serves that particular TCP client will terminate; otherwise, the TCP server will send a random number back to the TCP client. The for loop makes sure that the TCP client will be served for as long as the TCP client desires. The Go code in the for loop reads the data from the TCP client, line by line, using bufio.NewReader(c).ReadString('\n') and sends back data using c.Write([]byte(string(result))). (You may find the net standard Go package documentation helpful.)

Being concurrent

The implementation of the main() function tells the TCP server to start a new goroutine each time it has to serve a TCP client:

func main() {
        arguments := os.Args
        if len(arguments) == 1 {
                fmt.Println("Please provide a port number!")
                return
        }

        PORT := ":" + arguments[1]
        l, err := net.Listen("tcp4", PORT)
        if err != nil {
                fmt.Println(err)
                return
        }
        defer l.Close()
        rand.Seed(time.Now().Unix())

        for {
                c, err := l.Accept()
                if err != nil {
                        fmt.Println(err)
                        return
                }
                go handleConnection(c)
        }
}

First, main() makes sure that the program has at least one command line argument. Notice that the existing code does not check whether the given command line argument is a valid TCP port number or not. However, if the given value is not a valid TCP port number, the call to net.Listen() will fail with an error message similar to the following:

$ go run concTCP.go 12a
listen tcp4: lookup tcp4/12a: nodename nor servname provided, or not known
$ go run concTCP.go -10
listen tcp4: address -10: invalid port

The net.Listen() call is used for telling a Go program to accept network connections and thus act as a server. The return value of net.Listen() is of the net.Conn type, which implements the io.Reader and io.Writer interfaces. The main() function also calls the rand.Seed() function in order to initialize the random number generator. Finally, the for loop allows the program to keep accepting new TCP clients using Accept() that will be handled by instances of the handleConnection() function, which are executed as goroutines.

The first parameter of net.Listen()

The first parameter of the net.Listen() function defines the type of network that will be used, while the second parameter defines the server address as well as the port number the server will listen to. Valid values for the first parameter are tcp, tcp4 (IPv4-only), tcp6 (IPv6-only), udp, udp4 (IPv4- only), udp6 (IPv6-only), ip, ip4 (IPv4-only), ip6 (IPv6-only), Unix (Unix sockets), Unixgram, and Unixpacket.

The concurrent TCP server in action

concTCP.go requires a single command line argument, which is the port number that it will listen to. The output you will get from concTCP.go when serving TCP clients will be similar to the following:

$ go run concTCP.go 8001
Serving 127.0.0.1:62554
Serving 127.0.0.1:62556

The output of the netstat(1) can verify that concTCP.go serves multiple TCP clients while listening for more connections:

$ netstat -anp TCP | grep 8001
tcp4       0      0  127.0.0.1.8001         127.0.0.1.62556        ESTABLISHED
tcp4       0      0  127.0.0.1.62556        127.0.0.1.8001         ESTABLISHED
tcp4       0      0  127.0.0.1.8001         127.0.0.1.62554        ESTABLISHED
tcp4       0      0  127.0.0.1.62554        127.0.0.1.8001         ESTABLISHED
tcp4       0      0  *.8001                 *.*                    LISTEN

The last line of the output of the preceding command informs us that there is a process that listens to port 8001, which means that you can still connect to TCP port 8001. The first two lines verify that there is an established TCP network connection that uses port numbers 8001 and 62556. Similarly, the third and fourth lines verify that there is another established TCP connection that uses port numbers 8001 and 62554.

This image shows the output of concTCP.go when serving multiple TCP clients:

The concTCP.go TCP server in action.

Analogously, the following image shows the output from two TCP clients that are implemented using the nc(1) utility:

Using the nc(1) utility as the TCP client to concTCP.go.

Using the nc(1) utility as the TCP client to concTCP.go.

You can find more information about nc(1), which is also called netcat(1), on Wikipedia.

Summary

So, you have just learned how to develop a concurrent TCP server that generates random numbers using about 65 lines of Go code, which is pretty impressive! If you want your TCP server to perform a different job, just change the implementation of the handleConnection() function.

User profile image.
Mihalis Tsoukalos is a Technical author, a UNIX Administrator and Developer, a DBA and a Mathematician. He is the author of Go Systems Programming and Mastering Go. You can reach him at http://www.mtsoukalos.eu/ and https://twitter.com/mactsouk.

Comments are closed.

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.