NSURLConnection is run many times - iphone

I connect asynchronously with server each 5 seconds. The URL is the same, but POST-body is changed each time. Now I create NSURL, NSURLRequest and NSURLConnection from the scratch each time.
I think it'd be more effective to set connection once and just use that one further.
I am a newbie and not sure if that possible. There is no mutable NSURLConnection, but may it's need to create NSURLConnection like:
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL: url];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
and change NSMutableURLRequest POST-data to send another request to server.
Which way is right?

I assume what you're concerned about is the overhead of creating the HTTP connection. NSURLConnection is smart enough to handle this for you using HTTP/1.1 and reusing existing connections. It does not use pipelining last time I checked, but for your purpose, connection reuse should be sufficient. I do encourage you to put a network sniffer on this and make sure that it's working as you want them to.
The cost of creating the objects themselves is trivial on the order of once per 5s and you shouldn't try to optimize that (though of course you should reuse the NSURL). It's the opening a connection to the server that's expensive, especially on iPhone.
If you find you really do need pipelining, you unfortunately will have to roll your own. I've heard that CFHTTPStream can do it, but I don't see a lot of evidence of that. CocoaAsyncSocket is your best bet for low-level access to the sockets without having to write low-level code.
Since latency on the cell network can be very bad, it's possible that your connection will take longer than 5s to complete. Do make sure that one connection is done before starting the next, or you'll start making more and more open connections.

Just to clarify things, NSURLConnection will reuse existing sockets, but only for a relatively small time frame (12 seconds). If you send a request, get back a response, and send a subsequent request within 12 seconds that 2nd request will got out on the same socket. Otherwise the socket will be closed by the client. A bug has been filed with Apple to increase this timer or to make it configurable.

