Quantcast
Channel: Stories by Uday Hiwarale on Medium
Viewing all articles
Browse latest Browse all 145

Running multiple HTTP servers in Go

$
0
0

HTTP SERVERS: INTERMEDIATE LESSON

In this article, we are going to create some customized HTTP servers and run them at the same time concurrently.

(source: pexels.com)

In the Hello World HTTP Server tutorial, we learned about basic APIs to launch an HTTP server in Go. As we’ve learned from this tutorial, once we run http.ListenAndServe() function, our process gets blocked and we can’t run anything below this line.

What if we want to run multiple servers? In this article, we are going to address this question and learn more about server configuration.

If we look at the ListenAndServe function, it has the following syntax.

func ListenAndServe(addr string, handler Handler) error

When we call http.ListenAndServer() with appropriate arguments, it starts an HTTP server and blocks the current goroutine. If we run this function inside the main function, it will block the main goroutine (started by the main function).

💡 If you haven’t read my articles about concurrency & channels, you can follow this link for lesson on goroutines and this link for channels and WaitGroup.

Once you call ListenAndServe function, you do not have another chance to spawn a new server, unless you run a goroutine that launches another server. Let’s see a quick example of how this can be achieved.

(https://play.golang.org/p/pGaC4A3HEOg)

In the above example, we created a default route handler on DefaultServeMux request multiplexer using HandleFunc function provided by http package. This handler responds to the request with Hello:<host> message.

Then using an Immediately-invoked function expression (IIFE), we’ve launched a goroutine that spawns an HTTP server on 9000 port with DefaultServeMux as the handler argument of the ListenAndServe (when nil value is passed, DefaultServeMux is used).

Inside the main goroutine, we are doing the same thing except we are running the HTTP server with DefaultServeMux request multiplexer on 9001 port. Let’s understand how this works behind the scenes.

Once we register a handler function on DefaultServeMux, we are immediately creating a goroutine that will spawn an HTTP server (line no. 17). There is no guarantee that this goroutine will run immediately, it all depends on how many processors our system has and the Go scheduler. But let’s consider the worst and assume that goroutine is not yet invoked.

Once this goroutine is created, code below it will start executing and the last line of code will spawn another HTTP server. Since this line of code blocks the main goroutine, the Go scheduler will schedule other pending goroutines and the goroutine routine we previously created will be invoked, launching a brand new HTTP server.

At this point, we have two goroutines running concurrently or in parallel, accepting the requests on two separate ports. We can open up a browser and see if these two servers are working perfectly.

(http://localhost:9000/)

As you can see from the above screenshot, our servers are running fine. To run both of these servers on a single processor core, you can use runtime.

main(){
runtime.GOMAXPROCS(1) // use only 1 processor core
}

In most cases, you wouldn’t want to use main method to spawn anything but to orchestrate the goroutines. So let’s modify the example.

(https://play.golang.org/p/I6x9m50OE9x)

In the above example, we have created a WaitGroup wg which monitors goroutines. We incremented WaitGroup by 2 because we are creating two goroutines that have the HTTP server implementation.

💡 If you haven’t read about WaitGroup, I have explained it in the channels lesson or you can follow this documentation.

In the main method, we are calling wg.Wait() so that the main method would not exit until the counter in the WaitGroup reaches zero. Inside goroutines, we are calling wg.Done() once after the server exits to decrement the WaitGroup counter by 1.

This is a standard model to spawn multiple HTTP servers. However, we are using Go’s standard implementation of an HTTP server and both of our HTTP servers are using the same implementation.

We can create distinct ServeMux for each of these servers to provide separate routing mechanisms but let’s do this with a little more elegance.

Server type

To create and configure a custom HTTP server, http package provides Server structure. This structure has below public fields (from the doc).

type Server struct {
Addr string // TCP address to listen on, ":http" if empty
Handler Handler // http.DefaultServeMux if `nil`
    // TLS configuration for HTTPS protocol
TLSConfig *tls.Config
    // ReadTimeout is the maximum duration for reading the entire
// request, including the body.
ReadTimeout time.Duration
    // WriteTimeout is the maximum duration before timing out
// writes of the response.
WriteTimeout time.Duration
    // BaseContext optionally specifies a function that returns
// the base context for incoming requests on this server.
// If BaseContext is nil, the default is context.Background()
BaseContext func(net.Listener) context.Context
    // ErrorLog specifies an optional logger for errors accepting
// connections, unexpected behavior from handlers, and
// underlying FileSystem errors.
ErrorLog *log.Logger
}

We are familiar with the Addr and Handler fields. The ReadTimeout and WriteTimeout fields can be important for added safety. ErrorLog is crucial to log any errors thrown by the server while processing a request.

💡 There are other public fields that are not mentioned above but could be crucial for your use case.

The Server structure implements basic methods like ListenAndServe, SetKeepAlivesEnabled, Shutdown, etc. to configure and control a server.

Let’s create some custom servers and run them concurrently.

(https://play.golang.org/p/XXS_kHTb6pt)

In the above program, the createServer function creates a ServerMux object and implements a fresh routing mechanism. The server object contains the implementation of a brand new HTTP server based on input arguments.

It then returns a pointer to newly created Server object since most of the methods implemented by the Server structure has a pointer receiver.

In the main method, things are not so different. Instead of using http.ListenAndServe method to spawn an HTTP server with the default configuration, we are creating a brand new instance of an HTTP server inside goroutines and spawning it using Server.ListenAndServe() method call.

💡 Unlike http.ListenAndServe(addr string, handler Handler) method call, the server.ListenAndServe() method call does not need any arguments since the configuration of the server is present in the server struct itself.

Let’s look at the common methods that can be invoked on the Server object.

SetKeepAlivesEnabled method

The Server.SetKeepAliveEnabled method enables the HTTP persistent connection. By default, this is enabled for better performance.

server.SetKeepAlivesEnabled(false)

Close method

Once we no longer need a server, we can close it without terminating the main program. This is achieved using the server.Close method. This method forcefully shutdown a server without gracefully closing the active TCP connection.

error := server.Close()

Hence, this method can only be used when a server needs to be shut down immediately without caring about active TCP connections.

(https://play.golang.org/p/TzwV_gKHurh)

In the above program, we have created a bare minimal server and spawned it in the main method. We have also created a exitSignal channel to block the main goroutine unless the server is closed. Even after server exits, the main goroutine is blocked until unless some goroutine writes a value to or closes the exitSignal channel.

Using time.AfterFunc function, we have launched another goroutine that executes a function after 3 seconds. In this function, we are calling server.Close() method which closes the server. This function call may return an error but before that happens, the server may close already and exit with an error.

This is why exitSignal channel is so important in this scenario. Once server.Close() is returned, we are closing the exitSignal channel using built-in close function.

Once the exitSignal channel is closed, this goroutine will be killed and control will pass back to main goroutine. Since exitSignal channel is now closed, main goroutine is no longer blocked and it will start executing the code below the read from channel (<- chan) expression.

Once you run this Go program, after 3 seconds, you will be able to see this result in your console.

$ go run go-server.go (took 3s)
ListenAndServe(): http: Server closed
Close(): completed! <nil>
Main(): exit complete!

The only drawback of Close method is that it can not close hijacked connections like WebSockets. But we will look into that in other lessons.

Shutdown method

The server.Shutdown method behaves exactly like server.Close method but with some added safety. If you want to gracefully close active TCP connections, then Shutdown() is better compared to Close method.

One good advantage of using Shutdown method over Close is that we can perform some cleanup operations after a server shutdown is initiated. This can be done by registering one or more cleanup functions using RegisterOnShutdown method.

server.RegisterOnShutdown(f func())

These cleanup functions will be run as separate goroutines and they will be invoked as soon as the shutdown process is started. Like Close method, Shutdown method can not close upgraded or hijacked connections. Hence this is a good place to close these connections gracefully.

This method needs a base context of the incoming requests. If you haven’t configured the BaseContext field of the Server structure, you can use the context.Background() since it is the default value for the BaseContext field.

error := server.Shutdown(ctx context.Context)

The Shutdown method does not wait for cleanup job to be completed. Also, the server will return immediately once the Shutdown call is made. Hence it is mandatory to keep main goroutine alive until the server shutdown and server cleanup jobs are completed. For that, we can use channels or a WaitGroup like we did in the previous examples.

Let’s modify our earlier example and register a cleanup function.

(https://play.golang.org/p/56eWiDiNomA)

In the above example, instead of using two channels to signal exit, we are using a WaitGroup. WaitGroup wg will wait for 2 goroutines to finish their jobs, one that closes the server and the other that performs the cleanup.

The ideal output of the above program should be as following. However, results may vary depending upon your system. But in all the cases, Main(): exit complete! will be the last statement.

ListenAndServe(): http: Server closed
Shutdown(): completed! <nil>
RegisterOnShutdown(): completed!
Main(): exit complete!

If the server was closed using server.Close() or server.Shutdown() method, it returns http.ErrServerClosed error. If you do not want to perform any operations on this specific error, you can check if the error returned by the http.ListenAndServe() call is equal to http.ErrServerClosed.

(GitHubTwitter)

Running multiple HTTP servers in Go was originally published in RunGo on Medium, where people are continuing the conversation by highlighting and responding to this story.


Viewing all articles
Browse latest Browse all 145

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>