/* * Copyright (c) 2009-2012 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'jMonkeyEngine' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.jme3.network.base; import com.jme3.network.*; import com.jme3.network.ClientStateListener.DisconnectInfo; import com.jme3.network.kernel.Connector; import com.jme3.network.message.ChannelInfoMessage; import com.jme3.network.message.ClientRegistrationMessage; import com.jme3.network.message.DisconnectMessage; import com.jme3.network.service.ClientServiceManager; import com.jme3.network.service.serializer.ClientSerializerRegistrationsService; import java.io.IOException; import java.nio.ByteBuffer; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.logging.Level; import java.util.logging.Logger; /** * A default implementation of the Client interface that delegates * its network connectivity to a kernel.Connector. * * @version $Revision$ * @author Paul Speed */ public class DefaultClient implements Client { static final Logger log = Logger.getLogger(DefaultClient.class.getName()); // First two channels are reserved for reliable and // unreliable. Note: channels are endpoint specific so these // constants and the handling need not have anything to do with // the same constants in DefaultServer... which is why they are // separate. private static final int CH_RELIABLE = 0; private static final int CH_UNRELIABLE = 1; private static final int CH_FIRST = 2; private final ThreadLocal<ByteBuffer> dataBuffer = new ThreadLocal<ByteBuffer>(); private int id = -1; private boolean isRunning = false; private final CountDownLatch connecting = new CountDownLatch(1); private String gameName; private int version; private final MessageListenerRegistry<Client> messageListeners = new MessageListenerRegistry<Client>(); private final List<ClientStateListener> stateListeners = new CopyOnWriteArrayList<ClientStateListener>(); private final List<ErrorListener<? super Client>> errorListeners = new CopyOnWriteArrayList<ErrorListener<? super Client>>(); private final Redispatch dispatcher = new Redispatch(); private final List<ConnectorAdapter> channels = new ArrayList<ConnectorAdapter>(); private ConnectorFactory connectorFactory; private ClientServiceManager services; public DefaultClient( String gameName, int version ) { this.gameName = gameName; this.version = version; this.services = new ClientServiceManager(this); addStandardServices(); } public DefaultClient( String gameName, int version, Connector reliable, Connector fast, ConnectorFactory connectorFactory ) { this( gameName, version ); setPrimaryConnectors( reliable, fast, connectorFactory ); } protected void addStandardServices() { log.fine("Adding standard services..."); services.addService(new ClientSerializerRegistrationsService()); } protected void setPrimaryConnectors( Connector reliable, Connector fast, ConnectorFactory connectorFactory ) { if( reliable == null ) throw new IllegalArgumentException( "The reliable connector cannot be null." ); if( isRunning ) throw new IllegalStateException( "Client is already started." ); if( !channels.isEmpty() ) throw new IllegalStateException( "Channels already exist." ); this.connectorFactory = connectorFactory; channels.add(new ConnectorAdapter(reliable, dispatcher, dispatcher, true)); if( fast != null ) { channels.add(new ConnectorAdapter(fast, dispatcher, dispatcher, false)); } else { // Add the null adapter to keep the indexes right channels.add(null); } } protected void checkRunning() { if( !isRunning ) throw new IllegalStateException( "Client is not started." ); } @Override public void start() { if( isRunning ) throw new IllegalStateException( "Client is already started." ); // Start up the threads and stuff for the // connectors that we have for( ConnectorAdapter ca : channels ) { if( ca == null ) continue; ca.start(); } // Send our connection message with a generated ID until // we get one back from the server. We'll hash time in // millis and time in nanos. // This is used to match the TCP and UDP endpoints up on the // other end since they may take different routes to get there. // Behind NAT, many game clients may be coming over the same // IP address from the server's perspective and they may have // their UDP ports mapped all over the place. // // Since currentTimeMillis() is absolute time and nano time // is roughtly related to system start time, adding these two // together should be plenty unique for our purposes. It wouldn't // hurt to reconcile with IP on the server side, though. long tempId = System.currentTimeMillis() + System.nanoTime(); // Set it true here so we can send some messages. isRunning = true; ClientRegistrationMessage reg; reg = new ClientRegistrationMessage(); reg.setId(tempId); reg.setGameName(getGameName()); reg.setVersion(getVersion()); reg.setReliable(true); send(CH_RELIABLE, reg, false); // Send registration messages to any other configured // connectors reg = new ClientRegistrationMessage(); reg.setId(tempId); reg.setReliable(false); for( int ch = CH_UNRELIABLE; ch < channels.size(); ch++ ) { if( channels.get(ch) == null ) continue; send(ch, reg, false); } } @Override public boolean isStarted() { return isRunning; } protected void waitForConnected() { if( isConnected() ) return; try { connecting.await(); } catch( InterruptedException e ) { throw new RuntimeException( "Interrupted waiting for connect", e ); } } @Override public boolean isConnected() { return id != -1 && isRunning; } @Override public int getId() { return id; } @Override public String getGameName() { return gameName; } @Override public int getVersion() { return version; } @Override public ClientServiceManager getServices() { return services; } @Override public void send( Message message ) { if( log.isLoggable(Level.FINER) ) { log.log(Level.FINER, "send({0})", message); } if( message.isReliable() || channels.get(CH_UNRELIABLE) == null ) { send(CH_RELIABLE, message, true); } else { send(CH_UNRELIABLE, message, true); } } @Override public void send( int channel, Message message ) { if( log.isLoggable(Level.FINER) ) { log.log(Level.FINER, "send({0}, {1})", new Object[]{channel, message}); } if( channel >= 0 ) { // Make sure we aren't still connecting. Channels // won't be valid until we are fully connected since // we receive the channel list from the server. // The default channels don't require the connection // to be fully up before sending. waitForConnected(); } if( channel < CHANNEL_DEFAULT_RELIABLE || channel + CH_FIRST >= channels.size() ) { throw new IllegalArgumentException( "Channel is undefined:" + channel ); } send(channel + CH_FIRST, message, true); } protected void send( int channel, Message message, boolean waitForConnected ) { checkRunning(); if( waitForConnected ) { // Make sure we aren't still connecting waitForConnected(); } ByteBuffer buffer = dataBuffer.get(); if( buffer == null ) { buffer = ByteBuffer.allocate( 65536 + 2 ); dataBuffer.set(buffer); } buffer.clear(); // Convert the message to bytes buffer = MessageProtocol.messageToBuffer(message, buffer); // Since we share the buffer between invocations, we will need to // copy this message's part out of it. This is because we actually // do the send on a background thread. byte[] temp = new byte[buffer.remaining()]; System.arraycopy(buffer.array(), buffer.position(), temp, 0, buffer.remaining()); buffer = ByteBuffer.wrap(temp); channels.get(channel).write(buffer); } @Override public void close() { checkRunning(); closeConnections( null ); } protected void closeConnections( DisconnectInfo info ) { synchronized(this) { if( !isRunning ) return; if( services.isStarted() ) { // Let the services get a chance to stop before we // kill the connection. services.stop(); } // Send a close message // Tell the thread it's ok to die for( ConnectorAdapter ca : channels ) { if( ca == null ) continue; ca.close(); } // Wait for the threads? // Just in case we never fully connected connecting.countDown(); isRunning = false; // Terminate the services services.terminate(); } // Make sure we aren't synched while firing events fireDisconnected(info); } @Override public void addClientStateListener( ClientStateListener listener ) { stateListeners.add( listener ); } @Override public void removeClientStateListener( ClientStateListener listener ) { stateListeners.remove( listener ); } @Override public void addMessageListener( MessageListener<? super Client> listener ) { messageListeners.addMessageListener( listener ); } @Override public void addMessageListener( MessageListener<? super Client> listener, Class... classes ) { messageListeners.addMessageListener( listener, classes ); } @Override public void removeMessageListener( MessageListener<? super Client> listener ) { messageListeners.removeMessageListener( listener ); } @Override public void removeMessageListener( MessageListener<? super Client> listener, Class... classes ) { messageListeners.removeMessageListener( listener, classes ); } @Override public void addErrorListener( ErrorListener<? super Client> listener ) { errorListeners.add( listener ); } @Override public void removeErrorListener( ErrorListener<? super Client> listener ) { errorListeners.remove( listener ); } protected void fireConnected() { for( ClientStateListener l : stateListeners ) { l.clientConnected( this ); } } protected void startServices() { log.fine("Starting client services."); // Let the services know we are finally started services.start(); } protected void fireDisconnected( DisconnectInfo info ) { for( ClientStateListener l : stateListeners ) { l.clientDisconnected( this, info ); } } /** * Either calls the ErrorListener or closes the connection * if there are no listeners. */ protected void handleError( Throwable t ) { // If there are no listeners then close the connection with // a reason if( errorListeners.isEmpty() ) { log.log( Level.SEVERE, "Termining connection due to unhandled error", t ); DisconnectInfo info = new DisconnectInfo(); info.reason = "Connection Error"; info.error = t; closeConnections(info); return; } for( ErrorListener l : errorListeners ) { l.handleError( this, t ); } } protected void configureChannels( long tempId, int[] ports ) { try { for( int i = 0; i < ports.length; i++ ) { Connector c = connectorFactory.createConnector( i, ports[i] ); ConnectorAdapter ca = new ConnectorAdapter(c, dispatcher, dispatcher, true); int ch = channels.size(); channels.add( ca ); // Need to send the connection its hook-up registration // and start it. ca.start(); ClientRegistrationMessage reg; reg = new ClientRegistrationMessage(); reg.setId(tempId); reg.setReliable(true); send( ch, reg, false ); } } catch( IOException e ) { throw new RuntimeException( "Error configuring channels", e ); } } protected void dispatch( Message m ) { if( log.isLoggable(Level.FINER) ) { log.log(Level.FINER, "{0} received:{1}", new Object[]{this, m}); } // Pull off the connection management messages we're // interested in and then pass on the rest. if( m instanceof ClientRegistrationMessage ) { ClientRegistrationMessage crm = (ClientRegistrationMessage)m; // See if it has a real ID if( crm.getId() >= 0 ) { // Then we've gotten our real id this.id = (int)crm.getId(); log.log( Level.FINE, "Connection established, id:{0}.", this.id ); connecting.countDown(); //fireConnected(); } else { // Else it's a message letting us know that the // hosted services have been started startServices(); // Delay firing 'connected' until the services have all // been started to avoid odd race conditions. If there is some // need to get some kind of event before the services have been // started then we should create a new event step. fireConnected(); } return; } else if( m instanceof ChannelInfoMessage ) { // This is an interum step in the connection process and // now we need to add a bunch of connections configureChannels( ((ChannelInfoMessage)m).getId(), ((ChannelInfoMessage)m).getPorts() ); return; } else if( m instanceof DisconnectMessage ) { // Can't do too much else yet String reason = ((DisconnectMessage)m).getReason(); log.log( Level.SEVERE, "Connection terminated, reason:{0}.", reason ); DisconnectInfo info = new DisconnectInfo(); info.reason = reason; closeConnections(info); } // Make sure client MessageListeners are called single-threaded // since it could receive messages from the TCP and UDP // thread simultaneously. synchronized( this ) { messageListeners.messageReceived( this, m ); } } protected class Redispatch implements MessageListener<Object>, ErrorListener<Object> { @Override public void messageReceived( Object source, Message m ) { dispatch( m ); } @Override public void handleError( Object source, Throwable t ) { // Only doing the DefaultClient.this to make the code // checker happy... it compiles fine without it but I // don't like red lines in my editor. :P DefaultClient.this.handleError( t ); } } }