2

I am using GCDAsyncSocket (CocoaAsyncSocket) for the socket communication in my app. Due to the asynchronous nature of GCDAsyncSocket, my network request (submitMessage below) is decoupled from the callback block that runs when data is received (socket:didReadData).

- (void)submitMessage:(NSDictionary *)messageObject onCompletion:(completionBlock)block {
    ...
    [_socket writeData:requestData withTimeout:self.timeout tag:0];
    [_socket readDataToLength:4 withTimeout:self.timeout tag:TAG_HEADER];
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    ...
    NSDictionary *responseObject = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
    if (self.completionBlock != nil)
        self.completionBlock(responseObject);
    }
}

This approach works fine for one-off exchanges. But there are some cases when I need to post a request, then using the received data, post another request. I can't get this to work properly. Basically, I need something like this:

[self submitMessage:request1 onCompletion:^(NSDictionary *response1) {
    (...callback 1...)
    }];
    [self submitMessage:request2 onCompletion:^(NSDictionary *response2) {
        (...callback 2...)
    }];
}];

or

[self submitMessage:request1 onCompletion:^(NSDictionary *response1) {
    (...callback 1...)
}];
[self submitMessage:request2 onCompletion:^(NSDictionary *response2) {
    (...callback 2...)
}];

where the order is strictly request1 - callback1 - request2 - callback2.

So the question is, how can I block the second request to run after the callback of the first request? Would GCD (dispatch_sync?) be the way to go?

Edit

I ended up using a solution similar to what @tigloo suggested (hence accepting his answer), but using NSCondition instead of GCD (if anyone's interested in details, I followed this great discussion). I am already running multiple threads (UI in main, high-level socket comms in another thread, and the socket operations in a third thread). Setting a class property and using NSCondition to lock the GCDAsyncSocket delegate until the response arrive seems the cleanest approach.

Community
  • 1
  • 1
lucianf
  • 547
  • 7
  • 17

2 Answers2

2

I think you were almost there. What about

[self submitMessage:request1 onCompletion:^(NSDictionary *response1) {
    // here, do something with response1 and create request2...
    // then you can make request2 directly at the end of the callback:
    [self submitMessage:request2 onCompletion:^(NSDictionary *response2) {
        // here, do something with response2...
    }];
}];

No need for the GCD directives, no need to block execution (which is a bad practice anyway). Does this solve your problem?

ilmiacs
  • 2,566
  • 15
  • 20
  • That would work well as long as main block (request1) doesn't finish immediately. Yes, the first callback block will eventually run, and yes, the second request and its (second) callback will also eventually run. But I need to fire a set of groups like this (request1-request2), and if they don't run sequentially the socket will get confused, as the writes will get mixed and the reads in the callbacks will get wrong data. Makes sense? – lucianf Apr 23 '13 at 11:10
  • Uh... I think your implementation should guarantee that no such issues arise. It's not the socket that gets confused. It's your protocol implementation. So 1st: The callback gets called when you did receive data, so in my implementation request2 would get called after the response has been received, which answers your original question. And 2nd: Your comment suggests that invoking another request1 while one is already running would not work. (I agree.) And blocking writes to the socket would be one approach, albeit not one I would recommend. Better fix the protocol so it doesn't get confused. – ilmiacs Apr 23 '13 at 13:14
  • What would you recommend then? Basically, I have a "sync" method that is made out of such a series of _send/receive/send/receive_ exchanges; I need to fire off a sequence of "_sync_"s - in a synchronous manner. I don't see how I could improve my protocol, that's as basic as it can get. – lucianf Apr 23 '13 at 16:58
  • Socket programming is far from trivial. Improvement is not getting more basic. Improvement is robustness. First off, I'd recommend you to review whether the protocol, basic as it may seem, does make sense at all. Consider all the cases that may occur. Network interruption, timeout, partial message loss, repeated delivery of data, etc. If you have a plan for all of these cases, and you still think sync is the right path, consider whether it is really worth the hassle or alternatively go with a higher level protocol handling all those issues for which there is an implementation. – ilmiacs Apr 23 '13 at 19:28
  • If you still insist, then make the request1 - callback1 - request2 - callback2 sequence one operation of your protocol. Then enqueue the operations and make sure the next operation is started as soon as the last one finishes. This will solve the issue raised in your comment. Without providing any further details I can not guess which issue will arise next. Good luck. – ilmiacs Apr 23 '13 at 19:35
  • Thank you very much for your time, +1 for that. I do agree that socket programming cannot be trivial, and I look forward to changing my implementation in the future (switching to a proper RESTful interface, as soon as the component I'm interfacing to is rewritten). – lucianf Apr 25 '13 at 18:43
1

The easiest approach is to append your requests to a serial dispatch queue and then wait for them to be completed by using dispatch_sync(). A discussion on StackOverflow can be found here.

The actual way of implementing it is up to your preferences. A possible idea is the following:

  • Create a new class "SyncRequest"
  • This class ideally has a private property of type bool "requestFinished", initialized to NO in the class' init method
  • In a method such as "sendSyncRequest" you call submitMessage:completionBlock:
  • The completion block will set the "requestFinished" property to YES
  • The last line in "sendSyncRequest" will be dispatch_sync(syncRequestQueue, ^(void){while(!requestFinished);});

This way you can construct multiple instances of SyncRequest, each handling a synchronized request. Rough sketch implementation:

@interface SyncRequest
@property bool requestFinished;
@end

@implementation SyncRequest

dispatch_queue_t syncRequestQueue;    

-(id)init
{
   self = [super init];
   if ( !self )
      return nil;

   self.requestFinished = NO;
   syncRequestQueue = dispatch_queue_create("com.yourid.syncrequest", DISPATCH_QUEUE_SERIAL);

   return self;
}

-(void) sendSyncRequest:(NSDictionary*)messageObject
{
   // submit message here and set requestFinished = YES in completion block

   // wait for completion here
   dispatch_sync(syncRequestQueue, ^(void){while(!self.requestFinished);});
}

@end

NOTE: I wrote the code without having the compiler at hand, you may have to create an indirect reference to "self" in the dispatch_sync call in order to avoid a cyclic reference.

Community
  • 1
  • 1
tigloo
  • 644
  • 3
  • 8
  • Thanks, I had already read the thread you posted; I know in principle how GCD is supposed to work, I just don't know how to use it in this particular case. If I send the request on the serial queue, how do I wait for the callback to execute? The request would return straight away since it's asynchronous (the way GCDAsyncSocket works). – lucianf Apr 22 '13 at 15:44
  • I've added an example of what I mean. – tigloo Apr 22 '13 at 16:14