category-group: server
layer: 10
header file: z_server.h

synopsis.
class object for making a server

description.
A server is a daemon process that has a specific task. Other terms for server include "actor", "agent", or "daemon" This document uses the term 'server'. Agent could be used to describe a server in a specific role.

A server is a complex object. It uses many other objects and has its own protocols for usage. Servers provide an application a tool for creating servers for specific purposes. In order to do that, you need to subclass from the server_o class. However, server_o is not an Abstract Data Type (ADT) - the implementation is complete except for the part where it does something with an incoming message. A cluster of "server" can be made to work together as a single aggregate unit to carry out a task. Servers also provide a framework for a distributed architecture that allows software to span machines. Putting servers on other hosts can achieve load balancing.

A server processes messages that are queued on one or more input channels; a simple server typically has one input channel, though multiple input channels are common. The input channels can be configured either prior to usage or dynamically, as the server is running. Each message received is processed and disposed of sequentially (multi-threaded servers will be available in the future). When a message arrives, the server "wakes up" (e.g., leaves a state of waiting for an event), processes the message, sends the message out on one of its output paths as appropriate, then returns to a wait-state until another message arrives. How the message is processed is deemed "application level code", and defines the semantics of a particular server. These mechanics are similar to that of a GUI-type window server (X-Windows, Microsoft Windows, Apple Mac). In this case, a message that arrived from a server's internal message queue constitutes an event. When the server is done with its request, it forwards a result (which is can be the incoming message, with additional information added by the current server) to one of a set of predefined outputs. This architecture implies a static set of directed paths between servers through which messages can flow. However, the server group provides just the mechanics for moving messages - how the messages are to flow is up to you.

Servers communicate with each other by means of information message packets. The data sent between servers is stored in a common data representation format using data bags. This format is used for internal communication between servers, and can as well be used with external interfaces. The data transported between the server is automatically packed and unpacked by a subsystem dedicated to that purpose. Data bags provide a simple, convenient storage mechanism for a developer of application logic to hold and manipulate application-defined data in a typeless manner. For more information on how to use data bags, see the Z Directory group dbag.

The internal architecture of a server is an event and loop, which looks something like this:

while (STILL_ALIVE)
{
    if (an_event_happened())
    {
        // get the current [data-bag format] message
        do_something();
    }
}
When there are no messages on a server's queue, it is quiescent (in a waiting state). By default, a server does nothing until a message arrives. There must be at least 1 input channel, for inter-server communication. The input channel can be used to also process queries from external systems. There may be more than one channel, but any additional ones do not fall within the "network of servers" domain. A server can access lower-level primitives that deal with I/O channels, but doing so falls outside of the server-network context.

Some ideas what servers can do:

  • Updating a database: revenue department marks a bill paid; a war department updates troop movements and a battle as won or lost; a manufacturer updates inventory and revenues as sales information is recieved.
  • -
  • A message gets updated and forwarded - a server adds some information to it and forwards it to the next server: a medical billing system recieves a request for a procedure to be done to a patient. The first server looks up insurance information, to see if the patient is in the system, and has coverage. If approved, the message is updated and passed on to another server in charge of looking up providers.
  • -
  • Web server support: A web server gets a request from a shopping cart to purchase some items (including perhaps an object-oriented library?). A CGI program kicks off a request to a local server to approve the transaction. The server forwards the message to another server that interacts with VISA or mastercard.
  • -
  • A message gets forwarded to an external system: unified messaging: a message arrives from either a pager or an OCR program scanning an incoming fax. A server program takes the message, looks up the customer in a local database to get his e-mail address, formats the message, and sends it as an e-mail.

Message Routing for servers.

A star topology for routing messages between a group of servers can simplify the job of message routing management. Each server has 1 input channel, and 1 output channel. All messages in a server are routed out to the server's output channel, whereupon the message goes to a special server whose sole job is to dispatch the message to the next server. This frees the server from the restriction of knowing about a fixed, static set of servers to which a given server can send messages. The "dispatcher agent" is in charge of sending all messages (or possibly discard the message). This routing topology resembles a star network. One server now handles all inter-server message-passing. This could result in loss of performance in high-traffic systems, if the message dispatcher causes a bottleneck.

