Hcrypt

Building a Simple Load Balancer Part 1

load

Hello everyone! I saw this awesome newsletter called codingchallenges. We will be building a very basic load balancer in go by following the challenge. I will divide the blog post in 3 parts. I am still in early stages of learning go therefore some mistakes are bound to happen. With that out of the way, let’s start our small journey.


Building a simple HTTP server

Let’s start with a simple HTTP server. I will keep it short and simple as I feel like there are countless tutorials and articles about the same.

Create server.go file. After that, initialize go mod file.

touch server.go
go mod init load

Now, we can start writing the code for it.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    fmt.Printf("starting the server at port 8081\n")
    err := http.ListenAndServe(":8081", nil)

    if err != nil {
        log.Fatal(err)
    }
}

We are starting the server at port 8081. That’s pretty much all we need to start a simple HTTP server.


Logging

Now, we would need to log a few things:

  • HTTP request
  • Hostname of the client who made the request.

We will define a different endpoint for this. Let’s name it /hello.

First, let’s create a Handler for that endpoint.

http.HandleFunc("/hello", hello)

Now, let’s define the function. We will simply print "hello" to the console:

func hello(res http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(res, "hello!")
}

We are writing the response as "hello!" by using the res object of http.ResponseWriter.

Here’s the complete program:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/hello", hello)

    fmt.Printf("starting the server at port 8081\n")
    err := http.ListenAndServe(":8081", nil)

    if err != nil {
        log.Fatal(err)
    }
}

func hello(res http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(res, "hello!")
}

Start the server by running:

go run server.go

You can then send a request using curl to localhost:8081/hello

curl localhost:8081/hello
hello!


Let’s log

For logging HTTP request, we will use httputil package. Scrolling through the documentation, there’s a func called DumpRequest which does exactly what we need.

dump

Let’s use it in our /hello endpoint.

reqDump, err := httputil.DumpRequest(req, true)

if err != nil {
    log.Fatalf(err)
}

We pass in our request object which is req and bool argument to true for getting the body of request.

Now, we will log it on the console.

log.Printf("\n%s", string(reqDump))

Run it and make a curl request again, we should get a message like this one:

2024/03/03 13:02:44
GET /hello HTTP/1.1
Host: localhost:8081
Accept: */*
Test-Header: test-value
User-Agent: curl/7.81.0

Neat! But we don’t want the date and time which is displayed by default in log message. Let’s remove it by making a new logger.

logger := log.New(os.Stdout, "", 0)
logger.Printf("\n%s", string(reqDump))

Alright. Only one thing left to do, log the hostname of the client. That’s simple as well.

Going over the docs again, we can see that in the Request struct there is a type called RemoteAddr.

Let’s use it.

logger.Printf("Received request from: %s\n", req.RemoteAddr)

Edit: added a port flag for specifying the port.

This should give you the client IP and port.

Here’s the complete program:

package main

import (
    "fmt"
    "os"
    "log"
    "net/http"
    "net/http/httputil"
)

func main() {
    port := flag.Int("port", 8081, "Port to listen on")
    flag.Parse()

    fmt.Println("hello world!") 
    http.HandleFunc("/", hello)

    fmt.Printf("starting the server at port %d\n", *port)
    if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), nil); err != nil {
        log.Fatal(err)
    }
}

func hello(res http.ResponseWriter, req *http.Request) {

    logger := log.New(os.Stdout, "", 0)

    reqDump, err := httputil.DumpRequest(req, true)

    if err != nil {
        log.Fatal(err)
    }

    logger.Printf("Received request from: %s\n", req.RemoteAddr)
    logger.Printf("\n%s", string(reqDump))

}

You should get something like this after making a request:

final

That’s it for the Part 1! In the next part, as mentioned in the challenge, we will be using round robin to route requests to multiple servers.

Keep experimenting!