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