Easily create a C++ microservice using HydraExpress and Docker
March 10, 2021

Building Custom Servlets for C++ Microservices in Docker

DevOps

In a previous post, C++ Microservices in Docker, we worked through the steps for creating a docker container that exposes a HydraExpress servlet container. We successfully deployed our HydraExpress server instance in Docker, however all that was available were the default example servlets. User application code wasn’t exposed.

Let’s fix that and look at deploying custom C++ Servlet instances within the HydraExpress Docker container. We’ll build on the Dockerfile and entrypoint.sh script we developed as a part of the previous blog post.

Deploying a Custom Servlet Within a Container

To deploy a custom servlet within the container, we’re going to need to extend our Dockerfile to perform a number of additional tasks:

  • Install the compiler toolchain so that we can perform a build.
  • Copy the servlet source into the container.
  • Perform a build.
  • Deploy the servlet instance to the container.

Define the Servlet

Let's start by defining the new servlet that we’re going to deploy. For this example, we’ll create a simple “Hello World” servlet just to demonstrate the build process.

Details on the process for writing, building and deploying servlets can be found in the Introduction to Servlet Programming, section of the HydraExpress User’s Guide.

For our servlet, we’re going to need at least two files, the source code for the servlet, HelloWorldServlet.cpp, and the web.xml configuration file that registers the servlet with the HydraExpress container.

For this example, we’re also going to use CMake to build the servlet, so we’ll need the appropriate CMakeLists.txt files as well. We’ll isolate the servlet source files into a src/ subdirectory within our Docker definition, and the files for each servlet context will be placed in their own subdirectories, resulting in the following directory structure:

Custom Servlet in C++ Microservices Docker

Let’s start with the servlet itself. Our servlet will respond to an HTTP request, sending back the message “Hello World” as HTML. This can be implemented as follows:

src/hello/HelloWorldServlet.cpp
#include <string>

#include <rwsf/servlet/ServletOutputStream.h>
#include <rwsf/servlet/http/HttpServlet.h>
#include <rwsf/servlet/http/HttpServletRequest.h>
#include <rwsf/servlet/http/HttpServletResponse.h>

class HelloWorldServlet : public rwsf::HttpServlet {
public:
    void doGet(rwsf::HttpServletRequest& request, rwsf::HttpServletResponse& response) {
        response.setContentType("text/html");
        rwsf::ServletOutputStream& out = response.getOutputStream();

        out.println("<html><body><h1>Hello World!</h1></body></html>");
    }
};

RWSF_DEFINE_SERVLET(HelloWorldServlet)

We’ll also need to register our servlet with the HydraExpress container. Each servlet context has a web.xml file that registers how to create each servlet within the context, as well has the mapping between a URL within the context and the servlet that should be invoked. We’ll establish this as the default servlet for the context so that it is invoked regardless of the URL that’s provided.

src/hello/WEB-INF/web.xml
<web-app>
    <servlet>
        <servlet-name>HelloWorld</servlet-name>
        <servlet-class>hello.createHelloWorldServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>HelloWorld</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

Pay particular attention to the <servlet-class> in the  XML above. The string is composed of two parts, the name of the library where the Servlet is stored (hello), and the name of the creation function for instantiating that servlet. The creation function is automatically created by the RWSF_DEFINE_SERVLET macro from src/hello/HelloWorldServlet.cpp, and must match create<name> where <name> is the argument to RWSF_DEFINE_SERVLET.

Compile the Servlet

Now that our context and servlet are defined, we’ll need to be able to compile them. For this project we’ll use CMake, however HydraExpress doesn’t prescribe any particular build process, so the specifics are up to the developer.

We’ll define two CMakeLists.txt files, one in src/ that will setup common attributes required for any context, and one within src/hello/ that’s specific to that servlet context. For the root context, we’ll establish the libraries, flags, and attributes required to link against the HydraExpress servlet library:

src/CMakeLists.txt
cmake_minimum_required(VERSION 3.9)

project(servlets VERSION 1.0 LANGUAGES CXX)

add_library(RWSF::Servlet SHARED IMPORTED)
set_target_properties(RWSF::Servlet PROPERTIES
    IMPORTED_LOCATION $ENV{RWSF_HOME}/lib/librwsf_servlet20012d.so)
target_compile_definitions(RWSF::Servlet INTERFACE -D_RWCONFIG=12d)
target_include_directories(RWSF::Servlet INTERFACE $ENV{RWSF_HOME}/include)

add_subdirectory(hello)

For the src/hello/ servlet context, our CMakeLists.txt specifies how to build the servlet context library:

src/CMakeLists.txt
cmake_minimum_required(VERSION 3.9)

project(hello VERSION 1.0 LANGUAGES CXX)

add_library(hello SHARED HelloWorldServlet.cpp)
target_compile_features(hello PRIVATE cxx_auto_type)
target_link_libraries(hello RWSF::Servlet)

Build and Deploy a Custom Servlet in Docker

With our servlet defined and our build infrastructure in place, we’re ready to expand our original Dockerfile to include building and deploying our new servlet. We’ll insert our new instructions immediately after the step to install HydraExpress and will start with copying our src/ directory into the container.

