dsys
****

dsys is a package of C++ classes used to create distributed network applications.  The design of a distributed network application involves two things: figuring out who will talk to whom, and what protocol to use to communicate.  dsys is made to translate such a design into working code as quickly as possible.  To insure this, dsys provides several models for designing distributed applications.  These models are the ones that are best supported by the dsys library.

The models provided by dsys are as flexible as the models provided by the Interprocess Communication (IP) library in UNIX, because dsys is based on this library.  dsys provides mechanisms for using UDP packets, TCP/IP streams, and creating telnet servers and clients.  dsys also has extensive server/client support, featuring servers which can handle multiple simultaneous connections to clients.  dsys also contains support for poll blocking mechanisms, which allows processes to efficiently poll network connections and file descriptors.

While providing all these features, the dsys library is still designed in a modular hierarchy, so that a user only has to know enough about the dsys package to get the job done.

To use dsys, include the following line:

     #include <dsys++/dsys++.hh>

and link with the following libraries:

     -ldsys++ -lm

The remainder of this documentation explains the models for designing distributed applications using dsys, then details the actual classes and methods in the dsys library.

The dsys design model
*********************

Networking basics
=================

The goal of networking is to pass information between running programs. Networking implementations have evolved to create a set of robust concepts general enough to cover most communication needs.  This section will cover those concepts and the terms associated with them.

Network addresses
-----------------

All messages sent through the network have a source and a destination. T he network machinery specifies sources and destinations by using network addresses.  A network address is made of two parts, a hostname and a port.  The hostname identifies a physical machine on the network (in some cases, a single machine might have more than one hostname, usually if it has more than one network adapter in it).  This hostname is a string, such as "repo-man" or "media-lab.mit.edu".  The port defines a point of communication on that machine.  The port is an integer between 0 and 65535.  Together, the hostname string and the port number make up a single network address, which can specify the source or destination of any network communication.

When a process on a machine wishes to use the network, it must obtain a network address to use.  It already knows its own hostname, so all it needs is a port number, which it gets from the operating system.  Once the process has the address, then it can use that address to send out messages, and any messages that someone else is sending to that address will be received by that process.  A single process may obtain and monitor several network addresses for its own use, but a specific network address can only be used by one process at a time.

Network services
----------------

When one process wishes to communicate with someone else, it must know a network address that the destination process is using.  At the very least, the process should know the hostname of the destination, but it must still figure out the port number.  The best way to do this is to agree that certain programs will always use a specific port number no matter what machine they are on.

The system supports this by providing a table of services.  This table maps service names to port numbers.  The way this works is as follows: your program wishes to talk to an audio server program.  You know in advance that the audio server is listening on the port number specified by the service name audioserver.  You would then ask the system to translate the string "audioserver" into a port number, which the system does by consulting the table of services.

Note that there is no "magic" involved here.  The only reason this works is that the table of services is copied to every machine that is in "agreement" about the services.  This table is kept in a file called /etc/services on UNIX machines, and a typical UNIX maintenance task is to make sure that all machines at a site have exact copies of the /etc/services table.

