fe3d89132d
is known by any invocation marshaller, and so the client does not need to be passed in. Just here on the actionscript side for now, but this change may make its way to Java. Other changes may follow... git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5904 542714f4-19e9-0310-aa3c-eee0fc999fb1
395 lines
13 KiB
ActionScript
395 lines
13 KiB
ActionScript
//
|
|
// $Id$
|
|
//
|
|
// Narya library - tools for developing networked games
|
|
// Copyright (C) 2002-2009 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 {
|
|
|
|
import flash.errors.IOError;
|
|
import flash.events.Event;
|
|
import flash.events.IOErrorEvent;
|
|
import flash.events.SecurityErrorEvent;
|
|
import flash.events.TimerEvent;
|
|
|
|
import flash.net.Socket;
|
|
import flash.utils.ByteArray;
|
|
import flash.utils.Endian;
|
|
import flash.utils.Timer;
|
|
|
|
import com.threerings.util.Log;
|
|
|
|
import com.threerings.io.FrameAvailableEvent;
|
|
import com.threerings.io.FrameReader;
|
|
import com.threerings.io.ObjectInputStream;
|
|
import com.threerings.io.ObjectOutputStream;
|
|
|
|
import com.threerings.presents.data.AuthCodes;
|
|
import com.threerings.presents.net.AuthRequest;
|
|
import com.threerings.presents.net.AuthResponse;
|
|
import com.threerings.presents.net.AuthResponseData;
|
|
import com.threerings.presents.net.DownstreamMessage;
|
|
import com.threerings.presents.net.LogoffRequest;
|
|
import com.threerings.presents.net.UpstreamMessage;
|
|
import com.threerings.presents.net.ThrottleUpdatedMessage;
|
|
|
|
public class Communicator
|
|
{
|
|
public function Communicator (client :Client)
|
|
{
|
|
_client = client;
|
|
}
|
|
|
|
public function logon () :void
|
|
{
|
|
// create our input/output business
|
|
_outBuffer = new ByteArray();
|
|
_outBuffer.endian = Endian.BIG_ENDIAN;
|
|
_outStream = new ObjectOutputStream(_outBuffer);
|
|
_inStream = new ObjectInputStream(null, { invDir: _client.getInvocationDirector() });
|
|
|
|
attemptLogon(0);
|
|
}
|
|
|
|
public function logoff () :void
|
|
{
|
|
if (_socket == null) {
|
|
return;
|
|
}
|
|
postMessage(new LogoffRequest());
|
|
}
|
|
|
|
public function postMessage (msg :UpstreamMessage) :void
|
|
{
|
|
_outq.push(msg);
|
|
if (_writer != null) {
|
|
sendPendingMessages(null);
|
|
|
|
} else {
|
|
log.warning("Posting message prior to opening socket", "msg", msg);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detects if the communicator currently has an open channel to the server. This is guaranteed
|
|
* to return false while the logoff event is being dispatched. It also returns true during
|
|
* authentication, but prior to the client appearing to be logged on.
|
|
*/
|
|
public function isConnected () :Boolean
|
|
{
|
|
return _writer != null;
|
|
}
|
|
|
|
/**
|
|
* Detects is the communicator has a logoff request pending, i.e. will logoff in the near
|
|
* future.
|
|
*/
|
|
public function hasPendingLogoff () :Boolean
|
|
{
|
|
// check for a logoff message
|
|
for each (var message :UpstreamMessage in _outq) {
|
|
if (message is LogoffRequest) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Attempts to logon on using the port at the specified index.
|
|
*/
|
|
protected function attemptLogon (portIdx :int) :Boolean
|
|
{
|
|
var ports :Array = _client.getPorts();
|
|
_portIdx = portIdx; // note the port we're about to try
|
|
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(SecurityErrorEvent.SECURITY_ERROR, socketError);
|
|
_socket.addEventListener(Event.CLOSE, socketClosed);
|
|
|
|
_frameReader = new FrameReader(_socket);
|
|
_frameReader.addEventListener(FrameAvailableEvent.FRAME_AVAILABLE, inputFrameReceived);
|
|
|
|
var host :String = _client.getHostname();
|
|
var pport :int = ports[0];
|
|
var ppidx :int = Math.max(0, ports.indexOf(pport));
|
|
var port :int = (ports[(_portIdx + ppidx) % ports.length] as int);
|
|
|
|
log.info("Connecting", "host", host, "port", port, "svcs", _client.getBootGroups());
|
|
_socket.connect(host, port);
|
|
|
|
return true;
|
|
}
|
|
|
|
protected function removeListeners () :void
|
|
{
|
|
_socket.removeEventListener(Event.CONNECT, socketOpened);
|
|
_socket.removeEventListener(IOErrorEvent.IO_ERROR, socketError);
|
|
_socket.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, socketError);
|
|
_socket.removeEventListener(Event.CLOSE, socketClosed);
|
|
|
|
_frameReader.removeEventListener(FrameAvailableEvent.FRAME_AVAILABLE, inputFrameReceived);
|
|
_frameReader.shutdown();
|
|
}
|
|
|
|
protected function shutdown (logonError :Error) :void
|
|
{
|
|
if (_socket != null) {
|
|
if (_socket.connected) {
|
|
try {
|
|
_socket.close();
|
|
} catch (err :Error) {
|
|
log.warning("Error closing failed socket [error=" + err + "].");
|
|
}
|
|
}
|
|
removeListeners();
|
|
_socket = null;
|
|
_outStream = null;
|
|
_inStream = null;
|
|
_frameReader = null;
|
|
_outBuffer = null;
|
|
}
|
|
|
|
if (_writer != null) {
|
|
_writer.stop();
|
|
_writer = null;
|
|
}
|
|
|
|
// if we never got our client object, we never dispatched a DID_LOGON and therefore we
|
|
// don't want to dispatch a DID_LOGOFF, the observers basically never know anything ever
|
|
// happened
|
|
if (_client.getClientObject() != null) {
|
|
_client.notifyObservers(ClientEvent.CLIENT_DID_LOGOFF, null);
|
|
}
|
|
_client.cleanup(logonError);
|
|
}
|
|
|
|
/**
|
|
* Sends all pending messages from our outgoing message queue. If we hit our throttle while
|
|
* sending, we stop and wait for the next time around when we'll try sending them again.
|
|
*/
|
|
protected function sendPendingMessages (event :TimerEvent) :void
|
|
{
|
|
while (_outq.length > 0) {
|
|
// if we've exceeded our throttle, stop for now
|
|
if (_client.getOutgoingMessageThrottle().throttleOp()) {
|
|
if (!_notedThrottle) {
|
|
log.info("Throttling outgoing messages", "queue", _outq.length,
|
|
"throttle", _client.getOutgoingMessageThrottle());
|
|
_notedThrottle = true;
|
|
}
|
|
return;
|
|
}
|
|
_notedThrottle = false;
|
|
|
|
// grab the next message from the queue and send it
|
|
var msg :UpstreamMessage = (_outq.shift() as UpstreamMessage);
|
|
sendMessage(msg);
|
|
|
|
// if this was a logoff request, shutdown
|
|
if (msg is LogoffRequest) {
|
|
shutdown(null);
|
|
} else if (msg is ThrottleUpdatedMessage) {
|
|
_client.finalizeOutgoingMessageThrottle(ThrottleUpdatedMessage(msg).messagesPerSec);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes a single message to our outgoing socket.
|
|
*/
|
|
protected function sendMessage (msg :UpstreamMessage) :void
|
|
{
|
|
if (_outStream == null) {
|
|
log.warning("No socket, dropping msg [msg=" + msg + "].");
|
|
return;
|
|
}
|
|
|
|
// write the message (ends up in _outBuffer)
|
|
_outStream.writeObject(msg);
|
|
|
|
// Log.debug("outBuffer: " + StringUtil.unhexlate(_outBuffer));
|
|
|
|
// Frame it by writing the length, then the bytes.
|
|
// We add 4 to the length, because the length is of the entire frame
|
|
// including the 4 bytes used to encode the length!
|
|
_socket.writeInt(_outBuffer.length + 4);
|
|
_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();
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
var frameData :ByteArray = event.getFrameData();
|
|
_inStream.setSource(frameData);
|
|
var msg :DownstreamMessage;
|
|
try {
|
|
msg = (_inStream.readObject(DownstreamMessage) as DownstreamMessage);
|
|
} catch (e :Error) {
|
|
log.warning("Error processing downstream message: " + e);
|
|
log.logStackTrace(e);
|
|
return;
|
|
}
|
|
|
|
if (frameData.bytesAvailable > 0) {
|
|
log.warning("Beans! We didn't fully read a frame, is there a bug in some streaming " +
|
|
"code? [bytesLeftOver=" + frameData.bytesAvailable + ", msg=" + msg + "].");
|
|
}
|
|
|
|
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);
|
|
var data :AuthResponseData = rsp.getData();
|
|
if (data.code !== AuthResponseData.SUCCESS) {
|
|
shutdown(new Error(data.code));
|
|
return;
|
|
}
|
|
|
|
// logon success
|
|
_omgr = new ClientDObjectMgr(this, _client);
|
|
_client.setAuthResponseData(data);
|
|
}
|
|
|
|
/**
|
|
* Called when the connection to the server was successfully opened.
|
|
*/
|
|
protected function socketOpened (event :Event) :void
|
|
{
|
|
// reset our port index now that we're successfully logged on; this way if the socket
|
|
// fails, we won't think that we're in the middle of trying to logon
|
|
_portIdx = -1;
|
|
|
|
// check for a logoff message
|
|
if (hasPendingLogoff()) {
|
|
// don't bother authing, just bail
|
|
log.info("Logged off prior to socket opening, shutting down");
|
|
shutdown(null);
|
|
return;
|
|
}
|
|
|
|
// send our authentication request (do so directly rather than putting it on the outgoing
|
|
// write queue)
|
|
sendMessage(new AuthRequest(_client.getCredentials(), _client.getVersion(),
|
|
_client.getBootGroups()));
|
|
|
|
// kick off our writer thread now that we know we're ready to write
|
|
_writer = new Timer(1);
|
|
_writer.addEventListener(TimerEvent.TIMER, sendPendingMessages);
|
|
_writer.start();
|
|
|
|
// clear the queue, the server doesn't like anything sent prior to auth
|
|
_outq.length = 0;
|
|
}
|
|
|
|
/**
|
|
* Called when there is an io error with the socket.
|
|
*/
|
|
protected function socketError (event :Event) :void
|
|
{
|
|
// if we're still trying ports, try the next one.
|
|
if (_portIdx != -1) {
|
|
if (attemptLogon(_portIdx+1)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// total failure
|
|
log.warning("Socket error: " + event, "target", event.target);
|
|
Log.dumpStack();
|
|
shutdown(new Error(AuthCodes.NETWORK_ERROR));
|
|
}
|
|
|
|
/**
|
|
* Called when the connection to the server was closed.
|
|
*/
|
|
protected function socketClosed (event :Event) :void
|
|
{
|
|
log.info("Socket was closed: " + event);
|
|
_client.notifyObservers(ClientEvent.CLIENT_CONNECTION_FAILED);
|
|
// if we hadn't loaded our client object yet, behave as if this was a logon failure because
|
|
// we failed before we dispatched a DID_LOGON
|
|
var wasLoggedOn :Boolean = (_client.getClientObject() != null);
|
|
shutdown(wasLoggedOn ? null : new LogonError(AuthCodes.NETWORK_ERROR));
|
|
}
|
|
|
|
/**
|
|
* Returns the time at which we last sent a packet to the server.
|
|
*/
|
|
internal function getLastWrite () :uint
|
|
{
|
|
return _lastWrite;
|
|
}
|
|
|
|
/**
|
|
* Makes a note of the time at which we last communicated with the server.
|
|
*/
|
|
internal function updateWriteStamp () :void
|
|
{
|
|
_lastWrite = flash.utils.getTimer();
|
|
}
|
|
|
|
protected var _client :Client;
|
|
protected var _omgr :ClientDObjectMgr;
|
|
|
|
protected var _outBuffer :ByteArray;
|
|
protected var _outStream :ObjectOutputStream;
|
|
|
|
protected var _inStream :ObjectInputStream;
|
|
protected var _frameReader :FrameReader;
|
|
|
|
protected var _socket :Socket;
|
|
protected var _lastWrite :uint;
|
|
|
|
protected var _outq :Array = new Array();
|
|
protected var _writer :Timer;
|
|
protected var _notedThrottle :Boolean = false;
|
|
|
|
protected const log :Log = Log.getLog(this);
|
|
|
|
/** The current port we'll try to connect to. */
|
|
protected var _portIdx :int = -1;
|
|
}
|
|
}
|