Servers require a way to order data bags. This is so a server can store them temporarily when they arrive, in a container, and retrieve them later, given a known key. This functionality is implemented by creating a special data bags. The server-databag (an "ordered recursive data bag") is sub-typed of the recursive data-bag class. This class adds pure virtual ordering member functions. The implementation of ordering is deferred to a sub-class:

class server_dbag_o : public rec_dbag_o
{
public:
    int operator == (const server_dbag_o &) const;
    int operator >= (const server_dbag_o &) const;
    int operator <= (const server_dbag_o &) const;
    int operator >  (const server_dbag_o &) const;
    int operator <  (const server_dbag_o &) const;
};

For the server paradigm, a sub-class is added, derived from ordered_rec_dbag, simply called "server_dbag". This class implements the pure virtual functions listed above. An instance of a server_dbag contains a special [list] sub-data-bag, called "ServerRoute":


(
    ServerRoute
    (
        
        (
            Name 
            Host 
            ServerID 
            ConnectionID   
            ArrivalTime 
            DepartureTime 
        )
        
        (
            Name 
            Host 
            ServerID 
            ConnectionID   
            ArrivalTime 
            DepartureTime 
        )
    )
)

When a server receives such a data bag, it adds a sub-data-bag to the "ServerRoute" list. This entry contains:

  • the server name (if any)
  • the process ID of the server
  • the name of the host the server habitats in
  • the ID of the server's input transport endpoint (e.g., socket number); used as a data-bag return address
  • a sequential key that identifies the data bag in that server.

The combined key "" acts as a unique, sortable identifier of that data-bag for that server. When the data-bag leaves the server (and presumably, goes to another server), a new data-bag entry is inserted into the head of the "ServerRoute" list, whose name is a number that that has a value 1 greater than the previous first member (if there is none, the value used is "0"). Thus, should the data bounce back to the original server, it can identify any previous ownership of the data. The data-bag has state with respect to the servers. This is illustrated in the following example, which shows the contents of a data bag's "ServerRoute" while it is in 3 different servers:

server messages

server message routing

The transport and handling of data in data bags is implemented in 3 layers:

1. Controller class
Provides an "easy" interface for the application code in a server to manipulate data bags:

class Databag_RoutingControl
{
public:
    int send_dbag (rec_dbag_o &, count_t channel_id);
    int save_dbag (rec_dbag_o &, count_t storage_id);
    int save_n_send_dbag (rec_dbag_o &, count_t storage_id, count_t channel_id);
    int get_dbag (rec_dbag_o &, count_t channel_id);
    int get_match_merge (rec_dbag_o &, count_t channel_id);
};

2. data bag storage
This sits next to, rather than on top of, the transport mechanism. It simply maintains a list of data bags. It looks like this:

class mover_msgtrans
{
public:
    int save_dbag (rec_dbag_o &, count_t storage_id);
    int get_dbag  (rec_dbag_o &, count_t storage_id);
};

3. Message transport
This does end-to-end delivery of data bags between servers. A prototype declaration might look as follows:

class Server_Mover
{
public:
    int open ();
    int close ();
    int connect ();             // designate "client" mode
    int listen ();              // designate "server" mode
    int get (string_o &, size_t *p_size);
    int put (string_o &, size_t  size);
    int data_size () const;
    int is_data_pending () const;
};

Note: The class "string" (above) is the bucket for all messages handled by this class. This class should be able to handle 'binary' data (e.g., any sequence of bytes).

Example.
One must first subclass from the server class:

class server_o
{
public:
    virtual int process_dbag(const msgtrans_addr_o &, server_dbag_o &);
    int run():
};
Suppose the program is to have a message server. One could create a sub-class from the server class, called, say, "msg_server". The application coder would write:
class msg_server : public server_o
{
public:
    int process_dbag (const msgtrans_addr_o &, server_dbag_o &);
};
int msg_server::process_dbag (const msgtrans_addr_o &mta, server_dbag_o &data)
{
    //..
}

Server Cluster Issues.

