Locate Your Laptop with Go Lang


programming
guide go

Introduction

Socket programming was a mystery to me before I took Computer Networks in university. The first time I wrote a simple ping server, I was blown away at how simple the code really was. There are two programs, a client and a server and they communicated by TCP connection.

Recently I dabbled in golang. What is a better way to learn a new language by making some useful stuff with it? Thus, I adapted a ping client-server program into program that help you locate your lost laptop!

In this first part, we are going to make a simple client-server program that communicates with UNIX socket. Later on, we will modify our programs to use TCP and more features.

Before starting, make sure go-lang is installed and have $GOPATH set. As a convention, I put my code in $GOPATH/src/github.com/gmhafiz/go-locate-ip. Full git repo can be download with go get github.com/gmhafiz/go-locate-ip or simply clone from my github repository with git with git clone git@github.com:gmhafiz/go-locate-ip.git

Making server.go

Go makes it simple to write network program. For a server, it needs to listen for incoming data and optionally take that data and write it into a log.

For starters, we are going to make both client and server on the same computer. Later on, we will modify the code so that the programs can run on different computers across the internet.

func main() {
	l, err := net.Listen("unix", "/tmp/echo.sock")
	if err != nil {
		log.Fatal("listen error:", err)
	}

	for {
		fd, err := l.Accept()
		if err != nil {
			log.Fatal("accept error:", err)
		}

		go echoServer(fd)
	}
}
Line l, err := net.Listen("unix", "/tmp/echo.sock") makes the program to listen to a UNIX socket. It is stored in the variable l. The second variable, err is go’s way of handling errors. If there’s some connection error, it will be stored into that variable and we will print out what the error is in the terminal.

In the for loop, go will continuously monitor incoming data and store it into the first variable. echoServer(fd) is function to do something about incoming data. It is prefixed with go. It is a special keyword that tells to run this function concurrently. Concurrency is built into go which is awesome.

func echoServer(c net.Conn) {
	for {
		buf := make([]byte, 512)
		nr, err := c.Read(buf)
		if err != nil {
			return
		}

		data := buf[0:nr]
		println("Server got:", string(data))
		
		_, err = c.Write(data)
        		if err != nil {
        		log.Fatal("Write: ", err)
        }
	}
}

Next, we take a look at echoServer. The function takes a parameter of net.Conn type. It contains a for loop and inside that loop, a slice buffer is created with 512 bytes size. It fills that buffer and stores it into a variable called buf. c.Read(buf) function is used to determine the length of the data in the buffer. Finally, the buffer is written into another variable, data and is printed into the terminal with data := buf[0:nr].

The reason why we take buffer length is we will be able to minimize the data to be processed. 512 bytes is the maximum length of data but we are not going to send all 512 bytes because it would be wasteful.

Lastly, we send back data we received, back to the client by using c.Write(data).

Making client.go

To make a client, it needs to ‘dial’ a connection through a unix socket and send through the machine’s external IP address.

func main() {
	c, err := net.Dial("unix", "/tmp/echo.sock")
	if err != nil {
		panic(err)
	}
	defer c.Close()

	go reader(c)
	for {
		_, err := c.Write([]byte(GetOutboundIP()))
		if err != nil {
			log.Fatal("write error:", err)
			break
		}
		time.Sleep(10e9)  // 10 seconds
	}
}

In this line, c, err := net.Dial("unix", "/tmp/echo.sock") a connection is referred to by c. Second variable err is to handle connection error.

Next, we use Go’s built-in function GetOutBoundIP() to get the machine’s external IP address and write it into the variable c. _, err := c.Write([]byte(GetOutboundIP())). Underscore variable is used because we do not need to use it after this line of code.

Finally, we let the program to wait for an hour with time.Sleep(10e9). Go counts time in nanoseconds. 1e9 is actually 1x10^(9) which is equal to one second and thus, 10 seconds.

Before we finish, we are going to write input from server into the terminal with reader function.

func reader(r io.Reader) {
	buf := make([]byte, 1024)
	for {
		n, err := r.Read(buf[:])
		if err != nil {
			return
		}
		println("Client got:", string(buf[0:n]))
	}
}

Run Them

Here is the full code

server.go

package main

import (
	"log"
	"net"
)

func echoServer(c net.Conn) {
	for {
		buf := make([]byte, 512)
		nr, err := c.Read(buf)
		if err != nil {
			return
		}

		data := buf[0:nr]
		println("Server got:", string(data))
		_, err = c.Write(data)
        		if err != nil {
        		log.Fatal("Write: ", err)
        }
	}
}

func main() {
	l, err := net.Listen("unix", "/tmp/echo.sock")
	if err != nil {
		log.Fatal("listen error:", err)
	}

	for {
		fd, err := l.Accept()
		if err != nil {
			log.Fatal("accept error:", err)
		}

		go echoServer(fd)
	}
}

client.go

package main

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

func reader(r io.Reader) {
	buf := make([]byte, 1024)
	for {
		n, err := r.Read(buf[:])
		if err != nil {
			return
		}
		println("Client got:", string(buf[0:n]))
	}
}

func main() {
	c, err := net.Dial("unix", "/tmp/echo.sock")
	if err != nil {
		panic(err)
	}
	defer c.Close()

	go reader(c)
	for {
		_, err := c.Write([]byte(GetOutboundIP()))
		if err != nil {
			log.Fatal("write error:", err)
			break
		}
		time.Sleep(10e9)  // 10 seconds
	}
}

// Get preferred outbound ip of this machine
func GetOutboundIP() string {
	conn, err := net.Dial("udp", "8.8.8.8:80")
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	localAddr := conn.LocalAddr().String()
	idx := strings.LastIndex(localAddr, ":")

	return localAddr[0:idx]
}

In each terminal, run go run server.go and go run client.go. Watch both server and client printing out incoming inputs from each other every ten seconds.

$ go run server.go
Server got: 192.168.2.3

comments powered by Disqus