Tntnet quick start guide

Authors: Tommi Mäkitalo, Andreas Welchlin

This quick start guide includes:

Tntnet is developed and tested on GNU/Linux. It is known to run on Sun Solaris, IBM AIX and FreeBSD.

Installation

You can install through the package manager in your operating system, if those packages are outdated (< cxxtools, tntnet 2.3), follow this instruction:

To install Tntnet you will first need to install cxxtools.

You can find cxxtools on the tntnet homepage and install it with:

$ tar xzf cxxtools-2.x.tar.gz
$ cd cxxtools-2.x
$ ./configure
$ make
$ sudo make install
$ sudo ldconfig

The same installation procedure is used for tntnet. Install it with:

$ tar xzf tntnet-2.x.tar.gz
$ cd tntnet-2.x
$ ./configure
$ make
$ sudo make install
$ sudo ldconfig

Now you have a working Tntnet environment.

How to create your first web application

To create a web application we need to create a project. The easiest way is to use the helper script tntnet-project. We execute on the command line:

$ tntnet-project myfirstproject

This creates a initial web project, which uses autotools as a build system. It is created in a directory named myfirstproject. The most interesting files, which are created are:

For quick information about autotools using, read this mini howto: http://www.niksula.hut.fi/~mkomu/docs/autohowto.html

To build and execute your first application enter the following commands:

$ cd myfirstproject
$ make
$ tntnet

Now you can start your web browser and navigate to http://localhost:8000/myfirstproject.

You can see the result of your first running tntnet application, which prints the name of the application.

What have we done?

The source file myfirstproject.ecpp has been translated to C++. This C++ program was used to build a shared library which contains the whole web application.

A tntnet web application is a simple web page with special tags like <$ ...$>. The ecpp compiler ecppc creates a C++ source file and a header file with the same base name. These contain a class which has also the same name as the file. You can look into the generated code if you want, and sometimes it is useful to read it for further understanding of tntnet applications. If the C++ compiler has problems with your application it is a good idea to first check the generated code.

The tags <$ ... $> output a C++ expression. The result of the expression is printed into the resulting page when the page is requested (on runtime). For this purpose, a std::ostream is used, so that the type of the result can be any object, which has an output operator operator<<(ostream&, T) defined.

The configuration file tntnet.xml contains the settings. The two most important ones are the definitions of the listeners and URL mappings.

The listeners define, on which interface tntnet waits for requests. You define an IP address of the local interface and a port. The IP address may be empty or omitted. If omitted tntnet listens on all local interfaces.

The mappings tell tntnet what to do with incoming requests. Without this entry tntnet answers every request with http error 404 – not found. A mapping maps the URL - which is sent from a web browser - to a tntnet component. A component is the piece of code, which is normally generated by the ecpp compiler (ecppc).

That's what we did above with myfirstproject.ecpp. Components are identified by their (class) name and the shared library which contains this class. We named our class "myfirstproject" and our shared library "myfirstproject.so". The component identifier is then myfirstproject@myfirstproject.

So the mapping tells tntnet to call this component, when the URL /test.html is requested.

How to add an image to your web application

A nice web application is colorful and has some images. Let's add one.

Create or fetch some pictures. Say you have a picture "picture.jpg". Put it into your working directory. Modify your html page "myfirstproject.ecpp" to show an image:

<html>
    <head>
        <title>ecpp application myfirstproject</title>
    </head>
    <body>
        <h1>myfirstproject</h1>
        <img src="picture.jpg">
    </body>
</html>

Next we compile the modified web page including the picture and link everything together. We need to tell the ecpp compiler (ecppc), that the picture is a binary file and which mime type to generate. The flag -b tells ecppc not to look for tags like <$...$>. The component needs to tell the browser the mime type which is "image/jpeg" for your picture. The option -m is used to tell ecppc the MIME type. The picture will be compiled into the component.

$ ecppc myfirstproject.ecpp
$ g++ -c -fPIC myfirstproject.cpp
$ ecppc -b -m image/jpeg picture.jpg
$ g++ -c -fPIC picture.cpp
$ g++ -o myfirstproject.so -shared myfirstproject.o picture.o -lecpp

Or, more simply, you can edit the generated Makefile and change the line:

myfirstproject.so: myfirstproject.o

to:

