speech-tools/doc/estserver.md
2015-09-19 10:52:26 +02:00

11 KiB

Client-Server Mechanisms

[TOC]

The C++ class \ref EST_Server provides the core mechanisms required for simple client-server applications. It is currently used to implement the fringe_client program and client-server mechanisms for SIOD. It is planned to use it to re-implement the festival client-server mechanism.

Servers have types and names. When a server is started it records it's type, name and location in a `services' file. When a client wishes to connect to a server it looks for it's location in that file by giving a name and type.

Once connected, a client must present a magic cookie to the server as a simple form of authentication. Once authenticated the client sends requests, consisting of a package name, operation name and a set of arguments, to the server. The server responds with an error report or a sequence of values.

An instance of \ref EST_Server embodies each side of the client-server relationship. In the server an instance of \ref EST_Server is created and told how to process requests from clients, a call to the EST_Server::run() method then starts the server. In a client an instance of \ref EST_Server represents the server, and calls to the EST_Server::execute() method send requests to the server.

The Services Table

The first problem which needs to be addressed by any client-server system is how the client finds the server. Servers based on \ref EST_Server handle this problem by writing a record into a file giving their name, type and location. Clients can then look servers up by namd and type.

By default the file .estServices is used for this purpose, meaning that each user has their own list of servers. An alternative file could be specified to record public services.

The services table also provides a simple authorisation mechanism. Each server records a random string in the table, and clients must send this string before making any requests. Thus people who can't read the services table can't make requests of the server, and the file permissions on the services table can be used to control access to the server. \par Important:

This `magic cookie' authorisation scheme is not very secure. The cookie is sent as plain text over the network and so anyone who can snoop on the network can break the security.

A more secure `challange-responce' authorisation scheme should be implemented.

The in-file format of the services table is based on the Java properties file format. A typical file might look as follows:

@code #Services fringe.type=fringe fringe.host=foo.bar.com fringe.cookie=511341634 fringe.port=56362 fringe.address=123.456.789.654 siod.type=siod siod.cookie=492588950 siod.host=foo.bar.com siod.address=123.456.789.654 siod.port=56382 labeling.type=fringe labeling.host=foo.bar.com labeling.cookie=511341634 labeling.port=56362 labeling.address=123.456.789.654 @endcode

This file lists three services, a fringe server with the default name of fringe, a scheme interpreter running as a server, also with the default name, and a second fringe server named labeling.

The programing interface to the services table is provided by the \ref EST_ServiceTable class.

Writing Clients and Servers

If a service type (that is a sub-class of \ref EST_Server ) has already been defined for the job you need to do, creating clients and servers is quite straight forward. For this section I will use the \ref EST_SiodServer class, which defines a simple scheme execution service service, as an example.

A Simple Server

To run a siod server we have to read the server table, create the server object and update the table, then start the service running.

First we read the default service table.

@code{.cpp} EST_ServiceTable::read(); @endcode

Now we create the new scheme service called "mySiod". The sm_sequential parameter to the \ref Mode server constructor tells the server to deal with one client at a time. The NULL turns off trace output, replace this with &cout to see what the server is doing.

@code{.cpp} EST_SiodServer *server = new EST_SiodServer(EST_Server::sm_sequential, "mySiod", NULL); @endcode

Write the table back out so clients can find us.

@code{.cpp} EST_ServiceTable::write(); @endcode

Create the object which handles the client requests. The handler object actually does the work the client requests. \ref EST_SiodServer provides the obvious default handler (it executes the scheme code and returns the results), so we use that.

@code{.cpp} EST_SiodServer::RequestHandler handler; @endcode

Finally, start the service. This call never returns.

@code{.cpp} server->run(handler); @endcode

A Simple Client

A client is created by reading the service table, and then asking for a server by name. Again the NULL means `no trace output'.

@code{.cpp} EST_ServiceTable::read();

     EST_SiodServer *server 
	= new EST_SiodServer("mySiod", NULL);

@endcode

Now we have a representation of the server we must connect before we can do anything. We can connect and dissconnect a server object any number of times over it's life. This may or may not have some meaning to the server. The return value of the connect operation tells us if we managed to connect.

@code{.cpp} if (server->connect() != connect_ok) EST_sys_error("Error Connecting"); @endcode

Once we are connected we can send requests to the server. The siod server executes scheme for us, assume that the function \ref get_sexp() returns something we want evaluated.

@code{.cpp} LISP expression = get_sexp(); @endcode

We pass arguments to requests in an \ref Args structure, a special type of \ref EST_Features . The siod server wants the expression to execute as the value of sexp.

@code{.cpp} EST_SiodServer::Args args; args.set_val("sexp", est_val(expression)); @endcode

As in the server, the behaviour of the client is defined by a handler' object. The handler \ref EST_SiodServer defines for us does nothing with the result, leaving it for us to deal with in the \ref EST_Features structure handler.res`. Again this is good enough for us.

