The first thing I thought, right at the start of looking at how to use the API, was “why do I have to register a class to serialize it?” Would it not be better if it could auto-register unrecognised classes? Particularly those ones marked as Serializable?
The Connection.id is short which means it will easily overflow, and you can (and will, eventually!) end up with concurrent connections with the same id.
Why does even Client have it’s own Selector + Thread? Even connecting can be non-blocking.
I think that if an API can’t be reasonably described on a single page, something’s wrong!
Well, the quick answer is, making you register classes is easier for me. Registering means each class is given the same ID on both the server and clients. Only the ID needs to be sent across the wire.
Lesser reasons include… Registering means you have to think about what you are sending, and how it gets serialized. It provides a chance to optimize serialization by specifying things like “can never be null” or the type of items in an ArrayList. This is a bit of an extreme case of optimization, but could save a reasonable amount of bandwidth in some situations. Here is the (much cleaner) unoptimized class, and here is a chart showing the differences (8.4% in this case, could be more with different data). The optimization lets it beat protobuf in the benchmarks. Sorry I have no more links for this paragraph. :-*
How would an auto-registration scheme work? I’m assuming what classes you want to send wouldn’t be known until the first time they are sent. The first time a class is sent it’d be sent with an ID and the class name String. The receiving side would keep track of the remote mappings of ID to class and deserialization would have a lookup per object. It gets tricky if the first time a class is sent over UDP, since it isn’t known if the other side will receive it.
This would be a reasonably complex orchestration for TCP and no clear solution for UDP, all just to avoid registration. What do you guys think?
You’re right, the enum is much clearer.
For the start method, there could be a no-arg version that did the more popular option. I wonder if daemon or non-daemon is usually what people want? If they are using the method I assume they have a game thread, so daemon is probably fine.
Overflow is ok, only -1 and 0 are special connection IDs. Thanks for pointing it out though, I wasn’t handling the case of skipping -1 and 0. To completely wrap around and have two connections with the same IDs, a client would have to connect, and then 65,535 clients would have to connect before the first one disconnects. This doesn’t seem very likely!
Hmm. I was assuming only a handful of Client instances would be needed at most for a single client. Usually only one is needed. Is there a use case were you would need more than a couple Client instances? I guess you could try to implement a P2P network, though KryoNet is specifically targeting client/server.
Originally I was doing non-blocking connects, but it got messy. I decided what I really was doing was a blocking connect anyway. After switching to blocking it made my code in that area so much simpler.
Thanks again for taking a look. The NIO stuff has all kinds of crazy corners, half of which I think I’ve forgotten immediately after figuring them out. The more eyes the better! I’ve also put a lot of effort into the API, so feedback on ease of use, discoverability, etc is highly appreciated.
Pretty neat idea to build a robot with an Android phone as the brain. Gives you a camera, wifi, accelerometer, etc. Apparently these robots shoot airsoft pellets and they fight them!
For every request you have a little class, sometimes a class that doesnt even have fields.
Fine.
If I want for many reasons to do it differently:
There is only one class which has a string, and what I do is sending script commands over TCP using this string
So instead of the above I do:
if (object instanceof Message)
{
Message request = (Message)object;
System.out.println("("+connection.getRemoteAddressTCP()+"): "+request.data);
Message response = new Message();
if (request.data.toLowerCase().startsWith("join"))
{
System.out.println("Appears to be a join command");
response.data = "You have joined the Server !";
}
else if (request.data.toLowerCase().startsWith("echo"))
{
System.out.println("Appears to be an echo command");
response.data = "You are saying: "+request.data;
}
else
{
System.out.println("Unknown command");
response.data = "Shut up.";
}
Something like that. Of course it works… but what about performance ?
I do want the very best performance, especially for network stuff, so if this is somehow slower, I would refrain from using it…
public class Messages {
public static final byte JOIN = 0;
public static final byte ECHO = 1;
public static final byte ANYTING = 2; // Just an example of more stuff...
}
So then you’d test it like that:
if (object instanceof Message) {
Message msg = (Message) object;
switch (msg.message) {
case Messages.JOIN:
// Whatever you want to do here...
break;
case Messages.ECHO:
// Whatever you want to do here too...
break;
case Messages.ANYTHING:
// Heh...
break;
default:
// Something unkown happened here ?!?
break;
}
}
You could even use an enum for the Messages class I think… But It could be possible, that the performance isn’t too good with that one then…
At least use equalsIgnoreCase instead of toLowerCase().equals. A switch on an int or enum will be much better, though I doubt this will be a performance bottleneck any time soon. A scripting language is usually not terribly fast, but probably fine enough.
That is an easy attitude to have, and honestly it can be fun to optimize, but it isn’t necessary the most productive attitude. Switching on an enum or int will be the fastest, at the cost of 1-2 bytes to send the enum/int. Using separate classes with instanceof is slower, but convenient to have strongly typed messages. If that isn’t needed, then a single class holding script as a string is totally reasonable. However, I very much doubt the difference between a switch and instanceof will make any noticeable difference. In such cases, choose the easiest to implement and/or read.
Not sure what you mean. You can put serialized bytes through Arrays.hashCode(byte[]). Kryo 1 had a delta compressor. It was removed in 2 as no one made use of it, but it could be resurrected. It remembers the last bytes sent and only sends the delta bytes using GDIFF.