A server carries out a specific task. The size of the task is defined by the implementer of the servers (or at least by whoever designates which roles each server gets). Determining the mapping of task to server is similar to deciding how big a subroutine (function) will be. If the servers are allotted big tasks, there is less overhead in the transmission of messages between them. However, the messages passed between big servers are larger, so there is some penalty incurred even though the number of messages transmitted decreases. With a large cluster of small servers, the message size decreases, and the number of messages sent increases linearly with the number of servers. There is another performance penalty incurred: as the tasks of the servers get smaller, the tasks get more specialized, and the data they require gets smaller, since the task done requires a more specialized data component. The messages sent to each server needs to get extracted and shipped to the server. Hence, there is a penalty of breaking up and reconstructing data within servers. Alternatively, each server could simply get larger messages, even the entire message, and use only the component it needs. Which route is taken is left to the system designer.

server scaling

servers: scalability considerations

member functions (primary)

server_o()
SIGNATURE: server_o ()
SYNOPSIS:
creates a a new server object, completely devoid of contents. The object has no internal structure - no string has been given to it to define any data structure.
 

server_o(server_o)
SIGNATURE: server_o (const server_o &that)
SYNOPSIS: copy constructor: creates a a new server object which is an exact image of the copied server object "that".
 

operator = (server_o)
SIGNATURE: const server_o &operator = (const server_o &rhs)
SYNOPSIS:
copies exactly the RHS object ("rhs") to the existing server object instance. Any data existing prior to this call gets destroyed.
RETURNS: a reference to the current server object
 

destructor
SIGNATURE: ~server_o ()
SYNOPSIS: virtual destructor. The server instance is wiped out and all contents are reinitialized to the at-construction state.
 

current_msgtrans()
SIGNATURE: msgtrans_o *current_msgtrans ()
SYNOPSIS: [WIP]
 

name()
SIGNATURE: const string_o &name () const
SYNOPSIS: [WIP]
 

timed_out()
SIGNATURE: boolean timed_out () const
SYNOPSIS: [WIP]
 

set_name()
SIGNATURE: int set_name (const string_o &)
SYNOPSIS: [WIP]
TRAITS: this function is inline
 

do_daemonize()
SIGNATURE: int do_daemonize ()
SYNOPSIS: [WIP]
TRAITS: this function is inline
 

dont_daemonize()
SIGNATURE: int dont_daemonize ()
SYNOPSIS: [WIP]
TRAITS: this function is inline
 

allow_executive_input()
SIGNATURE: int allow_executive_input ()
SYNOPSIS: [WIP]
TRAITS: this function is inline
 

ignore_executive_input()
SIGNATURE: int ignore_executive_input ()
SYNOPSIS: [WIP]
TRAITS: this function is inline
 

exit_bad_startup()
SIGNATURE: int exit_bad_startup (boolean = TRUE)
SYNOPSIS: [WIP]
TRAITS: this function is inline
 

exit_bad_cleanup()
SIGNATURE: int exit_bad_cleanup (boolean = TRUE)
SYNOPSIS: [WIP]
TRAITS: this function is inline
 

do_add_routing_info()
SIGNATURE: int do_add_routing_info (boolean = TRUE)
SYNOPSIS: [WIP]
TRAITS: this function is inline
 

dont_add_routing_info()
SIGNATURE: int dont_add_routing_info ()
SYNOPSIS: [WIP]
TRAITS: this function is inline
 

dump_dbag_to_stdout()
SIGNATURE: int dump_dbag_to_stdout ()
SYNOPSIS: [WIP]
TRAITS: this function is inline
 

dont_dump_dbag_to_stdout()
SIGNATURE: int dont_dump_dbag_to_stdout ()
SYNOPSIS: [WIP]
TRAITS: this function is inline
 

