/* Copyright (c) 2008, Nathan Sweet * 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 Esoteric Software 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 HOLDER 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.esotericsoftware.kryonet.rmi; import static com.esotericsoftware.minlog.Log.*; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.KryoException; import com.esotericsoftware.kryo.KryoSerializable; import com.esotericsoftware.kryo.Serializer; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; import com.esotericsoftware.kryo.serializers.FieldSerializer; import com.esotericsoftware.kryo.util.IntMap; import com.esotericsoftware.kryo.util.Util; import com.esotericsoftware.kryonet.Connection; import com.esotericsoftware.kryonet.EndPoint; import com.esotericsoftware.kryonet.FrameworkMessage; import com.esotericsoftware.kryonet.KryoNetException; import com.esotericsoftware.kryonet.KryoSerialization; import com.esotericsoftware.kryonet.Listener; import com.esotericsoftware.kryonet.util.ObjectIntMap; import com.esotericsoftware.reflectasm.MethodAccess; /** Allows methods on objects to be invoked remotely over TCP or UDP. Objects are {@link #register(int, Object) registered} with * an ID. The remote end of connections that have been {@link #addConnection(Connection) added} are allowed to * {@link #getRemoteObject(Connection, int, Class) access} registered objects. * <p> * It costs at least 2 bytes more to use remote method invocation than just sending the parameters. If the method has a return * value which is not {@link RemoteObject#setNonBlocking(boolean) ignored}, an extra byte is written. If the type of a parameter * is not final (note primitives are final) then an extra byte is written for that parameter. * <p> * ObjectSpace requires {@link KryoSerialization}. * @author Nathan Sweet <[email protected]> */ public class ObjectSpace { static private final int returnValueMask = 1 << 7; static private final int returnExceptionMask = 1 << 6; static private final int responseIdMask = 0xff & ~returnValueMask & ~returnExceptionMask; static private final Object instancesLock = new Object(); static ObjectSpace[] instances = new ObjectSpace[0]; static private final HashMap<Class, CachedMethod[]> methodCache = new HashMap(); static private boolean asm = true; final IntMap idToObject = new IntMap(); final ObjectIntMap objectToID = new ObjectIntMap(); Connection[] connections = {}; final Object connectionsLock = new Object(); Executor executor; private final Listener invokeListener = new Listener() { public void received (final Connection connection, Object object) { if (!(object instanceof InvokeMethod)) return; if (connections != null) { int i = 0, n = connections.length; for (; i < n; i++) if (connection == connections[i]) break; if (i == n) return; // The InvokeMethod message is not for a connection in this ObjectSpace. } final InvokeMethod invokeMethod = (InvokeMethod)object; final Object target = idToObject.get(invokeMethod.objectID); if (target == null) { if (WARN) warn("kryonet", "Ignoring remote invocation request for unknown object ID: " + invokeMethod.objectID); return; } if (executor == null) invoke(connection, target, invokeMethod); else { executor.execute(new Runnable() { public void run () { invoke(connection, target, invokeMethod); } }); } } public void disconnected (Connection connection) { removeConnection(connection); } }; /** Creates an ObjectSpace with no connections. Connections must be {@link #addConnection(Connection) added} to allow the * remote end of the connections to access objects in this ObjectSpace. */ public ObjectSpace () { synchronized (instancesLock) { ObjectSpace[] instances = ObjectSpace.instances; ObjectSpace[] newInstances = new ObjectSpace[instances.length + 1]; newInstances[0] = this; System.arraycopy(instances, 0, newInstances, 1, instances.length); ObjectSpace.instances = newInstances; } } /** Creates an ObjectSpace with the specified connection. More connections can be {@link #addConnection(Connection) added}. */ public ObjectSpace (Connection connection) { this(); addConnection(connection); } /** Sets the executor used to invoke methods when an invocation is received from a remote endpoint. By default, no executor is * set and invocations occur on the network thread, which should not be blocked for long. * @param executor May be null. */ public void setExecutor (Executor executor) { this.executor = executor; } /** Registers an object to allow the remote end of the ObjectSpace's connections to access it using the specified ID. * <p> * If a connection is added to multiple ObjectSpaces, the same object ID should not be registered in more than one of those * ObjectSpaces. * @param objectID Must not be Integer.MAX_VALUE. * @see #getRemoteObject(Connection, int, Class...) */ public void register (int objectID, Object object) { if (objectID == Integer.MAX_VALUE) throw new IllegalArgumentException("objectID cannot be Integer.MAX_VALUE."); if (object == null) throw new IllegalArgumentException("object cannot be null."); idToObject.put(objectID, object); objectToID.put(object, objectID); if (TRACE) trace("kryonet", "Object registered with ObjectSpace as " + objectID + ": " + object); } /** Removes an object. The remote end of the ObjectSpace's connections will no longer be able to access it. */ public void remove (int objectID) { Object object = idToObject.remove(objectID); if (object != null) objectToID.remove(object, 0); if (TRACE) trace("kryonet", "Object " + objectID + " removed from ObjectSpace: " + object); } /** Removes an object. The remote end of the ObjectSpace's connections will no longer be able to access it. */ public void remove (Object object) { if (!idToObject.containsValue(object, true)) return; int objectID = idToObject.findKey(object, true, -1); idToObject.remove(objectID); objectToID.remove(object, 0); if (TRACE) trace("kryonet", "Object " + objectID + " removed from ObjectSpace: " + object); } /** Causes this ObjectSpace to stop listening to the connections for method invocation messages. */ public void close () { Connection[] connections = this.connections; for (int i = 0; i < connections.length; i++) connections[i].removeListener(invokeListener); synchronized (instancesLock) { ArrayList<ObjectSpace> temp = new ArrayList(Arrays.asList(instances)); temp.remove(this); instances = temp.toArray(new ObjectSpace[temp.size()]); } if (TRACE) trace("kryonet", "Closed ObjectSpace."); } /** Allows the remote end of the specified connection to access objects registered in this ObjectSpace. */ public void addConnection (Connection connection) { if (connection == null) throw new IllegalArgumentException("connection cannot be null."); synchronized (connectionsLock) { Connection[] newConnections = new Connection[connections.length + 1]; newConnections[0] = connection; System.arraycopy(connections, 0, newConnections, 1, connections.length); connections = newConnections; } connection.addListener(invokeListener); if (TRACE) trace("kryonet", "Added connection to ObjectSpace: " + connection); } /** Removes the specified connection, it will no longer be able to access objects registered in this ObjectSpace. */ public void removeConnection (Connection connection) { if (connection == null) throw new IllegalArgumentException("connection cannot be null."); connection.removeListener(invokeListener); synchronized (connectionsLock) { ArrayList<Connection> temp = new ArrayList(Arrays.asList(connections)); temp.remove(connection); connections = temp.toArray(new Connection[temp.size()]); } if (TRACE) trace("kryonet", "Removed connection from ObjectSpace: " + connection); } /** Invokes the method on the object and, if necessary, sends the result back to the connection that made the invocation * request. This method is invoked on the update thread of the {@link EndPoint} for this ObjectSpace and unless an * {@link #setExecutor(Executor) executor} has been set. * @param connection The remote side of this connection requested the invocation. */ protected void invoke (Connection connection, Object target, InvokeMethod invokeMethod) { if (DEBUG) { String argString = ""; if (invokeMethod.args != null) { argString = Arrays.deepToString(invokeMethod.args); argString = argString.substring(1, argString.length() - 1); } debug("kryonet", connection + " received: " + target.getClass().getSimpleName() + "#" + invokeMethod.cachedMethod.method.getName() + "(" + argString + ")"); } byte responseData = invokeMethod.responseData; boolean transmitReturnValue = (responseData & returnValueMask) == returnValueMask; boolean transmitExceptions = (responseData & returnExceptionMask) == returnExceptionMask; int responseID = responseData & responseIdMask; CachedMethod cachedMethod = invokeMethod.cachedMethod; Object result = null; try { result = cachedMethod.invoke(target, invokeMethod.args); } catch (InvocationTargetException ex) { if (transmitExceptions) result = ex.getCause(); else throw new KryoNetException("Error invoking method: " + cachedMethod.method.getDeclaringClass().getName() + "." + cachedMethod.method.getName(), ex); } catch (Exception ex) { throw new KryoNetException( "Error invoking method: " + cachedMethod.method.getDeclaringClass().getName() + "." + cachedMethod.method.getName(), ex); } if (responseID == 0) return; InvokeMethodResult invokeMethodResult = new InvokeMethodResult(); invokeMethodResult.objectID = invokeMethod.objectID; invokeMethodResult.responseID = (byte)responseID; // Do not return non-primitives if transmitReturnValue is false. if (!transmitReturnValue && !invokeMethod.cachedMethod.method.getReturnType().isPrimitive()) { invokeMethodResult.result = null; } else { invokeMethodResult.result = result; } int length = connection.sendTCP(invokeMethodResult); if (DEBUG) debug("kryonet", connection + " sent TCP: " + result + " (" + length + ")"); } /** Identical to {@link #getRemoteObject(Connection, int, Class...)} except returns the object cast to the specified interface * type. The returned object still implements {@link RemoteObject}. */ static public <T> T getRemoteObject (final Connection connection, int objectID, Class<T> iface) { return (T)getRemoteObject(connection, objectID, new Class[] {iface}); } /** Returns a proxy object that implements the specified interfaces. Methods invoked on the proxy object will be invoked * remotely on the object with the specified ID in the ObjectSpace for the specified connection. If the remote end of the * connection has not {@link #addConnection(Connection) added} the connection to the ObjectSpace, the remote method invocations * will be ignored. * <p> * Methods that return a value will throw {@link TimeoutException} if the response is not received with the * {@link RemoteObject#setResponseTimeout(int) response timeout}. * <p> * If {@link RemoteObject#setNonBlocking(boolean) non-blocking} is false (the default), then methods that return a value must * not be called from the update thread for the connection. An exception will be thrown if this occurs. Methods with a void * return value can be called on the update thread. * <p> * If a proxy returned from this method is part of an object graph sent over the network, the object graph on the receiving * side will have the proxy object replaced with the registered object. * @see RemoteObject */ static public RemoteObject getRemoteObject (Connection connection, int objectID, Class... ifaces) { if (connection == null) throw new IllegalArgumentException("connection cannot be null."); if (ifaces == null) throw new IllegalArgumentException("ifaces cannot be null."); Class[] temp = new Class[ifaces.length + 1]; temp[0] = RemoteObject.class; System.arraycopy(ifaces, 0, temp, 1, ifaces.length); return (RemoteObject)Proxy.newProxyInstance(ObjectSpace.class.getClassLoader(), temp, new RemoteInvocationHandler(connection, objectID)); } /** Handles network communication when methods are invoked on a proxy. */ static private class RemoteInvocationHandler implements InvocationHandler { private final Connection connection; final int objectID; private int timeoutMillis = 3000; private boolean nonBlocking; private boolean transmitReturnValue = true; private boolean transmitExceptions = true; private boolean remoteToString; private boolean udp; private Byte lastResponseID; private byte nextResponseId = 1; private Listener responseListener; final ReentrantLock lock = new ReentrantLock(); final Condition responseCondition = lock.newCondition(); final InvokeMethodResult[] responseTable = new InvokeMethodResult[64]; final boolean[] pendingResponses = new boolean[64]; public RemoteInvocationHandler (Connection connection, final int objectID) { super(); this.connection = connection; this.objectID = objectID; responseListener = new Listener() { public void received (Connection connection, Object object) { if (!(object instanceof InvokeMethodResult)) return; InvokeMethodResult invokeMethodResult = (InvokeMethodResult)object; if (invokeMethodResult.objectID != objectID) return; int responseID = invokeMethodResult.responseID; synchronized (this) { if (pendingResponses[responseID]) responseTable[responseID] = invokeMethodResult; } lock.lock(); try { responseCondition.signalAll(); } finally { lock.unlock(); } } public void disconnected (Connection connection) { close(); } }; connection.addListener(responseListener); } public Object invoke (Object proxy, Method method, Object[] args) throws Exception { Class declaringClass = method.getDeclaringClass(); if (declaringClass == RemoteObject.class) { String name = method.getName(); if (name.equals("close")) { close(); return null; } else if (name.equals("setResponseTimeout")) { timeoutMillis = (Integer)args[0]; return null; } else if (name.equals("setNonBlocking")) { nonBlocking = (Boolean)args[0]; return null; } else if (name.equals("setTransmitReturnValue")) { transmitReturnValue = (Boolean)args[0]; return null; } else if (name.equals("setUDP")) { udp = (Boolean)args[0]; return null; } else if (name.equals("setTransmitExceptions")) { transmitExceptions = (Boolean)args[0]; return null; } else if (name.equals("setRemoteToString")) { remoteToString = (Boolean)args[0]; return null; } else if (name.equals("waitForLastResponse")) { if (lastResponseID == null) throw new IllegalStateException("There is no last response to wait for."); return waitForResponse(lastResponseID); } else if (name.equals("hasLastResponse")) { if (lastResponseID == null) throw new IllegalStateException("There is no last response."); synchronized (this) { return responseTable[lastResponseID] != null; } } else if (name.equals("getLastResponseID")) { if (lastResponseID == null) throw new IllegalStateException("There is no last response ID."); return lastResponseID; } else if (name.equals("waitForResponse")) { if (!transmitReturnValue && !transmitExceptions && nonBlocking) throw new IllegalStateException("This RemoteObject is currently set to ignore all responses."); return waitForResponse((Byte)args[0]); } else if (name.equals("hasResponse")) { synchronized (this) { return responseTable[(Byte)args[0]] != null; } } else if (name.equals("getConnection")) { return connection; } // Should never happen, for debugging purposes only throw new KryoNetException("Invocation handler could not find RemoteObject method. Check ObjectSpace.java"); } else if (!remoteToString && declaringClass == Object.class && method.getName().equals("toString")) // return "<proxy>"; InvokeMethod invokeMethod = new InvokeMethod(); invokeMethod.objectID = objectID; invokeMethod.args = args; CachedMethod[] cachedMethods = getMethods(connection.getEndPoint().getKryo(), method.getDeclaringClass()); for (int i = 0, n = cachedMethods.length; i < n; i++) { CachedMethod cachedMethod = cachedMethods[i]; if (cachedMethod.method.equals(method)) { invokeMethod.cachedMethod = cachedMethod; break; } } if (invokeMethod.cachedMethod == null) throw new KryoNetException("Method not found: " + method); // A invocation doesn't need a response if it's async and no return values or exceptions are wanted back. boolean needsResponse = !udp && (transmitReturnValue || transmitExceptions || !nonBlocking); byte responseID = 0; if (needsResponse) { synchronized (this) { // Increment the response counter and put it into the low bits of the responseID. responseID = nextResponseId++; if (nextResponseId > responseIdMask) nextResponseId = 1; pendingResponses[responseID] = true; } // Pack other data into the high bits. byte responseData = responseID; if (transmitReturnValue) responseData |= returnValueMask; if (transmitExceptions) responseData |= returnExceptionMask; invokeMethod.responseData = responseData; } else { invokeMethod.responseData = 0; // A response data of 0 means to not respond. } int length = udp ? connection.sendUDP(invokeMethod) : connection.sendTCP(invokeMethod); if (DEBUG) { String argString = ""; if (args != null) { argString = Arrays.deepToString(args); argString = argString.substring(1, argString.length() - 1); } debug("kryonet", connection + " sent " + (udp ? "UDP" : "TCP") + ": " + method.getDeclaringClass().getSimpleName() + "#" + method.getName() + "(" + argString + ") (" + length + ")"); } lastResponseID = (byte)(invokeMethod.responseData & responseIdMask); if (nonBlocking || udp) { Class returnType = method.getReturnType(); if (returnType.isPrimitive()) { if (returnType == int.class) return 0; if (returnType == boolean.class) return Boolean.FALSE; if (returnType == float.class) return 0f; if (returnType == char.class) return (char)0; if (returnType == long.class) return 0l; if (returnType == short.class) return (short)0; if (returnType == byte.class) return (byte)0; if (returnType == double.class) return 0d; } return null; } try { Object result = waitForResponse(lastResponseID); if (result != null && result instanceof Exception) throw (Exception)result; else return result; } catch (TimeoutException ex) { throw new TimeoutException("Response timed out: " + method.getDeclaringClass().getName() + "." + method.getName()); } finally { synchronized (this) { pendingResponses[responseID] = false; responseTable[responseID] = null; } } } private Object waitForResponse (byte responseID) { if (connection.getEndPoint().getUpdateThread() == Thread.currentThread()) throw new IllegalStateException("Cannot wait for an RMI response on the connection's update thread."); long endTime = System.currentTimeMillis() + timeoutMillis; while (true) { long remaining = endTime - System.currentTimeMillis(); InvokeMethodResult invokeMethodResult; synchronized (this) { invokeMethodResult = responseTable[responseID]; } if (invokeMethodResult != null) { lastResponseID = null; return invokeMethodResult.result; } else { if (remaining <= 0) throw new TimeoutException("Response timed out."); lock.lock(); try { responseCondition.await(remaining, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new KryoNetException(e); } finally { lock.unlock(); } } } } void close () { connection.removeListener(responseListener); } } /** Internal message to invoke methods remotely. */ static public class InvokeMethod implements FrameworkMessage, KryoSerializable { public int objectID; public CachedMethod cachedMethod; public Object[] args; // The top bits of the ID indicate if the remote invocation should respond with return values and exceptions, respectively. // The remaining bites are a counter. This means up to 63 responses can be stored before undefined behavior occurs due to // possible duplicate IDs. A response data of 0 means to not respond. public byte responseData; public void write (Kryo kryo, Output output) { output.writeInt(objectID, true); output.writeInt(cachedMethod.methodClassID, true); output.writeByte(cachedMethod.methodIndex); Serializer[] serializers = cachedMethod.serializers; Object[] args = this.args; for (int i = 0, n = serializers.length; i < n; i++) { Serializer serializer = serializers[i]; if (serializer != null) kryo.writeObjectOrNull(output, args[i], serializer); else kryo.writeClassAndObject(output, args[i]); } output.writeByte(responseData); } public void read (Kryo kryo, Input input) { objectID = input.readInt(true); int methodClassID = input.readInt(true); Class methodClass = kryo.getRegistration(methodClassID).getType(); byte methodIndex = input.readByte(); try { cachedMethod = getMethods(kryo, methodClass)[methodIndex]; } catch (IndexOutOfBoundsException ex) { throw new KryoException("Invalid method index " + methodIndex + " for class: " + methodClass.getName()); } Serializer[] serializers = cachedMethod.serializers; Class[] parameterTypes = cachedMethod.method.getParameterTypes(); Object[] args = new Object[serializers.length]; this.args = args; for (int i = 0, n = args.length; i < n; i++) { Serializer serializer = serializers[i]; if (serializer != null) args[i] = kryo.readObjectOrNull(input, parameterTypes[i], serializer); else args[i] = kryo.readClassAndObject(input); } responseData = input.readByte(); } } /** Internal message to return the result of a remotely invoked method. */ static public class InvokeMethodResult implements FrameworkMessage { public int objectID; public byte responseID; public Object result; } static CachedMethod[] getMethods (Kryo kryo, Class type) { CachedMethod[] cachedMethods = methodCache.get(type); // Maybe should cache per Kryo instance? if (cachedMethods != null) return cachedMethods; ArrayList<Method> allMethods = new ArrayList(); Class nextClass = type; while (nextClass != null) { Collections.addAll(allMethods, nextClass.getDeclaredMethods()); nextClass = nextClass.getSuperclass(); if (nextClass == Object.class) break; } ArrayList<Method> methods = new ArrayList(Math.max(1, allMethods.size())); for (int i = 0, n = allMethods.size(); i < n; i++) { Method method = allMethods.get(i); int modifiers = method.getModifiers(); if (Modifier.isStatic(modifiers)) continue; if (Modifier.isPrivate(modifiers)) continue; if (method.isSynthetic()) continue; methods.add(method); } Collections.sort(methods, new Comparator<Method>() { public int compare (Method o1, Method o2) { // Methods are sorted so they can be represented as an index. int diff = o1.getName().compareTo(o2.getName()); if (diff != 0) return diff; Class[] argTypes1 = o1.getParameterTypes(); Class[] argTypes2 = o2.getParameterTypes(); if (argTypes1.length > argTypes2.length) return 1; if (argTypes1.length < argTypes2.length) return -1; for (int i = 0; i < argTypes1.length; i++) { diff = argTypes1[i].getName().compareTo(argTypes2[i].getName()); if (diff != 0) return diff; } throw new RuntimeException("Two methods with same signature!"); // Impossible. } }); Object methodAccess = null; if (asm && !Util.isAndroid && Modifier.isPublic(type.getModifiers())) methodAccess = MethodAccess.get(type); int n = methods.size(); cachedMethods = new CachedMethod[n]; for (int i = 0; i < n; i++) { Method method = methods.get(i); Class[] parameterTypes = method.getParameterTypes(); CachedMethod cachedMethod = null; if (methodAccess != null) { try { AsmCachedMethod asmCachedMethod = new AsmCachedMethod(); asmCachedMethod.methodAccessIndex = ((MethodAccess)methodAccess).getIndex(method.getName(), parameterTypes); asmCachedMethod.methodAccess = (MethodAccess)methodAccess; cachedMethod = asmCachedMethod; } catch (RuntimeException ignored) { } } if (cachedMethod == null) cachedMethod = new CachedMethod(); cachedMethod.method = method; cachedMethod.methodClassID = kryo.getRegistration(method.getDeclaringClass()).getId(); cachedMethod.methodIndex = i; // Store the serializer for each final parameter. cachedMethod.serializers = new Serializer[parameterTypes.length]; for (int ii = 0, nn = parameterTypes.length; ii < nn; ii++) if (kryo.isFinal(parameterTypes[ii])) cachedMethod.serializers[ii] = kryo.getSerializer(parameterTypes[ii]); cachedMethods[i] = cachedMethod; } methodCache.put(type, cachedMethods); return cachedMethods; } /** Returns the first object registered with the specified ID in any of the ObjectSpaces the specified connection belongs * to. */ static Object getRegisteredObject (Connection connection, int objectID) { ObjectSpace[] instances = ObjectSpace.instances; for (int i = 0, n = instances.length; i < n; i++) { ObjectSpace objectSpace = instances[i]; // Check if the connection is in this ObjectSpace. Connection[] connections = objectSpace.connections; for (int j = 0; j < connections.length; j++) { if (connections[j] != connection) continue; // Find an object with the objectID. Object object = objectSpace.idToObject.get(objectID); if (object != null) return object; } } return null; } /** Returns the first ID registered for the specified object with any of the ObjectSpaces the specified connection belongs to, * or Integer.MAX_VALUE if not found. */ static int getRegisteredID (Connection connection, Object object) { ObjectSpace[] instances = ObjectSpace.instances; for (int i = 0, n = instances.length; i < n; i++) { ObjectSpace objectSpace = instances[i]; // Check if the connection is in this ObjectSpace. Connection[] connections = objectSpace.connections; for (int j = 0; j < connections.length; j++) { if (connections[j] != connection) continue; // Find an ID with the object. int id = objectSpace.objectToID.get(object, Integer.MAX_VALUE); if (id != Integer.MAX_VALUE) return id; } } return Integer.MAX_VALUE; } /** Registers the classes needed to use ObjectSpaces. This should be called before any connections are opened. * @see Kryo#register(Class, Serializer) */ static public void registerClasses (final Kryo kryo) { kryo.register(Object[].class); kryo.register(InvokeMethod.class); FieldSerializer<InvokeMethodResult> resultSerializer = new FieldSerializer<InvokeMethodResult>(kryo, InvokeMethodResult.class) { public void write (Kryo kryo, Output output, InvokeMethodResult result) { super.write(kryo, output, result); output.writeInt(result.objectID, true); } public InvokeMethodResult read (Kryo kryo, Input input, Class type) { InvokeMethodResult result = super.read(kryo, input, type); result.objectID = input.readInt(true); return result; } }; resultSerializer.removeField("objectID"); kryo.register(InvokeMethodResult.class, resultSerializer); kryo.register(InvocationHandler.class, new Serializer() { public void write (Kryo kryo, Output output, Object object) { RemoteInvocationHandler handler = (RemoteInvocationHandler)Proxy.getInvocationHandler(object); output.writeInt(handler.objectID, true); } public Object read (Kryo kryo, Input input, Class type) { int objectID = input.readInt(true); Connection connection = (Connection)kryo.getContext().get("connection"); Object object = getRegisteredObject(connection, objectID); if (WARN && object == null) warn("kryonet", "Unknown object ID " + objectID + " for connection: " + connection); return object; } }); } /** If true, an attempt will be made to use ReflectASM for invoking methods. Default is true. */ static public void setAsm (boolean asm) { ObjectSpace.asm = asm; } static class CachedMethod { Method method; int methodClassID; int methodIndex; Serializer[] serializers; public Object invoke (Object target, Object[] args) throws IllegalAccessException, InvocationTargetException { return method.invoke(target, args); } } static class AsmCachedMethod extends CachedMethod { MethodAccess methodAccess; int methodAccessIndex = -1; public Object invoke (Object target, Object[] args) throws IllegalAccessException, InvocationTargetException { try { return methodAccess.invoke(target, methodAccessIndex, args); } catch (Exception ex) { throw new InvocationTargetException(ex); } } } /** Serializes an object registered with an ObjectSpace so the receiving side gets a {@link RemoteObject} proxy rather than the * bytes for the serialized object. * @author Nathan Sweet <[email protected]> */ static public class RemoteObjectSerializer extends Serializer { public void write (Kryo kryo, Output output, Object object) { Connection connection = (Connection)kryo.getContext().get("connection"); int id = getRegisteredID(connection, object); if (id == Integer.MAX_VALUE) throw new KryoNetException("Object not found in an ObjectSpace: " + object); output.writeInt(id, true); } public Object read (Kryo kryo, Input input, Class type) { int objectID = input.readInt(true); Connection connection = (Connection)kryo.getContext().get("connection"); return ObjectSpace.getRemoteObject(connection, objectID, type); } } }