When creating a software system, regardless of size, composability is a key feature. This applies to functions and classes, and in distributed systems it applies to microservices as well. A programming language encourages composability by making the units of composition easy to create. Thus, microservices should be easy to create also. And, in some languages, they are.
This isn’t always true in Java, however. Java’s robust ecosystem makes me want to use it for developing distributed systems, but it simply takes way too long to go from zero lines of code to a functional REST service.
Hence the non-creatively-named “servers” component of my Zoomulus project. The goals are:
- Build atop
- Create a generic and flexible base-level implementation for future uses
- Abstract as many details away from the consumer as possible, so you focus more on business logic and less on setup
- Make it easy to create a stand-alone service quickly
A Simple Netty HTTP Server
Netty was an obvious choice to get started here. It’s built atop java.nio and is really powerful and flexible. It also requires a bit of study to know how to get it set up. Here’s an example of a fairly “simple” HTTP server with Netty. (You can download the full example from my gist.)
You’re right - this doesn’t look “simple” at all. It turns out setting up something that seems fairly basic, even with a great foundation like Netty, is more complicated than it feels like it should be. You’re trying to solve a business problem, not write a server.
Of course, we would never actually write our server like this, putting everything into a single function in a single class. I wrote the example this way to help answer this question: What does it take to create a Netty HTTP server anyway?
First, you need it to act like a server. In our case, this is essentially done with the
start() (#1) and
shutdown() (#2) methods. When the application runs, we invoke the
start() method. One of the first things it does is set up a shutdown hook to call
shutdown(). That way a simple CTRL+C or SIGTERM can cause the server to gracefully exit. No matter how many servers we create, we will probably want code like this for all of them.
Second, we have to bootstrap the server. In this example this is done within the
try block in the
start() method (#3). In Netty, bootstrapping the server consists of building a
ServerBootstrap object and then keeping track of the
ChannelFuture it creates. When the server shuts down, we will close that
ChannelFuture. Again, no matter how many servers we create, we will probably want code like this for all of them. Really the only things that might vary from one HTTP server to another would be the port we are listening on and the child handlers we register with the
Third, we have to add a child handler. In this example we defined the child handler inline and added it to the
ServerBootstrap via its
childHandler() method (#4). The
initHandler() method of the HTTP child handler sets up a request handling pipeline, running it through a number of Netty-provided handlers before it gets handed off to our own custom handler. Again, this step would probably be the same for most HTTP servers.
Finally, we add our own custom handler (#5). This handles the details of the HTTP request for our particular implementation. But even here, there’s a lot of common things to handle, including a lot that we didn’t take care of in this example, like whether the request has permission to perform the requested action, is the requested method supported, is the protocol version supported, is the requested path found, etc. Most of this could be handled for us also.
To really accomplish my goals, I would need to make this a lot simpler.
The Zoomulus HTTP Server
Beginning at the End
When I started out, I wrote a test server first - an example of how a consumer would consume this library. I wanted to know where I was heading.
It didn’t start out exactly like this, but was pretty close.
Using Guice for dependency injection offers consumers a lot of flexibility at the implementation level. The dependency injection is defined in the class
TestHttpServerModule. This post isn’t about dependency injection or Guice here so I won’t go into details. Suffice it to say that in the simple case it only defines two injectable things: The port I want to listen to, and an abstract class implementation that will actually generate the responses.
Defining the Server
HttpServer is a class in the
Notice how it accepts the
LISTEN_PORT_NAME. This is the port defined in our injection module which tells us the port we will listen on.
HttpServer itself extends the class
BasicNettyServer, which is where a lot of the fun happens:
BasicNettyServer class takes care of two of the things we discussed earlier. First, it sets up the basic server
shutdown() methods, and second, it takes care of bootstrapping the server. In this case, we support any number of
ServerConnector objects and create a bootstrap for each one, managing the lifecycle of them all. This allows a single server to listen on multiple ports if we so choose.
ServerConnector is part of
com.zoomulus.servers; we will look at that next.
Setting Up the ChannelInitializer
ServerConnector interface is used to contain implementation details needed to bootstrap the Netty server.
com.zoomulus.servers includes a default implementation called
HttpServerConnector which is usually sufficient.
This snippet isn’t all of
HttpServerConnector but shows the main part - how the Netty
ChannelInitializer gets set up, which was the third item in our list of things our simple server needed to handle. Using our
injector to obtain an
HttpHandler instance allows us to inject an
HttpResponder which is where we will implement the custom details of how this HTTP server responds to requests.
HttpResponder are both part of
com.zoomulus.servers. Let’s take a look at how those finish the picture.
Defining our Responder
The last thing to do to get the server set up is to define a responder. Responders extend
HttpResponder which does most of the work for you. Here’s a very simple responder that always answers with a static message:
HttpHandler take care of the bulk of the HTTP processing we had left to do in our final step in the “simple” server above, meaning all our custom responder has to do is generate the response text.
Now we can simply run the server at the command line and test it with a simple tool like
Creating a Custom HTTP Server
Now that we have this framework in place, creating our own custom HTTP server should be as simple as:
- Define our own
HttpRespondersubclass that does our custom response handling
- Define our own Guice module that sets up our
HttpRespondersubclass for injection
- Define our own
HttpServersubclass that constructs the server using our module for dependency injection
That’s it. In a future post I’ll show how easy this is to do.