myfirstproject.so: myfirstproject.o picture.o

Before tntnet is started it is necessary to extend our configuration. Tntnet needs to know, that "picture.jpg" is found in the shared library "myfirstproject.so". Our new tntnet.xml looks like this:

<tntnet>
  <mappings>
    <mapping>
      <url>^/myfirstproject.html$</url>
      <target>myfirstproject@myfirstproject</target>
    </mapping>
    <mapping>
      <url>^/picture.jpg$</url>
      <target>picture@myfirstproject</target>
    </mapping>
  </mappings>
  <listeners>
    <listener>
      <port>8000</port>
    </listener>
  </listeners>
</tntnet>

Now we start our modified web application which is found in myfirstproject.so. Start tntnet like before and look at the modified page now including your image.

Generalize the configuration

When adding new pages to tntnet applications you have to ensure that tntnet finds all the components. Until now we have added each single component into tntnet.xml. There is a way to generalise this by using regular expressions. Just modify tntnet.xml like so:

<tntnet> 
  <mappings>
    <mapping>
      <url>^/(.*).html$</url>
      <target>$1@myfirstproject</target>
    </mapping>
    <mapping>
      <url>^/(.*).jpg$</url>
      <target>$1@myfirstproject</target>
    </mapping>
  </mappings>
  <listeners>
    <listener>
      <port>8000</port>
    </listener>
  <listeners>
</tntnet>

Every request will be checked by tntnet for matching the first of all regular expressions which are defined. Every request with the suffix ".html" or ".jpg" tells tntnet to look for a component with the basename of the request. Ok there is one funny thing in our configuration: we get our picture with http://localhost:8000/picture.html. But tntnet does not care and nor does the browser.

Adding some C++ processing

Tntnet is made for writing web applications in C++. In the first example you saw one type of tag: <$...$>. This encloses a C++ expression, which is evaluated and printed on the resulting page.

Web applications often need to do some processing like fetching data from a database or something. The tags <{ ... }> enclose a C++ processing block. This C++ code is executed, when a browser sends a request to fetch the page.

As a short form you can put the character '%' into the first column, which means, that the rest of the line is C++.

We change our myfirstproject.ecpp to look like this:

<html>
    <head>
        <title>ecpp application myfirstproject</title>
    </head>
    <body>
        <h1>myfirstproject</h1>
        <{
            // we have a c++ section here
            double arg1 = 1.0;
            double arg2 = 3.0;
            double result = arg1 + arg2;
        }>
        <p>
            <$ arg1 $> + <$ arg2 $> =
            % if (result == 0.0) {
                nothing
            % } else {
                <$ result $>
            % }
        </p>
    </body>
</html>

Compile and run the application with:

$ make
$ tntnet

Maybe we should call it calc.ecpp. Sounds like a better name for a little calculator. But to be a real calculator the user should be able to enter the values. There is a solution to this, so go on reading.

Processing parameters

Html has forms for dealing with user input. Forms send their values to a web application. The application needs to receive these values as parameters. Tntnet supports this by using the ecpp tags <%args> ... </%args> which enclose a parameter definition.

Let's start with a simple example:

<%args>
    namefield;
</%args>
<html>
    <body>
        <form>
            What's your name?
            <input type="text" name="namefield">
            <input type="submit">
        </form>
        <hr>

        Hello <$ namefield $>

    </body>
</html>

We put a variablename into an args section. This defines a C++ variable of type std::string, which receives the value of the request parameter. The first time we call our application, there are no parameters, so 'namefield' is an empty string.

It is possible to assign a variable a non empty default value by changing the definition to:

<%args>
    namefield = "World!";
</%args>

The first time our application is called we get this famous "Hello World!" output (sorry that it took so long to get to it).

Now we know all the elements needed to create a slightly more functional calculator:

<%args>
    arg1;
    arg2;
</%args>
<{
    double v1, v2;
    std::istringstream s1(arg1);
    s1 >> v1;
    std::istringstream s2(arg2);
    s2 >> v2;
}>
<html>
    <body>
        <form>
            <input type="text" name="arg1" value="<$arg1$>">
            +
            <input type="text" name="arg2" value="<$arg2$>">
            % if (s1 && s2) { // if both input streams were successful extracting values
                = <$ v1 + v2 $>
            % }
        </form>
    </body>
