gSOAP Docker Container Example ============================== Overview -------- This article walks you through the steps to create and deploy Docker containers with images of gSOAP Web services. Any gSOAP service application can be deployed as a container with Docker, as long as these SOAP/XML and JSON Web APIs are developed with gSOAP. The gSOAP service we have in mind for this example runs as a stand-alone gSOAP Web server in a Docker container. Once the container runs, the service can be accessed via a browser, by a gSOAP client application, and interactively with CURL from the command line. This article guides you through the following steps: 1. Preparing the C++ source code files to build a Docker container image. 2. Preparing the Dockerfile to build an image with a compiled Web service executable. 3. Running the Web service image with the executable in a container. 4. Accessing the containerized Web service via a browser and with CURL. 5. Invoking the containerized Web service in an example C++ client application. 6. Terminating the Web service container. You can download one or both of the following projects with all source code files that are mentioned in this article: 1. [GCC project files](gcc8-gsoap.zip) based on the GCC DockerHub image. 2. [Alpine project files](alpine-gsoap.zip) based on the Alpine DockerHub image. While this article is largely self-contained, some familiarity with the basics of Docker containers is assumed. In particular, we recommend the following Docker articles to get started: 1. [Set up your Docker environment.](https://docs.docker.com/get-started/) 2. [Build an image and run it as one container](https://docs.docker.com/get-started/part2/) Optionally, read the following Docker articles that explain scaling services by running one or more containers and distributing services accross a cluster (swarms): 3. [Scale your app to run multiple containers](https://docs.docker.com/get-started/part3/) 4. [Distribute your app across a cluster](https://docs.docker.com/get-started/part4/) Now you're all set to build an example Web service image with gSOAP and deploy it in a Docker container. [![To top](../../images/go-up.png) To top](#) 1. Preparing the C++ source code -------------------------------- The example gSOAP service we will develop returns the time in GMT via a standard SOAP HTTP POST API. The service also returns a web page when accessed by a browser (i.e. HTTP GET) and returns the WSDL file when accessed with the URL query `?wsdl`. While this example offers a SOAP Web API, also non-SOAP XML and JSON Web APIs can be developed in the same way as will be explained later at the end of this article. In this example we will use a very simple interface header file to demonstrate building a container image with a tiny SOAP/XML Web service application. However, any large interface header file for a large Web service application will work too, such as files generated by the gSOAP wsdl2h tool from WSDLs and XSDs. The first step is to create an empty directory for our project. Then we add a new file to this directory that specifies the example SOAP/XML Web service in an interface header file [`gettime.h`](gettime.h). This file declares a single service operation `GetTime` that returns the date and time in GMT: ns__GetTime(time_t *GMT); The `GetTime` operation associated with the `ns` XML namespace has no request parameters (input arguments) an a single response parameter (output value) `GMT`. This Web service interface file is then processed by the gSOAP soapcpp2 tool with option `-j` to generate all of the necessary C++ source code classes that we need to implement the server and client sides: [command] soapcpp2 -j gettime.h This generates the following files in our project directory: * [`soapStub.h`](soapStub.h) a copy of the specification in plain C/C++ header file syntax without annotations. * [`soapH.h`](soapH.h) declares XML serializers. * [`soapC.cpp`](soapC.cpp) implements XML serializers. * [`soapService.h`](soapService.h) defines the service-side XML services API class `Service`. * [`soapService.cpp`](soapService.cpp) implements the service-side XML services API class `Service`. * [`soapProxy.h`](soapProxy.h) defines the client-side XML services API class `Proxy`. * [`soapProxy.cpp`](soapProxy.cpp) implements the client-side XML services API class `Proxy`. * [`ns.nsmap`](ns.nsmap) an XML namespace binding table to #include. * [`ns.wsdl`](ns.wsdl) a WSDL description of the Web service. * [`ns.GetTime.req.xml`](ns.GetTime.req.xml) example XML request message. * [`ns.GetTime.res.xml`](ns.GetTime.res.xml) example XML response message. Next, we copy the `stdsoap2.h` and `stdsoap2.cpp` source code from the gSOAP source code distribution to our project directory: * [`stdsoap2.h`](stdsoap2.h) the common gSOAP library header file. * [`stdsoap2.cpp`](stdsoap2.cpp) the common gSOAP library for C++. These two source code files together with `soapStub.h`, `soapH.h`, `soapC.cpp`, `soapService.h`, and `soapService.cpp` permit us to build our service application with Docker, provided that we have a C++ compiler image. We will not be using the `libgsoap++` library but instead use source code directly. This is much simpler and saves space, because we can build our application within the container image from minimal sources without having to install gSOAP in the container with its full set of libraries and tools. To complete this first step, we implement our service application in C++. The full source code of our service application is saved as [`gettime.cpp`](gettime.cpp): #include "soapService.h" // generated by soapcpp2 -j #include "ns.nsmap" // generated by soapcpp2 -j static int http_get_handler(struct soap*); static int copy_file(struct soap*, const char *name, const char *type); int main() { // create a new service Service service; // set timeout parameters (max 5 sec delay per message) service.soap->transfer_timeout = service.soap->send_timeout = service.soap->recv_timeout = 5; // optional: let the server terminate after 24 hours idle time service.soap->accept_timeout = 24*60*60; // optional: set the user variable to pass a pointer to the service object to callbacks service.soap->user = (void*)&service; // register the HTTP GET handler callback function service.soap->fget = http_get_handler; // serve requests on port 80 (note: this runs a limited iterative server) while (service.run(80)) service.soap_stream_fault(std::cerr); // clean up service.destroy(); } // service operation implementation of Service ns__GetTime() int Service::GetTime(time_t *GMT) { *GMT = time(0); return SOAP_OK; } // HTTP GET handler callback function int http_get_handler(struct soap *soap) { // if the request URL ends in ?wsdl then respond with the ns.wsdl file contents if (!strcmp(soap->path, "/?wsdl")) return copy_file(soap, "ns.wsdl", "text/xml"); // produce a HTML response to a HTTP GET request if (soap_response(soap, SOAP_HTML) || soap_send(soap, "<html>" "<body>" "<h1>Welcome to gSOAP on Docker!</h1>" "If you see this page, your gSOAP server is successfully running." "</body>" "</html>") || soap_end_send(soap)) ; // all OK return soap_closesock(soap); } // copy a file as a HTTP response body static int copy_file(struct soap *soap, const char *name, const char *type) { FILE *fd = fopen(name, "rb"); // open file to copy if (fd == NULL) return 404; // return HTTP not found soap->http_content = type; // set HTTP content type header // OK HTTP response header with content type header if (!soap_response(soap, SOAP_FILE)) { // copy file contents while (true) { size_t r = fread(soap->tmpbuf, 1, sizeof(soap->tmpbuf), fd); if (!r) break; if (soap_send_raw(soap, soap->tmpbuf, r)) break; } } fclose(fd); soap_end_send(soap); return soap_closesock(soap); } Our service application implements a SOAP HTTP POST operation `Service::GetTime`, which was specified with `ns__GetTime` in `gettime.h` for namespace `ns` associated with the `Service` class. This service operation returns the current time in GMT. Our service also implements a HTTP GET handler to return a readable HTML response to check that our service runs, or returns the WSDL file when the URL query string `?wsdl` is used. We will see more about this later in step 4. Note that we invoke `service.run(80)` to serve client requests on port 80. This runs a simple iterative service loop, which does not scale to multiple concurrent client connections. To scale the service and improve robustness, see our [tutorials](https://www.genivia.com/tutorials.html) for details. We are now ready to prepare the `Dockerfile` to build an image with a compiled Web service executable. [![To top](../../images/go-up.png) To top](#) 2. Preparing the Dockerfile --------------------------- A `Dockerfile` specifies the build steps and tasks to run to create a Docker container image. We have many choices among image libraries to use a a basis for our service image. We need at least a C++ compiler to build the executable of the service that lives in our container image. That is, we need an image of a C++ compiler or an image of a OS with a C++ compiler (or at least an OS image in which we can install a C++ compiler). For this example we made two choices to evaluate in this article: 1. The DockerHub [GCC image](https://hub.docker.com/_/gcc) which has everything we need, but this image is rather large (over 1GB). 2. The DockerHub [alpine image](https://hub.docker.com/_/alpine) which is a tiny OS (about 5MB) in which we need to install the GCC compiler (adding about 160MB). Next we write our `Dockerfile`. If you don't want to copy-paste from this article then download one or both of the following projects with all source code files: 1. [GCC project files](gcc8-service.zip) based on the GCC DockerHub image. 2. [Alpine project files](alpine-service.zip) based on the Alpine DockerHub image. The [`Dockerfile`](Dockerfile) for the first choice of library to use may use GCC 8 as specified by `FROM gcc:8`: [command] FROM gcc:8 COPY . /usr/src/gsoap WORKDIR /usr/src/gsoap EXPOSE 80 RUN g++ -o myserver gettime.cpp stdsoap2.cpp soapC.cpp soapService.cpp CMD ["./myserver"] Save this file as `Dockerfile` in your project directory. Note that we expose port 80, which our service is bound to with `service.run(80)`. Furthermore, the service executable `myserver` is built from the source code files with `RUN g++ -o myserver gettime.cpp stdsoap2.cpp soapC.cpp soapService.cpp`. These source code files were copied from the project directory with `COPY . /usr/src/gsoap` and placed in the working directory `/usr/src/gsoap` of the image we're building. To build this image, execute: [command] docker build -t gsoap-server . The `Dockerfile` for the second choice of image library to use may use Alpine 3.10 as specified by `FROM alpine:3.10`: [command] FROM alpine:3.10 COPY . /usr/src/gsoap WORKDIR /usr/src/gsoap EXPOSE 80 RUN apk add build-base RUN g++ -o myserver gettime.cpp stdsoap2.cpp soapC.cpp soapService.cpp CMD ["./myserver"] Note that we install GCC with `RUN apk add build-base`. To build this image, execute: [command] docker build -t gsoap-server . Either way, when the GCC-based image or the Alpine-based image is successfully built, we can run the container. This will execute `./myserver` in the working directory as we will see next. You can check that the `gsoap-server` image is containerized by listing the images: [command] docker image ls To remove any `<none>` images that are residues of unsuccessful builds, execute: [command] docker system prune If you encounter a compilation error in `stdsoap2.cpp` in function `tcp_gethostbyname()` when building the Alpine-based image, then this is probably an error with `gethostbyname_r` that is not detected due to missing/incorrect compile-time macros on Alpine. To fix this, make sure that two `#elif` in this function are changed to: #elif ((!defined(__GLIBC__) || (!_GNU_SOURCE && !defined(_POSIX_C_SOURCE) && !defined(_XOPEN_SOURCE))) && defined(HAVE_GETHOSTBYNAME_R)) || _POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600 || defined(__ANDROID__) || (defined(HAVE_GETHOSTBYNAME_R) && (defined(FREEBSD) || defined(__FreeBSD__))) and two `#if` near the end of this function are changed to: #if ((!defined(__GLIBC__) || (!_GNU_SOURCE && !defined(_POSIX_C_SOURCE) && !defined(_XOPEN_SOURCE))) && defined(HAVE_GETHOSTBYNAME_R)) || _POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600 || defined(__ANDROID__) || (defined(HAVE_GETHOSTBYNAME_R) && (defined(FREEBSD) || defined(__FreeBSD__))) [![To top](../../images/go-up.png) To top](#) 3. Running the Web service in a container ----------------------------------------- Once our `gsoap-server` image is build, we can run our container: [command] docker run -p 4000:80 -it --rm --name my-running-app gsoap-server Option `-p 4000:80` redirects port 80 of the container to port 4000 on our local machine. Options `-i` and `-t` are related to TTY behavior and are not required to be used. Option `--rm` removes the container automatically when it exits. You can assign a name to the running container with `--name my-running-app` which shows up when listing the container processes: [command] docker container ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 84b55b1262f3 gsoap-server "./myserver" 9 seconds ago Up 7 seconds 0.0.0.0:4000->80/tcp my-running-app If you see this then your service runs and is ready to respond to service requests, for example from a browser on your local machine. [![To top](../../images/go-up.png) To top](#) 4. Accessing the containerized Web service ------------------------------------------ Open a browser on your local machine and point it to `http://localhost:4000`: <div class="chart"><a href="response.png" data-lightbox="image-1"><img alt="Service response in a browser" src="response.png"/></a></div> Next, let's check our service using CURL on the command line: [command] curl 'http://localhost:4000' <html><body><h1>Welcome to gSOAP on Docker!</h1>If you see this page, your gSOAP server is successfully running.</body></html> We can retrieve the WSDL of the service in a browser using `http://localhost:4000/?wsdl` or view it with CURL: [command] curl 'http://localhost:4000/?wsdl' <?xml version="1.0" encoding="UTF-8"?> <definitions name="Service" ... </definitions> [![To top](../../images/go-up.png) To top](#) 5. Invoking the containerized Web service ----------------------------------------- In step 1 we generated both server-side and client-side source code with: [command] soapcpp2 -j gettime.h We now use the generated `soapProxy.h` and other files to implement an example client application that we save as [`gettimeclient.cpp`](gettimeclient.cpp): #include "soapProxy.h" // generated by soapcpp2 -j #include "ns.nsmap" // generated by soapcpp2 -j int main(int argc, char **argv) { // create a proxy object Proxy proxy; // set timeout parameters (max 5 sec delay per message) proxy.soap->transfer_timeout = proxy.soap->send_timeout = proxy.soap->recv_timeout = 5; // specify a server endpoint URL const char *endpoint = argc > 1 ? argv[1] : "http://localhost:4000"; // the response value time_t GMT; // invoke the service and display the response if (proxy.GetTime(endpoint, NULL, &GMT)) { std::cerr << "Cannot connect to " << endpoint << " (error " << proxy.soap->error << "):" << std::endl; proxy.soap_stream_fault(std::cerr); } else { std::cout << "Time = " << soap_dateTime2s(proxy.soap, GMT) << " (GMT)" << std::endl; } // clean up proxy.destroy(); } The client application is compiled on the local machine as follows: [command] c++ -o gettimeclient gettimeclient.cpp stdsoap2.cpp soapC.cpp soapxProxy.cpp If our containerized service is still running (check with `docker container ps`), then we invoke it with the client application from the command line: [command] ./gettimeclient Time = 2019-06-25T20:22:34Z (GMT) This command invokes the service on local port 4000. To use a different port, say 8000, specify the URL of the containerized service as an argument to the `gettimeclient` command: [command] ./gettimeclient http://localhost:8000 [![To top](../../images/go-up.png) To top](#) 6. Terminating the Web service container ---------------------------------------- To terminate the service container we need its ID: [command] docker container ps We then use the ID, say d76343327706, to stop the service container: [command] docker container stop d76343327706 To remove the `gsoap-service` image altogether: [command] docker image rm gsoap-server [![To top](../../images/go-up.png) To top](#) Summary and suggestions ----------------------- In this article we have shown how to implement and deploy a gSOAP stand-alone service in a Docker container. The stand-alone service is a simple iterative (i.e. non-concurrent) Web server. To add concurrency and improve robustness, please see our [tutorials](https://www.genivia.com/tutorials.html) to update the example source code with thread spawning. To implement more advanced Web services follows the same approach. The main difference is that the interface header file processed by soapcpp2 is more complex (e.g. as generated by wsdl2h given one or more WSDL and XSD files) and the application is more complex. Basically, any C/C++ data structure can be serialized and used as Web service operation parameters. See our [XML data bindings](http://www.genivia.com/doc/databinding/html/index.html) documentation for details. While this example was provided in C++, the same steps apply to C service applications. Just run soapcpp2 with option `-c` to generate C source code, specify `gcc` in `Dockerfile`, and specify files with `.c` extensions (note that `stdsoap2.c` should be used for C, which is included in the gSOAP source code distribution). Once you build your Docker container images, you can orchestrate and manage them with Kubernetes and deploy them on an elastic cloud, for example with [Amazon Elastic Container Service](https://aws.amazon.com/getting-started/tutorials/deploy-docker-containers/), [Docker for Azure](https://azure.microsoft.com/en-us/services/kubernetes-service/docker/), or with the [Google Compute Engine](https://cloud.google.com/compute/docs/containers/) (requires installing Docker on Linux VM instances). Next, we will give some suggestions on how to deploy non-SOAP and JSON services, and how to deploy secure services with https. To remove SOAP in favor of a plain XML API that uses HTTP POST is easy with gSOAP. Just change the `gettime.h` file as follows to specify a HTTP protocol for the operation: //gsoap ns service method-protocol: GetTime POST ns__GetTime(time_t *GMT); Warning: do not use protocol `GET` to specify a HTTP GET REST service operation, because this requires a GET handler on the server side to handle GET requests which is not automated. To implement a JSON Web API please see [XML-RPC/JSON and jsoncpp](doc/xml-rpc-json/html/index.html) for more details. You can deploy secure https-based services as container images with the openssl-dev package of Alpine. The `Dockerfile` should be changed by adding `RUN apk add openssl-dev` and by compiling our service application using `-DWITH_OPENSSL` and lining `libcrypto` and `libssl`: [command] FROM alpine:3.10 COPY . /usr/src/gsoap WORKDIR /usr/src/gsoap EXPOSE 80 RUN apk add build-base RUN apk add openssl-dev RUN g++ -DWITH_OPENSSL -o myserver gettime.cpp stdsoap2.cpp soapC.cpp soapService.cpp -lcrypto -lssl CMD ["./myserver"] In addition, we change the `gettime.cpp` source code to configure TLS using `soap_ssl_server_context()` and by invoking `service.ssl_run(80)` to bind our https server to port 80: int main() { // create a new service Service service; // set timeout parameters (max 5 sec delay per message) service.soap->transfer_timeout = service.soap->send_timeout = service.soap->recv_timeout = 5; // optional: let the server terminate after 24 hours idle time service.soap->accept_timeout = 24*60*60; // optional: set the user variable to pass a pointer to the service object to callbacks service.soap->user = (void*)&service; // register the HTTP GET handler callback function service.soap->fget = http_get_handler; // set up TLS for https if (soap_ssl_server_context(service.soap, SOAP_SSL_DEFAULT, "server.pem", "password", NULL, NULL, NULL, NULL, "sslserver" )) { service.soap_stream_fault(std::cerr); exit(EXIT_FAILURE); } // serve requests on port 80 (note: this runs a limited iterative server) while (service.ssl_run(80)) service.soap_stream_fault(std::cerr); // clean up service.destroy(); } Note that `server.pem` should be placed in the project directory. You can generate this file or get a self-signed `server.pem` certificate from the `gsoap/samples/ssl` directory in the gSOAP source code package. This https server runs a single thread by invoking `server.ssl_run(80)`. To improve the response time using multi-threading of the https server, please see our [tutorials](https://www.genivia.com/tutorials.html) for details. After modifying `Dockerfile` and `gettime.cpp` and placing `server.pem` in the project directory, we are ready to build the container image and run it: [command] docker build -t gsoap-server . docker run -p 4000:80 -it --rm --name my-running-app gsoap-server Open a browser on your local machine and point it to `https://localhost:4000` (note the https here). If you are using the self-signed certificate `server.pem` then your browser may complain about an insecure connection, but you should accept the risk of connecting. You can rebuild the client application using `-DWITH_OPENSSL` and by linking `libgcrypto` and `libssl` on your local machine, then run it: [command] c++ -DWITH_OPENSSL -o gettimeclient gettimeclient.cpp stdsoap2.cpp soapC.cpp soapProxy.cpp -lcrypto -lssl ./gettimeclient https://localhost:4000 If the client refuses to connect because you're using a self-signed certificate, then add the following to the client to work around this for now: if (soap_ssl_client_context(proxy.soap, SOAP_SSL_DEFAULT | SOAP_SSL_ALLOW_EXPIRED_CERTIFICATE, NULL, NULL, "cacert.pem", NULL, NULL )) { proxy.soap_stream_fault(std::cerr); exit(EXIT_FAILURE); } Place `cacert.pem` in the project directory (get it from the `gsoap/samples/ssl` directory in the gSOAP source code package) and recompile. Beware of the potential security implications of self-signed certificates. [![To top](../../images/go-up.png) To top](#) Readme report ------------- See the auto-generated [soapReadme](soapReadme.html) for this example. [![To top](../../images/go-up.png) To top](#)