4
\$\begingroup\$

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 {"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 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 NSStreamEventHasSpaceAvailable: {
            if (_dataToSend != nil) {
                [(NSOutputStream *)theStream write:[_dataToSend bytes] maxLength:[_dataToSend length]];
                _dataToSend = nil;
            }
            break;
        }

        default:
            break;
    }
}

Here is the header file of my parser:

#import <Foundation/Foundation.h>

@interface CSCJsonParser : NSObject

@property (readonly) NSMutableArray *receivedJsons;

-(NSDictionary *)lastReceivedJson;

-(void) acceptString:(NSString *)string;

@end

And here is the code that I need reviewed the most, the implementation:

#import "CSCJsonParser.h"

@implementation CSCJsonParser {
    NSString *_currentJson;
}

-(instancetype) init {
    self = [super init];
    if (self) {
        _receivedJsons = [[NSMutableArray alloc]init];
    }
    return self;
}

-(NSDictionary *)lastReceivedJson {
    return [_receivedJsons lastObject];
}

-(void) acceptString:(NSString *)string {
    if (!_currentJson) {
        _currentJson = @"";
    }

    _currentJson = [_currentJson stringByAppendingString:string];

    if ([string isEqual: @"}"]) {
        [_receivedJsons addObject:[self jsonDataForString:_currentJson]];
        _currentJson = nil;
    }
}

-(NSDictionary *) jsonDataForString:(NSString *)string {
    NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
    NSError *error = nil;
    NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];

    if (error) {
        return nil;
    } else {
        return dictionary;
    }
}

@end

I know this is a naive implementation, but is it too simple? Is there enough error checking? The basic premise is that server commands will always end with }, so that is where the parser divides the strings.

I tried to directly read the stream with JSON, but I could not get this to work. Every time the switch enters the NSStreamEventHasBytesAvailable case, a single string is received by the server. This is why I grab all of the strings for the JSON and then pass it to NSJSONSerialization to turn it into a dictionary. I should note that I also looked into a couple libraries to do this, but they seemed very heavyweight for what I am trying to accomplish.

This method does work, and it gives me a correct dictionary with the expected keys and values. My next step is to extend the program to be able to parse the dictionary and create an appropriate Message that can be used by the Client. I will also be making something that can do the opposite to correctly create and format a Message to send to the server. However, I wanted to post this question first to see if my approach so far is a valid one.

\$\endgroup\$
  • \$\begingroup\$ "someone else" = me :) \$\endgroup\$ – Simon Forsberg Sep 24 '14 at 17:38
  • \$\begingroup\$ This is a "valid" approach, but it certainly has room for improvement. The first question I have for you... 127.0.0.1 is obviously a placeholder address, and one way or the other, won't be in the final product, correct? Is the final code intended to make a TCP/IP connection to something on the local network, or would the final code connect to a server on the Internet using an address like http://example.com/myserver? \$\endgroup\$ – nhgrif Sep 25 '14 at 1:04
  • \$\begingroup\$ In my testing it is connecting to a local Java server. In the final version it will not connect to an http but rather directly to an ip address and port. The server is not set up to handle http. \$\endgroup\$ – bazola Sep 25 '14 at 1:11
4
\$\begingroup\$

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: 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 9B

So, there's 4 bytes to transfer 3 bytes worth of data, versus JSON using 25 bytes to transfer 3 bytes worth of data.

Ultimately, your header is probably a little more complex. You probably want two bytes for the total length. A byte or two to represent the "transaction ID" (you send the server a transaction ID, the server echoes it back to you, so you can pair up your reads with your writes), and then depending on what all sorts of things you can request, maybe a byte or two to represent the type of request this is a response to, etc., whatever. I'm kind of just guessing here because your question isn't really about this and you haven't included much detail about what's being passed... but I'd definitely, definitely look into serializing your data like this.

A 21 byte difference in data isn't very significant. But you're also not asking for very much in this example. The more you ask for, the larger the difference grows.

\$\endgroup\$
  • \$\begingroup\$ About closing the stream, the client and server will have an open connection to each other as long as the client is 'online', so I believe that closing the stream shouldn't be done until the client chooses to disconnect. Until then, there will always be more data (sooner or later) \$\endgroup\$ – Simon Forsberg Oct 1 '14 at 9:21
  • \$\begingroup\$ About the JSON format, indeed it is a bit of a 'waste of space', I prefer it currently because how easy it is to use in Java (using the Jackson library), and because it is easy to debug for a human. I did not expect reading/writing JSON to be so difficult in Objective-C though, so we might find a special way to send the data for Objective-C clients. \$\endgroup\$ – Simon Forsberg Oct 1 '14 at 9:23
  • \$\begingroup\$ It's not that hard. There's just a way better approach. @SimonAndréForsberg \$\endgroup\$ – nhgrif Oct 1 '14 at 14:52
  • \$\begingroup\$ @SimonAndréForsberg And it's an especially better approach if you're expecting a constant flow of data. \$\endgroup\$ – nhgrif Oct 2 '14 at 11:22
  • \$\begingroup\$ Well, the game is turn-based so it won't really be 'constant flow' of data. \$\endgroup\$ – Simon Forsberg Oct 2 '14 at 12:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.