Skip to content

Tutorial: Server

Marco Poletti edited this page Aug 27, 2018 · 8 revisions

In this chapter of the tutorial, we will see how to use Fruit's normalized components to write an HTTP-like server. We want to write a "server" that reads request URLs from standard input, and dispatches the request to the right handler based on the URL, handling each request in a separate thread.

The full source is available in examples/server.

First, let's define the Request type. For this simple server, a request is just a URL.

// request.h
struct Request {
    std::string path;
};

We'll also have a ServerContext struct, to show how request handlers can access non-request-specific information.

This class is not strictly necessary in our case, but we still use it to show how such information would be passed to the handlers in a real server.

// server_context.h
struct ServerContext {
  std::string startupTime;
};

Now let's write two request handlers: one for URLs starting with /foo/:

// foo_handler.h
#include "request.h"
#include "server_context.h"

class FooHandler {
public:
    // Handles a request for a subpath of "/foo/".
    // The request is injected, no need to pass it directly here.`
    virtual void handleRequest() = 0;
};

fruit::Component<fruit::Required<Request, ServerContext>, FooHandler>
    getFooHandlerComponent();
// foo_handler.cpp
#include "foo_handler.h"

class FooHandlerImpl : public FooHandler {
private:
    const Request& request;
    const ServerContext& serverContext;

public:
    INJECT(FooHandlerImpl(const Request& request, const ServerContext& serverContext))
        : request(request), serverContext(serverContext) {
    }

    void handleRequest() override {
        cout << "FooHandler handling request on server started at "
             << serverContext.startupTime << " for path: " << request.path << endl;
    }
};

fruit::Component<fruit::Required<Request, ServerContext>, FooHandler> getFooHandlerComponent() {
    return fruit::createComponent()
        .bind<FooHandler, FooHandlerImpl>();
}

And, as you might have expected, one for URLs starting with /bar/:

// bar_handler.h
#include "request.h"
#include "server_context.h"

class BarHandler {
public:
    // Handles a request for a subpath of "/bar/".
    // The request is injected, no need to pass it directly here.
    virtual void handleRequest() = 0;
};

fruit::Component<fruit::Required<Request, ServerContext>, BarHandler>
    getBarHandlerComponent();
// bar_handler.cpp
#include "bar_handler.h"

class BarHandlerImpl : public BarHandler {
private:
    const Request& request;
    const ServerContext& serverContext;

public:
    INJECT(BarHandlerImpl(const Request& request, const ServerContext& serverContext))
        : request(request), serverContext(serverContext) {
    }

    void handleRequest() override {
        cout << "BarHandler handling request on server started at "
             << serverContext.startupTime << " for path: " << request.path << endl;
    }
};

fruit::Component<Required<Request, ServerContext>, BarHandler> getBarHandlerComponent() {
    return fruit::createComponent()
        .bind<BarHandler, BarHandlerImpl>();
}

The interfaces of the two components require a Request and a ServerContext to be bound externally. This approach is more flexible than just passing them as parameter to handleRequest() for two reasons: first, the FooHandler and BarHandler interfaces don't need to depend on ServerContext. Also, this allows them to inject other classes that require Request and ServerContext, not just to inject Request and ServerContext themselves.

Now we need a class that dispatches the requests to the right handler:

// request_dispatcher.h
#include "request.h"
#include "server_context.h"

class RequestDispatcher {
public:
    // Handles the current request.
    // The request is injected, no need to pass it directly here.
    virtual void handleRequest() = 0;
};

fruit::Component<fruit::Required<Request, ServerContext>, RequestDispatcher>
    getRequestDispatcherComponent();
// request_dispatcher.cpp
#include "request_dispatcher.h"

#include "foo_handler.h"
#include "bar_handler.h"

class RequestDispatcherImpl : public RequestDispatcher {
private:
    const Request& request;
    Provider<FooHandler> fooHandler;
    Provider<BarHandler> barHandler;

public:
    INJECT(RequestDispatcherImpl(
        const Request& request,
        Provider<FooHandler> fooHandler,
        Provider<BarHandler> barHandler))
        : request(request),
          fooHandler(fooHandler),
          barHandler(barHandler) {
    }

    void handleRequest() override {
        if (stringStartsWith(request.path, "/foo/")) {
            fooHandler.get()->handleRequest();
        } else if (stringStartsWith(request.path, "/bar/")) {
            barHandler.get()->handleRequest();
        } else {
            cerr << "Error: no handler found for request path: '"
                 << request.path << "' , ignoring request." << endl;
        }
    }

private:
    static bool stringStartsWith(const string& s, const string& candidatePrefix) {
        return s.compare(0, candidatePrefix.size(), candidatePrefix) == 0;
    } 
};

fruit::Component<fruit::Required<Request, ServerContext>, RequestDispatcher>
    getRequestDispatcherComponent() {
    
    return fruit::createComponent()
        .bind<RequestDispatcher, RequestDispatcherImpl>()
        .install(getFooHandlerComponent)
        .install(getBarHandlerComponent);
}

Note that RequestDispatcherImpl holds providers of the handlers, not instances. This delays the injection of FooHandler and BarHandler to the point of use (when we call get() on the injector).

