<chapter> <title>Client-Server Mechanisms</title> <para> The &cpp; class <classname>EST_Server</classname> provides the core mechanisms required for simple client-server applications. It is currently used to implement the <literal role=program>fringe_client</literal> program and client-server mechanisms for SIOD. It is planned to use it to re-implement the festival client-server mechanism. </para> <para> 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. </para> <para> 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. </para> <para> An instance of <classname>EST_Server</classname> embodies each side of the client-server relationship. In the server an instance of <classname>EST_Server</classname> is created and told how to process requests from clients, a call to the <function role=method>run()</function> method then starts the server. In a client an instance of <classname>EST_Server</classname> represents the server, and calls to the <function role=method>execute(...)</function> method send requests to the server. </para> <sect1> <title>The Services Table</title> <para> The first problem which needs to be addressed by any client-server system is how the client finds the server. Servers based on <classname>EST_Server</classname> 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. </para> <para> By default the file <filename>.estServices</filename> 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. </para> <para> 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. <important> <para> 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. </para> <para> A more secure `challange-responce' authorisation scheme should be implemented. </para> </important> </para> <para> The in-file format of the services table is based on the &java; properties file format. A typical file might look as follows: <programlisting> #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 </programlisting> This file lists three services, a <literal role=program>fringe</literal> server with the default name of <literal>fringe</literal>, a scheme interpreter running as a server, also with the default name, and a second <literal role=program>fringe</literal> server named <literal>labeling</literal>. </para> <para> The programing interface to the services table is provided by the <link linkend='est-servicetable'><classname>EST_ServiceTable</classname></link> class. </para> &docpp-EST-ServiceTable-2; </sect1> <sect1> <title>Writing Clients and Servers</title> <para> If a service type (that is a sub-class of <classname>EST_Server</classname>) 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 <classname>EST_SiodServer</classname> class, which defines a simple &scheme; execution service service, as an example. </para> <sect2> <title>A Simple Server</title> <para> 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. </para> <para> First we read the default service table. <programlisting arch='cpp'> EST_ServiceTable::read(); </programlisting> Now we create the new scheme service called "mySiod". The <literal>EST_Server::sm_sequential</literal> parameter to the server constructor tells the server todeal with one client at a time. The <literal>NULL</literal> turns off trace output, replace this with <literal>&cout</literal> to see what the server is doing. <programlisting arch='cpp'> EST_SiodServer *server = new EST_SiodServer(EST_Server::sm_sequential, "mySiod", NULL); </programlisting> Write the table back out so clients can find us. <programlisting arch='cpp'> EST_ServiceTable::write(); </programlisting> Create the object which handles the client requests. The <parameter>handler</parameter> object actually does the work the client requests. <classname>EST_SiodServer</classname> provides the obvious default handler (it executes the scheme code and returns the results), so we use that. <programlisting arch='cpp'> EST_SiodServer::RequestHandler handler; </programlisting> Finally, start the service. This call never returns. <programlisting arch='cpp'> server->run(handler); </programlisting> </para> </sect2> <sect2 id='simple-client'> <title>A Simple Client</title> <para> A client is created by reading the service table, and then asking for a server by name. Again the <literal>NULL</literal> means `no trace output'. <programlisting arch='cpp'> EST_ServiceTable::read(); EST_SiodServer *server = new EST_SiodServer("mySiod", NULL); </programlisting> 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. <programlisting arch='cpp'> if (server->connect() != connect_ok) EST_sys_error("Error Connecting"); </programlisting> Once we are connected we can send requests to the server. The siod server executes scheme for us, assume that the function <literal>get_sexp()</literal> returns something we want evaluated. <programlisting arch='cpp'> LISP expression = get_sexp(); </programlisting> We pass arguments to requests in an <classname>Args</classname> structure, a special type of <classname>EST_Features</classname>. The sod server wants the expression to execute as the value of <literal>"sexp"</literal>. <programlisting arch='cpp'> EST_SiodServer::Args args; args.set_val("sexp", est_val(expression)); </programlisting> As in the server, the behaviour of the client is defined by a `handler' object. The handler <classname>EST_SiodServer</classname> defines for us does nothing with the result, leaving it for us to deal with in the <classname>EST_Features</classname> structure <literal>handler.res</literal>. Again this is good enough for us. <programlisting arch='cpp'> EST_SiodServer::ResultHandler handler; </programlisting> Finally we are ready to send the request to the server. The siod server provides only one operation, called <literal>"eval"</literal> in package <literal>"scheme"</literal>, this is the evaluate-expression operation we want. The return value of <function>execute(...)</function> is true of everything goes OK, false for an error. For an error the message is the value of <literal>"ERROR"</literal>. <programlisting arch='cpp'> if (!server->execute("scheme", "eval", args, handler)) EST_error("error from siod server '%s'", (const char *)handler.res.String("ERROR")); </programlisting> Now we can get the result of the evaluation, it is returned as the value of <literal>"sexp"</literal>. <programlisting arch='cpp'> LISP result = scheme(handler.res.Val("sexp")); </programlisting> 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: <programlisting arch='cpp'> args.set_val("sexp", est_val(expression)); if (!server->execute("scheme", "eval", args, handler)) [handle error] LISP result = scheme(handler.res.Val("sexp")); </programlisting> </para> </sect2> <sect2> <title>A Specialised Server</title> <para> 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 <classname>RequestHandler</classname> class. This class has a member function called <function role=method>process()</function> which deos the work. </para> <para> Here is a varient on the siod server which handles a new operation <literal>"print"</literal> 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). </para> <para> First we define the handler class. It is a sub-class of the default handler for siod servers. <programlisting arch='cpp'> class MyRequestHandler : public EST_SiodServer::RequestHandler { public: virtual EST_String process(void); }; </programlisting> Now, we define the processing method. For any operation other than <literal>"print"</literal> we call the default siod handler. (<function>leval</function> and <function>lprint</function> are functions provided by the siod interpreter). <programlisting arch='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(); } </programlisting> And now we can start a server which understands the new operation. <programlisting arch='cpp'> MyRequestHandler handler; server->run(handler); </programlisting> </para> </sect2> <sect2> <title>A Client Which Handles Multiple Results</title> <para> 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. </para> <para> Clearly a simple client of the kind described <link linkend='simple-client'>above</link> which gets the result of a request as a result of the call to <function role=method>execute(...)</function> can't handle multiple results of this kind. This is what the handler object is for. </para> <para> I'll asuume we need a client to del with a varient 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 <function>deal_with_result()</function>. </para> <para> Most of the client will be the same as for <link linkend='simple-client'>the simple client</link>, the exception is that we use our own result handler rather than the default one. <programlisting arch='cpp'> class MyResultHandler : public EST_SiodServer::ResultHandler { public: virtual void process(void); }; </programlisting> As for the server's request handler, the beahviour of the result handler is defeined by the <function role=method>process()</function> method of the handler. <programlisting arch='cpp'> EST_String MyResultHandler::process(void) { // Get the result. LISP result = scheme(handler.res.Val("sexp")); // And deal with it. deal_with_result(result); } </programlisting> With this definition in place we can make requests to the server as follows. <programlisting arch='cpp'> MyResultHandler handler; if (!server->execute("scheme", "multi-eval", args, handler)) [handle errors] </programlisting> The <function>deal_with_result()</function> 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 <function role=method>execute(...)</function> as in the simple client example. </para> </sect2> </sect1> <sect1> <title>Creating a new Service</title> <para> </para> <sect2> <title>Commands</title> <para> </para> </sect2> <sect2> <title>Results</title> <para> </para> </sect2> </sect1> <sect1> <title>The Network Protocol</title> <para> </para> </sect1> </chapter> <!-- Local Variables: mode: sgml sgml-doctype:"speechtools.sgml" sgml-parent-document:("speechtools.sgml" "chapter" "book") sgml-omittag:nil sgml-shorttag:t sgml-minimize-attributes:nil sgml-always-quote-attributes:t sgml-indent-step:2 sgml-indent-data:t sgml-exposed-tags:nil sgml-local-catalogs:nil sgml-local-ecat-files:nil End: -->