I am writing a network class for an iOS app. This class will take care of all logging and network traffic. I have a problem where I have to send possibly thousands of requests at one time, but NSURLConnections are timing out because the delegate methods will not be called until all the NSURLConnections are started, by which time the timeout period has expired. I am using a rest API for Drupal and, unfortunately, I do not know of a way to create multiple instances with one request. How can I receive responses while simultaneously sending them? If I use GCD to pass off the creation of the NSURLConnections, will that solve the problem? I think I would have to pass the entire operation of iterating over the objects to send and sending to GCD to free up the main thread to answer to responses.
-(BOOL)sendOperation:(NetworkOperation)op
NetworkDataType:(NetworkDataType)dataType
JsonToSend:(NSArray *)json
BackupData:(NSArray *)data
{
if(loggingMode)
{
return YES;
}
NSURLConnection *networkConnection;
NSData *send;
NSString *uuid = [self generateUUID];
NSMutableArray *connections = [[NSMutableArray alloc] init];
NSMutableURLRequest *networkRequest;
for (int i=0; i<[json count] && (data ? i<[data count] : YES); i++)
{
if(op == Login)
{
/*Grab all cookies from the server domain and delete them, this prevents login failure
because user was already logged in. Probably find a better solution like recovering
from the error*/
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:
[[NSURL alloc] initWithString:networkServerAddress]];
for (NSHTTPCookie *cookie in cookies)
{
[[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie];
}
networkRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:[networkServerAddress stringByAppendingString:#"/user/login"]]];
}
else if(op == StartExperiment)
{
networkRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:[networkServerAddress stringByAppendingString:#"/node"]]];
}
else if(op == Event || op == EndExperiment || op == SendAll)
{
networkRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:[networkServerAddress stringByAppendingString:#"/node"]]];
}
else if(op == Logout)
{
networkRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:[networkServerAddress stringByAppendingString:#"/user/logout"]]];
}
send = [[json objectAtIndex:i] dataUsingEncoding:NSUTF8StringEncoding];
//Set the headers appropriately
[networkRequest setHTTPMethod:#"POST"];
[networkRequest setValue:#"application/json"
forHTTPHeaderField: #"Content-type"];
[networkRequest setValue:[NSString stringWithFormat:#"%d", [send length]]
forHTTPHeaderField:#"Content-length"];
[networkRequest setValue:#"application/json"
forHTTPHeaderField:#"Accept"];
//Set the body to the json encoded string
[networkRequest setHTTPBody:send];
//Starts async request
networkConnection = [[NSURLConnection alloc] initWithRequest:networkRequest delegate:self];
//Successfully created, we are off
if(networkConnection)
{
[networkConnectionsAndData setValue:[[NSMutableArray alloc] initWithObjects:uuid,
[[NSNumber alloc] initWithInt:op], [[NSNumber alloc] initWithInt:dataType], [[NSMutableData alloc] init], (data ? [data objectAtIndex:i] : [NSNull null]), nil]
forKey:[networkConnection description]];
}
else //Failed to conn ect
{
NSLog(#"Failed to create NSURLConnection");
return NO;
}
}
[[self networkOperationAndConnections] setObject:[[NSMutableDictionary alloc] initWithObjectsAndKeys:[[NSMutableArray alloc] initWithObjects:connections, nil], #"connections", [[NSMutableArray alloc] init], #"errors", nil]
forKey:uuid];
return YES;
}
The dictionaries are used to keep track of the correlating data with each NSURLConnection and also to group the NSURLConnections together into one group to determine ultimate success or failure of an entire operation.
Update
AFNetworking was key in finishing this project. It not only cleaned up the code substantially, but dealt with all the threading issues inherit in sending so many requests. Not to mention with AFNetworking I could batch all the requests together into a single operation. Using blocks, like AFNetworking uses, was a much cleaner and better solution than the standard delegates for NSURLConnections.
You definitely need to allow the NSURLRequest / Connection to be operating on another thread. (Not the main thread!)
Edited for clarity**:
I noticed your comment of "//Starts async request" and I wanted to be sure you realized that your call there is not what you would expect out of a typical "asynch" function. Really its just firing off the request synchronously, but since its a web request it inherently behaves asynchronously. You want to actually place these requests on a another thread for full asynch behavior.
Everything else aside, I really suggest digging into Apple's networking example project here: MVCNetworking
As for specifics on your question, there's a couple ways to do this.
One is to keep your connection from starting immediately using initWithRequest:<blah> delegate:<blah> startImmediately:FALSE and then schedule your NSURLConnection instances on another thread's run-loop using: scheduleInRunLoop:forMode:
(Note: You then have to kick off the connection by calling start-- it's best to do this via an NSOperation + NSOperationQueue.)
Or use this static method on NSURLConnection to create/launch the connection instead of doing an alloc/init: sendAsynchronousRequest:queue:completionHandler:
(Note: this approach accomplishes pretty much same as above but obfuscates the details and takes some of the control out of your hands.)
To be honest my quick answers above won't be sufficient to finish this kind of project, and you'll need to do a bit of research to fill in the blanks, especially for the NSOperationQueue, and that's where the MVCNetworking project will help you.
Network connections are a fickle beast -- You can time-out and kill your connections even if they're running on a background thread simply by trying to perform too much work simultaneously! I would seriously reconsider opening up several thousand NSURLConnections at once, and using an NSOperationQueue would help work around this.
ADDITIONAL RESOURCES:
Here's a 3rd party library that may make your networking adventures less painful:
https://github.com/AFNetworking/AFNetworking
http://engineering.gowalla.com/2011/10/24/afnetworking/
Related
I'm calling a method that will enumerate through an array, create an NSURL, and call an NSURLSessionDataTask that returns JSON. The loop typically runs about 10 times but can vary depending on the day.
I need to wait for the for loop and all NSURLSessionDataTasks to complete before I can start processing the data.
I'm having a hard time figuring out when all the work is complete. Could anyone recommend any ways or logic to know when the entire method is complete (for loop and data tasks)?
-(void)findStationsByRoute{
for (NSString *stopID in self.allRoutes) {
NSString *urlString =[NSString stringWithFormat:#"http://truetime.csta.com/developer/api/v1/stopsbyroute?route=%#", stopID];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if(httpResponse.statusCode == 200){
NSError *jsonError = [[NSError alloc]init];
NSDictionary *stopLocationDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError];
NSArray *stopDirectionArray = [stopLocationDictionary objectForKey:#"direction"];
for (NSDictionary * _stopDictionary in stopDirectionArray) {
NSArray *stop = [_stopDictionary objectForKey:#"stop"];
[self.arrayOfStops addObject:stop];
}
}
}];
[task resume];
}
}
There are a number of options. The fundamental issue is that these individual data tasks run asynchronously, so you need some way to keep track of these asynchronous tasks and establish some dependency on their completion.
There are several possible approaches:
The typical solution is to employ a dispatch group. Enter the group before you start the request with dispatch_group_enter, leave the group with dispatch_group_leave inside the completion handler, which is called asynchronously, and then, at the end of the loop, supply a dispatch_group_notify block that will be called asynchronously when all of the "enter" calls are offset by corresponding "leave" calls:
- (void)findStationsByRoute {
dispatch_group_t group = dispatch_group_create();
for (NSString *stopID in self.allRoutes) {
NSString *urlString = [NSString stringWithFormat:#"http://truetime.csta.com/developer/api/v1/stopsbyroute?route=%#", stopID];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
dispatch_group_enter(group); // enter group before making request
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if(httpResponse.statusCode == 200){
NSError *jsonError; // Note, do not initialize this with [[NSError alloc]init];
NSDictionary *stopLocationDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
NSArray *stopDirectionArray = [stopLocationDictionary objectForKey:#"direction"];
for (NSDictionary *stopDictionary in stopDirectionArray) {
NSArray *stop = [stopDictionary objectForKey:#"stop"];
[self.arrayOfStops addObject:stop];
}
}
dispatch_group_leave(group); // leave group from within the completion handler
}];
[task resume];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// do something when they're all done
});
}
A more sophisticated way to handle this is to wrap the NSSessionDataTask in a NSOperation subclass and you can then use dependencies between your data task operations and your final completion operation. You'll want to ensure your individual data task operations are "concurrent" ones (i.e. do not issue isFinished notification until the asynchronous data task is done). The benefit of this approach is that you can set maxConcurrentOperationCount to constrain how many requests will be started at any given time. Generally you want to constrain it to 3-4 requests at a time.
Note, this can also address timeout issues from which the dispatch group approach can suffer from. Dispatch groups don't constrain how many requests are submitted at any given time, whereas this is easily accomplished with NSOperation.
For more information, see the discussion about "concurrent operations" in the Operation Queue section of the Concurrency Programming Guide.
For an example of wrapping NSURLSessionTask requests in asynchronous NSOperation subclass, see a simple implementation the latter half NSURLSession with NSBlockOperation and queues. This question was addressing a different topic, but I include a NSOperation subclass example at the end.
If instead of data tasks you used upload/download tasks, you could then use a [NSURLSessionConfiguration backgroundSessionConfiguration] and URLSessionDidFinishEventsForBackgroundURLSession: of your NSURLSessionDelegate would then get called when all of the tasks are done and the app is brought back into the foreground. (A little annoyingly, though, this is only called if your app was not active when the downloads finished: I wish there was a rendition of this delegate method that was called even if the app was in the foreground when the downloads finished.)
While you asked about data tasks (which cannot be used with background sessions), using background session with upload/download tasks enjoys a significant advantage of background operation. If your process really takes 10 minutes (which seems extraordinary), refactoring this for background session might offer significant advantages.
I hate to even mention this, but for the sake a completeness, I should acknowledge that you could theoretically just by maintain an mutable array or dictionary of pending data tasks, and upon the completion of every data task, remove an item from that list, and, if it concludes it is the last task, then manually initiate the completion process.
Look at dispatch groups, for your example it will look roughly like this:
create group
for (url in urls)
enter group
start_async_task
when complete leave group
wait on group to finish or supply a block to be run when completed
I know that people have asked this but I have not found satisfactory answers. I have one method that I send all my URLRequests through. I return the response of the request as a string when the method completes. I have recently added ssl to my program. This means that I can no longer use a synchronous request because I need to take advantage of the didReceiveAuthenticationChallenge function as my credentials are currently self-signing. The program needs the response from the URL in order to continue so there is not harm in waiting for the response. However, I cannot seem to find a way to just hold the code up and continue once completed. I can alert the original function that called to request function but I would like the program to pick up right after that call. And it has unique code below such calls so I cannot specialize the connectionDidFinishLoading: function because each method who calls this is different.
How can I pause the program so I can return the nsdata from the connection to the methods that called it?
Here is some pseudo-code to show you what I mean:
- (void) login:(NSString *)username :(NSString *)password {
NSString *str = [NSString stringWithFormat:%#"%#:::%#",username,password];
NSURL *url = [NSURL urlWithString:#"https://blahblahblah"];
NSString *result = [self connectToUrl:str:url];
if ([result isEqualToString:#"valid"]) {
//this would be more complex in here
NSLog(#"hooray");
} else {
NSLog(#"bummer");
}
}
- (NSString *)connectToUrl:(NSURL *)url :(NSString *)str {
NSData *FileData = [str dataUsingEncoding: NSUTF8StringEncoding];
NSMutableData *data = [[NSMutableData alloc] initWithCapacity:100];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:#"POST"];
//set up the rest of the request...
...
connection = [NSURLConnection connectionWithRequest:request delegate:self];
[connection start];
//WOULD LIKE TO PAUSE HERE UNTIL COMPLETE! THEN CONTINUE
// received data is assigned in didReceiveData: method
return [[[NSString alloc] initWithData:receivedData encoding:NSUTF8StringEncoding] autorelease];
}
But alas, I cannot do this because I cannot make the final line wait until the connection is complete... Please help me!
Very appreciative!
R
iOS and OS X and much of the Cocoa/Cocoa touch frameworks are built on an event model. You don't pause your app. That's not the proper approach. You need to start the connection and then move on. When the connection completes, you act on that event.
In other words, your login method can't sit and wait for the result. It should start the connection and return.
When you get the result of the connection you call some method to process the login result.
Making use of blocks can make things like this easier but there are other ways. You just need to stop thinking about such things in a linear fashion. Dealing with asynchronous processing requires a different approach.
In my app, the root view controller acquires information from the internet, parses it, and displays it in viewDidAppear. I am using this method because my app in embedded in a UINavigationController and this way the root view controller will reload its data when the user presses the back button and pops to the root view.
When this occurs, it takes some time for the information from the internet to be acquired and displayed. During this time, if the user clicks a button to move to a different view, the button action will not occur until the view controller has completed its process of acquiring the web data.
How can I make it so that the buttons will override the other processes and immediately switch the view? Is this safe? Thanks in advance.
Edit
Here's an example of a portion where I take the information off of the site (My app parses the HTML).
NSURL *siteURL = [NSURL URLWithString:#"http://www.ridgefield.org/ajax/dist/emergency-announcements"];
NSError *error;
NSString *source = [NSString stringWithContentsOfURL:siteURL
encoding:NSUTF8StringEncoding
error:&error];
This is where Apple folks would hound, "don't block the main thread!".
The primary suggestion for this kind of workflow is to use a separate thread (read: queue) for loading data from the web. Then the worker who completes the load can set some property on your view controller, and inside that setter is where the UI should be updated. Remember to call the setter back on the main thread.
There are several ways to skin the concurrency cat, but the answer to this particular question leaves them out of scope. The short answer is to not do the load in the main thread, and that should lead you to the right place.
The NSString method stringWithContentsOfURL is synchronous and will block your main thread.
Instead of using background threads to solve the problem, you can use asynchronous URL requests. This does not block the user interface because a delegate protocol is used to let you know when the request is complete. For example:
NSURL *url = [NSURL URLWithString:#""http://www.ridgefield.org/ajax/dist/emergency-announcements""];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSURLConnection* theConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
And then some of your delegate methods are:
-(void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)theResponse
{
// create received data array
_receivedData = [[NSMutableData alloc] init];
}
-(void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)theData
{
// append to received data.
[_receivedData appendData:theData];
}
-(void)connectionDidFinishLoading:(NSURLConnection*)connection
{
// now the connection is complete
NSString* strResult = [[NSString alloc] initWithData: _receivedData encoding:NSUTF8StringEncoding];
// now parse strResult
}
According to Best way to remove from NSMutableArray while iterating?, we can't remove an object from NSMutableArray while iterating, yes.
But, what if I have a code like the following
- (void)sendFeedback {
NSMutableArray *sentFeedback = [NSMutableArray array];
for (NSMutableDictionary *feedback in self.feedbackQueue){
NSURL *url = [NSURL URLWithString:#"someApiUrl"];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:[feedback objectForKey:#"data"] forKey:#"data"];
[request setCompletionBlock:^{
[sentFeedback addObject:feedback];
}];
[request startAsynchronous];
}
[self.feedbackQueue removeObjectsInArray:sentFeedback];
}
I'm using a NSRunLoop to create a NSThread to execute the sendFeedback method every a period of time. The way I sent data to the API is by using Asynchronous method (which will create a background thread for each request) Once the feedback has been sent, it has to be removed before NSRunner execute this method at the next period to avoid duplicate data submission.
By using asynchronous, the loop (the main thread) will continue running without waiting for the response from server. In some cases (maybe most cases), the loop will finish running before all the response from server of each request come back. If that so, the completion block's code will be execute after the removeObjectsInArray which will result in sent data remains in self.feedbackQueue
I'm pretty sure that there are several ways to avoid that problem. But the only one that I can think of is using Synchronous method instead so that the removeObjectsInArray will not be execute before all the request's response are come back (Either success or fail). But if I do so, it's mean that the internet connection has to be available for longer period. The time needed to the sendFeedback's thread will be longer. Even it will be run by newly created NSThread which will not cause the app to not respond, resources will be needed anyways.
So, is there any other way besides the one I mentioned above? Any suggestion are welcome.
Thank you.
There are a few ways to deal with this kind of problem. I suggest using a dispatch group to synchronize your feedback and using an instance variable to keep from executing a new feedback batch while one is still in progress. For this example, let's assume you create an instance variable named _feedbackUploadInProgress to your class, you could rewrite your -sendFeedback method like this:
- (void)sendFeedback
{
if( _feedbackUploadInProgress ) return;
_feedbackUploadInProgress = YES;
dispatch_group_t group = dispatch_group_create();
NSMutableArray *sentFeedback = [NSMutableArray array];
for (NSMutableDictionary *feedback in self.feedbackQueue) {
// enter the group for each item we're uploading
dispatch_group_enter(group);
NSURL *url = [NSURL URLWithString:#"someApiUrl"];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:[feedback objectForKey:#"data"] forKey:#"data"];
[request setCompletionBlock:^{
[sentFeedback addObject:feedback];
// signal the group each time we complete one of the feedback items
dispatch_group_leave(group);
}];
[request startAsynchronous];
}
// this next block will execute on the specified queue as soon as all the
// requests complete
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[self.feedbackQueue removeObjectsInArray:sentFeedback];
_feedbackUploadInProgress = NO;
dispatch_release(group);
});
}
One approach is to keep track of the requests inflight and do the queue clean up when they are all done. Keeping track with the block is a little tricky because the naive approach will generate a retain cycle. Here's what to do:
- (void)sendFeedback {
NSMutableArray *sentFeedback = [NSMutableArray array];
// to keep track of requests
NSMutableArray *inflightRequests = [NSMutableArray array];
for (NSMutableDictionary *feedback in self.feedbackQueue){
NSURL *url = [NSURL URLWithString:#"someApiUrl"];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
// save it
[inflightRequests addObject:request];
// this is the ugly part. but this way, you can safely refer
// to the request in it's block without generating a retain cycle
__unsafe_unretained ASIFormDataRequest *requestCopy = request;
[request setPostValue:[feedback objectForKey:#"data"] forKey:#"data"];
[request setCompletionBlock:^{
[sentFeedback addObject:feedback];
// this one is done, remove it
// notice, since we refer to the request array here in the block,
// it gets retained by the block, so don't worry about it getting released
[inflightRequests removeObject:requestCopy];
// are they all done? if so, cleanup
if (inflightRequests.count == 0) {
[self.feedbackQueue removeObjectsInArray:sentFeedback];
}
}];
[request startAsynchronous];
}
// no cleanup here. you're right that it will run too soon here
}
I'm developing a newsstand application and use NSURLRequest to download issue assets.
NSArray *contents = [issue.tableOfContents objectForKey:kSNTableOfContentsContents];
NSHTTPCookie *cookie;
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
NSLog(#"HERE GO MY COOKIES");
for (cookie in [cookieJar cookies]) {
NSLog(#"%#", cookie);
}
for (NSDictionary *contentItem in contents) {
NSString *contentURL_string = [contentItem objectForKey:kSNTableOfContentsRemoteURL];
NSURL *contentURL = [NSURL URLWithString:contentURL_string];
NSString *fileName = [contentItem objectForKey:kSNTableOfContentsContentsURL];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:contentURL];
NKAssetDownload *asset = [newsstandIssue addAssetWithRequest:request];
[request release];
....
[asset downloadWithDelegate:self];
....
}
When the first for loop is executed my cookies appear to be in NSHTTPCookieStorage, but when actual requests are sent, there are no cookie information in headers. I use CharlesProxy to look that up. Could anyone please give some advice what might be causing this issue?
From this thread, the magic incantation appears to be:
NSDictionary * headers = [NSHTTPCookie requestHeaderFieldsWithCookies:
[cookieJar cookies]];
[request setAllHTTPHeaderFields:headers];
(Warning: untested code.)
This will convert your cookie jar into an array of cookies, then to an NSDictionary of headers, and finally, staple those headers to your request. This is comparable to doing it manually, as Adam Shiemke linked in the question errata, but much cleaner in my opinion.
As per the documentation, you may also want to check HTTPShouldHandleCookies to see if your default cookie policy is being used properly.
On iOS projects I found the ASIHTTPRequest very useful for this kind of problems. It does things like authentication and cookies a lot better that the build-in functions:
http://allseeing-i.com/ASIHTTPRequest/