| /* |
| * Copyright (c) 2011 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.ClientStateListener.DisconnectInfo; |
| import com.jme3.network.*; |
| 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 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: 8938 $ |
| * @author Paul Speed |
| */ |
| public class DefaultClient implements Client |
| { |
| static 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 ThreadLocal<ByteBuffer> dataBuffer = new ThreadLocal<ByteBuffer>(); |
| |
| private int id = -1; |
| private boolean isRunning = false; |
| private CountDownLatch connecting = new CountDownLatch(1); |
| private String gameName; |
| private int version; |
| private MessageListenerRegistry<Client> messageListeners = new MessageListenerRegistry<Client>(); |
| private List<ClientStateListener> stateListeners = new CopyOnWriteArrayList<ClientStateListener>(); |
| private List<ErrorListener<? super Client>> errorListeners = new CopyOnWriteArrayList<ErrorListener<? super Client>>(); |
| private Redispatch dispatcher = new Redispatch(); |
| private List<ConnectorAdapter> channels = new ArrayList<ConnectorAdapter>(); |
| |
| private ConnectorFactory connectorFactory; |
| |
| public DefaultClient( String gameName, int version ) |
| { |
| this.gameName = gameName; |
| this.version = version; |
| } |
| |
| public DefaultClient( String gameName, int version, Connector reliable, Connector fast, |
| ConnectorFactory connectorFactory ) |
| { |
| this( gameName, version ); |
| setPrimaryConnectors( reliable, fast, connectorFactory ); |
| } |
| |
| 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." ); |
| } |
| |
| 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); |
| } |
| } |
| |
| protected void waitForConnected() |
| { |
| if( isConnected() ) |
| return; |
| |
| try { |
| connecting.await(); |
| } catch( InterruptedException e ) { |
| throw new RuntimeException( "Interrupted waiting for connect", e ); |
| } |
| } |
| |
| public boolean isConnected() |
| { |
| return id != -1 && isRunning; |
| } |
| |
| public int getId() |
| { |
| return id; |
| } |
| |
| public String getGameName() |
| { |
| return gameName; |
| } |
| |
| public int getVersion() |
| { |
| return version; |
| } |
| |
| public void send( Message message ) |
| { |
| if( message.isReliable() || channels.get(CH_UNRELIABLE) == null ) { |
| send(CH_RELIABLE, message, true); |
| } else { |
| send(CH_UNRELIABLE, message, true); |
| } |
| } |
| |
| public void send( int channel, Message message ) |
| { |
| if( channel < 0 || 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); |
| } |
| |
| public void close() |
| { |
| checkRunning(); |
| |
| closeConnections( null ); |
| } |
| |
| protected void closeConnections( DisconnectInfo info ) |
| { |
| if( !isRunning ) |
| return; |
| |
| // 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(); |
| |
| fireDisconnected(info); |
| |
| isRunning = false; |
| } |
| |
| public void addClientStateListener( ClientStateListener listener ) |
| { |
| stateListeners.add( listener ); |
| } |
| |
| public void removeClientStateListener( ClientStateListener listener ) |
| { |
| stateListeners.remove( listener ); |
| } |
| |
| public void addMessageListener( MessageListener<? super Client> listener ) |
| { |
| messageListeners.addMessageListener( listener ); |
| } |
| |
| public void addMessageListener( MessageListener<? super Client> listener, Class... classes ) |
| { |
| messageListeners.addMessageListener( listener, classes ); |
| } |
| |
| public void removeMessageListener( MessageListener<? super Client> listener ) |
| { |
| messageListeners.removeMessageListener( listener ); |
| } |
| |
| public void removeMessageListener( MessageListener<? super Client> listener, Class... classes ) |
| { |
| messageListeners.removeMessageListener( listener, classes ); |
| } |
| |
| public void addErrorListener( ErrorListener<? super Client> listener ) |
| { |
| errorListeners.add( listener ); |
| } |
| |
| public void removeErrorListener( ErrorListener<? super Client> listener ) |
| { |
| errorListeners.remove( listener ); |
| } |
| |
| protected void fireConnected() |
| { |
| for( ClientStateListener l : stateListeners ) { |
| l.clientConnected( this ); |
| } |
| } |
| |
| 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 ) |
| { |
| // Pull off the connection management messages we're |
| // interested in and then pass on the rest. |
| if( m instanceof ClientRegistrationMessage ) { |
| // Then we've gotten our real id |
| this.id = (int)((ClientRegistrationMessage)m).getId(); |
| log.log( Level.INFO, "Connection established, id:{0}.", this.id ); |
| connecting.countDown(); |
| 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> |
| { |
| public void messageReceived( Object source, Message m ) |
| { |
| dispatch( m ); |
| } |
| |
| 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 ); |
| } |
| } |
| } |