From 40ecd80e098a6b2febf0fdf336c84cae726b9aef Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 22 May 2001 06:08:00 +0000 Subject: [PATCH] Initial revision of cocktail multiplayer networked gaming platform. (Far from complete.) git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@2 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- build.xml | 54 +++++ docs/design.txt | 55 +++++ docs/presents/design.txt | 38 ++++ docs/presents/notes.txt | 94 +++++++++ src/java/com/threerings/presents/Log.java | 38 ++++ .../threerings/presents/client/Client.java | 197 ++++++++++++++++++ .../presents/client/ClientObserver.java | 72 +++++++ .../presents/client/Communicator.java | 175 ++++++++++++++++ .../presents/client/LogonException.java | 20 ++ .../com/threerings/presents/dobj/Event.java | 21 ++ .../presents/io/ObjectStreamException.java | 19 ++ .../threerings/presents/io/TypedObject.java | 49 +++++ .../presents/io/TypedObjectFactory.java | 77 +++++++ .../threerings/presents/net/AuthRequest.java | 57 +++++ .../threerings/presents/net/Credentials.java | 33 +++ .../presents/net/DownstreamMessage.java | 72 +++++++ .../presents/net/EventNotification.java | 48 +++++ .../threerings/presents/net/FetchRequest.java | 55 +++++ .../presents/net/ForwardEventRequest.java | 55 +++++ .../presents/net/LogoffRequest.java | 23 ++ .../threerings/presents/net/PingRequest.java | 23 ++ .../presents/net/SubscribeRequest.java | 56 +++++ .../presents/net/UnsubscribeRequest.java | 56 +++++ .../presents/net/UpstreamMessage.java | 105 ++++++++++ .../presents/net/UsernamePasswordCreds.java | 52 +++++ 25 files changed, 1544 insertions(+) create mode 100644 build.xml create mode 100644 docs/design.txt create mode 100644 docs/presents/design.txt create mode 100644 docs/presents/notes.txt create mode 100644 src/java/com/threerings/presents/Log.java create mode 100644 src/java/com/threerings/presents/client/Client.java create mode 100644 src/java/com/threerings/presents/client/ClientObserver.java create mode 100644 src/java/com/threerings/presents/client/Communicator.java create mode 100644 src/java/com/threerings/presents/client/LogonException.java create mode 100644 src/java/com/threerings/presents/dobj/Event.java create mode 100644 src/java/com/threerings/presents/io/ObjectStreamException.java create mode 100644 src/java/com/threerings/presents/io/TypedObject.java create mode 100644 src/java/com/threerings/presents/io/TypedObjectFactory.java create mode 100644 src/java/com/threerings/presents/net/AuthRequest.java create mode 100644 src/java/com/threerings/presents/net/Credentials.java create mode 100644 src/java/com/threerings/presents/net/DownstreamMessage.java create mode 100644 src/java/com/threerings/presents/net/EventNotification.java create mode 100644 src/java/com/threerings/presents/net/FetchRequest.java create mode 100644 src/java/com/threerings/presents/net/ForwardEventRequest.java create mode 100644 src/java/com/threerings/presents/net/LogoffRequest.java create mode 100644 src/java/com/threerings/presents/net/PingRequest.java create mode 100644 src/java/com/threerings/presents/net/SubscribeRequest.java create mode 100644 src/java/com/threerings/presents/net/UnsubscribeRequest.java create mode 100644 src/java/com/threerings/presents/net/UpstreamMessage.java create mode 100644 src/java/com/threerings/presents/net/UsernamePasswordCreds.java diff --git a/build.xml b/build.xml new file mode 100644 index 000000000..02cb9c71b --- /dev/null +++ b/build.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/design.txt b/docs/design.txt new file mode 100644 index 000000000..3d9e1bbc5 --- /dev/null +++ b/docs/design.txt @@ -0,0 +1,55 @@ +Cocktail Mk3 -*- outline -*- + +* What is it? +Cocktail is a platform on which multiplayer networked games may be +developed. The platform is (extremely) loosely based around the idea of a +cocktail party. For the most part, this just gives us a useful context +from which to choose names. The basic service of the Cocktail platform is +a simple information sharing mechanism based on the concept of distributed +objects. This layer is called "Cher" for reasons outlined in the Cher +design notes. + +A distributed object has a set of subscribers. Whenever a modification is +made to that object, all of that object's subscribers are notified. This +has the beneficial effect of providing a framework in which to conceive a +distributed application based on who needs to know what. If information +need be shared among a set of clients, a distributed object can be created +to represent that information and those clients would subscribe. Then +modifications to that object (as well as simple notifications) can be +easily delivered to those clients and those clients only. + +As one discovers after further use of the system, the distributed object +concept turns out to be a useful one for other reasons when designing +distributed applications (a subset of which are multiplayer networked +games). The distributed objects fit nicely as the model in the model, +view, controller pattern as well as into other useful patterns. + +The primary value of the design is to bring the level of abstraction up +from network connections and packets, to objects and events. + +** Getting the party started +Atop the distributed object layer, we further develop the concept of the +cocktail party. Parties tend to take place in rooms and be attended by +groups of people. This is the essence of the next layer of the system: a +framework for providing rooms, with occupants and mechanisms for the +people to move between those rooms. Within the rooms, we provide some +useful basic services like the ability to chat among the occupants of the +room, as well as some non-room-specific facilities like person to person +messaging from anywhere in the system and a location directory. + +Not all games developed with the platform will want to use the room +concept, therefore we attempt at this layer and in all subsequent layers +to decouple our services as much as possible. This allows a game with +special needs or for whom our half-baked analogies don't apply, to +leverage some of the useful services without having to bend their design +in uncomfortable ways or hack up some additional interface to the services +we provide. + +** Let's play +From here, we branch off into all sorts of interesting directions based on +the different kinds of games that are implemented with the system. We +provide matchmaking lobbies, an extension of the room concept to the game +room, on top of which is provided a framework for managing generic +turn-based games, and various other useful services. Again the philosophy +is to provide consistently designed, but decoupled services that can be +used within and along side whatever design works best for your game. diff --git a/docs/presents/design.txt b/docs/presents/design.txt new file mode 100644 index 000000000..1f5d78fbb --- /dev/null +++ b/docs/presents/design.txt @@ -0,0 +1,38 @@ +Cher Mk3 Design -*- outline -*- + +* Why Cher? +The basic function of this layer is to allow the sharing (Cher-ing) of +information among different nodes in the network. Plus, I don't think Cher +has ever had a software system named after her and it's high time. Imagine +Cher as the social lubricant that allows the party goers to communicate. + +* Client components + +** DObjectManager +Manages object proxies; converts value change requests into events, +forwards them via the iomgr; dispatches events on incoming queue; reaps +proxies when last subscriber goes away + +** UI (AWT/Swing) +Standard AWT/Swing UI + +** UI (Controller)? +Provides a paradigm of controllers and commands; code can post commands +back to the controller queue for later execution; UI elements structured +to automatically generate commands; will probably opt not to use this in +favor of Swing's built-in paradigms + +** I/O Manager +*** Reader +Reads incoming data from the socket; decodes messages; posts events to +domgr queue; notifies object subscription penders (this should be done +asychronously) + +*** Writer +Encodes object subscription and event forwarding requests; writes them to +the outgoing socket + +** Client object +Informs exo-client about connection state changes; provides interface to +connection + authentication (logon) and disconnection (logoff); provides +access to omgr and client dobj diff --git a/docs/presents/notes.txt b/docs/presents/notes.txt new file mode 100644 index 000000000..a3ddfbc09 --- /dev/null +++ b/docs/presents/notes.txt @@ -0,0 +1,94 @@ +Cher Mk3 Notes -*- outline -*- + +* Server components: + + Connection manager + + Client manager + + Event dispatch thread (DObjectManager) + + Server side services? + + Room/Game managers + + Info repository for shared info? + +* Client components: + + Network client (auth + DObject services) + + Applet shell + + Panel management + + Components: chatbox, occupant info, turn indicator? + + Room/Game panels, controllers, etc. + +* Client detail: + + Supply credentials (username, password) + + Establishes, maintains connection: + login (connect), logout (disconnect) + + Creates, manages client-side DObjectManager + +* Events in the client will be dispatched on the AWT thread +- Provide pluggable "event dispatcher" on the client +- Allow extension of Event as DispatchableEvent so that event objects + themselves can extend Runnable and be popped right into + SwingUtil.invokeLater(). + +* Client perform all network ops on own thread, will call back to main + code through Observer interface to notify of state changes in the + authentication process/connectedness: + + public interface ClientObserver + { + public void didConnect (); + public void connectionFailed (); + public void didLogon (); + public void logonFailed (); + public void didDisconnect (); + public void didLogoff (); + } + +* Three Rings Client flowlist: + + Instantiate client + Obtain auth info + Obtain room info + Authenticate + +* Distributed objects are defined like a class with a set of public data + members which is then converted into an actual class with get/set + methods for each member. + + public dclass GameObject + { + public int[] players; + public String description; + } + + becomes + + public class GameObject extends DObject + { + public void setPlayers (int[] players); + public void setPlayersAt (int index, int value); + public int[] getPlayers (); + public int getPlayersAt (int index); + + public void setDescription (String description); + public String getDescription (); + } + +* Classes: + + DObjectManager + DObject + DEvent + DTransaction + Subscriber + +* Events: + + ATTRIBUTE_CHANGED + ATTRIBUTES_CHANGED + ATTRIBUTES_CHANGED_SPLIT + ELEMENT_CHANGED + OBJECT_DESTROYED + + ELEMENT_ADDED + ELEMENT_REMOVED + + OID_ADDED + OID_REMOVED diff --git a/src/java/com/threerings/presents/Log.java b/src/java/com/threerings/presents/Log.java new file mode 100644 index 000000000..dbc43ec4e --- /dev/null +++ b/src/java/com/threerings/presents/Log.java @@ -0,0 +1,38 @@ +// +// $Id: Log.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher; + +/** + * A placeholder class that contains a reference to the log object used by + * the Spine package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("robodj"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/presents/client/Client.java b/src/java/com/threerings/presents/client/Client.java new file mode 100644 index 000000000..013c0f395 --- /dev/null +++ b/src/java/com/threerings/presents/client/Client.java @@ -0,0 +1,197 @@ +// +// $Id: Client.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.client; + +import java.util.ArrayList; +import java.util.List; + +import com.samskivert.cocktail.cher.Log; +import com.samskivert.cocktail.cher.net.Credentials; + +/** + * Through the client object, a connection to the system is established + * and maintained. The client object maintains two separate threads (a + * reader and a writer) through which all network traffic is managed. + */ +public class Client +{ + /** + * Constructs a client object with the supplied credentials. These + * creds will be used to authenticate with any server to which this + * client attempts to connect. + */ + public Client (Credentials creds) + { + _creds = creds; + } + + /** + * Registers the supplied observer with this client. While registered + * the observer will receive notifications of state changes within the + * client. The function will refuse to register an already registered + * observer. + * + * @see ClientObserver + */ + public void addObserver (ClientObserver observer) + { + synchronized (_observers) { + // disallow multiple instances of the same observer + if (!_observers.contains(observer)) { + _observers.add(observer); + } + } + } + + /** + * Unregisters the supplied observer. Upon return of this function, + * the observer will no longer receive notifications of state changes + * within the client. + */ + public void removeObserver (ClientObserver observer) + { + synchronized (_observers) { + _observers.remove(observer); + } + } + + /** + * Configures the client to communicate with the server on the + * supplied hostname/port combination. + * + * @see #logon + */ + public void setServer (String hostname, int port) + { + _hostname = hostname; + _port = port; + } + + /** + * Returns the hostname of the server to which this client is + * currently configured to connect. + */ + public String getHostname () + { + return _hostname; + } + + /** + * Returns the port on which this client is currently configured to + * connect to the server. + */ + public int getPort () + { + return _port; + } + + /** + * Returns the credentials with which this client is currently + * configured to connect to the server. + */ + public Credentials getCredentials () + { + return _creds; + } + + /** + * Requests that this client connect and logon to the server with + * which it was previously configured. + */ + public synchronized void logon () + { + // if we have a communicator reference, we're already logged on + if (_comm != null) { + return; + } + + // otherwise create a new communicator instance and start it up. + // this will initiate the logon process + _comm = new Communicator(this); + _comm.logon(); + } + + /** + * Requests that the client log off of the server to which it is + * connected. + * + * @param abortable If true, the client will call + * clientWillDisconnect on all of the client observers + * and abort the logoff process if any of them return false. If false, + * clientWillDisconnect will not be called at all. + * + * @return true if the logoff succeeded, false if it failed due to a + * disagreeable observer. + */ + public boolean logoff (boolean abortable) + { + // if the request is abortable, let's run it past the observers + // before we act upon it + if (abortable && notifyObservers(CLIENT_WILL_LOGOFF, null)) { + return false; + } + + // ask the communicator to send a logoff message and disconnect + // from the server + _comm.logoff(); + + return true; + } + + boolean notifyObservers (int code, Exception cause) + { + boolean rejected = false; + synchronized (_observers) { + for (int i = 0; i < _observers.size(); i++) { + ClientObserver obs = (ClientObserver)_observers.get(i); + switch (code) { + case CLIENT_DID_LOGON: + obs.clientDidLogon(this); + break; + case CLIENT_FAILED_TO_LOGON: + obs.clientFailedToLogon(this, cause); + break; + case CLIENT_CONNETION_FAILED: + obs.clientConnectionFailed(this, cause); + break; + case CLIENT_WILL_LOGOFF: + if (!obs.clientWillLogoff(this)) { + rejected = true; + } + break; + case CLIENT_DID_LOGOFF: + obs.clientDidLogoff(this); + break; + default: + throw new RuntimeException("Invalid code supplied to " + + "notifyObservers: " + code); + } + } + } + return rejected; + } + + synchronized void communicatorDidExit () + { + // clear out our communicator reference + _comm = null; + } + + protected Credentials _creds; + protected String _hostname; + protected int _port; + + /** Our list of client observers. */ + protected List _observers = new ArrayList(); + + /** The entity that manages our network communications. */ + protected Communicator _comm; + + // client observer codes + static final int CLIENT_DID_LOGON = 0; + static final int CLIENT_FAILED_TO_LOGON = 1; + static final int CLIENT_CONNETION_FAILED = 2; + static final int CLIENT_WILL_LOGOFF = 3; + static final int CLIENT_DID_LOGOFF = 4; +} diff --git a/src/java/com/threerings/presents/client/ClientObserver.java b/src/java/com/threerings/presents/client/ClientObserver.java new file mode 100644 index 000000000..7289a16ad --- /dev/null +++ b/src/java/com/threerings/presents/client/ClientObserver.java @@ -0,0 +1,72 @@ +// +// $Id: ClientObserver.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.client; + +/** + * A client observer is registered with the client instance to be notified + * when state changes happen within the client. Specifically, logon + * sucess/failure and logoff. + * + *

