Revision: 443 http://skycastle.svn.sourceforge.net/skycastle/?rev=443&view=rev Author: zzorn Date: 2008-04-05 14:40:12 -0700 (Sat, 05 Apr 2008) Log Message: ----------- Extracted common protocol negotiation and message decoding and encoding logic from the server, which can also be used for the client. Modified Paths: -------------- trunk/skycastle/modules/core/src/main/java/org/skycastle/protocol/negotiation/AbstractProtocolNegotiator.java trunk/skycastle/modules/core/src/main/java/org/skycastle/protocol/negotiation/ProtocolNegotiator.java trunk/skycastle/modules/server/src/main/java/org/skycastle/server/SkycastleClientSessionHandler.java Added Paths: ----------- trunk/skycastle/modules/core/src/main/java/org/skycastle/protocol/ProtocolCommunicator.java Added: trunk/skycastle/modules/core/src/main/java/org/skycastle/protocol/ProtocolCommunicator.java =================================================================== --- trunk/skycastle/modules/core/src/main/java/org/skycastle/protocol/ProtocolCommunicator.java (rev 0) +++ trunk/skycastle/modules/core/src/main/java/org/skycastle/protocol/ProtocolCommunicator.java 2008-04-05 21:40:12 UTC (rev 443) @@ -0,0 +1,249 @@ +package org.skycastle.protocol; + +import org.skycastle.messaging.Message; +import org.skycastle.protocol.negotiation.NegotiationStatus; +import org.skycastle.protocol.negotiation.ProtocolNegotiator; +import org.skycastle.util.ParameterChecker; + +import java.io.Serializable; + +/** + * Takes care of negotiating a protocol, and after that forwarding decoded messages to a listener. + * <p/> + * Also notifies the listener when a protocol has been negotiated, or negotiations failed. + * <p/> + * Call startNegotiations to start the negotiation process. + * + * @author Hans Häggström + */ +public abstract class ProtocolCommunicator + implements Serializable +{ + + //====================================================================== + // Private Fields + + private final ProtocolNegotiator myProtocolNegotiator; + + private int myProtocolNegotiationTimeout_ms; + private boolean myNegotiationsStarted = false; + + //====================================================================== + // Private Constants + + private static final long serialVersionUID = 1L; + + //====================================================================== + // Public Methods + + //---------------------------------------------------------------------- + // Constructors + + /** + * Creates a new {@link org.skycastle.protocol.ProtocolCommunicator}. + * <p/> + * Call startNegotiations to start the negotiation process. + * + * @param protocolNegotiator the negotiator to use to determine the protocol. Different for + * server and client side. + * @param protocolNegotiationTimeout_ms the timeout in milliseconds after which protocol negotiation is + * aborted and considered failed. + */ + public ProtocolCommunicator( final ProtocolNegotiator protocolNegotiator, + final int protocolNegotiationTimeout_ms ) + { + myProtocolNegotiationTimeout_ms = protocolNegotiationTimeout_ms; + ParameterChecker.checkNotNull( protocolNegotiator, "protocolNegotiator" ); + ParameterChecker.checkPositiveNonZeroInteger( protocolNegotiationTimeout_ms, + "protocolNegotiationTimeout_ms" ); + + myProtocolNegotiator = protocolNegotiator; + } + + //---------------------------------------------------------------------- + // Other Public Methods + + /** + * Should be called when everything is ready to start the negotiations (the implementing class is + * initialized). + * <p/> + * Starts the negotiation timeout, and sends opening message to the other party (depending on the {@link + * ProtocolNegotiator} type). + */ + public final void startNegotiations() + { + if ( myNegotiationsStarted ) + { + throw new IllegalStateException( + "Negotiations have already been started once, can not be started twice." ); + } + else + { + myNegotiationsStarted = true; + + // Give ourselves a deadline until the protocol should be negotiated + scheduleTimeoutCallback( myProtocolNegotiationTimeout_ms ); + + // If this side should start protocol negotiations, send the opening message + if ( myProtocolNegotiator.startsNegotiations() ) + { + sendEncodedMessage( myProtocolNegotiator.handleMessage( null ) ); + } + } + } + + + /** + * Should be called after the time specified amount of time after scheduleTimeoutCallback is called. Used + * to check if the protocol negotiation timed out. + */ + public final void timeoutCallback() + { + if ( !myProtocolNegotiator.getStatus().isFinished() ) + { + myProtocolNegotiator.timeoutCheck(); + + checkForNegotiationFinish(); + } + } + + + /** + * @return true when a protocol has been agreed upon, and messages can be sent and recieved to/from the + * other party. + */ + public final boolean canSendMessages() + { + return myProtocolNegotiator.getStatus().isSuccess(); + } + + + /** + * Handles incoming messages from the other party. + * + * @throws ProtocolException if there was a problem when decoding this message. + */ + public final void handleReceivedEncodedMessage( final byte[] byteMessage ) + throws ProtocolException + { + final NegotiationStatus status = myProtocolNegotiator.getStatus(); + + if ( status.isSuccess() ) + { + handleMessage( byteMessage ); + } + else if ( status.isFailure() ) + { + throw new IllegalStateException( "Protocol negotiations failed, no more messages can be handled." ); + } + else + { + doNegotiationRound( byteMessage ); + } + } + + + /** + * Sends the specified message to the other party. + * + * @param message Message to send. Should not be null. + * + * @throws ProtocolException thrown if there was some problem in encoding the message. + */ + public final void sendMessage( final Message message ) + throws ProtocolException + { + ParameterChecker.checkNotNull( message, "message" ); + + if ( myProtocolNegotiator.getStatus().isSuccess() ) + { + // Encode message and send to other party + sendEncodedMessage( myProtocolNegotiator.getProtocol().encode( message ) ); + } + else + { + throw new IllegalStateException( + "No message can be sent when the protocol negotiation status is '" + myProtocolNegotiator.getStatus() + "' " ); + } + } + + //====================================================================== + // Protected Methods + + //---------------------------------------------------------------------- + // Abstract Protected Methods + + /** + * Should set up a timer to call the timeoutCallback after the specified time. Used to implement protocol + * negotiation timeout. + * + * @param timeout_ms number of milliseconds to wait before calling timeoutCallback. + */ + protected abstract void scheduleTimeoutCallback( final long timeout_ms ); + + /** + * Called when a byte array message should be sent to the other party. + */ + protected abstract void sendEncodedMessage( final byte[] encodedMessage ); + + /** + * Called if protocol negotiations failed for some reason. + * + * @param status a more detailed explanation of why the protocol negotiations failed. + */ + protected abstract void onProtocolNegotiationFailed( final NegotiationStatus status ); + + /** + * Called if a protocol could be agreed to. Now it is possible to send and recieve messages from the + * other party. + * + * @param protocolId the String id of the selected protocol, just for information. + */ + protected abstract void onProtocolNegotiationSucceeded( final String protocolId ); + + /** + * Called when a message is recieved from the other party. + * + * @param message the deocoded message. + */ + protected abstract void onMessage( final Message message ) throws ProtocolException; + + //====================================================================== + // Private Methods + + private void doNegotiationRound( final byte[] byteMessage ) + { + final byte[] reply = myProtocolNegotiator.handleMessage( byteMessage ); + + if ( reply != null ) + { + sendEncodedMessage( reply ); + } + + checkForNegotiationFinish(); + } + + + private void checkForNegotiationFinish() + { + if ( myProtocolNegotiator.getStatus().isFinished() ) + { + if ( myProtocolNegotiator.getStatus().isSuccess() ) + { + onProtocolNegotiationSucceeded( myProtocolNegotiator.getProtocol().getProtocolId() ); + } + else + { + onProtocolNegotiationFailed( myProtocolNegotiator.getStatus() ); + } + } + } + + + private void handleMessage( final byte[] byteMessage ) throws ProtocolException + { + // Decode incoming message and notify decendant implementation + onMessage( myProtocolNegotiator.getProtocol().decode( byteMessage ) ); + } + +} Modified: trunk/skycastle/modules/core/src/main/java/org/skycastle/protocol/negotiation/AbstractProtocolNegotiator.java =================================================================== --- trunk/skycastle/modules/core/src/main/java/org/skycastle/protocol/negotiation/AbstractProtocolNegotiator.java 2008-04-05 20:01:46 UTC (rev 442) +++ trunk/skycastle/modules/core/src/main/java/org/skycastle/protocol/negotiation/AbstractProtocolNegotiator.java 2008-04-05 21:40:12 UTC (rev 443) @@ -86,6 +86,14 @@ return myProtocol; } + public final void timeoutCheck() + { + if ( !myStatus.isFinished() ) + { + myStatus = NegotiationStatus.TIMEOUT; + } + } + //====================================================================== // Protected Methods Modified: trunk/skycastle/modules/core/src/main/java/org/skycastle/protocol/negotiation/ProtocolNegotiator.java =================================================================== --- trunk/skycastle/modules/core/src/main/java/org/skycastle/protocol/negotiation/ProtocolNegotiator.java 2008-04-05 20:01:46 UTC (rev 442) +++ trunk/skycastle/modules/core/src/main/java/org/skycastle/protocol/negotiation/ProtocolNegotiator.java 2008-04-05 21:40:12 UTC (rev 443) @@ -41,4 +41,10 @@ * the other party. */ boolean startsNegotiations(); + + /** + * Notifies the {@link ProtocolNegotiator} that it has ran out of time, and should change its status to + * timeout failure, if it has not yet managed to negotiate a protocol. + */ + void timeoutCheck(); } Modified: trunk/skycastle/modules/server/src/main/java/org/skycastle/server/SkycastleClientSessionHandler.java =================================================================== --- trunk/skycastle/modules/server/src/main/java/org/skycastle/server/SkycastleClientSessionHandler.java 2008-04-05 20:01:46 UTC (rev 442) +++ trunk/skycastle/modules/server/src/main/java/org/skycastle/server/SkycastleClientSessionHandler.java 2008-04-05 21:40:12 UTC (rev 443) @@ -1,19 +1,16 @@ package org.skycastle.server; -import com.sun.sgs.app.AppContext; -import com.sun.sgs.app.ClientSession; -import com.sun.sgs.app.ClientSessionListener; -import com.sun.sgs.app.DataManager; +import com.sun.sgs.app.*; import org.skycastle.core.*; import org.skycastle.messaging.Message; import org.skycastle.messaging.modifications.ModificationMessage; import org.skycastle.messaging.updates.UpdateMessage; +import org.skycastle.protocol.ProtocolCommunicator; import org.skycastle.protocol.ProtocolException; -import org.skycastle.protocol.negotiation.ProtocolNegotiator; +import org.skycastle.protocol.negotiation.NegotiationStatus; import org.skycastle.protocol.negotiation.ServerSideProtocolNegotiator; import org.skycastle.protocol.registry.ProtocolRegistry; -import java.io.Serializable; import java.util.logging.Level; import java.util.logging.Logger; @@ -21,7 +18,8 @@ * Initializes a client session and listens to messages (commands) and session events from the client. */ public class SkycastleClientSessionHandler - implements Serializable, ClientSessionListener + extends ProtocolCommunicator + implements ClientSessionListener, Task { //====================================================================== @@ -35,8 +33,6 @@ @SuppressWarnings( { "NonSerializableFieldInSerializableClass" } ) private final ClientSession myClientSession; - private final ProtocolNegotiator myProtocolNegotiator; - // NOTE: For some reason the ManagedReference interface is not serializable, but the implementation is, // so we supress a serialization warning for IDE:s that look for those problems. @SuppressWarnings( { "NonSerializableFieldInSerializableClass" } ) @@ -47,6 +43,8 @@ //====================================================================== // Private Constants + private static final int PROTOCOL_NEGOTIATION_TIMEOUT_MS = 10000; + /** * The {@link Logger} for this class. */ @@ -75,50 +73,34 @@ public SkycastleClientSessionHandler( final ClientSession clientSession, final PersistentReference<ProtocolRegistry> protocolRegistryReference ) { + super( new ServerSideProtocolNegotiator( protocolRegistryReference, + SERVER_TYPE, + SERVER_VERSION ), + PROTOCOL_NEGOTIATION_TIMEOUT_MS ); + myClientSession = clientSession; - myProtocolNegotiator = new ServerSideProtocolNegotiator( protocolRegistryReference, - SERVER_TYPE, - SERVER_VERSION ); - - // If the server should start protocol negotiations, have it send the opening message - if ( myProtocolNegotiator.startsNegotiations() ) - { - clientSession.send( myProtocolNegotiator.handleMessage( null ) ); - } + startNegotiations(); } //---------------------------------------------------------------------- // ClientSessionListener Implementation /** - * {@inheritDoc} - * <p/> - * Logs when data arrives from the client, and echoes the message back. + * Handles incoming messages from the client. */ public void receivedMessage( final byte[] byteMessage ) { - if ( myProtocolNegotiator.getStatus().isFinished() ) + try { - if ( myProtocolNegotiator.getStatus().isSuccess() ) - { - handleMessage( byteMessage ); - } + handleReceivedEncodedMessage( byteMessage ); } - else + catch ( ProtocolException e ) { - // Continue protocol negotiation - final byte[] reply = myProtocolNegotiator.handleMessage( byteMessage ); + // Log exception + LOGGER.log( Level.WARNING, "Problem when decoding message from a client: " + e.getMessage(), e ); - if ( reply != null ) - { - myClientSession.send( reply ); - } - - if ( myProtocolNegotiator.getStatus().isFinished() ) - { - onProtocolNegotiationFinished( myProtocolNegotiator.getStatus().isSuccess() ); - } + // TODO: Terminate connection in case of exceptions? } } @@ -132,87 +114,106 @@ { // TODO: Notify the users account that the user disconnected (the avatars can go into off-line mode, etc). - // DEBUG: final String grace = graceful ? "graceful" : "forced"; LOGGER.log( Level.INFO, - "User {0} has logged out {1}", + "User {0} has done a {1} logout.", new Object[]{ myClientSession.getName(), grace } ); } + //---------------------------------------------------------------------- + // Task Implementation + + + public void run() throws Exception + { + timeoutCallback(); + } + //====================================================================== - // Private Methods + // Protected Methods - private void handleMessage( final byte[] byteMessage ) + @Override + protected void scheduleTimeoutCallback( final long timeout_ms ) { - try - { - // Decode incoming message - final Message message = myProtocolNegotiator.getProtocol().decode( byteMessage ); + // TODO: Check if a ClientSessionListener is considered to be a Task, or if it can be one? + AppContext.getTaskManager().scheduleTask( this, timeout_ms ); + } - // Check the message sender id so that the client is not claiming to be someone else. - if ( !myClientAccountId.equals( message.getSenderId() ) ) - { - throw new ProtocolException( "The client claimed to have the ID '" + message.getSenderId() + - "', while in reality the ID of the client account was '" + myClientAccountId + "'." ); - } + @Override + protected void sendEncodedMessage( final byte[] encodedMessage ) + { + myClientSession.send( encodedMessage ); + } - if ( message instanceof ModificationMessage ) - { - handleModificationMessage( message, (ModificationMessage) message ); - } - else if ( message instanceof UpdateMessage ) - { - throw new ProtocolException( - "The client can not send UpdateMessages to the server. Recieved a message of type: '" + message.getClass() + "'." ); - } - else - { - throw new ProtocolException( "Unknown message type '" + message.getClass() + "'." ); - } - } - catch ( ProtocolException e ) - { - LOGGER.warning( "Problem when decoding message from a client: " + e.getMessage() ); - // TODO: Log exception with client/account somehow? Enable fast detection of errorneous or flooding DoS:in clients - e.printStackTrace(); - } + @Override + protected void onProtocolNegotiationFailed( final NegotiationStatus status ) + { + LOGGER.log( Level.INFO, + "Protocol negotiation failed for user '{0}': {1}", + new Object[]{ myClientSession.getName(), status.toString() } ); + + myClientSession.disconnect(); } - private void handleModificationMessage( final Message message, - final ModificationMessage modificationMessage ) + @Override + protected void onProtocolNegotiationSucceeded( final String protocolId ) { - final GameObjectId targetId = modificationMessage.getTargetId(); - final GameObject target = GameContext.getGameObjectContext().getGameObjectById( - targetId, - true ); + LOGGER.log( Level.INFO, + "User '{0}' logged in.", + new Object[]{ myClientSession.getName() } ); - target.onMessage( message ); + final GameObject account = getUserAccount( myClientSession.getName() ); + + // TODO: Notify the account object that the user logged in. + + myClientAccountReference = new GameObjectReference( account ); } - private void onProtocolNegotiationFinished( final boolean success ) + @Override + protected void onMessage( final Message message ) throws ProtocolException { - if ( success ) + // Check the message sender id so that the client is not claiming to be someone else. + if ( !myClientAccountId.equals( message.getSenderId() ) ) { - // If the protocol negotiation finished successfully, get the users account - final GameObject account = getUserAccount( myClientSession.getName() ); + throw new ProtocolException( "The client claimed to have the ID '" + message.getSenderId() + + "', while in reality the ID of the client account was '" + myClientAccountId + "'." ); + } - // TODO: Notify the account object that the user logged in. - - myClientAccountReference = new GameObjectReference( account ); + if ( message instanceof ModificationMessage ) + { + handleModificationMessage( message, (ModificationMessage) message ); } + else if ( message instanceof UpdateMessage ) + { + throw new ProtocolException( + "The client can not send UpdateMessages to the server. Recieved a message of type: '" + message.getClass() + "'." ); + } else { - // If negotiation failed with error, disconnect - myClientSession.disconnect(); + throw new ProtocolException( "Unknown message type '" + message.getClass() + "'." ); } } + //====================================================================== + // Private Methods + private void handleModificationMessage( final Message message, + final ModificationMessage modificationMessage ) + { + final GameObjectId targetId = modificationMessage.getTargetId(); + final GameObject target = GameContext.getGameObjectContext().getGameObjectById( + targetId, + true ); + + target.onMessage( message ); + } + + /** * Get users account. The account will typically contain actions for resuming some existing avatars or * creating new avatars, and doing general account management and maybe out-of-game chat. This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.