Building a Simple Load Balancer Part 1
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.
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:
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!