These callbacks happen on the client thread and should therefore + * not be used to perform any complex action or do much more than relay + * the signal to some other thread (like the AWT thread) to act more fully + * on the notice. + * + *

In the normal course of affairs, clientDidLogon will + * be called after the client successfully logs on to the server and + * clientDidLogoff will be called after the client logs off + * of the server. If logon fails for any reson, + * clientFailedToLogon will be called to explain the failure. + * + *

clientWillLogoff will only be called when an abortable + * logoff is requested (like when the user clicks on a logoff button of + * some sort). It will not be called during non-abortable logoff requests + * (like when the browser calls stop on the applet and is about to yank + * the rug out from under us). If an observer aborts the logoff request, + * it should notify the user in some way why the request was aborted + * (but it shouldn't do so on the thread that calls + * clientWillLogoff). + * + *

If the client connection fails unexpectedly, + * clientConnectionFailed will be called to let the + * observers know that we lost our connection to the + * server. clientDidLogoff will be called immediately + * afterwards as a normal logoff procedure is effected. + */ +public interface ClientObserver +{ + /** + * Called after the client successfully connected to and authenticated + * with the server. The entire object system is up and running by the + * time this method is called. + */ + public void clientDidLogon (Client client); + + /** + * Called if anything fails during the logon attempt. This could be a + * network failure, authentication failure or otherwise. The exception + * provided will indicate the cause of the failure. + */ + public void clientFailedToLogon (Client client, Exception cause); + + /** + * Called when the connection to the server went away for some + * unexpected reason. This will be followed by a call to + * clientDidLogoff. + */ + public void clientConnectionFailed (Client client, Exception cause); + + /** + * Called when an abortable logoff requrest is made. If the observer + * returns false from this method, the client will abort the logoff + * request. + */ + public boolean clientWillLogoff (Client client); + + /** + * Called after the client has been logged off of the server and has + * disconnected. + */ + public void clientDidLogoff (Client client); +} diff --git a/src/java/com/threerings/presents/client/Communicator.java b/src/java/com/threerings/presents/client/Communicator.java new file mode 100644 index 000000000..911017622 --- /dev/null +++ b/src/java/com/threerings/presents/client/Communicator.java @@ -0,0 +1,175 @@ +// +// $Id: Communicator.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.client; + +import java.io.*; +import java.net.Socket; +import java.net.InetAddress; + +import com.samskivert.util.Queue; +import com.samskivert.cocktail.cher.Log; +import com.samskivert.cocktail.cher.net.*; + +/** + * The client performs all network I/O on separate threads (one for + * reading and one for writing). The communicator class encapsulates that + * functionality. + * + *

+ * Logon synopsis:
+ *
+ * Client.logon():
+ * - Calls Communicator.start()
+ * Communicator.start():
+ * - spawn Reader thread
+ * Reader.run():
+ * { - connect
+ *   - authenticate
+ * } if either fail, notify observers of failed logon
+ * - start writer thread
+ * - notify observers that we're logged on
+ * - read loop
+ * Writer.run():
+ * - write loop
+ * 
+ */ +class Communicator +{ + /** + * Creates a new communicator instance which is associated with the + * supplied client. + */ + public Communicator (Client client) + { + _client = client; + } + + /** + * Logs on to the server and initiates our full-duplex message + * exchange. + */ + public void logon () + { + // make sure things are copacetic + if (_reader != null) { + throw new RuntimeException("Communicator already started."); + } + + // start up the reader thread. it will connect to the server and + // start up the writer thread if everything went successfully + _reader = new Reader(); + _reader.start(); + } + + /** + * Delivers a logoff notification to the server and shuts down the + * network connection. Also causes all communication threads to + * terminate. + */ + public void logoff () + { + } + + /** + * Queues up the specified message for delivery to the server. + */ + public void postMessage (UpstreamMessage msg) + { + // simply append the message to the queue + _msgq.append(msg); + } + + protected void startWriter () + { + if (_writer != null) { + throw new RuntimeException("Writer already started!?"); + } + + // create a new writer thread and start it up + _writer = new Writer(); + _writer.start(); + } + + protected synchronized void readerDidExit () + { + // clear out our reader reference + _reader = null; + + // let the client know when we finally go away + _client.communicatorDidExit(); + } + + protected class Reader extends Thread + { + public void run () + { + try { + // first we connect and authenticate with the server + try { + // connect to the server + connect(); + + // then authenticate + logon(); + + } catch (Exception e) { + // let the observers know that we've failed + _client.notifyObservers(Client.CLIENT_FAILED_TO_LOGON, e); + // and terminate our communicator thread + return; + } + + // once authenticated, we go into full-duplex mode, + // starting up another thread to listen for messages while + // we handle the delivery of messages + startWriter(); + listen(); + + } finally { + // let the communicator know when we finally go away + readerDidExit(); + } + } + + protected void connect () + throws IOException + { + // if we're already connected, we freak out + if (_socket != null) { + throw new IOException("Already connected."); + } + + // look up the address of the target server + InetAddress host = InetAddress.getByName(_client.getHostname()); + + // establish a socket connection to said server + _socket = new Socket(host, _client.getPort()); + } + + protected void logon () + throws LogonException + { + } + + protected void listen () + { + } + } + + protected class Writer extends Thread + { + public void run () + { + } + } + + protected Client _client; + protected Reader _reader; + protected Writer _writer; + + protected Socket _socket; + protected DataInputStream _din; + protected DataInputStream _dout; + protected Queue _msgq = new Queue(); +} diff --git a/src/java/com/threerings/presents/client/LogonException.java b/src/java/com/threerings/presents/client/LogonException.java new file mode 100644 index 000000000..74030412e --- /dev/null +++ b/src/java/com/threerings/presents/client/LogonException.java @@ -0,0 +1,20 @@ +// +// $Id: LogonException.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.client; + +/** + * A logon exception is used to indicate a failure to log on to the + * server. The reason for failure is encoded as a string and stored in the + * message field of the exception. + */ +public class LogonException extends Exception +{ + /** + * Constructs a logon exception with the supplied logon failure code. + */ + public LogonException (String code) + { + super(code); + } +} diff --git a/src/java/com/threerings/presents/dobj/Event.java b/src/java/com/threerings/presents/dobj/Event.java new file mode 100644 index 000000000..572e30ce3 --- /dev/null +++ b/src/java/com/threerings/presents/dobj/Event.java @@ -0,0 +1,21 @@ +// +// $Id: Event.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.dobj; + +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +public class Event +{ + public void writeTo (DataOutputStream out) + throws IOException + { + } + + public void readFrom (DataInputStream in) + throws IOException + { + } +} diff --git a/src/java/com/threerings/presents/io/ObjectStreamException.java b/src/java/com/threerings/presents/io/ObjectStreamException.java new file mode 100644 index 000000000..4627b8bab --- /dev/null +++ b/src/java/com/threerings/presents/io/ObjectStreamException.java @@ -0,0 +1,19 @@ +// +// $Id: ObjectStreamException.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.io; + +/** + * The object stream exception is used to communicate an error in + * processing a typed object stream. As any error is fatal, we don't + * differentiate exceptions with separate classes, but instead communicate + * the nature of the error in the exception message with the assumption + * that the caller will want to raise holy hell in every case. + */ +public class ObjectStreamException extends Exception +{ + public ObjectStreamException (String message) + { + super(message); + } +} diff --git a/src/java/com/threerings/presents/io/TypedObject.java b/src/java/com/threerings/presents/io/TypedObject.java new file mode 100644 index 000000000..da873c6d4 --- /dev/null +++ b/src/java/com/threerings/presents/io/TypedObject.java @@ -0,0 +1,49 @@ +// +// $Id: TypedObject.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.io; + +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +/** + * A typed object is one that is associated with a particular type code. + * The type code can be communicated on the wire and used by the receiving + * end to instantiate the proper typed object class for decoding. + */ +public abstract class TypedObject +{ + /** + * Each typed object class must associate itself with a type value via + * the TypedObjectFactory so that the factory can + * instantiate the proper TypedObject derived class when + * decoding an incoming message. + * + * @return The type code associated with this object. + */ + public abstract short getType (); + + /** + * Each typed object class must be able to write itself to a stream. + * It should first call super.writeTo() before writing + * its own fields to the stream. + */ + public void writeTo (DataOutputStream out) + throws IOException + { + out.writeShort(getType()); + } + + /** + * Each typed object class must be able to read itself from a stream. + * It should first call super.readFrom() before reading + * its own fields from the stream. + */ + public void readFrom (DataInputStream in) + throws IOException + { + // nothing to do because the TypedObjectFactory already read our + // type value from the stream + } +} diff --git a/src/java/com/threerings/presents/io/TypedObjectFactory.java b/src/java/com/threerings/presents/io/TypedObjectFactory.java new file mode 100644 index 000000000..97332a271 --- /dev/null +++ b/src/java/com/threerings/presents/io/TypedObjectFactory.java @@ -0,0 +1,77 @@ +// +// $Id: TypedObjectFactory.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.io; + +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.util.HashMap; + +/** + * The encodable factory is used to unserialize encodable object instances + * from a stream. It maintains a mapping of encodable object classes to + * identifier codes which it uses to determine what sort of object is on + * the stream. The encodable mechanism is used to associate a particular + * class with some compact code that can be transmitted on the wire to + * identify that class. + */ +public class TypedObjectFactory +{ + /** + * Reads (unserializes) a typed object from the supplied data input + * stream. + * + * @return The unserialized typed object instance. + */ + public static TypedObject readFrom (DataInputStream din) + throws IOException, ObjectStreamException + { + // first determine the type of the incoming object + short type = din.readShort(); + + // now instantiate the proper object and decode the remainder + TypedObject msg = newObjectByType(type); + msg.readFrom(din); + + return msg; + } + + protected static TypedObject newObjectByType (short type) + throws ObjectStreamException + { + Class clazz = (Class)_classes.get(new Short(type)); + if (clazz == null) { + String errmsg = "Unknown object type: " + type; + throw new ObjectStreamException(errmsg); + } + + try { + return (TypedObject)clazz.newInstance(); + } catch (Throwable t) { + String errmsg = "Unable to instantiate typed object " + + "[class=" + clazz.getName() + ", error=" + t + "]."; + throw new ObjectStreamException(errmsg); + } + } + + protected static void registerClass (short type, Class clazz) + { + Short key = new Short(type); + + // make sure no funny business is afoot + if (_classes.containsKey(key)) { + Class incumbent = (Class)_classes.get(key); + String errmsg = "Cannot register " + clazz.getName() + + " as type " + type + " because " + incumbent.getName() + + " is already registered with that type."; + throw new RuntimeException(errmsg); + } + + // set up the mapping + _classes.put(key, clazz); + } + + /** Our type to class mapping table. */ + protected static HashMap _classes = new HashMap(); +} diff --git a/src/java/com/threerings/presents/net/AuthRequest.java b/src/java/com/threerings/presents/net/AuthRequest.java new file mode 100644 index 000000000..9fc21cf7a --- /dev/null +++ b/src/java/com/threerings/presents/net/AuthRequest.java @@ -0,0 +1,57 @@ +// +// $Id: AuthRequest.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import com.samskivert.cocktail.cher.io.TypedObjectFactory; + +public class AuthenticationRequest extends UpstreamMessage +{ + /** The code for an object subscription request. */ + public static final short TYPE = TYPE_BASE + 0; + + /** + * Zero argument constructor used when unserializing an instance. + */ + public AuthenticationRequest () + { + super(); + } + + /** + * Constructs a authentication request with the supplied credentials. + */ + public AuthenticationRequest (Credentials creds) + { + _creds = creds; + } + + public short getType () + { + return TYPE; + } + + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + _creds.writeTo(out); + } + + public void readFrom (DataInputStream in) + throws IOException + { + super.readFrom(in); + _creds = (Credentials)TypedObjectFactory.readFrom(in); + } + + /** + * The object id of the distributed object to which we are + * subscribing. + */ + protected Credentials _creds; +} diff --git a/src/java/com/threerings/presents/net/Credentials.java b/src/java/com/threerings/presents/net/Credentials.java new file mode 100644 index 000000000..3dfc6d3fc --- /dev/null +++ b/src/java/com/threerings/presents/net/Credentials.java @@ -0,0 +1,33 @@ +// +// $Id: Credentials.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +import com.samskivert.cocktail.cher.io.TypedObject; +import com.samskivert.cocktail.cher.io.TypedObjectFactory; + +/** + * Credentials are supplied by the client implementation and sent along to + * the server during the authentication process. To provide support for a + * variety of authentication methods, the credentials class is meant to be + * subclassed for the particular method (ie. username + password) in use + * in a given system. + * + *

All derived classes should provide a no argument constructor so + * that they can be instantiated prior to reconstruction from a data input + * stream. + */ +public abstract class Credentials extends TypedObject +{ + /** + * All credential derived classes should base their typed object code + * on this base value. + */ + public static final short TYPE_BASE = 300; + + // register our credential classes + static { + TypedObjectFactory.registerClass(UsernamePasswordCreds.TYPE, + UsernamePasswordCreds.class); + } +} diff --git a/src/java/com/threerings/presents/net/DownstreamMessage.java b/src/java/com/threerings/presents/net/DownstreamMessage.java new file mode 100644 index 000000000..0cffaacb7 --- /dev/null +++ b/src/java/com/threerings/presents/net/DownstreamMessage.java @@ -0,0 +1,72 @@ +// +// $Id: DownstreamMessage.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +import com.samskivert.cocktail.cher.io.TypedObject; +import com.samskivert.cocktail.cher.io.TypedObjectFactory; + +/** + * The DownstreamMessage class encapsulates a message in the + * Distributed Object Protocol that flows from the server to the + * client. Downstream messages include object subscription, event + * forwarding and session management. + */ +public class DownstreamMessage +{ + /** + * All downstream message derived classes should base their typed + * object code on this base value. + */ + public static final short TYPE_BASE = 200; + + /** + * The message id of the upstream message with which this downstream + * message is associated (or -1 if it is not associated with any + * upstream message). Because not every downstream message class cares + * to provide an upstream message id, this field is not serialized + * when the base downstream message class is serialized. Thus derived + * classes that care about message id should take care to initialize, + * serialize and unserialize the value theirselves. + */ + public short messageId = -1; + + /** + * Each downstream message derived class must provide a zero argument + * constructor so that the TypedObjectFactory can create + * a new instance of said class prior to unserializing it. + */ + public DownstreamMessage () + { + // nothing to do... + } + + // register our downstream message classes + static { + TypedObjectFactory.registerClass(AuthenticationRequest.TYPE, + AuthenticationRequest.class); + TypedObjectFactory.registerClass(SubscribeRequest.TYPE, + SubscribeRequest.class); + TypedObjectFactory.registerClass(FetchRequest.TYPE, + FetchRequest.class); + TypedObjectFactory.registerClass(UnsubscribeNotification.TYPE, + UnsubscribeNotification.class); + TypedObjectFactory.registerClass(ForwardEventNotification.TYPE, + ForwardEventNotification.class); + TypedObjectFactory.registerClass(PingNotification.TYPE, + PingNotification.class); + TypedObjectFactory.registerClass(LogoffNotification.TYPE, + LogoffNotification.class); + } + + /** The code for an event notification. */ + public static final byte EVENT = 0x00; + /** The code for a fetch/subscribe object response. */ + public static final byte OBJECT = 0x01; + /** The code for a request failure. */ + public static final byte FAILURE = 0x02; + /** The code for a pong response. */ + public static final byte PONG = 0x03; + /** The code for an end of transmission notification. */ + public static final byte EOT = 0x04; +} diff --git a/src/java/com/threerings/presents/net/EventNotification.java b/src/java/com/threerings/presents/net/EventNotification.java new file mode 100644 index 000000000..52769a633 --- /dev/null +++ b/src/java/com/threerings/presents/net/EventNotification.java @@ -0,0 +1,48 @@ +// +// $Id: EventNotification.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +public class EventNotification extends DownstreamMessage +{ + /** The code for an event notification. */ + public static final short TYPE = TYPE_BASE + 1; + + /** + * Zero argument constructor used when unserializing an instance. + */ + public EventNotification () + { + super(); + } + + /** + * Constructs an event notification for the supplied event. + */ + public EventNotification (Event event) + { + _event = event; + } + + public short getType () + { + return TYPE; + } + + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + _event.writeTo(out); + } + + public void readFrom (DataInputStream in) + throws IOException + { + super.readFrom(in); + _event = EventFactory.readFrom(in); + } + + /** The event which we are forwarding. */ + protected Event _event; +} diff --git a/src/java/com/threerings/presents/net/FetchRequest.java b/src/java/com/threerings/presents/net/FetchRequest.java new file mode 100644 index 000000000..4d6acb527 --- /dev/null +++ b/src/java/com/threerings/presents/net/FetchRequest.java @@ -0,0 +1,55 @@ +// +// $Id: FetchRequest.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +public class FetchRequest extends UpstreamMessage +{ + /** The code for an object fetch request. */ + public static final short TYPE = TYPE_BASE + 2; + + /** + * Zero argument constructor used when unserializing an instance. + */ + public FetchRequest () + { + super(); + } + + /** + * Constructs a fetch request for the distributed object with the + * specified object id. + */ + public FetchRequest (int oid) + { + _oid = oid; + } + + public short getType () + { + return TYPE; + } + + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + out.writeInt(_oid); + } + + public void readFrom (DataInputStream in) + throws IOException + { + super.readFrom(in); + _oid = in.readInt(); + } + + /** + * The object id of the distributed object which we are fetching. + */ + protected int _oid; +} diff --git a/src/java/com/threerings/presents/net/ForwardEventRequest.java b/src/java/com/threerings/presents/net/ForwardEventRequest.java new file mode 100644 index 000000000..290f5f7ff --- /dev/null +++ b/src/java/com/threerings/presents/net/ForwardEventRequest.java @@ -0,0 +1,55 @@ +// +// $Id: ForwardEventRequest.java,v 1.1 2001/05/22 06:07:59 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import com.samskivert.cocktail.cher.dobj.Event; +import com.samskivert.cocktail.cher.dobj.EventFactory; + +public class ForwardEventNotification extends UpstreamMessage +{ + /** The code for a forward event notification. */ + public static final short TYPE = TYPE_BASE + 4; + + /** + * Zero argument constructor used when unserializing an instance. + */ + public ForwardEventNotification () + { + super(); + } + + /** + * Constructs a forward event notification for the supplied event. + */ + public ForwardEventNotification (Event event) + { + _event = event; + } + + public short getType () + { + return TYPE; + } + + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + _event.writeTo(out); + } + + public void readFrom (DataInputStream in) + throws IOException + { + super.readFrom(in); + _event = EventFactory.readFrom(in); + } + + /** The event which we are forwarding. */ + protected Event _event; +} diff --git a/src/java/com/threerings/presents/net/LogoffRequest.java b/src/java/com/threerings/presents/net/LogoffRequest.java new file mode 100644 index 000000000..4f536070e --- /dev/null +++ b/src/java/com/threerings/presents/net/LogoffRequest.java @@ -0,0 +1,23 @@ +// +// $Id: LogoffRequest.java,v 1.1 2001/05/22 06:08:00 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +public class LogoffNotification extends UpstreamMessage +{ + /** The code for a logoff notification. */ + public static final short TYPE = TYPE_BASE + 6; + + /** + * Zero argument constructor used when unserializing an instance. + */ + public LogoffNotification () + { + super(); + } + + public short getType () + { + return TYPE; + } +} diff --git a/src/java/com/threerings/presents/net/PingRequest.java b/src/java/com/threerings/presents/net/PingRequest.java new file mode 100644 index 000000000..c8a7d1032 --- /dev/null +++ b/src/java/com/threerings/presents/net/PingRequest.java @@ -0,0 +1,23 @@ +// +// $Id: PingRequest.java,v 1.1 2001/05/22 06:08:00 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +public class PingNotification extends UpstreamMessage +{ + /** The code for a ping notification. */ + public static final short TYPE = TYPE_BASE + 5; + + /** + * Zero argument constructor used when unserializing an instance. + */ + public PingNotification () + { + super(); + } + + public short getType () + { + return TYPE; + } +} diff --git a/src/java/com/threerings/presents/net/SubscribeRequest.java b/src/java/com/threerings/presents/net/SubscribeRequest.java new file mode 100644 index 000000000..dfa70dd5b --- /dev/null +++ b/src/java/com/threerings/presents/net/SubscribeRequest.java @@ -0,0 +1,56 @@ +// +// $Id: SubscribeRequest.java,v 1.1 2001/05/22 06:08:00 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +public class SubscribeRequest extends UpstreamMessage +{ + /** The code for an object subscription request. */ + public static final short TYPE = TYPE_BASE + 1; + + /** + * Zero argument constructor used when unserializing an instance. + */ + public SubscribeRequest () + { + super(); + } + + /** + * Constructs a subscribe request for the distributed object with the + * specified object id. + */ + public SubscribeRequest (int oid) + { + _oid = oid; + } + + public short getType () + { + return TYPE; + } + + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + out.writeInt(_oid); + } + + public void readFrom (DataInputStream in) + throws IOException + { + super.readFrom(in); + _oid = in.readInt(); + } + + /** + * The object id of the distributed object to which we are + * subscribing. + */ + protected int _oid; +} diff --git a/src/java/com/threerings/presents/net/UnsubscribeRequest.java b/src/java/com/threerings/presents/net/UnsubscribeRequest.java new file mode 100644 index 000000000..3e25f95e1 --- /dev/null +++ b/src/java/com/threerings/presents/net/UnsubscribeRequest.java @@ -0,0 +1,56 @@ +// +// $Id: UnsubscribeRequest.java,v 1.1 2001/05/22 06:08:00 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +public class UnsubscribeNotification extends UpstreamMessage +{ + /** The code for an unsubscribe notification. */ + public static final short TYPE = TYPE_BASE + 3; + + /** + * Zero argument constructor used when unserializing an instance. + */ + public UnsubscribeNotification () + { + super(); + } + + /** + * Constructs a unsubscribe notification for the distributed object + * with the specified object id. + */ + public UnsubscribeNotification (int oid) + { + _oid = oid; + } + + public short getType () + { + return TYPE; + } + + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + out.writeInt(_oid); + } + + public void readFrom (DataInputStream in) + throws IOException + { + super.readFrom(in); + _oid = in.readInt(); + } + + /** + * The object id of the distributed object from which we are + * unsubscribing. + */ + protected int _oid; +} diff --git a/src/java/com/threerings/presents/net/UpstreamMessage.java b/src/java/com/threerings/presents/net/UpstreamMessage.java new file mode 100644 index 000000000..6e80787bc --- /dev/null +++ b/src/java/com/threerings/presents/net/UpstreamMessage.java @@ -0,0 +1,105 @@ +// +// $Id: UpstreamMessage.java,v 1.1 2001/05/22 06:08:00 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +import java.io.IOException; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import com.samskivert.cocktail.cher.io.TypedObject; +import com.samskivert.cocktail.cher.io.TypedObjectFactory; + +/** + * The UpstreamMessage class encapsulates a message in the + * Distributed Object Protocol that flows from the client to the server. + * Upstream messages include object subscription, event forwarding and + * session management. + */ +public abstract class UpstreamMessage extends TypedObject +{ + /** + * All upstream message derived classes should base their typed object + * code on this base value. + */ + public static final short TYPE_BASE = 100; + + /** + * This is a unique (within the context of a reasonable period of + * time) identifier assigned to each upstream message. The message ids + * are used to correlate a downstream response message to the + * appropriate upstream request message. + */ + public short messageId; + + /** + * Each upstream message derived class must provide a zero argument + * constructor so that the TypedObjectFactory can create + * a new instance of said class prior to unserializing it. + */ + public UpstreamMessage () + { + // automatically generate a valid message id; on the client, this + // will be used, on the server it will be overwritten by the + // unserialized value + this.messageId = nextMessageId(); + } + + /** + * Derived classes should override this function to write their fields + * out to the supplied data output stream. They must be sure + * to first call super.writeTo(). + */ + public void writeTo (DataOutputStream out) + throws IOException + { + super.writeTo(out); + out.writeShort(messageId); + } + + /** + * Derived classes should override this function to read their fields + * from the supplied data input stream. They must be sure to + * first call super.readFrom(). + */ + public void readFrom (DataInputStream in) + throws IOException + { + super.readFrom(in); + messageId = in.readShort(); + } + + /** + * Returns the next message id suitable for use by an upstream + * message. + */ + protected static synchronized short nextMessageId () + { + _nextMessageId = (short)((_nextMessageId + 1) % Short.MAX_VALUE); + return _nextMessageId; + } + + /** + * This is used to generate monotonically increasing message ids on + * the client as new messages are generated. + */ + protected static short _nextMessageId; + + // register our upstream message classes + static { + TypedObjectFactory.registerClass(AuthenticationRequest.TYPE, + AuthenticationRequest.class); + TypedObjectFactory.registerClass(SubscribeRequest.TYPE, + SubscribeRequest.class); + TypedObjectFactory.registerClass(FetchRequest.TYPE, + FetchRequest.class); + TypedObjectFactory.registerClass(UnsubscribeNotification.TYPE, + UnsubscribeNotification.class); + TypedObjectFactory.registerClass(ForwardEventNotification.TYPE, + ForwardEventNotification.class); + TypedObjectFactory.registerClass(PingNotification.TYPE, + PingNotification.class); + TypedObjectFactory.registerClass(LogoffNotification.TYPE, + LogoffNotification.class); + } +} diff --git a/src/java/com/threerings/presents/net/UsernamePasswordCreds.java b/src/java/com/threerings/presents/net/UsernamePasswordCreds.java new file mode 100644 index 000000000..bfeac031a --- /dev/null +++ b/src/java/com/threerings/presents/net/UsernamePasswordCreds.java @@ -0,0 +1,52 @@ +// +// $Id: UsernamePasswordCreds.java,v 1.1 2001/05/22 06:08:00 mdb Exp $ + +package com.samskivert.cocktail.cher.net; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class UsernamePasswordCreds +{ + public static final short TYPE = TYPE_BASE + 0; + + /** + * Zero argument constructor used when unserializing an instance. + */ + public UsernamePasswordCreds () + { + super(); + } + + /** + * Construct credentials with the supplied username and password. + */ + public UsernamePasswordCreds (String username, String password) + { + _username = username; + _password = password; + } + + public short getType () + { + return TYPE; + } + + public void writeTo (DataOutputStream out) + throws IOException + { + out.writeUTF(_username); + out.writeUTF(_password); + } + + public void readFrom (DataInputStream in) + throws IOException + { + _username = in.readUTF(); + _password = in.readUTF(); + } + + protected String _username; + protected String _password; +}