We do this because we only want to inject the handler that is actually used for the request. In a large system, there will likely be many handlers, and many of those will have lots of dependencies that would also end up being injected if we injected all handler classes directly in RequestDispatcherImpl instead of injecting Providers.

And now we just need the Server class:

// server.h
#include "request.h"
#include "server_context.h"
#include "request_dispatcher.h"

class Server {
public:
    virtual void run(fruit::Component<fruit::Required<Request, ServerContext>,
                                      RequestDispatcher>
                                          (*)()) = 0;
};

fruit::Component<Server> getServerComponent();

Note that the run() method of the server takes a Component function. This is because we will use two injectors - one for the server startup (that will be used to inject the Server instance) and one for each request, that will be used to inject the request dispatcher.

// server.cpp
#include "server.h"

#include "server_context.h"
#include "request_dispatcher.h"

class ServerImpl : public Server {
private:
    std::vector<std::thread> threads;

public:
    INJECT(ServerImpl()) {
    }

    ~ServerImpl() {
        for (std::thread& t : threads) {
          t.join();
        }
    }

    void run(fruit::Component<fruit::Required<Request, ServerContext>,
                                              RequestDispatcher>
                                              (*getRequestDispatcherComponent)()) 
            override {
        ServerContext serverContext;
        serverContext.startupTime = getTime();

        const fruit::NormalizedComponent<fruit::Required<Request>, RequestDispatcher>
            requestDispatcherNormalizedComponent(
                getRequestDispatcherComponentWithContext,
                getRequestDispatcherComponent,
                &serverContext);

        cerr << "Server started." << endl;
        
        while (1) {
            cerr << endl;
            cerr << "Enter the request (absolute path starting with \"/foo/\" or "
                 << "\"/bar/\"), or an empty line to exit." << endl;
            Request request;
            getline(cin, request.path);
            cerr << "Server received request: " + request.path << endl;
            if (request.path.empty()) {
                cerr << "Server received empty line, shutting down." << endl;
                break;
            }
            
            threads.push_back(std::thread(
                worker_thread_main, 
                std::ref(requestDispatcherNormalizedComponent), request));
        }
    }

private:
    static void worker_thread_main(
        const fruit::NormalizedComponent<
            fruit::Required<Request>,
            RequestDispatcher>& 
                requestDispatcherNormalizedComponent,
        Request request) {

        fruit::Injector<RequestDispatcher> injector(
            requestDispatcherNormalizedComponent, getRequestComponent, &request);

        RequestDispatcher* requestDispatcher(injector);
        requestDispatcher->handleRequest();
    }

    static string getTime() {
        time_t now = time(nullptr);
        tm* localTime = localtime(&now);
        string result = asctime(localTime);
        if (result.size() != 0 && result.back() == '\n') {
            result.pop_back();
        }
        return result;
    }

    static fruit::Component<Request> getRequestComponent(Request* request) {
        return fruit::createComponent()
            .bindInstance(*request);
    }
    
    static fruit::Component<fruit::Required<Request>, RequestDispatcher> 
        getRequestDispatcherComponentWithContext(
            fruit::Component<fruit::Required<Request, ServerContext>, RequestDispatcher>(
                *getRequestDispatcherComponent)(),
            ServerContext* serverContext) {
      return fruit::createComponent()
          .install(getRequestDispatcherComponent)
          .bindInstance(*serverContext);
    }
};

fruit::Component<Server> getServerComponent() {
    return fruit::createComponent()
        .bind<Server, ServerImpl>();
}

The run() method is a main-like method that is called at startup and will only terminate when the server is shut down.

For every request (i.e. a line read from standard input) a new worker thread is created. worker_thread_main() creates the injector that will handle the request and calls RequestDispatcher to determine the correct handler. Note the use of NormalizedComponent instead of Component here. A normalized component is a transformed version of a component from which it's very efficient to create an injector; most of the computation that would otherwise take place when the injector is created is done in advance when the NormalizedComponent is created. In this example, this allows the server to do that computation only once, at startup, instead of repeating it in every request.

It's still allowed to add some bindings to a NormalizedComponent before creating an injector. This is what happens in worker_thread_main(), that has to bind a different Request in each injector. The two-argument constructor of Injector takes a NormalizedComponent and a Component and creates an injector with the union of the bindings. Creating an injector for each request is faster than you might expect; look at the benchmarks page for the details.

Finally, we have the main() function, that creates the outer injector and calls Server::run().

// main.cpp
#include "server.h"
#include "request_dispatcher.h"

int main() {
    Injector<Server> injector(getServerComponent);

    Server* server(injector);
    server->run(getRequestDispatcherComponent);
    
    return 0;
}

These are the dependencies between the source files:

Note that changes to a handler (say, bar_handler.cpp) only require recompilation of that translation unit. When adding a handler, we need to recompile the new handler and request_dispatcher.cpp (that will likely need changes anyway); we still don't need to recompile the server nor the other handlers, as there's no dependency from main.cpp and server.cpp to the handlers, not even to their header files.

In the next part of the tutorial we'll learn how to test code using Fruit.