Plain Socket Communication Between Two Go Programs, the Easy Way

ยท ยท 712 words ยท 4 minute read

Because there was an interesting discussion at reddit , suggesting using SSE, gRPC, Message queues (and there are plenty of others), I thought about making an article on (comparing) those communication protocols.

But in the end, I decided otherwise.

I see all those protocols as something that is adding more code to your project. I mean, you have to import something. You have to use an additional library.

And I wanted something, that have less code below. Make it simpler. But I also wanted to write an example, which will be easy to follow and understand. Nothing hard to read, nothing hard to use. And nothing low-level.

Because the original tutorial used websockets, this tutorial is using just plain sockets.

we will build a server and a client, but we will make it more interesting.

You can imagine the server part like running on a hardware device (like an Arduino collecting temperature and signals from a button), sending data, when connected.

And you can imagine the client part like a service, running on a physical server, that is receiving those data and saving them into database.

The link to a real-world code is at the end of the blog post (with that database saving).

Socket Server (running on a hardware device) ๐Ÿ”—

This code does basically three things.

  1. Connecting and disconnecting
  2. Receiving data from clients
  3. Sending data to clients
package main

import (
 "bufio"
 "fmt"
 "net"
 "strconv"
 "time"
)

func main() {
 fmt.Println("Server started...")
 ln, err := net.Listen("tcp", ":8000")
 if err != nil {
  fmt.Println("Error starting socket server: " + err.Error())
 }
 for {
  conn, err := ln.Accept()
  if err != nil {
   fmt.Println("Error listening to client: " + err.Error())
   continue
  }
  fmt.Println(conn.RemoteAddr().String() + ": client connected")
  go receiveData(conn)
  go sendData(conn)
 }
}

func sendData(conn net.Conn) {
 i := 0
 for {
  _, err := fmt.Fprintf(conn, strconv.Itoa(i)+". data from server\n")
  i++
  if err != nil {
   fmt.Println(conn.RemoteAddr().String() + ": end sending data")
   return
  }
  time.Sleep(time.Duration(1) * time.Second)
 }
}

func receiveData(conn net.Conn) {
 for {
  message, err := bufio.NewReader(conn).ReadString('\n')
  if err != nil {
   fmt.Println(conn.RemoteAddr().String() + ": client disconnected")
   conn.Close()
   fmt.Println(conn.RemoteAddr().String() + ": end receiving data")
   return
  }
  fmt.Print(conn.RemoteAddr().String() + ": received " + message)
 }
}

If you run the program, you will see, that the server has started. And nothing more. Reason: the server is waiting for a connection from a client.

Socket Client (running on server) ๐Ÿ”—

Client code does again only three things.

  1. Connecting and disconnecting
  2. Receiving data from server
  3. Sending data to server
package main

import (
 "bufio"
 "fmt"
 "net"
 "strconv"
 "sync"
 "time"
)

var (
 connected     bool
 connectedSync sync.Mutex
)

func main() {
 fmt.Println("Client started...")
 for {
  connectedSync.Lock()
  alreadyConnected := connected
  connectedSync.Unlock()
  if !alreadyConnected {
   conn, err := net.Dial("tcp", "127.0.0.1:8000")
   if err != nil {
    fmt.Println(err.Error())
    time.Sleep(time.Duration(5) * time.Second)
    continue
   }
   fmt.Println(conn.RemoteAddr().String() + ": connected")
   connectedSync.Lock()
   connected = true
   connectedSync.Unlock()
   go sendData(conn)
   go receiveData(conn)
  }
  time.Sleep(time.Duration(5) * time.Second)
 }
}

func receiveData(conn net.Conn) {
 for {
  message, err := bufio.NewReader(conn).ReadString('\n')
  if err != nil {
   fmt.Println(conn.RemoteAddr().String() + ": disconnected")
   conn.Close()
   connectedSync.Lock()
   connected = false
   connectedSync.Unlock()
   fmt.Println(conn.RemoteAddr().String() + ": end receiving data")
   return
  }
  fmt.Print(conn.RemoteAddr().String() + ": received " + message)
 }
}

func sendData(conn net.Conn) {
 i := 0
 for {
  _, err := fmt.Fprintf(conn, strconv.Itoa(i)+". data from client\n")
  i++
  if err != nil {
   fmt.Println(conn.RemoteAddr().String() + ": end sending data")
   return
  }
  time.Sleep(time.Duration(1) * time.Second)
 }
}

You can run this program independently from that server part (and you can run the server part independently from client).

The client program checks for a connection in a loop. If connected, the client prints received data and sends another data to server.

Testing the communication ๐Ÿ”—

Run them both aside and you will see the result.

Below you can see the output of the server program on the left, and the output of the client program on the right.

As you can see in an animation below, the client and the server are running independently. The server code reacts to client connecting and disconnecting, and vice versa.

socket server and client

Result ๐Ÿ”—

When dealing with server to client side of programming, there are plenty of choices you can make.

Every choice you make, have some advantages and disadvantages.

Please treat this blog post like adding another tool into your toolset.

Share: