patternMinor
Reading and Parsing JSON from a Game Server
Viewed 0 times
readingserverjsongameparsingandfrom
Problem
This is my naive (but currently working) attempt at reading JSON commands from a Java game server in Objective-C. I am afraid I might be doing something wrong, so hopefully this question will be a learning experience for me.
To give a little background, someone else built the game server in Java in order to communicate with a Java client. All communications sent and received are JSON commands formatted like
Here is the code that I use to connect to the server. I found this code on Stack Overflow but I believe it is boilerplate code from a reference manual. I have not really changed it much, but it should be useful to understand why I do things the way I do them.
Then I have this method which actually processes the sending and receiving of data from the server:
```
switch (streamEvent) {
case NSStreamEventHasBy
To give a little background, someone else built the game server in Java in order to communicate with a Java client. All communications sent and received are JSON commands formatted like
{"command":"nameOfCommand","message":"messageText"}. I realized that it would be easily possible to interface with this server from whatever platform I want. The only technical hurdle to overcome is parsing the JSON strings and turning them into useable objects. Here is the code that I use to connect to the server. I found this code on Stack Overflow but I believe it is boilerplate code from a reference manual. I have not really changed it much, but it should be useful to understand why I do things the way I do them.
-(BOOL) initializeConnection {
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, (CFStringRef)@"127.0.0.1", 4242, &readStream, &writeStream);
NSInputStream *inputStream = (NSInputStream *)CFBridgingRelease(readStream);
NSOutputStream *outputStream = (NSOutputStream *)CFBridgingRelease(writeStream);
[inputStream setDelegate:self];
[outputStream setDelegate:self];
[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream open];
[outputStream open];
if (inputStream) {
[self sendInitialGameMessage];
return YES;
} else {
return NO;
}
}Then I have this method which actually processes the sending and receiving of data from the server:
```
- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent {
switch (streamEvent) {
case NSStreamEventHasBy
Solution
A good starting point for any question regarding Objective-C will always be the official Apple documentation on the subject, and in this case, more specifically, we want this page: Reading from Input Streams.
There are a few things about reading from input streams that you miss here.
First, we shouldn't be spending time converting data to strings and appending to JSON string, etc., in the midst of an open read stream. We should read from our stream as quickly as the stream provides it and close the stream.
There's another case in your switch that is useful:
But the biggest thing we're missing is defining an actual max length we want to read out of the input stream and checking the actual length read.
We should instead write it a bit more like this...
First, declare an
Now we spend 100% of our execution time while the stream is open just dealing with the stream and the data it's giving us.
The value of
Also, for what it's worth, you can transfer a lot more data in a significantly smaller package if you don't use JSON or XML or any sort of format like this. These formats are largely designed for either a) web services where you'll have a large public audience and you want to make consuming the webservice as easy as possible or b) dealing with things like .plist (just as an example) where you're keeping a file on hand that you want to be able to easily edit with a plain text editor but still want to transfer around.
But if you're developing the server yourself, or working hand-in-hand with the server developer, it would be far better to agree on how to serialize the data and just pass it as plain bytes. Use 1-4 bytes at the front as a header to describe some meta data about what you're passing, and use the rest of the bytes as the plain data.
Consider if I want to ask the server for the values of
That's as condensed as you can make it. And I don't know whether or not that's even valid JSON... I'm not sure on the white space requirements, but even best case scenario, that's sent as plain text. It's 1 byte per character. There are 25 characters. So the total package is 25 bytes... but we're only sending 3 bytes of data.
Instead, we could just send the data as data. If we know that we're always going to get
So why don't we just send this 4-byte package? The first byte is a very simple header that tells us how long the total package is (you and the server developer have to agree on what sort of information, if any, you need to put in the header). The remaining three bytes are the values of
So, there's 4 bytes to transfer 3 bytes worth of data, versus JSON using 25 bytes to transfer 3 bytes worth of data.
There are a few things about reading from input streams that you miss here.
First, we shouldn't be spending time converting data to strings and appending to JSON string, etc., in the midst of an open read stream. We should read from our stream as quickly as the stream provides it and close the stream.
There's another case in your switch that is useful:
NSStreamEventEndEncountered. Also, we of course need NSStreamEventErrorOccurred.But the biggest thing we're missing is defining an actual max length we want to read out of the input stream and checking the actual length read.
case NSStreamEventHasBytesAvailable: {
uint8_t *buffer;
NSUInteger length;
[(NSInputStream *)theStream read:buffer maxLength:length];
NSData *data = [NSData dataWithBytes:buffer length:length];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[_jsonParser acceptString:string];
#if DEBUG == 1
if ([_jonParser lastReceivedJson]) {
NSLog(@"%@", [_jonParser lastReceivedJson]);
}
#endif
break;
}We should instead write it a bit more like this...
First, declare an
NSMutableData property, which for this example will be called jsonData. Now, make our HasBytesAvailable look like this:case NSStreamHasBytesAvailable: {
if (!self.jsonData) {
self.jsonData = [NSMutableData data];
}
static int const BUFFER_LENGTH = 1024; // read 1kb at a time
uint8_t buffer[BUFFER_LENGTH];
NSInteger length = 0;
length = [(NSInputStream *)theStream read:buffer maxLength:BUFFER_LENGTH];
if (length > 0) {
[self.jsonData appendBytes:(const void *)buffer length:length];
if (length < BUFFER_LENGTH) {
// there will be nothing left in the buffer
// we can now convert self.jsonData directly into a dictionary
// with some NSJSONSerialization probably
}
} else {
// there was probably an error reading from the buffer
// probably have to start this whole process over
// although I think it's not wholly impossible to get 0 as a valid read
// a negative number is definitely invalid data, which is why we
// must use NSInteger for length rather than NSUInteger
}
}Now we spend 100% of our execution time while the stream is open just dealing with the stream and the data it's giving us.
The value of
BUFFER_LENGTH doesn't have to be 1kb. If you know the maximum size of the data you'll ever receive from this particular request, you can make this value just slightly larger than that (so that if (length < BUFFER_LENGTH) returns true the first time every time).Also, for what it's worth, you can transfer a lot more data in a significantly smaller package if you don't use JSON or XML or any sort of format like this. These formats are largely designed for either a) web services where you'll have a large public audience and you want to make consuming the webservice as easy as possible or b) dealing with things like .plist (just as an example) where you're keeping a file on hand that you want to be able to easily edit with a plain text editor but still want to transfer around.
But if you're developing the server yourself, or working hand-in-hand with the server developer, it would be far better to agree on how to serialize the data and just pass it as plain bytes. Use 1-4 bytes at the front as a header to describe some meta data about what you're passing, and use the rest of the bytes as the plain data.
Consider if I want to ask the server for the values of
foo, bar, and baz. Let's say these values will always be in the range of 0-255, a uint8 value. Consider that as JSON, it will look like this:{foo=115,bar=205,baz=155}That's as condensed as you can make it. And I don't know whether or not that's even valid JSON... I'm not sure on the white space requirements, but even best case scenario, that's sent as plain text. It's 1 byte per character. There are 25 characters. So the total package is 25 bytes... but we're only sending 3 bytes of data.
Instead, we could just send the data as data. If we know that we're always going to get
foo, bar, and baz, we don't need them to be labeled.So why don't we just send this 4-byte package? The first byte is a very simple header that tells us how long the total package is (you and the server developer have to agree on what sort of information, if any, you need to put in the header). The remaining three bytes are the values of
foo, bar, and baz. In hex, the 4-byte package would look like this:04 73 CD 9BSo, there's 4 bytes to transfer 3 bytes worth of data, versus JSON using 25 bytes to transfer 3 bytes worth of data.
Code Snippets
case NSStreamEventHasBytesAvailable: {
uint8_t *buffer;
NSUInteger length;
[(NSInputStream *)theStream read:buffer maxLength:length];
NSData *data = [NSData dataWithBytes:buffer length:length];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[_jsonParser acceptString:string];
#if DEBUG == 1
if ([_jonParser lastReceivedJson]) {
NSLog(@"%@", [_jonParser lastReceivedJson]);
}
#endif
break;
}case NSStreamHasBytesAvailable: {
if (!self.jsonData) {
self.jsonData = [NSMutableData data];
}
static int const BUFFER_LENGTH = 1024; // read 1kb at a time
uint8_t buffer[BUFFER_LENGTH];
NSInteger length = 0;
length = [(NSInputStream *)theStream read:buffer maxLength:BUFFER_LENGTH];
if (length > 0) {
[self.jsonData appendBytes:(const void *)buffer length:length];
if (length < BUFFER_LENGTH) {
// there will be nothing left in the buffer
// we can now convert self.jsonData directly into a dictionary
// with some NSJSONSerialization probably
}
} else {
// there was probably an error reading from the buffer
// probably have to start this whole process over
// although I think it's not wholly impossible to get 0 as a valid read
// a negative number is definitely invalid data, which is why we
// must use NSInteger for length rather than NSUInteger
}
}{foo=115,bar=205,baz=155}04 73 CD 9BContext
StackExchange Code Review Q#63769, answer score: 4
Revisions (0)
No revisions yet.