@code{.cpp} EST_SiodServer::ResultHandler handler; @endcode

Finally we are ready to send the request to the server. The siod server provides only one operation, called "eval" in package "scheme", this is the evaluate-expression operation we want. The return value of \ref execute() is true of everything goes OK, false for an error. For an error the message is the value of "ERROR".

@code{.cpp} if (!server->execute("scheme", "eval", args, handler)) EST_error("error from siod server '%s'", (const char *)handler.res.String("ERROR")); @endcode

Now we can get the result of the evaluation, it is returned as the value of "sexp".

@code{.cpp} LISP result = scheme(handler.res.Val("sexp")); @endcode

Although this may seem a lot of work just to evaluate one expression, once a connection is established, only the three steps set arguments, execute, extract results need to be done for each request. So the following would be the code for a single request:

@code{.cpp} args.set_val("sexp", est_val(expression)); if (!server->execute("scheme", "eval", args, handler)) [handle error] LISP result = scheme(handler.res.Val("sexp")); @endcode

A Specialised Server

If you need to create a server similar to an existing one but which handles requests slightly differently, all you need to do is define your own \ref RequestHandler class. This class has a member function called RequestHandler::process() which does the work.

Here is a variant on the siod server which handles a new operation "print" which evaluates an expression and prints the result to standard output as well as retruning it. (In this example some details of error catching and so on necessary for dealing with scheme are omitted so as not to obscure the main points).

First we define the handler class. It is a sub-class of the default handler for siod servers.

@code{.cpp} class MyRequestHandler : public EST_SiodServer::RequestHandler { public: virtual EST_String process(void); }; @endcode

Now, we define the processing method. For any operation other than "print" we call the default siod handler. (\ref leval and \ref lprint are functions provided by the siod interpreter).

@code{.cpp} EST_String MyRequestHandler::process(void) { if (operation == "print") { // Get the expression. LISP sexp = scheme(args.Val("sexp"));

        // Evaluate it.
        LISP result = leval(sexp, current_env);

        // Print it.
        lprint(result);
        
        // Return it.
        res.set_val("sexp", est_val(result));
        return "";
        }
    else
        // Let the default handler deal with other operations.
        return EST_SiodServer::RequestHandler::process();
    }

@endcode

And now we can start a server which understands the new operation.

@code{.cpp} MyRequestHandler handler; server->run(handler); @endcode

A Client Which Handles Multiple Results

Servers have the option to return more than one value for a single request. This can be used to return the results of a request a piece at a time as they become available, for instance festival returns a waveform for each sentence in a piece of text it is given to synthesise.

Clearly a simple client of the kind described \link simple-client above \endlink which gets the result of a request as a result of the call to EST_SiodServer::execute() can't handle multiple results of this kind. This is what the handler object is for.

I'll asume we need a client to deal with a variant on the normal siod sever which returns multiple values, say it evaluates the expression in each of a number of environments and returns each result separately. I'll also assume that the work to be done for each result is defined by the fucntion \ref deal_with_result().

Most of the client will be the same as for \link simple-client above \endlink, the exception is that we use our own result handler rather than the default one.

@code{.cpp} class MyResultHandler : public EST_SiodServer::ResultHandler { public: virtual void process(void); }; @endcode

As for the server's request handler, the behaviour of the result handler is defined by the process() method of the handler.

@code{.cpp} EST_String MyResultHandler::process(void) { // Get the result. LISP result = scheme(handler.res.Val("sexp"));

    // And deal with it.
    deal_with_result(result);
    }

@endcode

With this definition in place we can make requests to the server as follows.

@code{.cpp} MyResultHandler handler; if (!server->execute("scheme", "multi-eval", args, handler)) [handle errors] @endcode

The \ref deal_with_result() function will be called on each result which is returned. If anything special needs to be done with the final value, it can be done after the call to EST_SiodServer::execute() as in the simple client example.

Creating a new Service

Not written

Commands

Not written

Results

Not written

The Network Protocol

Not written