You can use simple example below to create a concurrent TCP (Transmission Control Protocol) client and server example with Golang. The benefit of having a concurrent server is your server can serve multiple clients (connections) at a time. TCP is a reliable communication protocol by design. When TCP detects packet delivery problems, it requests re-transmission of lost data, rearranges out-of-order data, helps minimise network congestion to reduce the occurrence of the other problems so on. It is used between applications running on hosts communicating via an IP network. It has its own security vulnerabilities like any other network protocols which is outside the scope of this post. TCP is more reliable but slower compared with UDP (User Datagram Protocol) protocol. Although it is outside the scope of this post let me just give you a very short info on UPD. UDP does not provide error checking, correction or packet re-transmission hence reason it is unreliable but fast. It is commonly used for video conferencing, live streaming, online gaming like real-time applications.


Client


package main

import (
"bufio"
"io"
"log"
"net"
"os"
"strings"
)

func main() {
con, err := net.Dial("tcp", "0.0.0.0:9999")
if err != nil {
log.Fatalln(err)
}
defer con.Close()

clientReader := bufio.NewReader(os.Stdin)
serverReader := bufio.NewReader(con)

for {
// Waiting for the client request
clientRequest, err := clientReader.ReadString('\n')

switch err {
case nil:
clientRequest := strings.TrimSpace(clientRequest)
if _, err = con.Write([]byte(clientRequest + "\n")); err != nil {
log.Printf("failed to send the client request: %v\n", err)
}
case io.EOF:
log.Println("client closed the connection")
return
default:
log.Printf("client error: %v\n", err)
return
}

// Waiting for the server response
serverResponse, err := serverReader.ReadString('\n')

switch err {
case nil:
log.Println(strings.TrimSpace(serverResponse))
case io.EOF:
log.Println("server closed the connection")
return
default:
log.Printf("server error: %v\n", err)
return
}
}
}

Concurrent Server


package main

import (
"bufio"
"io"
"log"
"net"
"strings"
)

func main() {
listener, err := net.Listen("tcp", "0.0.0.0:9999")
if err != nil {
log.Fatalln(err)
}
defer listener.Close()

for {
con, err := listener.Accept()
if err != nil {
log.Println(err)
continue
}

// If you want, you can increment a counter here and inject to handleClientRequest below as client identifier
go handleClientRequest(con)
}
}

func handleClientRequest(con net.Conn) {
defer con.Close()

clientReader := bufio.NewReader(con)

for {
// Waiting for the client request
clientRequest, err := clientReader.ReadString('\n')

switch err {
case nil:
clientRequest := strings.TrimSpace(clientRequest)
if clientRequest == ":QUIT" {
log.Println("client requested server to close the connection so closing")
return
} else {
log.Println(clientRequest)
}
case io.EOF:
log.Println("client closed the connection by terminating the process")
return
default:
log.Printf("error: %v\n", err)
return
}

// Responding to the client request
if _, err = con.Write([]byte("GOT IT!\n")); err != nil {
log.Printf("failed to respond to client: %v\n", err)
}
}
}

Notes


The client sends a request to server. Server responds to it with GOT IT!. The client can terminate the connection by typing :QUIT and hitting enter. The server will understand the request and close the relevant connection between the client. The server also knows if the client terminated the connection by just forcing exit with for example Ctrl+C terminal signal or Ctrl+] then telnet> close signal. If you wish to use Telnet rather than our client script above for testing purposes, you can do so as shown below. The client closing the connection doesn't mean that the server goes down. It is always up unless explicitly closed.


The TCP server will always be up and waiting for a new TCP client connection. Everytime a new client connects to the TCP server, a new unique TCP connection is established. You can verify this by running netstat command as shown below.


# When we have two clients connected to the server. One with Telnet and another with our Go script.
$ netstat -anp TCP | grep 9999
tcp4 0 0 127.0.0.1.9999 127.0.0.1.51877 ESTABLISHED # -> Client 2: established conn
tcp4 0 0 127.0.0.1.51877 127.0.0.1.9999 ESTABLISHED # -> Server: listening Client 2 conn
tcp4 0 0 127.0.0.1.9999 127.0.0.1.51872 ESTABLISHED # -> Client 1: established conn
tcp4 0 0 127.0.0.1.51872 127.0.0.1.9999 ESTABLISHED # -> Server: listening Client 1 conn
tcp46 0 0 *.9999 *.* LISTEN # -> Server listening

# After clients close connections.
$ netstat -anp TCP | grep 9999
tcp46 0 0 *.9999 *.* LISTEN

One thing to note, the connections will turn from ESTABLISHED to TIME_WAIT for a few seconds before completely disappearing. The TIME_WAIT indicates that you/client closed the connection hence reason the connection is kept open for a bit so that any undelivered packets can be delivered to the connection for handling before actually closing the connection. We mentioned this right at the beginning.


Test


Once you are in a client session, just type something to see what happens. You might as well test exit operations too.


Run server


$ go run -race main.go

Run client


$ go run -race main.go

Telnet


$ telnet 0.0.0.0 9999