diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java new file mode 100644 index 000000000..8deada70b --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/Asynchronous.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2015 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.service.rmi; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.*; + + +/** + * Indicates that a given method should be executed asynchronously + * through the RMI service. This must annotate the method on the + * shared interface for it to have an effect. + * + * @author Paul Speed + */ +@Retention(value=RUNTIME) +@Target(value=METHOD) +public @interface Asynchronous { + boolean reliable() default true; +} + + diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java new file mode 100644 index 000000000..6b218891d --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/CallType.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2015 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.service.rmi; + + +/** + * + * + * @author Paul Speed + */ +public enum CallType { + Synchronous, Asynchronous, Unreliable +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java new file mode 100644 index 000000000..88d91a686 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfo.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2015 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.service.rmi; + +import com.jme3.network.serializing.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + + +/** + * + * + * @author Paul Speed + */ +@Serializable +public final class ClassInfo { + private String name; + private short typeId; + private MethodInfo[] methods; + + /** + * For serialization only. + */ + public ClassInfo() { + } + + public ClassInfo( short typeId, Class type ) { + this.typeId = typeId; + this.name = type.getName(); + this.methods = toMethodInfo(type, type.getMethods()); + } + + public String getName() { + return name; + } + + public Class getType() { + try { + return Class.forName(name); + } catch( ClassNotFoundException e ) { + throw new RuntimeException("Error finding class for:" + this, e); + } + } + + public short getId() { + return typeId; + } + + public MethodInfo getMethod( short id ) { + return methods[id]; + } + + public MethodInfo getMethod( Method m ) { + for( MethodInfo mi : methods ) { + if( mi.matches(m) ) { + return mi; + } + } + return null; + } + + private MethodInfo[] toMethodInfo( Class type, Method[] methods ) { + List result = new ArrayList(); + short methodId = 0; + for( Method m : methods ) { + // Simple... add all methods exposed through the interface + result.add(new MethodInfo(methodId++, m)); + } + return result.toArray(new MethodInfo[result.size()]); + } + + public MethodInfo[] getMethods() { + return methods; + } + + @Override + public String toString() { + return "ClassInfo[" + name + "]"; + } +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java new file mode 100644 index 000000000..557948042 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/ClassInfoRegistry.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2015 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.service.rmi; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + + +/** + * + * + * @author Paul Speed + */ +public class ClassInfoRegistry { + + //private final LoadingCache cache; // Guava version + private final Map cache = new HashMap(); + private final AtomicInteger nextClassId = new AtomicInteger(); + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + public ClassInfoRegistry() { + //this.cache = CacheBuilder.newBuilder().build(new ClassInfoLoader()); // Guava version + } + + public ClassInfo getClassInfo( Class type ) { + //return cache.getUnchecked(type); // Guava version + + // More complicated without guava + lock.readLock().lock(); + try { + ClassInfo result = cache.get(type); + if( result != null ) { + return result; + } + // Else we need to create it and store it... so grab the write + // lock + lock.readLock().unlock(); + lock.writeLock().lock(); + try { + // Note: it's technically possible that a race with another thread + // asking for the same class already created one between our read unlock + // and our write lock. No matter as it's cheap to create one and does + // no harm. Code is simpler without the double-check. + result = new ClassInfo((short)nextClassId.getAndIncrement(), type); + cache.put(type, result); + + // Regrab the read lock before leaving... kind of unnecessary but + // it makes the method cleaner and widens the gap of lock races. + // Downgrading a write lock to read is ok. + lock.readLock().lock(); + + return result; + } finally { + // Unlock the write lock while still holding onto read + lock.writeLock().unlock(); + } + } finally { + lock.readLock().unlock(); + } + } + + /* + would be more straight-forward with guava Guava version + private class ClassInfoLoader extends CacheLoader { + @Override + public ClassInfo load( Class type ) { + return new ClassInfo((short)nextClassId.getAndIncrement(), type); + } + }*/ +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java new file mode 100644 index 000000000..a0138556d --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/MethodInfo.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2015 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.service.rmi; + +import com.jme3.network.serializing.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import javax.jws.Oneway; + + +/** + * + * + * @author Paul Speed + */ +@Serializable +public final class MethodInfo { + + public static final MethodInfo NULL_INFO = new MethodInfo(); + + private String representation; + private short id; + private CallType callType; + private transient Method method; + + /** + * For serialization only. + */ + public MethodInfo() { + } + + public MethodInfo( short id, Method m ) { + this.id = id; + this.method = m; + this.representation = methodToString(m); + this.callType = getCallType(m); + } + + public Object invoke( Object target, Object... parms ) { + try { + return method.invoke(target, parms); + } catch (IllegalAccessException e) { + throw new RuntimeException("Error invoking:" + method + " on:" + target, e); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Error invoking:" + method + " on:" + target, e); + } catch (InvocationTargetException e) { + throw new RuntimeException("Error invoking:" + method + " on:" + target, e); + } + } + + public short getId() { + return id; + } + + public CallType getCallType() { + return callType; + } + + public boolean matches( Method m ) { + return representation.equals(methodToString(m)); + } + + public static String methodToString( Method m ) { + StringBuilder sb = new StringBuilder(); + for( Class t : m.getParameterTypes() ) { + if( sb.length() > 0 ) + sb.append(", "); + sb.append(t.getName()); + } + return m.getReturnType().getName() + " " + m.getName() + "(" + sb + ")"; + } + + public static CallType getCallType( Method m ) { + if( m.getReturnType() != Void.TYPE ) + return CallType.Synchronous; + if( m.getAnnotation(Oneway.class) != null ) + return CallType.Asynchronous; + if( m.getAnnotation(Asynchronous.class) == null ) + return CallType.Synchronous; + + Asynchronous async = m.getAnnotation(Asynchronous.class); + return async.reliable() ? CallType.Asynchronous : CallType.Unreliable; + } + + @Override + public int hashCode() { + return representation.hashCode(); + } + + @Override + public boolean equals( Object o ) { + if( o == this ) { + return true; + } + if( o == null || o.getClass() != getClass() ) { + return false; + } + MethodInfo other = (MethodInfo)o; + return representation.equals(other.representation); + } + + @Override + public String toString() { + return "MethodInfo[#" + getId() + ", callType=" + callType + ", " + representation + "]"; + } +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java new file mode 100644 index 000000000..03309a4fa --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RemoteObjectHandler.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2015 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.service.rmi; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + + +/** + * + * + * @author Paul Speed + */ +public class RemoteObjectHandler implements InvocationHandler { + + private final RmiRegistry rmi; + private final byte channel; + private final short objectId; + private final ClassInfo typeInfo; + private final Map methodIndex = new ConcurrentHashMap(); + + public RemoteObjectHandler( RmiRegistry rmi, byte channel, short objectId, ClassInfo typeInfo ) { + this.rmi = rmi; + this.channel = channel; + this.objectId = objectId; + this.typeInfo = typeInfo; + } + + protected MethodInfo getMethodInfo( Method method ) { + MethodInfo mi = methodIndex.get(method); + if( mi == null ) { + mi = typeInfo.getMethod(method); + if( mi == null ) { + mi = MethodInfo.NULL_INFO; + } + methodIndex.put(method, mi); + } + return mi == MethodInfo.NULL_INFO ? null : mi; + } + + @Override + public Object invoke(Object o, Method method, Object[] os) throws Throwable { + MethodInfo mi = getMethodInfo(method); + if( mi == null ) { + // Try to invoke locally + return method.invoke(this, os); + } + return rmi.invokeRemote(channel, objectId, mi.getId(), mi.getCallType(), os); + } + + @Override + public String toString() { + return "RemoteObject[#" + objectId + ", " + typeInfo.getName() + "]"; + } +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java new file mode 100644 index 000000000..437a697f2 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiClientService.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015 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.service.rmi; + +import com.jme3.network.MessageConnection; +import com.jme3.network.service.AbstractClientService; +import com.jme3.network.service.ClientServiceManager; +import com.jme3.network.service.rpc.RpcClientService; +import java.util.ArrayList; +import java.util.List; + + +/** + * + * + * @author Paul Speed + */ +public class RmiClientService extends AbstractClientService { + + private RpcClientService rpc; + private byte defaultChannel; + private short rmiObjectId; + private RmiRegistry rmi; + private volatile boolean isStarted = false; + + private final List pending = new ArrayList(); + + public RmiClientService() { + this((short)-1, (byte)MessageConnection.CHANNEL_DEFAULT_RELIABLE); + } + + public RmiClientService( short rmiObjectId, byte defaultChannel ) { + this.defaultChannel = defaultChannel; + this.rmiObjectId = rmiObjectId; + } + + public void share( T object, Class type ) { + share(defaultChannel, object, type); + } + + public void share( byte channel, T object, Class type ) { + share(channel, type.getName(), object, type); + } + + public void share( String name, T object, Class type ) { + share(defaultChannel, name, object, type); + } + + public void share( byte channel, String name, T object, Class type ) { + if( !isStarted ) { + synchronized(pending) { + if( !isStarted ) { + pending.add(new ObjectInfo(channel, name, object, type)); + return; + } + } + } + + // Else we can add it directly. + rmi.share(channel, name, object, type); + } + + public T getRemoteObject( Class type ) { + return rmi.getRemoteObject(type); + } + + public T getRemoteObject( String name, Class type ) { + return rmi.getRemoteObject(name, type); + } + + @Override + protected void onInitialize( ClientServiceManager s ) { + rpc = getService(RpcClientService.class); + if( rpc == null ) { + throw new RuntimeException("RmiClientService requires RpcClientService"); + } + + // Register it now so that it is available when the + // server starts to send us stuff. Waiting until start() + // is too late in this case. + rmi = new RmiRegistry(rpc.getRpcConnection(), rmiObjectId, defaultChannel); + } + + @Override + public void start() { + super.start(); + + // Register all of the classes that have been waiting. + synchronized(pending) { + for( ObjectInfo info : pending ) { + rmi.share(info.channel, info.name, info.object, info.type); + } + pending.clear(); + isStarted = true; + } + } + + private class ObjectInfo { + byte channel; + String name; + Object object; + Class type; + + public ObjectInfo( byte channel, String name, Object object, Class type ) { + this.channel = channel; + this.name = name; + this.object = object; + this.type = type; + } + + @Override + public String toString() { + return "ObjectInfo[" + channel + ", " + name + ", " + object + ", " + type + "]"; + } + } +} + diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java new file mode 100644 index 000000000..47d115d5b --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiContext.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2015 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.service.rmi; + +import com.jme3.network.HostedConnection; + + +/** + * Keeps track of the current connection performing a particular + * RMI call. RMI-based services can use this to find out which + * connection is calling a particular method without having to + * pass additional problematic data on the method calls. + * + * @author Paul Speed + */ +public class RmiContext { + private static final ThreadLocal connection = new ThreadLocal(); + + public static HostedConnection getRmiConnection() { + return connection.get(); + } + + static void setRmiConnection( HostedConnection conn ) { + connection.set(conn); + } +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java new file mode 100644 index 000000000..647fe4fb6 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiHostedService.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2015 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.service.rmi; + +import com.jme3.network.HostedConnection; +import com.jme3.network.MessageConnection; +import com.jme3.network.Server; +import com.jme3.network.serializing.Serializer; +import com.jme3.network.service.AbstractHostedService; +import com.jme3.network.service.HostedServiceManager; +import com.jme3.network.service.rpc.RpcHostedService; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * + * + * @author Paul Speed + */ +public class RmiHostedService extends AbstractHostedService { + + static final Logger log = Logger.getLogger(RpcHostedService.class.getName()); + + public static final String ATTRIBUTE_NAME = "rmi"; + + private RpcHostedService rpcService; + private short rmiId; + private byte defaultChannel; + private boolean autoHost; + private final Map globalShares = new ConcurrentHashMap(); + + public RmiHostedService() { + this((short)-1, (byte)MessageConnection.CHANNEL_DEFAULT_RELIABLE, true); + } + + public RmiHostedService( short rmiId, byte defaultChannel, boolean autoHost ) { + this.rmiId = rmiId; + this.defaultChannel = defaultChannel; + this.autoHost = autoHost; + + Serializer.registerClasses(ClassInfo.class, MethodInfo.class); + } + + public void shareGlobal( T object, Class type ) { + shareGlobal(defaultChannel, type.getName(), object, type); + } + + public void shareGlobal( String name, T object, Class type ) { + shareGlobal(defaultChannel, name, object, type); + } + + public void shareGlobal( byte channel, String name, T object, Class type ) { + GlobalShare share = new GlobalShare(channel, object, type); + GlobalShare existing = globalShares.put(name, share); + if( existing != null ) { + // Shouldn't need to do anything actually. + } + + // Go through all of the children + for( HostedConnection conn : getServer().getConnections() ) { + RmiRegistry child = getRmiRegistry(conn); + if( child == null ) { + continue; + } + child.share(channel, name, object, type); + } + } + + public void setAutoHost( boolean b ) { + this.autoHost = b; + } + + public boolean getAutoHost() { + return autoHost; + } + + public RmiRegistry getRmiRegistry( HostedConnection hc ) { + return hc.getAttribute(ATTRIBUTE_NAME); + } + + /** + * Sets up RMI hosting services for the hosted connection allowing + * getRmiRegistry() to return a valid RmiRegistry object. + * This method is called automatically for all new connections if + * autohost is set to true. + */ + public void startHostingOnConnection( HostedConnection hc ) { + if( log.isLoggable(Level.FINEST) ) { + log.log(Level.FINEST, "startHostingOnConnection:{0}", hc); + } + RmiRegistry rmi = new RmiRegistry(hc, rpcService.getRpcConnection(hc), + rmiId, defaultChannel); + hc.setAttribute(ATTRIBUTE_NAME, rmi); + + // Register any global shares + for( Map.Entry e : globalShares.entrySet() ) { + GlobalShare share = e.getValue(); + rmi.share(share.channel, e.getKey(), share.object, share.type); + } + } + + /** + * Removes any RMI hosting services associated with the specified + * connection. Calls to getRmiRegistry() will return null for + * this connection. + * This method is called automatically for all leaving connections if + * autohost is set to true. + */ + public void stopHostingOnConnection( HostedConnection hc ) { + RmiRegistry rmi = hc.getAttribute(ATTRIBUTE_NAME); + if( rmi == null ) { + return; + } + if( log.isLoggable(Level.FINEST) ) { + log.log(Level.FINEST, "stopHostingOnConnection:{0}", hc); + } + hc.setAttribute(ATTRIBUTE_NAME, null); + //rpc.close(); + } + + @Override + protected void onInitialize( HostedServiceManager s ) { + this.rpcService = getService(RpcHostedService.class); + if( rpcService == null ) { + throw new RuntimeException("RmiHostedService requires RpcHostedService"); + } + } + + /** + * Called internally when a new connection is detected for + * the server. If the current autoHost property is true then + * startHostingOnConnection(hc) is called. + */ + @Override + public void connectionAdded(Server server, HostedConnection hc) { + if( log.isLoggable(Level.FINEST) ) { + log.log(Level.FINEST, "connectionAdded({0}, {1})", new Object[]{server, hc}); + } + if( autoHost ) { + startHostingOnConnection(hc); + } + } + + /** + * Called internally when an existing connection is leaving + * the server. If the current autoHost property is true then + * stopHostingOnConnection(hc) is called. + */ + @Override + public void connectionRemoved(Server server, HostedConnection hc) { + if( log.isLoggable(Level.FINEST) ) { + log.log(Level.FINEST, "connectionRemoved({0}, {1})", new Object[]{server, hc}); + } + if( autoHost ) { + stopHostingOnConnection(hc); + } + } + + private class GlobalShare { + byte channel; + Object object; + Class type; + + public GlobalShare( byte channel, Object object, Class type ) { + this.channel = channel; + this.object = object; + this.type = type; + } + } +} diff --git a/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java new file mode 100644 index 000000000..6d1722307 --- /dev/null +++ b/jme3-networking/src/main/java/com/jme3/network/service/rmi/RmiRegistry.java @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2015 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.service.rmi; + +import com.jme3.network.HostedConnection; +import com.jme3.network.MessageConnection; +import com.jme3.network.service.rpc.RpcConnection; +import com.jme3.network.service.rpc.RpcHandler; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.logging.Logger; + + +/** + * + * + * @author Paul Speed + */ +public class RmiRegistry { + + static final Logger log = Logger.getLogger(RmiRegistry.class.getName()); + + // RPC IDs for calling our remote endpoint + private static final short NEW_CLASS = 0; + private static final short ADD_OBJECT = 1; + private static final short REMOVE_OBJECT = 2; + + private RpcConnection rpc; + private short rmiId; + private byte defaultChannel; + private final RmiHandler rmiHandler = new RmiHandler(); + private final ClassInfoRegistry classCache = new ClassInfoRegistry(); + private final AtomicInteger nextObjectId = new AtomicInteger(); + + private final ObjectIndex local = new ObjectIndex(); + private final ObjectIndex remote = new ObjectIndex(); + + // Only used on the server to provide thread-local context for + // local RMI calls. + private HostedConnection context; + + public RmiRegistry( RpcConnection rpc, short rmiId, byte defaultChannel ) { + this(null, rpc, rmiId, defaultChannel); + } + + public RmiRegistry( HostedConnection context, RpcConnection rpc, short rmiId, byte defaultChannel ) { + this.context = context; + this.rpc = rpc; + this.rmiId = rmiId; + this.defaultChannel = defaultChannel; + rpc.registerHandler(rmiId, rmiHandler); + } + + public void share( T object, Class type ) { + share(defaultChannel, object, type); + } + + public void share( byte channel, T object, Class type ) { + share(channel, type.getName(), object, type); + } + + public void share( String name, T object, Class type ) { + share(defaultChannel, name, object, type); + } + + public void share( byte channel, String name, T object, Class type ) { + + ClassInfo typeInfo = classCache.getClassInfo(type); + + local.lock.writeLock().lock(); + try { + + // First see if we've told the remote end about this class + // before + if( local.classes.put(typeInfo.getId(), typeInfo) == null ) { + // It's new + rpc.callAsync(defaultChannel, rmiId, NEW_CLASS, typeInfo); + + // Because type info IDs are global to the class cache, + // we could in theory keep a global index that we broadcast + // on first connection setup... we need only prepopulate + // the index in that case. + } + + // See if we already shared an object under that name + SharedObject existing = local.byName.remove(name); + if( existing != null ) { + local.byId.remove(existing.objectId); + rpc.removeHandler(existing.objectId, rmiHandler); + + // Need to delete the old one from the remote end + rpc.callAsync(defaultChannel, rmiId, REMOVE_OBJECT, existing.objectId); + + // We don't reuse the ID because it's kind of dangerous. + // Churning through a new ID is our safety net for accidents. + } + + SharedObject newShare = new SharedObject(name, object, type, typeInfo); + local.byName.put(name, newShare); + local.byId.put(newShare.objectId, newShare); + + // Make sure we are setup to receive the remote method calls through + // the RPC service + rpc.registerHandler(newShare.objectId, rmiHandler); + + // Let the other end know + rpc.callAsync(defaultChannel, rmiId, ADD_OBJECT, channel, newShare.objectId, name, typeInfo.getId()); + + // We send the ADD_OBJECT to the other end before releasing the + // lock to avoid a potential inconsistency if two threads try to + // jam the same name at the same time. Otherwise, if the timing were + // right, the remove for one object could get there before its add. + + } finally { + local.lock.writeLock().unlock(); + } + } + + /** + * Returns an object that was previously registered with share(). + */ + public T getSharedObject( Class type ) { + return getSharedObject(type.getName(), type); + } + + /** + * Returns an object that was previously registered with share(). + */ + public T getSharedObject( String name, Class type ) { + local.lock.readLock().lock(); + try { + return type.cast(local.byName.get(name)); + } finally { + local.lock.readLock().unlock(); + } + } + + public T getRemoteObject( Class type ) { + return getRemoteObject(type.getName(), type); + } + + public T getRemoteObject( String name, Class type ) { + remote.lock.readLock().lock(); + try { + return type.cast(remote.byName.get(name)); + } finally { + remote.lock.readLock().unlock(); + } + } + + protected void addRemoteClass( ClassInfo info ) { + if( remote.classes.put(info.getId(), info) != null ) { + throw new RuntimeException("Error class already exists for ID:" + info.getId()); + } + } + + protected void removeRemoteObject( short objectId ) { + if( log.isLoggable(Level.FINEST) ) { + log.log(Level.FINEST, "removeRemoteObject({0})", objectId); + } + throw new UnsupportedOperationException("Removal not yet implemented."); + } + + protected void addRemoteObject( byte channel, short objectId, String name, ClassInfo typeInfo ) { + if( log.isLoggable(Level.FINEST) ) { + log.finest("addRemoveObject(" + objectId + ", " + name + ", " + typeInfo + ")"); + } + remote.lock.writeLock().lock(); + try { + Object existing = remote.byName.get(name); + if( existing != null ) { + throw new RuntimeException("Object already registered for:" + name); + } + + RemoteObjectHandler remoteHandler = new RemoteObjectHandler(this, channel, objectId, typeInfo); + + Object remoteObject = Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {typeInfo.getType()}, + remoteHandler); + + remote.byName.put(name, remoteObject); + remote.byId.put(objectId, remoteObject); + } finally { + remote.lock.writeLock().unlock(); + } + } + + protected Object invokeRemote( byte channel, short objectId, short procId, CallType callType, Object[] args ) { + if( log.isLoggable(Level.FINEST) ) { + log.finest("invokeRemote(" + channel + ", " + objectId + ", " + procId + ", " + + callType + ", " + (args == null ? "null" : Arrays.asList(args)) + ")"); + } + switch( callType ) { + case Asynchronous: + log.finest("Sending reliable asynchronous."); + rpc.callAsync(channel, objectId, procId, args); + return null; + case Unreliable: + log.finest("Sending unreliable asynchronous."); + rpc.callAsync((byte)MessageConnection.CHANNEL_DEFAULT_UNRELIABLE, objectId, procId, args); + return null; + default: + case Synchronous: + log.finest("Sending synchronous."); + Object result = rpc.callAndWait(channel, objectId, procId, args); + if( log.isLoggable(Level.FINEST) ) { + log.finest("->got:" + result); + } + return result; + } + } + + /** + * Handle remote object registry updates from the other end. + */ + protected void rmiUpdate( short procId, Object[] args ) { + if( log.isLoggable(Level.FINEST) ) { + log.finest("rmiUpdate(" + procId + ", " + Arrays.asList(args) + ")"); + } + switch( procId ) { + case NEW_CLASS: + addRemoteClass((ClassInfo)args[0]); + break; + case REMOVE_OBJECT: + removeRemoteObject((Short)args[0]); + break; + case ADD_OBJECT: + ClassInfo info = remote.classes.get((Short)args[3]); + addRemoteObject((Byte)args[0], (Short)args[1], (String)args[2], info); + break; + } + } + + /** + * Handle the actual remote object method calls. + */ + protected Object invokeLocal( short objectId, short procId, Object[] args ) { + // Actually could use a regular concurrent map for this + + // Only lock the local registry during lookup and + // not invocation. It prevents a deadlock if the invoked method + // tries to share an object. It should be safe. + SharedObject share = local.byId.get(objectId); + local.lock.readLock().lock(); + try { + share = local.byId.get(objectId); + } finally { + local.lock.readLock().unlock(); + } + + try { + RmiContext.setRmiConnection(context); + return share.invoke(procId, args); + } finally { + RmiContext.setRmiConnection(null); + } + } + + private class SharedObject { + private final short objectId; + private final String name; + private final Object object; + private final Class type; + private final ClassInfo classInfo; + + public SharedObject( String name, Object object, Class type, ClassInfo classInfo ) { + this.objectId = (short)nextObjectId.incrementAndGet(); + this.name = name; + this.object = object; + this.type = type; + this.classInfo = classInfo; + } + + public Object invoke( short procId, Object[] args ) { + return classInfo.getMethod(procId).invoke(object, args); + } + } + + private class RmiHandler implements RpcHandler { + @Override + public Object call( RpcConnection conn, short objectId, short procId, Object... args ) { + if( objectId == rmiId ) { + rmiUpdate(procId, args); + return null; + } else { + return invokeLocal(objectId, procId, args); + } + } + } + + /** + * Keeps a coincident index between short ID, name, and related class info. + * There will be one of these to track our local objects and one to track + * the remote objects and a lock that can guard them. + */ + private class ObjectIndex { + final Map byName = new HashMap(); + final Map byId = new HashMap(); + final Map classes = new HashMap(); + final ReadWriteLock lock = new ReentrantReadWriteLock(); + + public ObjectIndex() { + } + } + +} + +