</html>

Since it is so common to convert the values as numbers or some other types apart from strings tntnet can do that for you. We can just specify the type of the arguments with a default values in the <%args> section:

<%args>
    double arg1 = 0.0;
    double arg2 = 0.0;
</%args>
<html>
    <body>
        <form>
            <input type="text" name="arg1" value="<$arg1$>">
            +
            <input type="text" name="arg2" value="<$arg2$>">
            = <$ v1 + v2 $>
        </form>
    </body>
</html>

The downside is, that we do not have a good way to determine, if the conversion was successful. We may define a different default value like the maximum double value using std::numeric_limits<double>::max(). Normally we can trust, that we do not receive a value, which converts to that value.

Modularise a web application

A great feature of tntnet is the possibility to create web pages by calling subroutines. You can create small html snippets and put them together into one big page. First we create a menu, so we create a file with the name menu.ecpp:

<a href="page1.html">Page 1</a><br>
<a href="page2.html">Page 2</a><br>
<a href="page3.html">Page 3</a><br>
<a href="page4.html">Page 4</a><br>

Now we create 4 pages, page1.ecpp to page4.ecpp with some content. We want our menu to be on each of our pages and the following code shows how the menu component is embedded.

This is page 1:

<html>
    <body>
        <table>
            <tr>
                <td><& "menu" &></td>
                <td><h1>Here is page 1</h1></td>
            </tr>
        </table>
    </body>
</html>

It should not be too hard to derive page 2 to 4 from here. Our Makefile looks like this:

OBJECTS=page1.o page2.o page3.o page4.o menu.o
CC=g++
CXXFLAGS=-fPIC

%.cpp %.h: %.ecpp
   ecppc $<

pages.so: $(OBJECTS)
   g++ -o $@ -shared -ltntnet $^

The configuration does not differ too much from the first example. Just replace @myfirstproject with @pages, because our module name is now pages.so instead of myfirstproject.so.

Call make to compile it and as usual run the application with tntnet.

A block <& ... &> contains a subcomponent call. In our simple case we have a normal C++ string constant here. It can also be a variable or a function call, which returns a std::string.

Moving business logic from web pages to C++ and using a session

So far we have seen how to embed C++ code fragments into web pages. But we do not use C++ just because it has such a nice syntax. We want to use more of C++. We want to organize our business logic in C++ classes instead of mixing it with our web application. Moving the business logic into C++ classes helps in testing the business logic or using this logic in other applications.

So what do we need? Classic C++ code normally uses headers with declarations and C++ files with the actual implementation. To use the implementation, we need to include the header to make the interface usable.

In ecpp we use the tag <%pre>. Code inside this tag is placed outside our component class by the ecpp compiler. Normally we put it at the top. This is exactly the place, where one would add #include statements. So let's look at an example.

Let's come back to our calculator. To make it a little more sophisticated, we decide to add some state to our application. We want to accumulate new values on each request to a existing value. This accumulation is our business logic, which is held in a C++ class. Another new concept, what we need here is some sort of persistence between requests. We actually need a session.

We create a project accumulate using tntnet-config --project=accumulate.

We create a C++ class Accumulator next, which implements the actual calculation. So our header accumulator.h might look like that:

#ifndef ACCUMULATOR_H
#define ACCUMULATOR_H
class Accumulator
{
    double _sum;

  public:
    Accumulator()
      : _sum(0)
    { }

    void add(double value);
    double sum() const;
    void reset();
};
#endif

And our accumulator.cpp is here:

#include "accumulator.h"
#include <sstream>

void Accumulator::add(double value)
{
  _sum += value;
}

double Accumulator::sum() const
{
  return _sum;
}

void Accumulator::reset()
{
  _sum = 0;
}

We defined a class, which adds a new value to the result - nothing special here. In real world applications this business logic is far more complex but this is good enough to see how to handle it.

To make the state persistent, we add a session to our application. This is done using the tag <%session>. We can define variables there and they keep their value between requests. Tntnet automatically sends a session cookie to the browser, when it sees that tag.

So here is our accumulate.ecpp:

<%pre>
    #include "accumulator.h"
</%pre>
<%args>
    double value = 0.0;
    bool add;
    bool reset;
</%args>
<%session>
    Accumulator accumulator;
