398 lines
11 KiB
Markdown
398 lines
11 KiB
Markdown
Client-Server Mechanisms {#estserver}
|
|
==========================
|
|
|
|
[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 {#estserverservicetable}
|
|
|
|
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 {#estserverwritingclientserver}
|
|
|
|
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 {#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 {#estserverspecializedserver}
|
|
|
|
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 {#estserverclientmultiplereq}
|
|
|
|
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
|