Dockerfile
…
RUN /opt/download/hydraexpress.run  \
    --mode unattended  \
    --prefix /opt/perforce/hydraexpress  \
    --license-file /opt/download/license.key

COPY src/ /src/

COPY entrypoint.sh /entrypoint.sh
…

Install a Compiler Toolchain

With our source code in place, we’ll turn our attention to the compiler toolchain. To build our servlet library we’ll need to install the compiler (GCC), our build tool (CMake), and since CMake will be used to generate makefiles, we’ll need make as well. Most of these are available through the default package repositories for CentOS 7, however to use a recent version of CMake, we’ll need to access to the Extra Packages for Enterprise Linux (EPEL) repository:

Dockerfile
…
RUN /opt/download/hydraexpress.run  \
    --mode unattended  \
    --prefix /opt/perforce/hydraexpress  \
    --license-file /opt/download/license.key

RUN yum install -y epel-release
RUN yum install -y gcc-c++ make cmake3

COPY src/ /src/
…

We now have all the tools we need to build our servlet context library. We’ll place our build artifacts in a build/ directory within the container:

Dockerfile
…

COPY src/ /src/

RUN mkdir -p /build
RUN cd /build && cmake3 ../src && make

COPY entrypoint.sh /entrypoint.sh

…

With our library built, we can now deploy it to the HydraExpress servlet container. To deploy a servlet context, we need two things:

  1. The servlet context configuration file needs to be locatable by the servlet container. By default, the servlet container is configured to look for servlet contexts in its apps/servlets/ directory.
  2. The servlet context library must be in the library path for HydraExpress. Since HydraExpress automatically adds the apps-lib/ directory to its library path, we’ll store our servlet context library there.
Dockerfile
…

RUN cd /build && cmake ../src && make

RUN mkdir -p ${RWSF_HOME}/apps-lib &&  \
    cp -f /build/hello/libhello.so ${RWSF_HOME}/apps-lib
RUN mkdir -p ${RWSF_HOME}/apps/servlets/hello &&  \
    cp -Rf /src/hello/WEB-INF ${RWSF_HOME}/apps/servlets/hello/WEB-INF

COPY entrypoint.sh /entrypoint.sh

…

With our servlet deployed, we can rebuild our image and restart our server:

$ docker build -t hydraexpress .

…

$ docker run --rm -it -p 8090:8090 hydraexpress
*******************************************************************************
 RWSF (TM) - Server Control Script
 Copyright (c) 2001-2020 Rogue Wave Software, Inc., a Perforce company.
 All Rights Reserved.
*******************************************************************************
  RWSF_HOME = /opt/perforce/hydraexpress
  RWSP_HOME = /opt/perforce/hydraexpress/3rdparty/sourcepro
  Starting Rogue Wave Agent...
   INFO| Loading context: /examples/
   INFO| Loading context: /hello/
   INFO| Locale directory set to [/opt/perforce/hydraexpress/conf/locale]
   INFO| Default locale set to [en_US]
   INFO| Loading locale [en_US], catalog [messages_en_US.xml]
   INFO| Starting 'AJP 1.3' connector...
   INFO| Starting 'HTTP/1.1' connector...
   INFO| Starting 'HTTPS (HTTP/1.1)' connector...

As the server starts, it prints the names of each servlet context that it encounters, and we can see that our new “hello” context has been picked up by the server. If we execute a request against our context, http://localhost:8090/hello/, we get back the message we expect:

Building a Custom Servlet in Docker with HydraExpress

Easily Create a C++ Microservice Using HydraExpress and Docker

We’ve built on the Docker image created in the previous blog post by adding a custom servlet and servlet context. This new container serves as an example of how easy it is to create a C++ microservice using HydraExpress and Docker.

This basic model can be extended to publish your application logic as a C++ microservice, enabling its usage within a microservices framework. In our next installment, we’ll look at optimizing our container instance to minimize its size and startup costs.

Want to try this yourself? Contact us for an evaluation version.

Contact Us

For Reference and Further Reading

The complete Dockerfile with all changes applied follows:

Dockerfile
FROM centos:7

RUN yum install -y wget

RUN mkdir -p /opt/download

RUN wget -q -O /opt/download/hydraexpress.run \
    https://dslwuu69twiif.cloudfront.net/hydraexpress/2020/hydraexpress_2020_eval_linux_x86-64_gcc_4.8.run

RUN chmod a+x /opt/download/hydraexpress.run

COPY license.key /opt/download/license.key

ENV RWSF_HOME /opt/perforce/hydraexpress

RUN /opt/download/hydraexpress.run  \
    --mode unattended  \
    --prefix /opt/perforce/hydraexpress  \
    --license-file /opt/download/license.key

RUN yum install -y epel-release
RUN yum install -y gcc-c++ make cmake3

COPY src/ /src/

RUN mkdir -p /build
RUN cd /build && cmake3 ../src && make

RUN mkdir -p ${RWSF_HOME}/apps-lib &&  \
    cp -f /build/hello/libhello.so ${RWSF_HOME}/apps-lib
RUN mkdir -p ${RWSF_HOME}/apps/servlets/hello &&  \
    cp -Rf /src/hello/WEB-INF ${RWSF_HOME}/apps/servlets/hello/WEB-INF

COPY entrypoint.sh /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

Send Feedback