From 4d7fb64b8c6f02204fc89f8a4fb2a27436c69d5d Mon Sep 17 00:00:00 2001 From: Ray Greenwell Date: Tue, 14 Feb 2006 04:07:02 +0000 Subject: [PATCH] More progress. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@3849 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- src/as/com/threerings/README.txt | 53 ++++++ .../com/threerings/io/FrameAvailableEvent.as | 25 +++ src/as/com/threerings/io/FrameReader.as | 65 ++++++++ src/as/com/threerings/io/ObjectInputStream.as | 33 ++-- .../com/threerings/presents/client/Client.as | 110 ++++++++++-- .../presents/client/Communicator.as | 157 ++++++++++++++++++ .../com/threerings/presents/dobj/DObject.as | 17 ++ .../threerings/presents/net/AuthResponse.as | 21 +++ .../presents/net/AuthResponseData.as | 31 ++++ .../threerings/presents/net/BootstrapData.as | 38 +++++ .../presents/net/BootstrapNotification.as | 21 +++ .../presents/net/DownstreamMessage.as | 5 +- .../threerings/presents/net/PingRequest.as | 6 - .../threerings/presents/net/PongResponse.as | 5 - .../presents/net/UpstreamMessage.as | 3 +- .../threerings/util/StreamableArrayList.as | 30 ++++ 16 files changed, 582 insertions(+), 38 deletions(-) create mode 100644 src/as/com/threerings/README.txt create mode 100644 src/as/com/threerings/io/FrameAvailableEvent.as create mode 100644 src/as/com/threerings/io/FrameReader.as create mode 100644 src/as/com/threerings/presents/net/AuthResponse.as create mode 100644 src/as/com/threerings/presents/net/AuthResponseData.as create mode 100644 src/as/com/threerings/presents/net/BootstrapData.as create mode 100644 src/as/com/threerings/presents/net/BootstrapNotification.as create mode 100644 src/as/com/threerings/util/StreamableArrayList.as diff --git a/src/as/com/threerings/README.txt b/src/as/com/threerings/README.txt new file mode 100644 index 000000000..29dc50b1e --- /dev/null +++ b/src/as/com/threerings/README.txt @@ -0,0 +1,53 @@ +This document contains a couple of notes about some design decisions +and some notes about flash that you may find useful. + + +Design decisions +---------------- +- I have kept accessors named like their Java counterparts, rather + than embracing flash's property setter/getter methods (which are really + cool), but I am starting to lean the other way and may rewrite some stuff. + +- I am embracing flash's event distribution model because it saved me a bunch + of work. + + + +Notes +----- +- In actionscript, 'package' is simply a block command to sweep whatever is + defined inside the block so that it's in that package. This means + that in addition to classes being in a package, freestanding functions + and I believe variables and constants can be in a package. + + We are not putting freestanding functions anywhere. Make a util class with + static methods. + +- ActionScript does not have inner classes. Only one public class may be + defined in a file, and the filename must match the public class. + However, protected classes cannot be defined within the package block! + So it seems like the model is: + + package com.foo { + public class FooBar { + // stuff + } + } // end: package foo + + class HelperClass { + // helper stuff + } + + + To me, this makes it seem as if the helper class is now globally scoped, + which of course is the exact opposite of what is desired. This may + not be the case, I haven't played with it much yet. + +- Similarly, I'm unclear about sandboxes. If a user-created .swf is playing + inside ours, I don't know if it can interact with our classes, and if so, + what happens if it proceeds to define a class like + com.threerings.presents.client.Client? + +- constructors do not defaultly call super()- be sure to do it explicitely. + Maybe we should get in the habit of doing it in Java for consistency and + explicitness. diff --git a/src/as/com/threerings/io/FrameAvailableEvent.as b/src/as/com/threerings/io/FrameAvailableEvent.as new file mode 100644 index 000000000..ff19efa49 --- /dev/null +++ b/src/as/com/threerings/io/FrameAvailableEvent.as @@ -0,0 +1,25 @@ +package com.threerings.io { + +import flash.events.Event; + +import flash.util.ByteArray; + +public class FrameAvailableEvent extends Event +{ + /** The event code for a frame available. */ + public static const FRAME_AVAILABLE :String = "frameAvail"; + + public function FrameAvailableEvent (frameData :ByteArray) + { + super(FRAME_AVAILABLE); + _frameData = frameData; + } + + public function getFrameData () :ByteArray + { + return _frameData; + } + + protected var _frameData :ByteArray; +} +} diff --git a/src/as/com/threerings/io/FrameReader.as b/src/as/com/threerings/io/FrameReader.as new file mode 100644 index 000000000..4689de3c4 --- /dev/null +++ b/src/as/com/threerings/io/FrameReader.as @@ -0,0 +1,65 @@ +package com.threerings.io { + +import flash.events.EventDispatcher; +import flash.events.ProgressEvent; + +import flash.net.Socket; + +import flash.util.ByteArray; +import flash.util.Endian; + +/** + * Reads socket data until a complete frame is available. + * This dispatches a FrameAvailableEvent.FRAME_AVAILABLE once a frame + * has been fully read off the socket and is ready for decoding. + */ +public class FrameReader extends EventDispatcher +{ + public function FrameReader (socket :Socket) + { + _socket = socket; + _socket.addEventListener(ProgressEvent.SOCKET_DATA, socketHasData); + } + + /** + * Called when our socket has data that we can read. + */ + protected function socketHasData (event :ProgressEvent) :void + { + if (_curData == null) { + if (_socket.bytesAvailable < HEADER_SIZE) { + // if there are less bytes available than a header, let's + // just leave them on the socket until we can read the length + // all at once + return; + } + _length = _socket.readInt(); + _curData = new ByteArray(); + _curData.endian = Endian.BIG_ENDIAN; + } + + // read bytes: either as much as possible or up to the end of the frame + var toRead :int = Math.min(_length - _curData.length, + _socket.bytesAvailable); + _socket.readBytes(_curData, _curData.length, toRead); + + if (_length === _curData.length) { + // we have now read a complete frame, let us dispatch the data + _curData.position = 0; // move the read pointer to the beginning + dispatchEvent(new FrameAvailableEvent(_curData)); + _curData = null; // clear, so we know we need to first read length + + // there's a good chance there's more on the socket, recurse + // now to read it + socketHasData(event); + } + } + + protected var _socket :Socket; + protected var _curData :ByteArray; + protected var _length :int; + + /** The number of bytes in the frame header (a 32-bit integer). */ + protected const HEADER_SIZE :int = 4; +} +} diff --git a/src/as/com/threerings/io/ObjectInputStream.as b/src/as/com/threerings/io/ObjectInputStream.as index 8910d6ed8..8a9f9e262 100644 --- a/src/as/com/threerings/io/ObjectInputStream.as +++ b/src/as/com/threerings/io/ObjectInputStream.as @@ -10,9 +10,20 @@ import com.threerings.util.SimpleMap; public class ObjectInputStream { - public function ObjectInputStream (targ:IDataInput) + public function ObjectInputStream (source :IDataInput = null) { - _targ = targ; + if (source == null) { + source = new ByteArray(); + } + _source = source; + } + + /** + * Set a new source from which to read our data. + */ + public function setSource (source :IDataInput) + { + _source = source; } public function readObject () :* @@ -104,50 +115,50 @@ public class ObjectInputStream public function readBoolean () :Boolean //throws IOError { - return _targ.readBoolean(); + return _source.readBoolean(); } public function readByte () :int //throws IOError { - return _targ.readByte(); + return _source.readByte(); } public function readBytes (bytes :ByteArray, offset :uint = 0, length :uint = 0) :void //throws IOError { - _targ.readBytes(bytes, offset, length); + _source.readBytes(bytes, offset, length); } public function readDouble () :Number //throws IOError { - return _targ.readDouble(); + return _source.readDouble(); } public function readFloat () :Number //throws IOError { - return _targ.readFloat(); + return _source.readFloat(); } public function readInt () :int //throws IOError { - return _targ.readInt(); + return _source.readInt(); } public function readShort () :int //throws IOError { - return _targ.readShort(); + return _source.readShort(); } public function readUTF () :String //throws IOError { - return _targ.readUTF(); + return _source.readUTF(); } /** @@ -160,7 +171,7 @@ public class ObjectInputStream } /** The target DataInput that we route input from. */ - protected var _targ :IDataInput; + protected var _source :IDataInput; /** The object currently being read from the stream. */ protected var _current :*; diff --git a/src/as/com/threerings/presents/client/Client.as b/src/as/com/threerings/presents/client/Client.as index 3e1a8462a..285a30bac 100644 --- a/src/as/com/threerings/presents/client/Client.as +++ b/src/as/com/threerings/presents/client/Client.as @@ -30,16 +30,51 @@ public class Client extends EventDispatcher _port = port; } + public function getHostname () :String + { + return _hostname; + } + + public function getPort () :int + { + return _port; + } + + public function getCredentials () :Credentials + { + return _creds; + } + public function setCredentials (creds :Credentials) :void { _creds = creds; } + public function getVersion () :String + { + return _version; + } + + public function setVersion (version :String) + { + _version = version; + } + + public function getAuthResponseData () :AuthResponseData + { + return _authData; + } + public function getDObjectManager () :DObjectManager { return _omgr; } + public function getClientOid () :int + { + return _cloid; + } + public function getClientObject () :ClientObject { return _clobj; @@ -50,6 +85,24 @@ public class Client extends EventDispatcher return _invdir; } + public function getService (clazz :Class) :InvocationService + { + if (_bstrap == null) { + return null; + } + + // TODO + } + + public function requireService (clazz :Class) :InvocationService + { + var isvc :InvocationService = getService(clazz); + if (isvc == null) { + throw new Error(clazz + " isn't available. I can't bear to go on."); + } + return isvc; + } + public function getBootstrapData () :BootstrapData { return _bstrap; @@ -76,9 +129,11 @@ public class Client extends EventDispatcher _comm = new Communicator(this); _comm.logon(); - _tickInterval = new Timer(5000); - _tickInterval.addEventListener(TimerEvent.TIMER, tick); - _tickInterval.start(); + if (_tickInterval == null) { + _tickInterval = new Timer(5000); + _tickInterval.addEventListener(TimerEvent.TIMER, tick); + _tickInterval.start(); + } return true; } @@ -126,6 +181,22 @@ public class Client extends EventDispatcher _invdir.init(omgr, _cloid, this); } + /** + * Called every five seconds; ensures that we ping the server if we + * haven't communicated in a long while. + */ + protected function tick (event :TimerEvent) :void + { + if (_comm == null) { + return; + } + + var now :Number = new Date().getTime(); + if (now - _comm.getLastWrite() > PingRequest.PING_INTERVAL) { + _comm.postMessage(new PingRequest()); + } + } + protected function gotClientObject (clobj :ClientObject) :void { _clobj = clobj; @@ -145,22 +216,33 @@ public class Client extends EventDispatcher notifyObservers(ClientEvent.CLIENT_OBJECT_CHANGED); } - /** - * Called every five seconds; ensures that we ping the server if we - * haven't communicated in a long while. - */ - protected function tick (event :TimerEvent) :void + protected function cleanup (logonError :Error) :void { - if (_comm == null) { - return; - } + // clear out our references + _comm = null; + _omgr = null; + _clobj = null; + _cloid = -1; - var now :Number = new Date().getTime(); - if (now - _comm.getLastWrite() > PingRequest.PING_INTERVAL) { - _comm.postMessage(new PingRequest()); + // and let our invocation director know we're logged off + _invdir.cleanup(); + + // if this was due to a logon error, we can notify our listeners + // now that we're cleaned up: they may want to retry logon on + // another port, or something + if (logonError != null) { + notifyObservers(ClientEvent.CLIENT_FAILED_TO_LOGON, logonError); } } + /** + * Called by the omgr when we receive a pong packet. + */ + protected function gotPong (pong :PongResponse) :void + { + // TODO: compute time delta bowl-shit + } + /** * Convenience method to dispatch a client event to any listeners * and return the result of dispatchEvent. diff --git a/src/as/com/threerings/presents/client/Communicator.as b/src/as/com/threerings/presents/client/Communicator.as index 9479520ee..e217db525 100644 --- a/src/as/com/threerings/presents/client/Communicator.as +++ b/src/as/com/threerings/presents/client/Communicator.as @@ -1,5 +1,15 @@ package com.threerings.presents.client { +import flash.net.Socket; + +import flash.util.ByteArray; +import flash.util.Endian; + +import com.threerings.io.FrameAvailableEvent; +import com.threerings.io.FrameReader; +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; + public class Communicator { public function Communicator (client :Client) @@ -9,9 +19,156 @@ public class Communicator public function logon () :void { + // create the socket and set up listeners + _socket = new Socket(); + _socket.addEventListener(Event.CONNECT, socketOpened); + _socket.addEventListener(IOErrorEvent.IO_ERROR, socketError); + _socket.addEventListener(Event.CLOSE, socketClosed); + + // create our input/output business + _outBuffer = new ByteArray(); + _outBuffer.endian = Endian.BIG_ENDIAN; + _outStream = new ObjectOutputStream(_outBuffer); + + _frameReader = new FrameReader(_socket); + _frameReader.addEventListener(FrameAvailableEvent.FRAME_AVAILABLE, + inputFrameReceived); + _inStream = new ObjectInputStream(); + + _socket.connect(_client.getHostname(), _client.getPort()); + } + + public function logoff () :void + { + if (_socket == null) { + return; + } + + sendMessage(new LogoffRequest()); + + shutdown(null); + } + + protected function shutdown (logonError :Error) :void + { + if (_socket != null) { + try { + _socket.close(); + } catch (err :Error) { + trace("Error closing failed socket: " + err); + } + _socket = null; + _outStream = null; + _inStream = null; + _frameReader = null; + _outBuffer = null; + } + + _client.cleanup(logonError); + } + + protected function sendMessage (msg :UpstreamMessage) :void + { + // write the message (ends up in _outBuffer) + _outStream.writeObject(msg); + + // frame it by writing the length, then the bytes + _socket.writeInt(_outBuffer.length); + _socket.writeBytes(_outBuffer); + _socket.flush(); + + // clean up the output buffer + _outBuffer.length = 0; + _outBuffer.position = 0; + + // make a note of our most recent write time + updateWriteStamp(); + } + + /** + * Returns the time at which we last sent a packet to the server. + */ + protected function getLastWrite () :Number + { + return _lastWrite; + } + + /** + * Makes a note of the time at which we last communicated with the server. + */ + protected function updateWriteStamp () :void + { + _lastWrite = new Date().getTime(); + } + + /** + * Called when a frame of data from the server is ready to be + * decoded into a DownstreamMessage. + */ + protected function inputFrameReceived (event :FrameAvailableEvent) :void + { + // convert the frame data into a message from the server + _inStream.setSource(event.getFrameData()); + var msg :DownstreamMessage = _inStream.readObject(); + + if (_omgr != null) { + // if we're logged on, then just do the normal thing + _omgr.processMessage(msg); + return; + } + + // Otherwise, this would be the AuthResponse to our logon attempt. + var rsp :AuthResponse = (msg as AuthResponse); // TODO: as correct? + var data :AuthResponseData = rsp.getData(); + if (data.code !== AuthResponseData.SUCCESS) { + shutdown(new Error(data.code)); + return; + } + + // logon success + _omgr = new ClientDObjectMgr(this, _client); + _client._authData = data; + } + + /** + * Called when the connection to the server was successfully opened. + */ + protected function socketOpened (event :Event) :void + { + // well that's great! let's logon + var req :AuthRequest = new AuthRequest(_client.getCredentials(), + _client.getVersion()); + sendMessage(req); + } + + /** + * Called when there is an io error with the socket. + */ + protected function socketError (event :IOErrorEvent) :void + { + trace("socketError: " + event); + shutdown(new Error("socket closed unexpectedly.")); + } + + /** + * Called when the connection to the server was closed. + */ + protected function socketClosed (event :Event) :void + { + _client.notifyObserver(ClientEvent.CLIENT_CONNECTION_FAILED); + shutdown(null); } protected var _client :Client; protected var _omgr :ClientDObjectManager; + + protected var _outBuffer :ByteArray; + protected var _outStream :ObjectOutputStream; + + protected var _inStream :ObjectInputStream; + + protected var _socket :Socket; + + protected var _lastWrite :Number; } } diff --git a/src/as/com/threerings/presents/dobj/DObject.as b/src/as/com/threerings/presents/dobj/DObject.as index 3df856b78..dad3289d2 100644 --- a/src/as/com/threerings/presents/dobj/DObject.as +++ b/src/as/com/threerings/presents/dobj/DObject.as @@ -2,7 +2,12 @@ package com.threerings.presents.dobj { import flash.events.EventDispatcher; +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; +import com.threerings.io.Streamable; + public class DObject extends EventDispatcher + implements Streamable { public function getOid ():int { @@ -14,6 +19,18 @@ public class DObject extends EventDispatcher } + // documentation inherited from interface Streamable + public function writeObject (out :ObjectOutputStream) :void + { + out.writeInt(_oid); + } + + // documentation inherited from interface Streamable + public function readObject (ins :ObjectInputStream) :void + { + _oid = ins.readInt(); + } + protected var _oid :int; } } diff --git a/src/as/com/threerings/presents/net/AuthResponse.as b/src/as/com/threerings/presents/net/AuthResponse.as new file mode 100644 index 000000000..e33d64375 --- /dev/null +++ b/src/as/com/threerings/presents/net/AuthResponse.as @@ -0,0 +1,21 @@ +package com.threerings.presents.net { + +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; + +public class AuthResponse extends DownstreamMessage +{ + public function getData () :AuthResponseData + { + return _data; + } + + public function readObject (ins :ObjectInputStream) :void + { + super.readObject(ins); + _data = ins.readField(AuthResponseData); + } + + protected var _data :AuthResponseData; +} +} diff --git a/src/as/com/threerings/presents/net/AuthResponseData.as b/src/as/com/threerings/presents/net/AuthResponseData.as new file mode 100644 index 000000000..e335bafbc --- /dev/null +++ b/src/as/com/threerings/presents/net/AuthResponseData.as @@ -0,0 +1,31 @@ +package com.threerings.presents.net { + +import com.threerings.presents.dobj.DObject; + +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; + +public class AuthResponseData extends DObject +{ + /** A constant used to indicate a successful authentication. */ + public static const SUCCESS :String = "success"; + + /** Either the SUCCESS constant or a reason code indicating + * why the authentication failed. */ + public var code :String; + + // documentation inherited + public function writeObject (out :ObjectOutputStream) :void + { + super.writeObject(out); + out.writeField(code); + } + + // documentation inherited + public function readObject (ins :ObjectInputStream) :void + { + super.readObject(ins); + code = ins.readField(String); + } +} +} diff --git a/src/as/com/threerings/presents/net/BootstrapData.as b/src/as/com/threerings/presents/net/BootstrapData.as new file mode 100644 index 000000000..c3b98afa3 --- /dev/null +++ b/src/as/com/threerings/presents/net/BootstrapData.as @@ -0,0 +1,38 @@ +package com.threerings.presents.net { + +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; +import com.threerings.io.Streamable; + +import com.threerings.util.StreamableArrayList; + +/** + * A BoostrapData object is communicated back to the client + * after authentication has succeeded and after the server is fully + * prepared to deal with the client. It contains information the client + * will need to interact with the server. + */ +public class BootstrapData + implements Streamable +{ + /** The oid of this client's associated distributed object. */ + public var clientOid :int; + + /** A list of handles to invocation services. */ + public var services :StreamableArrayList; + + // documentation inherited from interface Streamable + public function writeObject (out :ObjectOutputStream) :void + { + trace("This is client code: BootstrapData shouldn't be written"); + //out.writeShort(messageId); + } + + // documentation inherited from interface Streamable + public function readObject (ins :ObjectInputStream) :void + { + clientOid = ins.readInt(); + services = ins.readField(StreamableArrayList); + } +} +} diff --git a/src/as/com/threerings/presents/net/BootstrapNotification.as b/src/as/com/threerings/presents/net/BootstrapNotification.as new file mode 100644 index 000000000..d0520283d --- /dev/null +++ b/src/as/com/threerings/presents/net/BootstrapNotification.as @@ -0,0 +1,21 @@ +package com.threerings.presents.net { + +import com.threerings.io.ObjectInputStream; + +public class BootstrapNotification extends DownstreamMessage +{ + public function getData () :BootstrapData + { + return _data; + } + + public function readObject (ins :ObjectInputStream) :void + { + super.readObject(ins); + _data = ins.readField(BootstrapData); + } + + /** The data associated with this notification. */ + protected var _data :BootstrapData; +} +} diff --git a/src/as/com/threerings/presents/net/DownstreamMessage.as b/src/as/com/threerings/presents/net/DownstreamMessage.as index 7d810f2f8..e2af144c2 100644 --- a/src/as/com/threerings/presents/net/DownstreamMessage.as +++ b/src/as/com/threerings/presents/net/DownstreamMessage.as @@ -1,5 +1,7 @@ package com.threerings.presents.net { +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; import com.threerings.io.Streamable; public class DownstreamMessage @@ -13,7 +15,8 @@ public class DownstreamMessage // documentation inherited from interface Streamable public function writeObject (out :ObjectOutputStream) :void { - out.writeShort(messageId); + trace("This is client code: Downstream messages shouldn't be written"); + //out.writeShort(messageId); } // documentation inherited from interface Streamable diff --git a/src/as/com/threerings/presents/net/PingRequest.as b/src/as/com/threerings/presents/net/PingRequest.as index 05c9de5da..4a8eadeb4 100644 --- a/src/as/com/threerings/presents/net/PingRequest.as +++ b/src/as/com/threerings/presents/net/PingRequest.as @@ -26,12 +26,6 @@ public class PingRequest extends UpstreamMessage super.writeObject(out); } - // documentation inherited - public function readObject (ins :ObjectInputStream) - { - trace("read PingRequest on the client?"); - } - /** A time stamp obtained when we serialize this object. */ protected var _packStamp :Number; } diff --git a/src/as/com/threerings/presents/net/PongResponse.as b/src/as/com/threerings/presents/net/PongResponse.as index 08648d5fc..b4a642f1c 100644 --- a/src/as/com/threerings/presents/net/PongResponse.as +++ b/src/as/com/threerings/presents/net/PongResponse.as @@ -28,11 +28,6 @@ public class PongResponse extends DownstreamMessage return _unpackStamp; } - public function writeObject (out :ObjectOutputStream) - { - trace("write a pong on the client??"); - } - public function readObject (ins :ObjectInputStream) { _unpackStamp = new Date().getTime(); diff --git a/src/as/com/threerings/presents/net/UpstreamMessage.as b/src/as/com/threerings/presents/net/UpstreamMessage.as index f9c352861..c3f924d04 100644 --- a/src/as/com/threerings/presents/net/UpstreamMessage.as +++ b/src/as/com/threerings/presents/net/UpstreamMessage.as @@ -29,7 +29,8 @@ public class UpstreamMessage // documentation inherited from interface Streamable public function readObject (ins :ObjectInputStream) :void { - messageId = ins.readShort(); + trace("This is client code: Upstream messages shouldn't be read"); + //messageId = ins.readShort(); } /** diff --git a/src/as/com/threerings/util/StreamableArrayList.as b/src/as/com/threerings/util/StreamableArrayList.as new file mode 100644 index 000000000..5691cf4d5 --- /dev/null +++ b/src/as/com/threerings/util/StreamableArrayList.as @@ -0,0 +1,30 @@ +package com.threerings.util { + +import mx.collections.ArrayCollection; + +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; +import com.threerings.io.Streamable; + +public class StreamableArrayList extends ArrayCollection + implements Streamable +{ + // documentation inherited from interface Streamable + public function writeObject (out :ObjectOutputStream) :void + { + out.writeInt(source.length); + for (var ii :int = 0; ii < source.length; ii++) { + out.writeObject(source[ii]); + } + } + + // documentation inherited from interface Streamable + public function readObject (ins :ObjectInputStream) :void + { + var ecount :int = ins.readInt(); + for (var ii :int = 0; ii < ecount; ii++) { + source[ii] = ins.readObject(); + } + } +} +}