Building Custom Servlets for C++ Microservices in Docker
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:
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:
- 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.
- 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:
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.
For Reference and Further Reading
- Part 1: C++ Microservices in Docker: How to deploy and enable HydraExpress.
- Part 3: Optimizing a Container Image.
- Part 4: Adding a Numerical Library.
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"]
Back to top