-
Notifications
You must be signed in to change notification settings - Fork 415
/
ObjectSpace.java
769 lines (684 loc) · 31.5 KB
/
ObjectSpace.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
/* 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 <misc@n4te.com> */
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<InvokeMethodResult> 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 <misc@n4te.com> */
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);
}
}
}