Adding your own service to /etc/services is easy --- the trick is making sure that your change is copied to all machines (see your system administrator for how to do that).  To add your own service, look at the format of /etc/services.  First, pick your own new service name and put it in the first column.  Then, pick a port number.  Two suggestions here - make sure that it is not being used by any other service, and try to pick one above 10000 (so it won't accidentally get stomped by inetd or some other UNIX bogey).  Your system administrator can help you with this.  Then pick a protocol - tcp or udp. These terms will be explained later, but in general the protocol will be tcp --- if you are using udp, you will know it.  Finally, the last column should be a short description of your service.

UDP and TCP/IP protocols
........................

There are several models, or protocols for actually sending data over the network.  Only two are in major use and supported by dsys++. These two are UDP and TCP/IP.  UDP is the fastest protocol, but cumbersome and unreliable.  TCP/IP, or stream protocol, is slower, but it is convenient and completely reliable.

Communicating with UDP protocol
...............................

UDP is the simplest protocol for network communication. Using the UDP protocol, messages that are sent over the network are small packets.  The typical maximum size of a UDP packet is about 1200 bytes.

UDP stands for "Unreliable Datagram Protocol", and lives up to its name. There is no guarantee that a UDP packet will make it from its source to its destination.  There is no guarantee that a series of UDP packets will arrive in the same order that they were sent out.  There is also no way for the source to know if a UDP packet successfully made it to the destination or not.  The only thing that is guaranteed is that if a UDP packet actually makes it to its destination, its data will be intact.

Given all these complications, there are three reasons why one might use UDP packets.  The first is that they are fast.  In a case where speed is more important than accuracy, UDP is the answer.  The second reason is flexibility --- there is no notion of being "connected" to a destination.  Instead, you can send your packets to whatever network address you please (as long as there is someone listening for UDP protocol at the destination).

The third reason is that UDP allows you to broadcast packets. This allows you send one message to all machines on your local network at the same time.  The restrictions on this are that a broadcast packet must be sent to the same port number on all machines, and the broadcaster machine will not know who successfully received the broadcast.

It is difficult to give actual measurements on how fast UDP packets can be.  There is a tradeoff on how much you are pushing the speed versus what percentage of packets actually make it across the network.  This depends on the network traffic, the speed of the source and destination machines, the source and destination network adapters, the source and destination operating systems, etc.  On a 10 Mbit Ethernet, the theoretical upper limit is about 1 Mbyte per second.  Some network adapters can actually send out packets faster than that, but they will certainly not all make it intact.  There is a program, called spray, which can be used as a rough measuring program.  This program sends out UDP packets as fast as it can to a destination machine which sends back acknowledgements.  The program can then report how many packets were successfully transferred at the given speed.

Communicating with TCP/IP streams
.................................

TCP/IP, or stream, protocol is based on the concept of private, bidirectional, reliable "connections" between two network addresses.  To establish a TCP/IP stream, a program must obtain a source network address and specify a destination address.  A connection is formed between the source and destination.  Any data sent out of the source address will appear verbatim at the destination address, and vice versa.  This will remain true until one side explicitly "breaks" the connection.  In general, the connection is exclusive to the source and destination.  No one else can "break-in" to the connection, or "tap" the connection.  (Technically, of course, this is not true, and anyone determined enough can ruin the most beautiful networking models...)

Like UDP packets, the speed of a TCP/IP stream is hard to predict. Unlike UDP, the stream has a variable transfer rate which is completely beyond the control of the user, because there are no variables for the user to adjust on a TCP/IP stream.  For a rough figure, a 10 Mbit ethernet on a good day can get about 240 Kbytes per second.

Servers and clients
-------------------

Setting up a TCP/IP stream connection requires some planning.  Both ends of the connection must be running, but one side must initiate the connection.  Thus there must be some agreement beforehand as to who will be initiating the connection, and who will be waiting for the connection to be initiated.  The side which waits for connections is called a server, and is said to be listening for connections.  The side which initiates the connection is called a client and is said to connect to the server.

The network support for TCP/IP streams is based on this server/client model, and provides the server end with a great deal of flexibility. Servers are inherently able to allow connections from multiple clients. Also, a server may accept a connection from any TCP/IP client (in fact, it is difficult for a server to refuse a connection).

This model leads to two important generalizations about servers.  Most servers which are in general use are stable, and will use a fixed port number which is stated in /etc/services.  The second generalization is that servers are designed to be passive entities. They usually spend most of their time waiting for clients to connect and disconnect, and will usually act only in response to a direct request from a client.

Of course, these generalizations are not the rule, and variations on this model are just as easy to implement as the model itself.  Servers need not be stable with a fixed port numbers --- they can be "transient", with port numbers assigned by the system (as long as their clients are informed what the port number is).  Servers also need not be passive --- they can go and perform their own activities while also monitoring their client connections.  And, there is nothing preventing a combination of servers and clients from being used within the same program, as long as each of the servers is using a different port number.

Telnet servers and clients
--------------------------

Telnet is a subset of TCP/IP communication in which both parties agree to limit their communication to human-readable ASCII, rather than arbitrary data structures.  Aside from this agreement, telnet servers and clients are no different from normal TCP/IP servers and clients.

Telnet communication tends to be a slow protocol, because representating data in ASCII takes more bytes to transfer over the network, and there must be conversion between data values and ASCII strings.  However, the big advantage to telnet communication is that a human being can connect to a telnet server, type ASCII strings to the server, and receive the server's reply in ASCII.  A parallel advantage is the possibility of a "general telnet client", which can connect to any telnet server because this client knows how to communicate with any telnet client.  In fact, such a general client exists, as a program called telnet.  To use this program simply run it with the hostname and port number of the server you want to talk to (you can supply a service name in place of a port number).

Dtype servers and clients
-------------------------

Dtype servers and clients are similar to telnet servers and clients, except that the communication protocol they have agreed to use is based on a data structure called a Dtype.  Dtypes are more efficient to transfer over the network and parse into data than ASCII, and Dtypes can take advantage of structure in the data.  However, Dtypes also have a human-readable representation which means that a user can type in Dtypes to send to a Dtype server, and can read the server's response.

Like telnet, the Dtype server/client protocol allows for a general Dtype client.  This client is called Dtypecl, and is very similar to telnet, except that it expects to have Dtypes typed into it, and will print Dtypes back out.

Polling and blocking
--------------------

Event-driven programming is a programming style in which a program sets up handlers for events, then sits back and waits for those events to occur.  As each event occurs, the handler appropriate to that event is activated and does its thing, after which the program goes back to waiting for events.

Many network programs, especially servers, will follow this model of event-driven programming.  A server, for example, spends most of its time waiting for clients to connect, disconnect, and send requests to it.  A server might also be doing other things, like listening for a signal from a presence detector, or occasionally checking the value of a thermometer.

The heart of an event-driven program is its polling loop.  In this loop, the program checks on every event that it is waiting for, and handles any events that have occurred.  When all the events have been checked, the program returns to the top of the loop and starts all over again.

Implementing a polling loop this way can be inefficient, since most of the time there will be no events and the process will be wasting processor cycles checking nothing.  For this reason, most operating systems provide a blocking mechanism which is a way of telling the operating system exactly what events the program is waiting for.  Given this information, the operating system can block the program from futher execution until one of the specified events has occurred, at which point the program can continue execution.

Blocking should be added to the top of a normal polling loop.  At the top of the loop, the program should tell the operating system all the events it is waiting for, then tell the operating system to block until one of the events has occurred.  After that, the program should continue with its normal polling loop.

The operating system understands events in terms of file descriptors and timeouts.  For example, if a client sends a message, then the file descriptor associated with that client will signal that it has "read activity", which is enough to get past the block.  Therefore all events must be described in terms of "read activity" or "available to write" on a file descriptor, or in terms of a timeout ("if nothing else happens, drop through the block in 10 seconds").

dsys supports blocking by providing a C++ class called dsys_pollblock, which takes care of all the necessary system calls.  The program tells dsys_pollblock object to add file descriptors, timeouts, and other dsys objects to block on, then tells the object to block.  The object returns control to the program when activity is detected on one of the things the object was told to watch out for.

dsys communication stations
===========================

The dsys library supports the given networking model by providing C++ classes called stations.  A station object represents a single interface to the network.

There are several types of dsys stations.  There are UDP stations, each of which represents a single UDP port on the network.  There are Client stations (and their variations), each of which represents a single TCP/IP client connection.  And there are Server stations, which represent TCP/IP servers.  There are actually three different kinds of servers and clients - normal servers and clients, buffered servers and clients, and telnet servers and clients.  Each type of client should only communicate with its type of server.

All dsys stations have one thing in common, which is their blocking mechanism.  Every station has a method called setblocks, which allows the station to set its own file descriptors and timeouts for blocking in a dsys_pollblock object.

dsys class hierarchy
********************

There are several classes in the dsys hierarchy.  Most of these are the dsys stations.  All dsys stations are derived from a common base class called dsysStation.  This base class provides the virtual function setblocks, which lets any dsysStation to set its own polling blocks.  Any user classes which wish to take advantage of this feature should also be subclasses of dsysStation.

The three main subclasses of dsysStation are dsys_udp, dsys_client, and dsys_server, representing UDP stations, and TCP/IP servers and clients.  As explained later, a dsys_server is actually a collection of dsys_clients.

The variations of dsys_client and dsys_server are implemented as subclasses.  dsys_bufclient and dsys_tclient are subclasses of dsys_client, and dsys_bufserver and dsys_tserver are subclasses of dsys_server.

dsys_pollblock is a class which is separate from the dsysStation hierarchy.  This class does assume that some of its arguments will be dsysStations, or subclasses of dsysStations, since it calls the setblocks methods in those arguments.

There are two other classes which are used internally by the dsys libraries, but are available in case anyone finds them useful.  These classes are dsysBuf and dsysQueue.  They are not part of any of the dsys hierarchies.

Common dsys routines and classes
********************************

In addition to the dsys classes, the following general routines are also included:

short dsysHtons(int VAL)
     Converts an integer VALS to a "network order" short.  The value of this network order short can be converted to the original value using dsysNtohs() on any machine, regardless of the byte orderings of the machine.

     Incidentally, dsys defines network order to be big-endian.

short dsysNtohs(int VAL)
     Converts an integer VAL from "network order" back to its original short value.

long dsysHtonl(int VAL)
     Same as dsysHtons(), except that it is for long integers.

long dsysNtohl(int VAL)
     Same as ntohs(), except that it is for long integers.

long dsysHtonf(double VAL)
     Converts a floating point value VAL to a network ordered long integer.

double dsysNtohf(long VAL)
     Converts a network ordered long integer produced by dsysHtonf() back to its original floating point value, no matter the floating point representations and byte orderings of the machines.

long dsysGetServByName(const char *NAME, const char *PROTO)
     Looks up the given service NAME in the machine's service table (usually stored in the file /etc/services) and returns the port number associated with that service.  If the service is not found, then 0 is returned.  PROTO must point to a string specifying the protocol for the service that you are looking for, usually either "tcp" or "udp".

readbuf: /* Reads an entire buffer of size into buf from socket sock. Returns 0 if successful read, or returns -1. If a 0 length message was received, indicating that the client closed the socket, then return a 1. */

readpending: /* Checks to see if a message is waiting to be read on socket sock. If so, a 1 is returned, otherwise a 0 is returned.  If an error occurred, an error message is printed and -1 is returned. */

writepending: /* Checks to see if socket sock is cleared for writing.  If so, a 1 is returned, otherwise a 0 is returned.  If an error occurred, and error message is printed and -1 is returned. */

There are also a few routines for dealing with dsys_udpAddr, which is a compact structure used by the network system to describe a network address.  A few of the classes use these addresses, so the following routines are provided to manipulate dsys_udpAddrs and convert back and forth between dsys_udpAddr and normal hostname/port network addresses.

dsys_udpAddrCopy(dsys_udpAddr *DEST, dsys_udpAddr *SRC)
     Copies the address in SRC to DEST.

dsys_udpAddrEqual(dsys_udpAddr *DEST, dsys_udpAddr *SRC)
     Returns 1 if SRC and DEST represent the same network address, 0 if not.

dsys_udpGetAddr(const char *HOSTNAME, int PORT, dsys_udpAddr *DSUA)
     Converts a HOSTNAME/PORT network address into a dsys_udpAddr address, DSUA.  If HOSTNAME is specified as "*", then the resulting address will be a broadcast address, meaning that any messages sent to that address will be sent to all machines on the network.

dsys_udpReadAddr(dsys_udpAddr *DSUA, char *HOSTNAME, int *PORT)
     Converts a dsys_udpAddr address DSUA into a HOSTNAME/PORT network address.

In addition to these routines, there are two "miscellaneous" classes which are used internally by dsys, but that users are welcome to as well.

dsys buffer
===========

A dsysBuf is a class representing a circular buffer of characters.  It is used by the dsys_bufclient class.  The class operates by keeping track of "head" and "tail" pointers, but never explicitly copies data into or out of the buffer.  Instead, it informs its user where to put or get data and how much to put or get.  This avoids any unnecessary copying.

Constructors:

dsysBuf dsb(long PSIZE)
     Initializes dsb to be a new dsysBuf with space for PSIZE characters.

Methods (assume DSB is a dsysBuf):

dsb.gethead()
     Returns an unsigned char * which points to where new data should be put into the buffer.  The amount of data that can be written can be found using writeavail().  After writing data, the += operator should be used to advance the head.

dsb.gettail()
     Returns an unsigned char * which points to where data should be read from the buffer.  The amount of data available for reading can be found using readavail().  After reading data, the -= operator should be used to advance the tail.

dsb.writeavail()
     Returns a long which indicates the amount of data which can be written into the buffer CONTINUOUSLY.  Since the buffer is circular, more than one continuous write may be needed to fill up the buffer.  The buffer is full only when writeavail() returns 0.

dsb.readavail()
     Returns a long which indicates the amount of data which can be read CONTINUOUSLY from the buffer.  Since the buffer is circular, more than one continuous read may be needed to read all the data from the buffer.  The buffer is empty only when readavail() returns 0.

dsb.totalreadavail()
     Returns a long which indicates the total amount of data which can be read from the buffer.  Note that since the buffer is circular and noncontinuous, multiple reads may be necessary to get out all the data (see readavail).

dsb += PSIZE;
     Notifies dsb that PSIZE (an int) bytes have been added to the buffer.  Advances the head pointer by the specified amount.  The head pointer will wrap around the end of the buffer.  If the head pointer crosses the tail pointer, an overflow error is reported.  This should be used after writing into the buffer.  Returns a reference to dsb.

dsb -= PSIZE;
     Notifies dsb that PSIZE (an int) bytes have been read from the buffer.  Advances the tail pointer by the specified amount.  The tail pointer will wrap around the end of the buffer.  If the tail pointer crosses the head pointer, an underflow error is reported.  This should be used after reading from the buffer.  Returns a reference to dsb.

dsb.flush()
     Flushes all the data from dsb, and returns a reference to dsb.

dsb.print(POS)
     Prints dsb to ostream & POS.

POS << dsb
     Also prints dsb to ostream & POS, and returns a reference to dsb.

dsys string queue
=================

A dsysQueue is a FIFO queue of strings, used by dsys_tclient.

Constructors:

dsysQueue dsq(long PSIZE)
     Initializes dsq to be a new dsysQueue with space for PSIZE strings.

Methods (assume DSQ is a dsysQueue):

dsq.flush()
     Flushes all the strings out of the queue.  Returns a reference to dsq.

dsq.next()
     Returns a char * pointing to the next string at the front of the queue.

dsq.add(const char *PSTR)
     Adds a new string PSTR to the end of the queue.  Space for the string is allocated and the string is copied into that space.  Returns a reference to dsq.

dsq.remove()
     Removes one string from the front of the queue and returns a reference to dsq.  To get the value of that string, use next() before calling remove().  Also keep in mind that remove() will also free that string, so make sure you're done with the string before calling remove().

dsq.isempty()
     Returns an int which is 1 if the queue is empty, 0 if not.

dsq.print(POS)
     Prints dsq to ostream & POS.

POS << dsq
     Also prints dsq to ostream & POS, and returns a reference to dsq.

dsys poll blocking mechanism
****************************

A dsys_pollblock is a convenient for class which handles all the needs of blocking on file descriptors and timeouts.  To use a dsys_pollblock you would first clear() it, then add all the file descriptors, dsys stations, and timeouts that you would want to block on.  The you would block() the dsys_pollblock, which would cause the program to "go to sleep" until one of the specified events occurred.  After the block() is finished, use examine() to help figure out which event occurred.

Constructors:

dsys_pollblock dpb
     Initializes a new dsys_pollblock.

The following methods tell a dsys_pollblock what to block on (assume DPB is a dsys_pollblock):

dpb.clear()
     Clears all the file descriptors and timeouts from dpb.  Should be called before you start adding file descriptors and timeouts to dpb.

dpb.addread(int PFD)
     Adds file descriptor PFD to dpb, so that dpb will react to any new data available to be read on PFD.  Returns a reference to dpb.

dpb.addwrite(int PFD)
     Adds file descriptor PFD to dpb, so that dpb will react to PFD being available for writing.

dpb.addex(int PFD)
     Adds file descriptor PFD to dpb, so that dpb will react to an "exceptional" condition pending on PFD.

dpb.addstation(dsysStation &PDSS)
     Adds a dsysStation PDSS to dpb, so that dpb will react to any of the conditions that the dsysStation requires.  Causes the station pdss to add its list of descriptors and timeouts to the dsys_pollblock.  Pdss must have a method "setblocks" which takes a dsys_pollblock& as an argument.

dpb.addtimeout(unsigned long PTIMEOUT)
     Adds a timeout to pdb, so that dpb will react at the specified PTIMEOUT.  PTIMEOUT is an absolute time specified in milliseconds.  The current time can be obtained with nowtime(). So, to time out 5 seconds from now, use dpb.nowtime()+5000.  If a timeout has already been added to dpb, then the smaller timeout will be used.

dpb.nowtime()
     Returns an unsigned long which is the current time in milliseconds.

dpb.block()
     Causes the program to "go to sleep" until one of the conditions entered into dpb is met.  Returns an int, which is 0 if activity was detected, DSYS_TIMEOUT if a timeout occurred, or -1 if an error occurred. Blocks until activity is detected on one of the file descriptors or until a timeout occurs.  Returns 0 if activity was detected, DSYS_TIMEOUT if a timeout occurred, or -1 if an error occurred.

After a block() has successfully completed, the following routines can be used to figure out which event caused the block.  Note, if the block was triggered by a timeout, then block() will have already returned DSYS_TIMEOUT.  Also note that an active file descriptor refers to a file descriptor which triggered the dsys_pollblock.

dpb.examine()
     This routine should always be called immediately after a block() completes.  Otherwise, the following routines will not be valid. Examines the dsys_pollblock, assuming that it already has made it through a block.  Calculates the amount of time that has elapsed since the block was initiated, and also places active file descriptors into the arrays, rdact/wract/exact, whose lengths are stored in numrdact/numwract/numexact.

dpb.getelapsed()
     Returns an unsigned long which indicates the number of milliseconds which have elapsed between the block() command and the examine() command.

dpb.getnumact()
     Returns an int which is the number of active file descriptors (the number of file descriptors where activity was detected).

dpb.numrdact()
     Returns an int which is the number of file descriptors active for reading.

dpb.rdact(int PDFNUM)
     Returns an int which is the active read file descriptor number PDFNUM (must be between 0 and numrdact()).

dpb.inread(int PFD)
     Returns 1 if PFD is one of the active read file descriptors, 0 if not.

dpb.numwract()
     Returns an int which is the number of file descriptors active for writing.

dpb.wract(int PDFNUM)
     Returns an int which is the active write file descriptor number PDFNUM (must be between 0 and numwract()).

dpb.inwrite(int PFD)
     Returns 1 if PFD is one of the active write file descriptors, 0 if not.

dpb.numexact()
     Returns an int which is the number of file descriptors active for exceptions.

dpb.exact(int PDFNUM)
     Returns an int which is the active exception file descriptor number PDFNUM (must be between 0 and numexact()).

dpb.inex(int PFD)
     Returns 1 if PFD is one of the active exception file descriptors, 0 if not.

dsys UDP stations
*****************

A dsys_udp station represents a single UDP port on the network.  A dsys_udp station can send messages to other UDP ports on the network, and can receive messages from other UDP ports.  Destinations and sources are specified as dsys_udpAddr addresses, which are discussed in the Common dsys routines and classes.

Constructors:

dsys_udp dsu
     Initializes a new dsys_udp station.  The port number of the station is chosen by the system.

dsys_udp dsu(int PORT)
     Initializes a new dsys_udp station, which will exist at the specified PORT.  If the PORT is already being used by someone else, an error message will be printed out.

Methods (assume DSU is a dsys_udp):

dsu.getport()
     Returns an int which is the port number being used by dsu.

dsu.sendto(void *PBUF, int PSIZE, dsys_udpAddr *PDSUA)
     Sends one packet of information pointed to by PBUF, of PSIZE bytes, to the destination specified by PDSUA.  The limit for a packet size is about 1500 bytes.  The behavior if this packet size is exceeded is undefined.  Returns an int which is 0 if successul, -1 if an error occurred.

dsu.connect(dsys_udpAddr *PDSUA)
     "Connects" dsu to the destination address specified by PDSUA.  All this does is provide a shortcut for sending messages --- any messages sent hereafter using send() will be sent to the connected address.  Connect() can be called and recalled at any time, and sendto() can still be used to send messages to places other than the connected destination.  Returns a dsys_udpAddr & which is a reference to PDSUA.

dsu.addr()
     Returns a dsys_udpAddr & which is the address of whomever dsu is connected to.  If dsu is not connected, then this will return something meaningless.

dsu.send(void *PBUF, int PSIZE)
     Same as sendto(), except that the message is sent to the destination to which dsu has been connect()ed.  If dsu has not been connected to any destination, then -1 is returned.

dsu.enableBroadcast()
     Obtains permission for dsu to send out broadcast packets.  This does not mean that all packets sent out by dsu will be broadcast packets, it simply means that those sent to broadcast addresses ("*" as hostname) will be allowed to go through.  This must be called before any broadcast packets will be allowed to be sent.

dsu.recv(void *PBUF)
     Waits until a packet is sent to the port that dsu is listening to, then reads in the packet.  The data for the packet is read into PBUF, and the size of the packet is returned.  If an error occurred, -1 is returned.  Note that PBUF should point to a space large enough to hold a packet (2048 is a safe size).

dsu.recvfrom(void *PBUF, dsys_udpAddr *PDSUA)
     Same as recv(), except that the address of the packet's source is placed into PDSUA.

dsu.poll()
     Checks the status of dsu and returns an int, which is DSYS_MSGRCVD if a packet has been sent to dsu (the packet must be read in with recv()/recvfrom()), DSYS_NONE if nothing has happened, or -1 if an error occurred.

dsu.setblocks(dsys_pollblock &PDPB)
     Sets all the file descriptors and timeouts required for blocking for dsu in PDPB.  This routine is automatically called by the dsys_pollblock routine addstation().

dsu.print(POS)
     Prints dsu to ostream & POS.

POS << dsu
     Also prints dsu to ostream & POS, and returns a reference to dsu.

dsys client stations
********************

A dsys_client can handle a single TCP/IP connection to a dsys_server.  A dsys_client may be created with a specific server to connect to, or created unconnected and later connected to a server.  The dsys_client and dsys_server communicate with messages, which are packets of bytes with no defined limits on length.  Messages are sent and received on a one for one basis --- messages can not be partially received.

Constructors:

dsys_client dsc
     Creates a dsys_client dsc which is not connected to any server. Dsc may later be connected to a server using the connect() command.

dsys_client dsc(const char *PHOSTNAME, int PPORT)
     Creates a dsys_client dsc which is connected to the server listening at the specified PHOSTNAME and PPORT.  If there is no server there, then an error message is printed.  If the specified machine is down, then there will be a timeout period before the error message is printed out.

dsys_client dsc(const char *PHOSTNAME, const char *PSERVNAME)
     Same as the above constructor, except that the port number is determined by looking up the service specified by PSERVNAME.  Usually the table of services and port numbers is kept in the file /etc/services.

Methods (assume DSC is a dsys_client):

dsc.write(void *PBUF, long PSIZE)
     Writes one message PSIZE bytes from buffer PBUF to the server.  The routine waits until the write is completed before returning.  However, this does not necessarily mean that when the routine has returned, the server has received the entire message. Returns 0 if successful, -1 if an error occurred.

dsc.read(void *PBUF)
     Reads one entire message from the server into buffer PBUF.  The routine does not return until an entire message has been read.  Returns the number of bytes in the message, or -1 if an error occurred.

dsc.isconnected()
     Returns an int 1 if dsc is connected to a server, 0 if not.

dsc.gethostname()
     Returns a char * which points to the hostname of the server to which dsc is connected.

dsc.getport()
     Returns a int which is the port of the server to which dsc is connected.

dsc.connectedto(const char *PHOSTNAME, int PPORT)
     If dsc is connected to the specified PHOSTNAME and PPORT, then 1 is returned, otherwise 0 is returned.

     ;;;

     Returns 1 if the dsys_client is connected to the specified phostname, pport, 0 otherwise.  Note that only the characters which are specified are compared, so looking for "repo-man" will match against "repo-man.media.mit.edu" or "repo-manniken". */

     accept: Accepts a connection from server socket psock and establishes this dsys_client to handle that connection.  Returns 0 if successful, -1 if an error occurred.

dsc.disconnect()
     If dsc is connected to a server, this will break the connection.  Dsc can always be reconnected using the connect() routine.

dsc.connect(const char *PHOSTNAME, int PPORT)
     Connects dsc to the server at the specified PHOSTNAME, PPORT.  If dsc is already connected to a server, then that connection is broken first.  Returns 0 if the connection is successful, -1 if an error occurred.

     ;;;

     Connects the dsys_client to the destination specified by phostname, pservname.  If the dsys_client is currently connected to something, it is disconnected first.  Returns 0 if successful, -1 if an error occurred.

     Connects the dsys_client to the destination specified by phostname, pport.  If the dsys_client is currently connected to something, it is disconnected first.  Returns 0 if successful, -1 if an error occurred.

dsc.connect(const char *PHOSTNAME, const char *PSERVNAME)
     Same as the above connect() call, except that a service name PSERVNAME is supplied instead of a port number.

dsc.poll([long *PMSGSIZE])
     Checks the status of dsc and returns an int, which is DSYS_MSGRCVD if a message has been sent to dsc, DSYS_LOGOUT if the server to which the client was connected broke the connection, DSYS_NONE if nothing has happened, or -1 if an error occurred. If a message has been sent to dsc, then it may be read in with read().  PMSGSIZE is an optional argument.  If it is supplied, and a message has been sent to dsc, then PMSGSIZE is filled with the number of bytes in the incoming message.  Polls the dsys_client station for any new activity.  If no message has been received, then DSYS_NONE is returned.  If a message has been received, then DSYS_MSGRCVD is returned.  If pmsgsize is supplied, then it is filled with the size of the message.  The message can be read using read().  If the other side disconnected, DSYS_LOGOUT is returned.  If an error occurs, -1 is returned.

dsc.setblocks(dsys_pollblock &PDPB)
     Sets all the file descriptors and timeouts required for blocking for dsc in PDPB.  This routine is automatically called by the dsys_pollblock routine addstation().

dsc.print(POS)
     Prints dsc to ostream & POS.

POS << dsc
     Also prints dsc to ostream & POS, and returns a reference to dsc.

dsys server stations
********************

dsys_servers and dsys_clients are very similar.  The only real differences are that a dsys_server can handle connections from multiple sources, and a dsys_server can "accept" connections from dsys_clients.

In fact, a dsys_server contains an array of dsys_clients to handle its connections.  When the dsys_server is first created, all the dsys_clients in it are created as unconnected.  When a remote client connects to the dsys_server station, the server will direct the incoming connection to one of its unconnected dsys_clients.  So, in the end, you'll have two dsys_clients connected to each other, but one of the dsys_clients will be part of an array of dsys_clients in the dsys_server.

The dsys_clients in the array in the dsys_server are given client numbers, starting with 0.  If the program wants to receive a message or send a message to one of the clients being handled by the dsys_server, the program tells the dsys_server which client number it wants to talk to.  The dsys_server responds by returning a reference to the dsys_client which represents the connection to that client.  The program can then use any of the dsys_client commands to communicate with that client.

Because a dsys_server is really an array of dsys_clients, there is no reason why a dsys_server can't use one of its dsys_clients to connect to another dsys_server.  This is called logging in to another dsys_server.

Constructors:

dsys_server dss([int PPORT, [int PNUMCL]])
     Creates a new dsys_server which is ready to handle clients and accept connections.  PPORT is an optional argument which specifies what port the dsys_server will be listening on.  If it is not supplied, then a port number will be picked for it.  Usually a port number should be supplied, just because clients will then know what port to connect to.  PNUMCL is also an optional argument which specifies the maximum number of clients which will be allowed to connect to this server at any one time.  The default is 64.

dsys_server dss([const char *PSERVNAME, [int PNUMCL]])
     Same as the above constructor, except that PSERVNAME specifies a service name instead of a port number.  The service name is looked up in the services table (usually stored in the file /etc/services) and converted to a port number.  This is the "most correct" way to create a server.

Methods (assume DSS is a dsys_server):

dss.getport()
     Returns an int which is the port number that dss is using to listen for incoming connections.  dsys_clients which connect to this port on this machine will be handled by this dsys_server.

dss[PCLNUM]
     Returns a dsys_client & to client number PCLNUM in dss. This dsys_client can be used like any other dsys_client --- you can write() to it, you can read() from it.  You should avoid connect()ing and disconnect()ing with it, since this will probably confuse the dsys_server, which provides its own way of doing these things.

dss.poll([int *PCLNUM, [long *PMSGSIZE]])
     Checks the status of dss and all the dsys_clients in dss, and returns an int, which is DSYS_LOGIN if a client connected to the server, DSYS_LOGOUT if a client disconnected from the server, DSYS_MSGRCVD if a message has been sent from one of the clients, DSYS_NONE if nothing has happened, or -1 if an error occurred.  For DSYS_LOGIN, DSYS_LOGOUT, DSYS_MSGRCVD, the client number where the activity was detected is saved in an internal variable.  If PCLNUM is specified (it is an optional argument), then it is filled with that client number.  That client number can then be supplied to the [] operator to get the actual dsys_client on which the activity occurred.  PMSGSIZE is another optional argument.  If a client is sending a message, the size of the incoming message will be placed into PMSGSIZE.

dss.gethostname()
     Returns a char * which is a pointer to the hostname of the dsys_server.

dss.getclnum()
     Returns the client number on which activity was last detected by poll().

dss.read(void *PBUF, [int PCLNUM])
     Reads one message from the dsys_client specified by PCLNUM. This is the same read as is used for dsys_client, and in fact this call is equivalent to dss[plcnum].read(pbuf).  PCLNUM is actually an optional argument.  If it is not specified, then the last client number on which activity was detected is used.  This would be equivalent to dss[dss.getclnum()].read(pbuf).  Returns the size of the message, or -1 if an error occurred.  Reads one message from the dsys_client connection into buf.  The size of the message is returned. If an error occurs, -1 is returned.

dss.login(const char *PHOSTNAME, int PPORT)
     Causes dss to select one of its unconnected dsys_clients and connects it to the dsys_server specified by PHOSTNAME, PPORT.  Returns the client number that was selected to do the login.  Once this is complete, that client number can be treated like any other connection --- it doesn't matter that there is a dsys_server on the other end of the connection.  If there are no more available dsys_clients, then -1 is returned.

dss.login(const char *PHOSTNAME, const char *PSERVNAME)
     Same as above, except that PSERVNAME is a service name used to obtain the port number.

dss.logout(int PCLNUM)
     Disconnects client number PCLNUM from whomever it is connected to.

     int dsys_server::firstavail(int pmincl) /* Returns the clnum of the first available unconnected slot in the dsys_server.  Returns -1 if there are no more slots. pmincl indicates the first clnum that should be searched. */

dss.slotsavail()
     Returns the number of unconnected dsys_clients in dss.

dss.find(const char *PHOSTNAME, int PPORT)
     Finds which of the dsys_clients in dss is connected to the specified PHOSTNAME, PPORT, and returns the client number of that dsys_client.  If none of the dsys_clients is connected, then -1 is returned.

dss.findorcreate(const char *PHOSTNAME, int PPORT, [int PMINCL])
     This is the same as find, except that if none of the dsys_clients is connected to the specified destination, then a login() is automatically performed to connect a dsys_client to that destination.  If all the dsys_clients are in use, then the dsys_client that was used the longest time ago is disconnected and its space is used for the connection.  PMINCL is an optional argument, which protects any client numbers under PMINCL from being disconnected in this process.

dss.findorcreate(const char *PHOSTNAME, const char*PSERVNAME, [int PMINCL])
     Same as above, except that the port number is specified as a service name in PSERVNAME.

dss.setblocks(dsys_pollblock &PDPB)
     Sets all the file descriptors and timeouts required for blocking for dss in PDPB.  This routine is automatically called by the dsys_pollblock routine addstation().

dss.print(POS)
     Prints dss to ostream & POS.

POS << dss
     Also prints dss to ostream & POS, and returns a reference to dss.

     int dsys_server::LRU(int pmincl) /* Returns the client number of the least recently used client, with the search starting at pmincl. */

dsys buffered client stations
*****************************

Buffered client stations, dsys_bufclients, are very similar to dsys_clients.  The difference is that the write() command buffers its input, so that the data is not actually sent out immediately with the write() command.  Instead, the data is built up into a buffer and sent all at once to the client, either when the buffer is full, or when the buffer is explicitly flushed, or when a write is done to another dsys_bufclient.

Conceptually, data is no longer being sent over in discrete messages, but is instead more like a stream of bytes which are divided up arbitrarily.  What this means is that the read() command must explicitly state how many bytes it wants to read.

The buffered clients are useful in a situation when you want to send out lots of small bits of data, and you don't want each of those small bits to be sent out as separate network messages.

dsys_bufclient is a subclass of dsys_client, which means that all the constructors and methods available to DysClient are also available to dsys_bufclient, except for the differences mentioned below:

Methods (assume DSBC is a dsys_bufclient):

dsbc.read(void *PBUF, long SIZE)
     Works the same as read() for dsys_client, except that the SIZE argument MUST be supplied.  The routine will not return until the specified amount of data has been read into PBUF, or an error occurred (returning -1).

dsbc.flush()
     Causes all the data write() has placed into the buffer to be written out immediately to the destination.  You should perform this call when it's time to make sure all the data has gone over the network.

dsys buffered server stations
*****************************

A dsys_bufserver is the same as a dsys_server, except that it uses dsys_bufclients instead of dsys_clients.  A dsys_bufserver is a subclass of dsys_server, so all the constructors and methods are the same as for dsys_server, except as described below:

Methods (assume DSBS is a dsys_bufserver):

     dsys_bufserver::dsys_bufserver(int pport, int pnumcl) /* Initializes the dsys_bufserver to listen at port pport, with room for numcl simultaneously connected clients.  If pport is 0, then a port number is chosen and put into the port field of the dsys_bufserver. */

     dsys_bufserver::dsys_bufserver(const char *pservname, int pnumcl) /* Initializes the dsys_bufserver to listen at the port specified by the service at pservname, with room for numcl simultaneously connected clients. */

     int dsys_bufserver::read(void *pbuf, dsys_int32 psize, int pclnum) /* Reads one message from the client number pclnum, or if pclnum is not supplied, a message is read from the client number which most recently had activity according to poll().  The size of the message psize must be specified.  Returns 0 if successful, -1 if an error occurred. */

     int dsys_bufserver::read(void *pbuf, dsys_int32 psize, int pclnum) /* Reads one message from the client number pclnum, or if pclnum is not supplied, a message is read from the client number which most recently had activity according to poll().  The size of the message psize must be specified.  Returns 0 if successful, -1 if an error occurred. */

dsbs[PCLNUM]
     Same as the [] operator for dsys_server, except that dsys_bufclient & is returned.

dsbs.read(void *PBUF, long PSIZE, [int PCLNUM])
     Same as read() for dsys_server, except that the PSIZE argument MUST be supplied.  The routine will not return until the specified amount of data has been read into PBUF, or an error occurred (returning -1).

dsys Telnet client stations
***************************

A dsys_tclient is very similar to a dsys_client, except that only strings may be read and written instead of arbitrary data.

In addition, the dsys_tclient offers a "string queue", so that writes can be directed to this queue and not actually sent out the network until the dsys_tclient is poll()ed.  The reason for this is that if the destination of the writes is too busy to read the data, then the writes might back up and cause the sender to wait.  By using the queue, the sending of the strings is deferred until the destination is free to receive the strings.

A dsys_bufserver is a subclass of dsys_server, so all the constructors and methods are the same as for dsys_server, except as described below:

Methods (assume DSTC is a dsys_tclient):

dstc.poll()
     Same as poll() for dsys_client, except that PMSGSIZE is not available as an optional argument.

dstc.read(char *STR)
     Reads a single string into STR.  The string is read up until a zero terminator, or until a newline character is encountered.  The newline is replaced with a zero terminator.  Returns 0 if successful, -1 if an error occurred.

dstc.write(const char *STR)
     Writes a single string STR to dstc.  Returns 0 if successful, -1 if an error occurred.

dstc.writequeue(const char *STR)
     Writes a single string STR to the string queue.  The string is not actually sent to the destination until dstc is poll()ed.

dsys Telnet server stations
***************************

A dsys_tserver is the same as a dsys_server, except that it uses dsys_tclients instead of dsys_clients.  A dsys_tserver is a subclass of dsys_server, so all the constructors and methods are the same as for dsys_server, except as described below:

Methods (assume DSTS is a dsys_tserver):

dsts[PCLNUM]
     Same as the [] operator for dsys_server, except that dsys_tclient & is returned.

dsts.read(char *PBUF, [int PCLNUM])
     Same as read() for dsys_server, except that the input is read into a string rather than a void *.

Index of functions
******************

The following are the shorthands for the various classes:

dsb
     dsysBuf

dsq
     dsysQueue

dpb
     dsys_pollblock

dsu
     dsys_udp

dsc
     dsys_client

dss
     dsys_server

dsbc
     dsys_bufclient

dsbs
     dsys_bufserver

dstc
     dsys_tclient

dsts
     dsys_tserver