#Rob Napier #Eric Nelson
As you mentioned: "NSURLConnection is smart enough to handle this for you using HTTP/1.1 and reusing existing connections".
However, I can not find such description in any Apple's document about that.
To make thing clear, I write some code to test it:
- (IBAction)onClickSend:(id)sender {
[self sendOneRequest];
}
-(void)sendOneRequest {
NSURL *url = [NSURL URLWithString:#"http://192.168.1.100:1234"];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:#"POST"];
[request addValue:[Base64 encodeFromString:kValueVersion] forHTTPHeaderField:kKeyVersion];
[request addValue:[Base64 encodeFromString:kValueDataTypeCmd] forHTTPHeaderField:kKeyDataType];
[request addValue:[Base64 encodeFromString:#"Test"] forHTTPHeaderField:kKeyCmdName];
[request addValue:[Base64 encodeFromString:#"Test"] forHTTPHeaderField:kKeyDeviceName];
[request addValue:[Base64 encodeFromString:#"xxdafadfadfa"] forHTTPHeaderField:kKeyDTLCookies];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[connection start];
}
And then, I start wireshark to catch packages on the server(192.168.1.xxx), using "(tcp.flags.syn==1 ) || (tcp.flags == 0x0010 && tcp.seq==1 && tcp.ack==1)" to filter tcp 3-way hand shake. Unfortunately, I can see the 3-way hand shake for each calling of "sendOneRequest". Which means, the NSURLConnection seems not reuse the existing connections. Can some one point out what's wrong in my code and how to send multiple requests via one socket connection by NSURLConnection?
I also tried synchronous way to send request:
-(void)sendOneRequest {
NSURL *url = [NSURL URLWithString:#"http://192.168.1.100:1234"];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:#"POST"];
[request addValue:[Base64 encodeFromString:kValueVersion] forHTTPHeaderField:kKeyVersion];
[request addValue:[Base64 encodeFromString:kValueDataTypeCmd] forHTTPHeaderField:kKeyDataType];
[request addValue:[Base64 encodeFromString:#"Test"] forHTTPHeaderField:kKeyCmdName];
[request addValue:[Base64 encodeFromString:#"Test"] forHTTPHeaderField:kKeyDeviceName];
[request addValue:[Base64 encodeFromString:#"xxdafadfadfa"] forHTTPHeaderField:kKeyDTLCookies];
[NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
sleep(1);
[NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
sleep(1);
[NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
}
And the result is the same.
=======UPDATE============================
Finally, I found the reason why my test is different from Rob and Eric say.
In short, Rob and Eric are correct. And NSURLConnection uses “keep-alive” as default for using HTTP/1.1 and reuses existing socket connection, but only for a relatively small time frame.
However, NSURLConnection has some problems for “chunked transfer-coding”(ie. without content-length).
In my test, server side send a response without content-length and response data, and it's chunked response and NSURLConnection will close the connection, thus 3-way hand shake occurs for each http post.
I changed my server code, set the length of response as 0, and the behavior is correct.

make a method that returns a request and do
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:[self requestMethod] delegate:self];
?

Related

Asynchronous network requests semi-crashing app

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.

No request information sent with ASIHTTPRequest

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.

How to POST multiple requests with XML parser?

I want to POST 3 requests within same class with XML parser. I can manage to do only one request at a time. When I POST multiple requests, it says Parser Error. This is how I tried.
NSURL *url = [[NSURL alloc] initWithString:getAllFoodsURL];
NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:url];
NSString *paramDataString = [NSString stringWithString:
#"<GetNames><DeviceId>1234</DeviceId><UserId>200</UserId></GetNames>"];
[req addValue:#"application/xml" forHTTPHeaderField:#"content-type"];
[req setHTTPMethod:#"POST"];
NSData *paramData = [paramDataString dataUsingEncoding:NSUTF8StringEncoding];
[req setHTTPBody: paramData];
NSURLConnection *theConnection=[[NSURLConnection alloc]initWithRequest:req delegate:self];
if (theConnection) {
NSMutableData *data = [[NSMutableData alloc] init];
self.receivedData=data;
[data release];
}
I have used NSXMLParser delegates methods. After one request is completed(connection release), then I create another connection and do the same process for second request.
But it doesnot work.
I want to know, how to manage multiple requests with NSXMLParser?
If you can give me a code example, its highly appreciated.
I guess NSThread is what you are looking for.
But I am not sure as I am not much aware about that.
Note that NSXMLParser isn't involved in HTTP requests -- do you mean NSURLRequest instead? You'll need to make your requests separately, possibly using separate blocks, operations, or threads.
Once you've retrieved the data for each request, you'll need to use separate NSXMLParser objects for each. A single instance of NSXMLParser is tied to its XML data at initialization -- you can't reuse a parser. You can use the same delegate for all your xml parsers, and the delegate can use the first parameter to each of the xml parser delegate methods (i.e. parser) to know which parser is calling a given method.

Sending multiple arguments to Drupal REST Services from iPhone

I am trying to access a Drupal service that takes more than one argument. The method is views.get and the server I'm using is REST 6.x-2.0-beta3. I am retrieving data from the server for 0 or 1 arguments with no trouble. Any argument after the first, however, is simply ignored. I have tested the view on the Drupal site, and it limits results correctly for every argument passed.
I've come to the conclusion that my problem must be the formatting, but I've tried nearly everything I can think of, not to mention a dozen suggestions I've found while Googling for an answer. My code is below:
responseData = [[NSMutableData data] retain];
NSMutableString *httpBodyString =[[NSMutableString alloc] initWithString:#"method=views.get&view_name=apps&args=1&display_id=default"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:#"http://drupalserver.com/services/rest/service_views/get.json"]];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:[httpBodyString dataUsingEncoding:NSUTF8StringEncoding]];
[httpBodyString release];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
I have tried:
args=1,2
args=[1,2]
args="1,2"
args=["1","2"]
and several others along that vein. Does anyone know the proper way to do this?
You should considering using, https://github.com/workhabitinc/drupal-ios-sdk

I need the correct syntax for the setValue:forHTTPHeaderField: method on NSMutableURLRequest

I am pulling my hair out trying to conjure up the correct syntax to set the HTTP header information do a byte-range load from an HTTP server.
This is the offending method on NSMutableURLRequest
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field
This is how I am using this method to load the first 512 byte of a URL request.
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"GET"];
[request setValue:#"0-512\r\n" forHTTPHeaderField:#"Range"];
So far it is ignored and I always receive the entire data payload. I just want the range of bytes specified (0 - 512). Can someone please relieve my headache?
Update:
I have used curl to confirm that my web server supports byte ranges thusly:
curl --range 0-2047 http://www.somewhere.com/humungodata.dat -o "foobar"
The file size of foobar is 2048
Cheers,
Doug
Problem solved.
By adding additional header fields the code immediately worked correctly. Why? Dunno. But it works:
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"GET"];
[request setValue:#"keep-live" forHTTPHeaderField:#"Connection"];
[request setValue:#"300" forHTTPHeaderField:#"Keep-Alive"];
[request setValue:#"bytes=0-2047" forHTTPHeaderField:#"Range"];
Your original code was wrong in the setValue line, it should be #"bytes=0-512". In your followup, you used the correct string, so the other headers should not be needed.
What you have there should be the correct way to add a header value to a URL request, however i thought only posts got header values, maybe im wrong, have you tried doing this on other enviroments and gotten it to work? Maybe take out the \r\n?