diff --git a/src/as/com/threerings/presents/client/Client.as b/src/as/com/threerings/presents/client/Client.as
index 193dfad3b..bd1d8f3ec 100644
--- a/src/as/com/threerings/presents/client/Client.as
+++ b/src/as/com/threerings/presents/client/Client.as
@@ -22,7 +22,7 @@ import com.threerings.presents.net.PongResponse;
public class Client extends EventDispatcher
{
/** The default port on which the server listens for client connections. */
- public static const DEFAULT_SERVER_PORT :int = 47624;
+ public static const DEFAULT_SERVER_PORTS :Array = [ 47624 ];
private static const log :Log = Log.getLog(Client);
@@ -84,10 +84,10 @@ public class Client extends EventDispatcher
}
}
- public function setServer (hostname :String, port :int) :void
+ public function setServer (hostname :String, ports :Array) :void
{
_hostname = hostname;
- _port = port;
+ _ports = ports;
}
/**
@@ -103,9 +103,12 @@ public class Client extends EventDispatcher
return _hostname;
}
- public function getPort () :int
+ /**
+ * Returns the ports on which this client is configured to connect.
+ */
+ public function getPorts () :Array
{
- return _port;
+ return _ports;
}
public function getCredentials () :Credentials
@@ -276,6 +279,15 @@ public class Client extends EventDispatcher
}
}
+ /**
+ * Called by the {@link Communicator} if it is experiencing trouble logging
+ * on but is still trying fallback strategies.
+ */
+ internal function reportLogonTribulations (cause :LogonError) :void
+ {
+ notifyObservers(ClientEvent.CLIENT_FAILED_TO_LOGON, cause);
+ }
+
// TODO: this should be 'internal' except for a fucking bug with AS3
public function gotClientObject (clobj :ClientObject) :void
{
@@ -359,7 +371,7 @@ public class Client extends EventDispatcher
protected var _hostname :String;
/** The port on which we connect to the game server. */
- protected var _port :int;
+ protected var _ports :Array; /* of int */
/** The entity that manages our network communications. */
protected var _comm :Communicator;
diff --git a/src/as/com/threerings/presents/client/ClientObserver.as b/src/as/com/threerings/presents/client/ClientObserver.as
index 2b29f7fce..0ca721075 100644
--- a/src/as/com/threerings/presents/client/ClientObserver.as
+++ b/src/as/com/threerings/presents/client/ClientObserver.as
@@ -53,6 +53,14 @@ public interface ClientObserver extends SessionObserver
* 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.
+ *
+ * @param cause an exception indicating the cause of the logon failure.
+ * Note: this may be a {@link LogonError} and if so, the
+ * caller must check {@link LogonException#isStillInProgress} to
+ * find out if the logon process has totally failed or if we are simply
+ * reporting intermediate status (we might be falling back to an
+ * alternative port or delaying our auto-retry attempt due to server
+ * overload).
*/
function clientFailedToLogon (event :ClientEvent) :void;
diff --git a/src/as/com/threerings/presents/client/Communicator.as b/src/as/com/threerings/presents/client/Communicator.as
index 9f9eeb72f..3ed161145 100644
--- a/src/as/com/threerings/presents/client/Communicator.as
+++ b/src/as/com/threerings/presents/client/Communicator.as
@@ -1,5 +1,7 @@
package com.threerings.presents.client {
+import flash.errors.IOError;
+
import flash.events.Event;
import flash.events.IOErrorEvent;
@@ -16,6 +18,7 @@ import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.Translations;
+import com.threerings.presents.data.AuthCodes;
import com.threerings.presents.net.AuthRequest;
import com.threerings.presents.net.AuthResponse;
import com.threerings.presents.net.AuthResponseData;
@@ -32,24 +35,77 @@ public class Communicator
public function logon () :void
{
- // create the socket and set up listeners
- _socket = new Socket();
- _socket.endian = Endian.BIG_ENDIAN;
- _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());
+ _portIdx = 0;
+ logonToPort();
+ }
+
+ /**
+ * This method is strangely named, and it does two things which is
+ * bad style. Either log on to the next port, or save that the port
+ * we just logged on to was a good one.
+ */
+ protected function logonToPort (logonWasSuccessful :Boolean = false) :Boolean
+ {
+ var ports :Array = _client.getPorts();
+
+ if (!logonWasSuccessful) {
+ if (_portIdx >= ports.length) {
+ return false;
+ }
+ if (_portIdx != 0) {
+ _client.reportLogonTribulations(
+ new LogonError(AuthCodes.TRYING_NEXT_PORT, true));
+
+ removeListeners();
+ }
+
+ // create the socket and set up listeners
+ _socket = new Socket();
+ _socket.endian = Endian.BIG_ENDIAN;
+ _socket.addEventListener(Event.CONNECT, socketOpened);
+ _socket.addEventListener(IOErrorEvent.IO_ERROR, socketError);
+ _socket.addEventListener(Event.CLOSE, socketClosed);
+
+ _frameReader = new FrameReader(_socket);
+ _frameReader.addEventListener(FrameAvailableEvent.FRAME_AVAILABLE,
+ inputFrameReceived);
+ }
+
+ var host :String = _client.getHostname();
+ var pportKey :String = host + ".preferred_port";
+ // TODO: extract preferred port from saved prefs
+ var pport :int = /** TODO Prefs.getValue(pportKey, **/ (ports[0] as int);
+ var ppidx :int = Math.max(0, ports.indexOf(pport));
+ var port :int = (ports[(_portIdx + ppidx) % ports.length] as int);
+
+ if (logonWasSuccessful) {
+ _portIdx = -1; // indicate that we're no longer trying new ports
+ // TODO: Prefs.setValue(pportKey, port);
+
+ } else {
+ Log.getLog(this).info(
+ "Connecting [host=" + host + ", port=" + port + "].");
+ _socket.connect(host, port);
+ }
+
+ return true;
+ }
+
+ protected function removeListeners () :void
+ {
+ _socket.removeEventListener(Event.CONNECT, socketOpened);
+ _socket.removeEventListener(IOErrorEvent.IO_ERROR, socketError);
+ _socket.removeEventListener(Event.CLOSE, socketClosed);
+
+ _frameReader.removeEventListener(FrameAvailableEvent.FRAME_AVAILABLE,
+ inputFrameReceived);
}
public function logoff () :void
@@ -77,6 +133,7 @@ public class Communicator
Log.getLog(this).warning(
"Error closing failed socket [error=" + err + "].");
}
+ removeListeners();
_socket = null;
_outStream = null;
_inStream = null;
@@ -169,6 +226,7 @@ public class Communicator
*/
protected function socketOpened (event :Event) :void
{
+ logonToPort(true);
// well that's great! let's logon
var req :AuthRequest = new AuthRequest(_client.getCredentials(),
_client.getVersion());
@@ -180,6 +238,16 @@ public class Communicator
*/
protected function socketError (event :IOErrorEvent) :void
{
+ _socket.close();
+ // if we're trying ports, try the next one.
+ if (_portIdx != -1) {
+ _portIdx++;
+ if (logonToPort()) {
+ return;
+ }
+ }
+
+ // total failure
Log.getLog(this).warning("socket error: " + event);
shutdown(new Error("socket closed unexpectedly."));
}
@@ -206,5 +274,8 @@ public class Communicator
protected var _socket :Socket;
protected var _lastWrite :uint;
+
+ /** The current port we'll try to connect to. */
+ protected var _portIdx :int = -1;
}
}
diff --git a/src/as/com/threerings/presents/client/LogonError.as b/src/as/com/threerings/presents/client/LogonError.as
new file mode 100644
index 000000000..0550d4a55
--- /dev/null
+++ b/src/as/com/threerings/presents/client/LogonError.as
@@ -0,0 +1,55 @@
+//
+// $Id: LogonException.java 4158 2006-05-30 22:12:15Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.presents.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 LogonError extends Error
+{
+ /**
+ * Constructs a logon exception with the supplied logon failure code.
+ */
+ public function LogonError (msg :String, stillInProgress :Boolean = false)
+ {
+ super(msg);
+ _stillInProgress = stillInProgress;
+ }
+
+ /**
+ * Returns true if this exception is reporting an intermediate status
+ * rather than total logon failure. The client may be falling back to an
+ * alternative port or delaying an auto-retry attempt due to server
+ * overload. If this method returns true, the client should not
+ * allow additional calls to {@link Client#logon} as the current attempt is
+ * still in progress.
+ */
+ public function isStillInProgress () :Boolean
+ {
+ return _stillInProgress;
+ }
+
+ protected var _stillInProgress :Boolean;
+}
+}
diff --git a/src/as/com/threerings/presents/data/AuthCodes.as b/src/as/com/threerings/presents/data/AuthCodes.as
new file mode 100644
index 000000000..6d0379609
--- /dev/null
+++ b/src/as/com/threerings/presents/data/AuthCodes.as
@@ -0,0 +1,47 @@
+//
+// $Id: AuthCodes.java 4158 2006-05-30 22:12:15Z mdb $
+//
+// Narya library - tools for developing networked games
+// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
+// http://www.threerings.net/code/narya/
+//
+// This library is free software; you can redistribute it and/or modify it
+// under the terms of the GNU Lesser General Public License as published
+// by the Free Software Foundation; either version 2.1 of the License, or
+// (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+package com.threerings.presents.data {
+
+/**
+ * Basic authentication response codes.
+ */
+public class AuthCodes
+{
+ /** A code indicating that no user exists with the specified
+ * username. */
+ public static const NO_SUCH_USER :String = "m.no_such_user";
+
+ /** A code indicating that the supplied password was invalid. */
+ public static const INVALID_PASSWORD :String = "m.invalid_password";
+
+ /** A code indicating that an internal server error occurred while
+ * trying to log the user on. */
+ public static const SERVER_ERROR :String = "m.server_error";
+
+ /** A code indicating that the server is not available at the moment. */
+ public static const SERVER_UNAVAILABLE :String = "m.server_unavailable";
+
+ /** A code indicating that we failed to connect to the server on a port and
+ * are trying the next port in the list. */
+ public static const TRYING_NEXT_PORT :String = "m.trying_next_port";
+}
+}