I am having a heck of time with something that seems rather
straightforward but I can't seem to get working. I am building an
iPhone app that retrieves data from a web host. I am trying to
establish an asynchronous connection to the host as I want to keep the
device freed up during the connection. (sendSynchronousRequest freezes
the phone until the request is done.) Here is my connection code:
//temp url to see if data is returned:
NSURL *theURL = [NSURL URLWithString:#"http://www.theappleblog.com/feed"];
NSURLRequest *dataRequest = [NSURLRequest requestWithURL:theURL
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:60];
/* establish the connection */
theConnection = [[NSURLConnection alloc]
initWithRequest:dataRequest
delegate:self
startImmediately:YES];
if (theConnection == nil) {
NSLog(#"Connection Failure!");
self.urlData = nil;
} else {
self.urlData = [[NSMutableData data] retain];
}
I have all of the appropriate delegate methods set up:
-(void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse*)response
{
[urlData setLength:0];
UIApplication *application = [UIApplication sharedApplication];
application.networkActivityIndicatorVisible = YES;
NSLog(#"Received Response!");
}
-(void)connection:(NSURLConnection *)connection
didReceiveData:(NSData*)incrementalData
{
[self.urlData appendData:incrementalData];
NSNumber *resourceLength = [NSNumber
numberWithUnsignedInteger:[self.urlData length]];
NSLog(#"resourceData length: %d", [resourceLength intValue]);
NSLog(#"filesize: %d", self.urlDataSize);
NSLog(#"float filesize: %f", [self.urlDataSize floatValue]);
}
-(void)connectionDidFinishLoading:(NSURLConnection*)connection
{
NSLog(#"Connection finished loading\n");
NSLog(#"Succeeded! Received %d bytes of data",[urlData length]);
_isFinished = YES;
UIApplication *application = [UIApplication sharedApplication];
application.networkActivityIndicatorVisible = NO;
}
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
NSLog(#"Error: %#",[error localizedDescription]);
}
As you can see, I've got a boat-load of log messages because I wanted
to see if anything was coming through at all. My connection test
comes back as TRUE but no data ever gets loaded. Like I said, I'm sure
I must be doing (or not doing) something really stupid. But what?
Any help would be most appreciated.
Thanks, Lawrence
The problem was that I was continuing program flow and not stopping to allow the connection to finish downloading. Thank you for all of the suggestions.
A few suggestions:
In your first snippet, instead of theConnection try assigning to self.theConnection to make sure the property accessor methods are called. In various places you also interchange self.urlData and urlData. Might want to make them all consistent by going through self.
You may want to make the dataRequest a property as well and go through self to make sure it gets retained.
self.urlData = [[NSMutableData data] retain]; -- you shouldn't need that extra retain. Going through self does it for you. The extra retain will make it so the object will stick around and leak even when all is done.
On a related note, in your NSLogs make sure you print out the retain count for each object. Also, you may want to have a general clean-up routine around to release these structures (or sets self.foo = nil) so at any point if it barfs out you can call the routine to get things cleaned up.
Get a copy of Charles or WireShark (or run tcpdump in the console) and watch the network traffic. It'll help pinpoint at what stage in the flow it's failing.
This doesn't answer your question directly, but you might consider looking into Ben Copsey's ASIHTTPRequest library, which wraps around CFNetwork classes and includes asynchronous, threaded request code that takes a lot of work out of doing this properly.
Initial thought - is the object that has all this code being retained properly? It could be it's being released, and thus deallocating the connection...
Otherwise you should at least see the response.
Nothing obviously wrong up there. I have code that's substantially the same and works fine. The only differences I see are:
I use NSURLRequestUseProtocolCachePolicy instead of
NSURLRequestReloadIgnoringLocalCacheData
I use connection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self]; instead of specifying startImmediately: YES (since it's the default)
It would be helpful to know which, if any, of your delegate methods are getting called. At the very least, you ought to get at least one or the other of connectionDidFinishLoading or connectionDidFail being called. If not, then something's probably wrong with your delegate setup - are you ABSOLUTELY sure that the delegate method signatures are correct, and that you're passing in the right item as the delegate?
It's hard to zero in on anything since it's not clear from your question which, if any of the NSLog statements executes. Please be a little more clear about the results you're getting.
From a quick look the only thing that looks like a potential problem is that you have a race condition when you set up the connection. If the network is especially fast there's a slim but nonzero chance that -connection: didReceiveData: will be called before self.urlData has been allocated. I'd suggest allocating urlData either before initiating the connection or in -connection:didReceiveResponse: to prevent this.
If that doesn't fix it, more information from you is needed.
You say this code works fine using the asynchronous method? Testing that URL, it redirects to another, so what if you implement the redirection delegate methods? What if you try some different URLs?
Is the app otherwise responsive? Is the runloop running? What does the app do after starting the request -- does it return from its event handler back to the runloop? (If not, you'll never get any of your callbacks, because they're delivered via the runloop.)
NSURLConnection is designed to work with any type of protocol, not just HTTP. This means that connection:didFail:withError is called for connection errors, but not for protocol errors. You're not checking for protocol errors -- you have to catch them in connection:didReceiveResponse:. The trick is that the response object that is passed in is actually a NSHTTPURLResponse object. If you check the response's statusCode, I'll bet you're getting an HTTP error such as 404 or server 500.
Related
I used NSXML parser to parse a SOAP response XML i recieved from the web service.
In my root method,
NSURLConnection *theConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
I used this code to send my SOAP request, where theRequest variable has my SOAP request.
so after the data is recieved ,
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
//codes to recieve webData
xmlParser = [[NSXMLParser alloc] initWithData: _webData];
[_xmlParser setDelegate: self];
[_xmlParser setShouldResolveExternalEntities: YES];
[_xmlParser parse];
}
Now, the program flows to didStartElement , didFinishDocument methods. My root method needs to return the result obtained after parsing the xml , but when I checked the program flow using breakpoints, the parsing methods doesnot end before the return statement is called in my code and hence I am not able to return the parsed values. How do I solve this?
NSXMLParser works completely synchronous. When [_xmlParser parse] returns, all the parsing is done (or aborted with an error).
According your description, the problem is not quite clear. It seems, the problem is, that - after the root element has been constructed - you continue to process your program while still being within the NSXMLParser's parse method.
To solve this, just invoke a selector on the main thread passing the parser's delegate result (root element) to a method which eventually handles it. Alternatively, use dispatch_async to the main thread, where you invoke a method which handles the root element.
So whenever my ViewController is loaded I do an upload to a server. There seems to be a problem however. When there are a substantial number of asynchronous requests it can semi-crash the app. What I mean by that is that the requests just won't go through, and no other requests (which are on another thread) proceed. On top of that, the keyboard goes supremely laggy (weird I know). Anyway, so It's a serious issue considering the other network requests don't get sent because of it. What I find weird is that the upload requests are the same in number as the download requests (and the uploads don't even do anything, they just make a normal http request), yet the download requests work fine in any quantity. Here is my code:
- (void)serverUploadAll:(myEntity *)send
{
NSMutableString *urlString = [[NSMutableString alloc] initWithString:ServerApiURL];
NSString *addTargetUrl = [NSString stringWithFormat:#"/addtarget?deviceToken=%#&appVersion=%#&targetId=%#", postToServer.deviceToken, postToServer.appVersion, send.Id];
[urlString appendString:addTargetUrl];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request queue:queueTwo completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
//NSLog(#"response=%#", response);
//NSLog(#"data=%#", data);
//NSLog(#"error=%#", error);
}];
}
The code above is called as a database is cycled through, with the problem arising when there are a substantial amount of calls, i.e. 120+. My download requests are actually done using AFNetworking, so perhaps that is why they work efficiently. Anyway, to summarise, why when the above code is called multiple times does it just jam, and stop?
Thanks for the help, really appreciated.
Regards,
Mike
UPDATE: So, thanks to the brilliant answer by runmad, I'm using the AFNetworking approach, however, it crashes with this error -[AFHTTPRequestOperation _propertyForKey:]: unrecognized selector sent to instance. I don't get why that isn't working, here is my code:
- (void)cycleThroughEntries
{
MyEntity *myEntity;
NSMutableArray *urlRequests;
urlRequests = [[NSMutableArray alloc] init];
for (id i in fetchedResultsController.fetchedObjects) {
myEntity = i;
NSMutableString *urlString = [[NSMutableString alloc] initWithString:ServerApiURL];
NSString *addTargetUrl = [NSString stringWithFormat:#"/addtarget?deviceToken=%#&appVersion=%#&targetId=%#", postToServer.deviceToken, postToServer.appVersion, myEntity.Id];
[urlString appendString:addTargetUrl];
//NSLog(#"URL sent is: %#", urlString);
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFHTTPRequestOperation *requestOperation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[urlRequests addObject:requestOperation];
}
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:#""]];
[client enqueueBatchOfHTTPRequestOperationsWithRequests:urlRequests
progressBlock:^(NSUInteger numberOfCompletedOperations, NSUInteger totalNumberOfOperations) {
NSLog(#"%d / %d", numberOfCompletedOperations, totalNumberOfOperations);
}
completionBlock:^(NSArray *operations) {
NSLog(#"All Done!");
}];
}
Not entirely sure what's wrong, so would appreciate the help. Thanks.
UPDATE 2: Fixed. I shouldn't have been making an array of AFHTTPRequestOperations, but of just normal NSURLRequests. Problem solved!!
There is a limit to how many asynchronous connections you can make - even if they occur on background threads. 120+ network calls at the same time sounds a bit crazy, especially if they include uploads which take longer than a GET request.
I would suggest you consider rewriting what you need to request/upload to the server to reduce the amount of requests. You need to ensure that the calls don't all happen at the same time and you hold off on most calls until others are completed.
Enter NSOperationQueue...
I would create a manager class which handles all your requests. In this class you create an NSOperationQueue where you can keep adding requests to it. Here's a good tutorial. You can set the number of concurrent requests and the NSOperationQueue will ensure the next requests in the queue wait until the currently running requests are done. It does a lot of heavy lifting for you with networking.
You may also consider having a look at AFNetworking's NSOperationQueues to queue up all your network calls so they don't all occur at the same time.
In AFNetworking's AFHTTPClient You may be able to use the following method depending on how you're setting up your requests:
- (void)enqueueBatchOfHTTPRequestOperationsWithRequests:(NSArray *)urlRequests
progressBlock:(void (^)(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations))progressBlock
completionBlock:(void (^)(NSArray *operations))completionBlock;
That should get you started :) Good luck.
UPDATE
You can also add operations to the AFNetworking operationsQueue if you want. You just have to make sure to start it if it's not currently running already. This way you're able to add additional requests from other parts of the app at various times. You can always check if any operations are running/in queue and the awesome thing is that is allows you to easily cancel any running operations. It can be found here.
I am experiencing an issue on iOS 4.3+ with ASIHTTPRequest where a request is fired but no data (Request methed, url, headers, etc) reaches the server. The connection times out because it never hears back.
The server hears the empty request (after some delay), then hears a valid request which is of course never reported to higher level code because the connection has timed out. This is all kind of strange because the request was not configured to resend data.
Often this happens after the app has been pushed to the background for some time (15 min or more) and the phone has been allowed to sleep.
My configuration of the request is as follows:
NSMutableData *postData = nil;
NSString *urlString = [NSString stringWithFormat:#"%#%#",[self baseURL],requestPath];
OTSHTTPRequest *request = [OTSHTTPRequest requestWithURL:[NSURL URLWithString:urlString]];
[request setCachePolicy:ASIFallbackToCacheIfLoadFailsCachePolicy];
[request setTimeOutSeconds:45];
//Set up body
NSString *queryString = [self _RPcreateQueryString:query];
if ([queryString length]>0) {
if(method == RPAsyncServerMethodPost || method == RPAsyncServerMethodPut){
postData = [[[queryString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES] mutableCopy] autorelease];
}else{
NSURL *url = [NSURL URLWithString:[urlString stringByAppendingFormat:#"?%#",queryString]];
[request setURL:url];
if (!url) return nil; //url String malformed.
}
}
// ... ///
// method setting stripped for brevity
[request addRequestHeader:#"Content-Type" value:#"application/x-www-form-urlencoded"];
if(headers){
for (NSString* head in headers) {
if ([[headers valueForKey:head] isKindOfClass:[NSString class]])
[request addRequestHeader:head value:[headers valueForKey:head]];
}
}
[request addRequestHeader:#"Content-Length" value:postLength];
[request setPostBody:postData];
OTSHTTPRequest is simply a subclass of ASIHTTPRequest that contains properties for a string tag, a pretty description, and other bling for use by consuming objects and does not override any ASI stuff.
Can anyone shed a light on why/how ASI could open a connection and then send absolutely nothing for minutes at a time?
Edit: Just to clarify. The connections DO make contact with the server, it just never sends any data through the connection from what my server logs can tell. This seems to always happen on app wake and effects all connections including NSURLConnections spawned by MapKit. the whole app just seems to loose its marbles.
I also see a bunch of background tasks ending badly right before this, but i can never catch them while in the debugger.
It doesn't look like you are starting your request based on the code that you have provided. Try call the -[startSynchronous] or -[startAsynchronous] methods of your OTSHTTPRequest object after you are done setting its various properties.
Are you setting the delegate, either I overlooked it or you stripped it out.
I didnt want to say anything till a few days passed with out the issue. The solution in this case was very obscure.
It appears the version of TestFlight i was using has a bug in it that may have contributed to this issue. Since its removal i have not experienced the issue.
I am using ASIHTTPRequest to download some data.
I have the following in a method:
// Request 1
request1.tag = 1;
[request1 setDelegate:self];
[request startAsynchronous];
// Request 2
request2.tag = 2;
[request2 setDelegate:self];
[request2 startAsynchronous];
// Call third request method
[self callThirdRequest];
Now from within callThirdRequest, I am grabbing some data that has been downloaded from request2 and in there, I am calling startAsynchronous. The reason I have the calling of the third request in a separate method is because it will get invoked more than once. After putting some console outputs, it seems that callThirdRequest is being invoked before request2 starts its download. Therefore, when callThirdRequest tries to grab some data which should have been downloaded by request2, it does not work as there is no data.
Why is that? How can I make sure callThirdRequest is only called when request2 is done downloading?
Thanks,
When a request is run asynchronously, the rest of your code will continue to run at the same time. Your first two requests are doing exactly that. You have two options.
You can run the second request synchronously. (Not a good idea, since your app will appear "frozen" until the second request completes. Also, using this method won't help you if the second request, in your list of three, fails.)
A much better idea would be to use delegate callback methods. This is the better way of dealing with this, for two reasons. First of all, you can handle failed requests properly, and also, you can handle successful requests properly. Yu want something like this:
// Request 2
request2.tag = 2;
[request2 setDelegate:self];
[request2 startAsynchronous];
- (void)requestFinished:(ASIHTTPRequest *)request{
if(request.tag == 2){
[self callThirdRequest];
}
}
Make sure to check the request in the delegate callback, to ensure that it's the "second" one, so that you don't end up firing the wrong action at the wrong time. In this case, I used the "tag" property of the request. If you would have retained the request as a property of your class, you can just check for that as well.
That is because request2 is running asynchronous. You should start the 3rd request in the request-did-finish-delegate-method of request2!
The reason is startAsynchronous. An asynchronous call will operate on another thread. You need to set the delegate on request2 and then call the third request when request2 is finished.
// Request 2
request2.tag = 2;
[request2 setDelegate:self];
[request2 startAsynchronous];
...
- (void)requestFinished:(ASIHTTPRequest *)request
{
[self callThirdRequest];
}
I have a method that I call to make web service requests using GET. It looks like this:
- (UIImage*)getImageWithSeriesGUID:(NSString*)seriesGUID ImageID:(int)imageID {
NSString * unescapedString = RIVERWOODS_GET_IMAGE(seriesGUID, imageID);
NSURL *url = [[NSURL alloc] initWithString:[unescapedString stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding]];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setRequestMethod:#"GET"];
[request addRequestHeader:#"Connection" value:#"Keep-Alive"];
[request startSynchronous];
NSError *error = [request error];
if (!error) {
NSData *response = [request responseData];
//NSLog(#"Size: %#",[response length]);
NSString *content = [[[NSString alloc] initWithData:response
encoding:NSUTF8StringEncoding] autorelease];
NSLog(#"Data: %#", content);
UIImage *image = [UIImage imageWithData:response];
return image;
}
return nil;
}
This approach works ok, but it is just REALLY slowly. On the other end I am iterating through a for loop so this method gets called 20 times if the picture array I need to collect has 20 images. I am looking to improve the efficiency of this process, and I am thinking that I should be able to iterate through all the image Id's I need to collect right here in this method.
It seems to me that the reason this goes so slowly is because the multiple requests that we keep opening and closing. THe images I am pulling in are on average ~15kb.
My question: What can I do to change this method around to make it so I can improve efficiency and take advantage of the HTTP keep-alive features? I am thinking that instead of passing in an image ID, I can just pass in the count of the array I need to make, and then setup a for-loop of some sorts here in the request method which would then allow me to pass back an array of images...
Is that the way to go? Is there something I am missing? I appreciate your help and input!!
The reason why this is slow as hell is that you're doing the requests synchronously (which is always a no-no anyway), one-by-one. You need to refactor your download method to work asynchronously, and concurrently.
My approach to requesting data on the wire in that manner is as follows:
Create a global network connection 'controller' (accessible from your App Delegate), which can create an ASINetworkQueue on the fly when required and release it when no requests remain
Wrap your requests into a subclass of ASIHTTPRequest, and override the done/fail methods in those subclasses (make them fire a notification with returned data if you like; or write to disk and update a db with their reference).
For every request, grab the queue reference, and add your request to the queue.
The queue will grow and shrink as needed
If I were at my computer I'd check into github an example of this, but really the only difficult part is the global connection manager, and the ASI* guys have written a great example here on gist.github. Also, a better explanation of the above (where I learnt it from) is here.