From e6e96c25fcc884ba928bf810d6321dd6858d924f Mon Sep 17 00:00:00 2001 From: Ray Greenwell Date: Thu, 8 Jun 2006 21:44:27 +0000 Subject: [PATCH] Ported over the automatic port-retrying code, but had to modify some things due to differences in the way flash throws errors (and apparently can't re-use a socket object). git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@4183 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- .../com/threerings/presents/client/Client.as | 24 +++-- .../presents/client/ClientObserver.as | 8 ++ .../presents/client/Communicator.as | 93 ++++++++++++++++--- .../threerings/presents/client/LogonError.as | 55 +++++++++++ .../com/threerings/presents/data/AuthCodes.as | 47 ++++++++++ 5 files changed, 210 insertions(+), 17 deletions(-) create mode 100644 src/as/com/threerings/presents/client/LogonError.as create mode 100644 src/as/com/threerings/presents/data/AuthCodes.as 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"; +} +}