set_maxlife()
SIGNATURE: int set_maxlife (const timespan_o &tsx)
SYNOPSIS: sets the maximum lifetime of the server to the value specified.
PARAMETERS

  • tsx: a timespan object with the amount of time preconfigured.
  • DESCRIPTION:
    this defines how much time the server can run. The clock starts the countdown as soon as run() is invoked. When the time expires, the server simply exits; that is, run() returns. The object is not destroyed at that point and can be reused.
    Note that the units of time are however parameter 'tsx' is configured to. Example:
    server_o x;
    timespan_o ts ("2 weeks");
    x.set_maxsleep(ts);
    x.run();
    

     

    set_maxsleep()
    SIGNATURE: int set_maxsleep (const timespan_o &tsx)
    SYNOPSIS: [WIP]
     

    add_channel()
    SIGNATURE: const msgtrans_addr_o &add_channel (const msgtrans_addr_o &mta, int *pi, boolean is_server = TRUE)
    SYNOPSIS:
    this adds a channel, eg a port, to the server object. The specifications of the channel (eg port number) must be set inside the input parameter 'mta' prior. A reference to the actual message transport object is returned.
    The type of messages coming in to the specified channel are not intended to be databags.
    PARAMETERS

    • mta: a message transport object instance, with the address preconfigured. This object is typically a subclass of msgtrans_addr_o, usually a msgtrans_sockaddr_o. Normally only the port number needs to be set, eg:
      msgtrans_sockaddr_o sa;
      sa.set_port (8080);
      
    • pi: [output] error indicator variable. The values of this variable assume those of msgtrans_addr_o::add_extern_channel(). The possible non-zero (failure) codes are:
      1: input/output maximum number of sockets limit exceeded
      2: memory apparently exhausted ("new" failure)
      3: internal error (panic)
      4: bad input parameter
    • is_server: if this variable is TRUE (the default), the channel will be designated as suitable for a server to process requests. It will accept input and be able to send replies.
     

    add_input_channel()
    SIGNATURE: const msgtrans_addr_o &add_input_channel (const msgtrans_addr_o &mta, int *pi)
    SYNOPSIS:
    this function is almost identical to add_channel(). It creates a server-type channel (port) in the server object. See add_channel() for more details.
     

    add_output_channel()
    SIGNATURE: const msgtrans_addr_o &add_output_channel (const msgtrans_addr_o &mta, int *pi)
    SYNOPSIS:
    this creates a channel within the server object. Messages can flow out of the server object only when a channel is designated as an output channel.
    DESCRIPTION:
    In all respects except message flow direction, this function is similar to add_input_channel() and add_channel() (which see).
     

    delete_channel()
    SIGNATURE: int delete_channel (const msgtrans_addr_o &mta, int *pi)
    SYNOPSIS: this member function is used to close a channel.
    PARAMETERS

    • mta: a message transport object instance specifying the channel's address. The address must be set prior to calling this function. This object is typically a subclass of msgtrans_addr_o, usually a msgtrans_sockaddr_o. Normally only the port number needs to be set.
    • pi: [output] error indicator variable. Its values:
      0: all ok; channel deleted from server object
      zErr_NoMatch: no such message transport address
      2: error during disconnect attempt
    RETURNS:
    0: successful removal
    -1: error occurred (see value of 'pi')
     

    packetize_channel()
    SIGNATURE: int packetize_channel (const msgtrans_addr_o &mta, const string_o &nam, int *pi)
    SYNOPSIS:
    turn on (or off) byte stream packetizing on a given port. 'sname' specifies the packetization protocol; if it's "none" (or "raw"), packetizing is turned off. This means the message will be passed through raw (unfiltered - unmodified), exactly as it comes into the port. If packetization is expected, the message will be unpacked by the server object before it gets delivered to the application code (in process_message() or process_dbag()).
    PARAMETERS

    • mta: a message transport object instance specifying the channel's address. The address must be set prior to calling this function. See add_channel() for more information on this parameter.
    • nam: the name of the protocol to use, as a string. The values of this string are determined by the packet buffer class (pktbuff_o). A value of "raw" or "none" turns off packetization.
    • pi: [output] error indicator variable. Its values:
      0: packetization set-unset successfully
     

    fetch_msgtrans()
    SIGNATURE: msgtrans_o &fetch_msgtrans (const msgtrans_addr_o &mta, int *pi)
    SYNOPSIS:
    returns a handle (as a c++ reference) to the message transport object specified by 'mta'. This is provided as a low-level accessor in specialized applications.
    PARAMETERS

    • mta: a message transport object instance specifying the channel's address to look up.
    • pi: [output] error indicator variable. Its values:
      0: item fetched
      zErr_Item_NotFound: item not found
     

    reset()
    SIGNATURE: int reset ()
    SYNOPSIS: resets the server object. All existing channels are wiped out. All timers are reset to 0.
     

    run()
    SIGNATURE: int run ()
    SYNOPSIS:
    This starts the server working. run() is the most important member function of this object. When invoked, the server object will check for input on any [input] ports, sleep afterwards (if the pause-sleep time has been set to a positive amount of time), do any background processing if instructed to do so, and repeat the cycle for the duration of time specified by set_maxlife(). If no expiration time has been set, this function will never return.
     

    setup()
    SIGNATURE: int setup (int argc, char *argv[], int *pi)
    SYNOPSIS:
    This function parses command-line arguments (as given by argc and argv), and sets up the server object based on the parameters therein. The function looks for the following specific keywords in the argc-argv parameter list: in: designate a server object input channel.
    out | exec_port: designate a server object output channel.
    cext: designate a client channel.
    sext: designate a server channel.
    max | life | seconds: if any of these keywords are encountered, the subsequent item in the argv[] list is expected to be a number representing seconds of life of the server object.
    daemon | d: "daemonize" the server. This applies to server objects in unix-type OSes. If set, the application will attempt to do the tasks required to set up a daemon process: disconnect from the file system and all terminal I-O. Note: this feature has not been adequately tested and is currently not encouraged.
    no_daemon | nd: Don't "daemonize" the server. This applies to servers running in unix environments.
    All of the parameters in, out, exec_port, cext, and sex can be of the form [host]:[portnumber] (eg, "localhost:3306").
    PARAMETERS

    • argc: the number of "char *" arguments in argv. This has the same meaning and semantics as that used in the argument list of main() in console-based programs.
    • argv: an array ("vector") of char * arguments. The number of these must match the value of 'argc'. This parameter has the same meaning and semantics as that used in the argument list of main() in console-based programs.
    • pi: [output] error indicator variable. Its values:
      0: success
      1: user didn't provide a parameter (to "-in"|"-out")
      2: invalid parameter furnished ("-in"|"-out"|"max")
      3: system call failure ("gethostname()")
    TRAITS: this function is a convenience function, with a home-grown standard for server parameters.
     

    process_dbag()
    SIGNATURE: int process_dbag (const msgtrans_addr_o &mta, server_dbag_o &bag)
    SYNOPSIS:
    this function is a "sibling" of process_message() and is intended to be defined by the application's subclass to the server_o class. This function expects the input message (in input parameter 'bag') to be of databag format. In the server_o class, this function is used by Executive Monitor servers. The bag is processed and is sent down the same channel as whence it came.
    TRAITS: this function is virtual
     

    process_message()
    SIGNATURE: int process_message (const msgtrans_addr_o &mta, string_o &s)
    SYNOPSIS:
    As with run(), this function is of primary importance. The work of processing a message from a connecting client is done entirely within this function. In the server_o class, this function does essentially nothing.
    TRAITS: this function is virtual
     

    process_connect()
    SIGNATURE: int process_connect (const msgtrans_addr_o &mta, msgtrans_addr_o &mta2)
    SYNOPSIS:
    This member function is normally not redefined by a subclass. It is provided for specialized applications that need to do non-standard actions when a client connects to the server.
    PARAMETERS

    • mta: the message transport object where a new connection has been established.
    • mta2: an output parameter; the resultant message transport address after the connection has been accepted.
    DESCRIPTION:
    this routine gets called if there is a new connection on a server port. the port is referenced by the 'mta' handle. the default action is to accept the connection; server sub-classes that implement their own "process_connect()" should call this function first.
    internally, the code to this function is essentially this:
    int ie;
    msgtrans_o *ptr = (msgtrans_o *) t.msgtrans_handle (mta, &ie);
    return ptr->accept(mta, mta2, &ie);
    

    RETURNS:
    0: successful connect
    1: continue processing, but don't add the new connection
    -1: error occurred (see value of 'pi')
    TRAITS: this function is virtual
     

    process_disconnect()
    SIGNATURE: int process_disconnect (msgtrans_addr_o &mta)
    SYNOPSIS:
    this member function is called when the other end closes the channel. There may need to be bookkeeping done in specialized applications; so, like the process_connect() member function, this can be over-ridden. But in doing so, you should call this [base class] member function, so that the internal "transporter" entry gets correctly deleted.
    RETURNS:
    0: successful disconnect
    -1: error occurred
    TRAITS: this function is virtual
     

    periodic_work()
    SIGNATURE: int periodic_work ()
    SYNOPSIS:
    this function is called after all channels have been inspected and proceessd. This is done at the end of the main loop in run(). If your application does not re-implement this function in the sub-class, the function, as provided in the server object, will do nothing (it simply returns 0).
    TRAITS: this function is virtual
     

    send_dbag()
    SIGNATURE: int send_dbag (const msgtrans_addr_o &mta, server_dbag_o &bag)
    SYNOPSIS:
    this is a lower-level member function that can be called inside process_message() or process_dbag(). It will send 'bag' out the channel specified by 'mta'.
     

    send_dbag()
    SIGNATURE: int send_dbag (server_dbag_o &bag)
    SYNOPSIS: This sends databag 'bag' out all output channels. This is essentially a "broadcast".
     

    send_message()
    SIGNATURE: int send_message (const msgtrans_addr_o &mta, const string_o &s, const size_t n = 0)
    SYNOPSIS:
    this is a lower-level member function that can be called inside process_message(). It will send 's' out the channel specified by 'mta'.
     

    send_message()
    SIGNATURE: int send_message (const string_o &s, const size_t n = 0)
    SYNOPSIS: This sends 's' out all output channels. This is in effect a "broadcast".
     

    examples.
    Here is a fairly complete example of a server that listens on port 1592 and adds two integers.

    #include "z_server.h"
    class Math_Server : public server_o
    {
    public:
        Math_Server () { x = y = 0.0; }
        int process_message    (const msgtrans_addr_o &, string_o &);
        int process_disconnect (msgtrans_addr_o &);
    private:
        int x, y, answer;
    };
    
    void main()
    {
        int ie;
        Math_Server x;
        msgtrans_sockaddr_o sa;
        msgtrans_sockaddr_o my_port1, my_port2;
        sa.set_port (1592);
        my_port1 = x.add_channel (sa, &ie);
        timespan_o ts ("20 seconds");
        x.set_maxsleep(ts);
        x.run();
    }
    
    int Math_Server::process_message (const msgtrans_addr_o &mta, string_o &sin)
    {
        int ie, ie2;
        boolean do_add = FALSE, do_subtract = FALSE, do_mult = FALSE;
        textstring_o ts(sin);
        string_o s = ts.yank_word(TRUE);
        x = z_str_to_int(s.data());
        if (ts[0] == '+')
            do_add = TRUE;
        else if (ts[0] == '-')
            do_subtract = TRUE;
        else if (ts[0] == '*')
            do_mult = TRUE;
        ts.eat_nchars(1);
        s = ts.yank_word(TRUE);
        y = z_str_to_int(s.data());
    
        if (do_add)
            answer = x + y;
        else if (do_subtract)
            answer = x - y;
        else if (do_mult)
            answer = x * y;
    
        char buff[80];
        s = "ANSWER = " +
        z_int_to_str (answer, buff, &ie2);
        s += buff;
        return send_message (mta, s);
    }
    
    int Math_Server::process_disconnect (msgtrans_addr_o &mta)
    {
        stime_o when;
        when.now();
        std::cout << "Math Server: DISCONNECT; port # is: " << bsa.port();
        std::cout << " time: " << when << std::endl << std::flush;
        return 0;
    }
    
    ----------------------------------------
    // The other part of this is a client to connect to it.
    // this client does a simple, round-trip synchronous communication
    // with the server. Note that it is devoid of error checking.
    
    #include "z_mtsocket.h"
    
    int main (int argc, char *argv[])
    {
        int ie, ie2;
        size_t nb;
        string_o s("50+49");
    
        msgtrans_sockaddr_o addr;
        addr.set_port (1592);
        addr.set_host ("localhost");
    
        msgtrans_o mx;
        mx.set_address (addr, &ie);
        ie = mx.connect();
        ie = mx.put (s, &ie2, nb);
        ie = mx.get (answer, &ie2, &nb);
        if (!ie)
            std::cout << "got this " << s << std::endl;
        ie = mx.disconnect();
        return 0;
    }
    
    
    

    history.

    Sun 09/06/1998: added executive monitor message handling
    Mon 09/07/1998: added exec. monitor shutdown processing
    Thu 06/26/1997: code started
    Sat 05/30/1998: 08:45am: added the "max" [/"life"] timer
    Mon 08/06/2001: cleaned up the main driver
    Thu 05/30/2002: new #define naming conventions ("zos_", "zcc_")