</%session>
<%cpp>
    if (add)
        accumulator.add(value);
    if (reset)
        accumulator.reset();
</%cpp>
<html>
    <head>
        <title>ecpp application accumulate</title>
    </head>
    <body>
        <h1>accumulate</h1>
        <form>
            <input type="text" name="value" value="<$ value $>">
            <input type="submit" name="add" value="add the value">
            <input type="submit" name="reset" value="reset the value">
        </form>
        The sum so far is <$ accumulator.sum() $>
    </body>
</html>

This is quite straight forward. Remember to add the accumulator.o to the list of object files in the Makefile. After building the applications and running tntnet, you can try the new application.

Creating a standalone web application

We have seen now how to create a web application with tntnet. We created a shared library, which is loaded into the web server tntnet. A configuration file tntnet.xml is needed to tell tntnet what to do.

But there is an alternative approach to running tntnet web applications. You can create a standalone application. Your application integrates the web server. You don't need to run tntnet anymore, but simply run your application.

What we need is to write a little main function, which instantiates an object of type tnt::Tntnet, configures and runs it. And this is very easy.

We convert now our accumulator application into a standalone application. Here is our main.cpp:

#include <tnt/tntnet.h>

int main(int argc, char* argv[])
{
  try
  {
    tnt::Tntnet app;
    app.listen(8000);
    app.mapUrl("^/$", "accumulate");
    app.mapUrl("^/([^.]+)(\\..+)?", "$1");
    app.run();
  }
  catch (const std::exception& e)
  {
    std::cerr << e.what() << std::endl;
  }
}

The Makefile needs some changes, since we do not want to build a shared library anymore but create a real C++ application. Here is our Makefile (I removed everything, we do not need here):

all: accumulate

accumulate: accumulate.o accumulator.o main.o
    ${CXX} -o $@ $^ ${LDFLAGS}

.SUFFIXES: .ecpp
ECPPC=/usr/local/bin/ecppc
CXXFLAGS+=-I/usr/local/include -O2
LDFLAGS+=-L/usr/local/lib -ltntnet -lcxxtools

.ecpp.cpp:
    ${ECPPC} ${ECPPFLAGS} ${ECPPFLAGS_CPP} -o $@ $<

If you run make, you get an executable accumulate. Now you run ./accumulate and you can access the web application as before. To stop it, just press ctrl-C.

Adding logging

You may miss the logging output. The logger is not configured. Let's add that to make the application complete. Logging is initialized using a macro log_init(). Optionally you can pass a file name of an xml configuration file as as a parameter. By default this looks for a file named log.xml, which describes, what to log. We will do that.

But first we add the initialization macro to our application. The macro is defined in the header cxxtools/log.h. So here is our new main.cpp with these 2 lines added:

#include <tnt/tntnet.h>
#include <cxxtools/log.h>

int main(int argc, char* argv[])
{
  try
  {
    log_init();
    tnt::Tntnet app;
    app.listen(8000);
    app.mapUrl("^/$", "accumulate");
    app.mapUrl("^/([^.]+)(\\..+)?", "$1");
    app.run();
  }
  catch (const std::exception& e)
  {
    std::cerr << e.what() << std::endl;
  }
}

That shouldn't be too hard. We need a configuration file log.xml. An easy way to create it is to use the command cxxtools-config --logxml accumulator >log.xml. You can look at the file to get an idea of what can be configured.

Hey, we are done already. Compile the application using make and run it using ./accumulator.

One further step is to add some logging to our application. If you look at log.xml, you already have an idea, how to do that. Lets follow the instructions. We add a call to logdefine to define a logging category and then we can log using the log macros e.g. loginfo. Here is our new main.cpp`:

#include <tnt/tntnet.h>
#include <cxxtools/log.h>

log_define("accumulator")

int main(int argc, char* argv[])
{
  try
  {
    log_init();

    unsigned short port = 8000;
    log_info("run accumulator on port " << port);

    tnt::Tntnet app;
    app.listen(port);
    app.mapUrl("^/$", "accumulate");
    app.mapUrl("^/([^.]+)(\\..+)?", "$1");
    app.run();
  }
  catch (const std::exception& e)
  {
    std::cerr << e.what() << std::endl;
  }
}

When you compile and run the application, you get the new log statement on the screen.

Further reading