How to handle internet connection interruption on iOS using CFNetwork? - iphone

I have implemented an FTP client on the iPhone, but when the connection is interrupted for a moment while download from the FTP server is in progress, the application informs me there is no connection and stops the client.
Here comes the problem: next time i try to start the download process again, the event stream:handleEvent: is not fired and the streamStatus of the networkStream stays on NSStreamStatusOpening.
If I manually stop the download process (using the same method which I fire when connection is interrupted), I can then re-download again. I have to relaunch the whole application for the downloading to work again.
Here are the key parts of the code:
- (void)downloadFile:(NSDictionary *)file {
NSURL *url;
CFReadStreamRef ftpStream;
url = [NSURL URLWithString:[#"PATH TO FTP FILE" stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
[self.fileStream open];
ftpStream = CFReadStreamCreateWithFTPURL(NULL, (__bridge CFURLRef) url);
self.networkStream = (__bridge NSInputStream *) ftpStream;
self.networkStream.delegate = self;
[self.networkStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[self.networkStream open];
CFRelease(ftpStream);
}
and the method that is fired when something happens with the stream
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
switch (eventCode) {
case NSStreamEventOpenCompleted: {
[self updateStatus:#"Opened connection"];
} break;
case NSStreamEventHasBytesAvailable: {
NSInteger bytesRead;
uint8_t buffer[32768];
bytesRead = [self.networkStream read:buffer maxLength:sizeof(buffer)];
if (bytesRead == -1) {
[self stopReceivingWithStatus:#"Network read error"];
} else if (bytesRead == 0) {
[self stopReceivingWithStatus:nil];
} else {
[self processStreamDataWithBuffer:buffer andReadBytes:bytesRead];
}
} break;
case NSStreamEventHasSpaceAvailable: {
assert(NO);
} break;
case NSStreamEventErrorOccurred: {
[self stopReceivingWithStatus:#"Stream open error"];
} break;
case NSStreamEventEndEncountered: {
} break;
default: {
assert(NO);
} break;
}
The problem as I said is that after connection interrupted, the events are not fired anymore.
Note: I am using iOS 5 with ARC.

Perhaps you should set the flag to force the stream to close the underlying native socket which by default is not done;
[self.networkStream setProperty:(id)kCFBooleanTrue forKey:(NSString *)kCFStreamPropertyShouldCloseNativeSocket];

Related

NSInputStream non-stop connection?

So I have both an input stream and an output stream and they are setup as the code provided below. The input stream's delegate method NSStreamEventHasBytesAvailable is called only when i write somedata onto the output stream. Why is that?
// connect to the server and bind the input/output streams
CFReadStreamRef readStream;
CFWriteStreamRef writeStream;
CFStreamCreatePairWithSocketToHost(NULL, _serverAddr, _serverPort,
&readStream, &writeStream);
_inputStream = (__bridge_transfer NSInputStream *)readStream;
_outputStream = (__bridge_transfer NSOutputStream *)writeStream;
// attach the streams to the current processor run loop
[_inputStream setDelegate:self];
dispatch_async(_inputStreamQueue, ^{
[_inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes];
[_inputStream open];
[[NSRunLoop currentRunLoop] run];
});
dispatch_async(_outputStreamQueue, ^{
[_outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
forMode:NSRunLoopCommonModes];
[_outputStream open];
[[NSRunLoop currentRunLoop] run];
});
InputStreamDelegate
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode
{
dispatch_async(dispatch_get_main_queue(), ^(void) {
if (stream == _inputStream) {
[self inputStreamHandleEvent:eventCode];
}
});
}
- (void)inputStreamHandleEvent:(NSStreamEvent)eventCode
{
switch (eventCode) {
case NSStreamEventHasSpaceAvailable:
{
break;
}
case NSStreamEventEndEncountered:
{
break;
}
case NSStreamEventNone:
{
break;
}
case NSStreamEventErrorOccurred:
{
NSLog(#"NSStreamEventErrorOccurred");
NSError* error = [_inputStream streamError];
NSString* errorMessage = [NSString stringWithFormat:#"%# (Code = %ld)",
[error localizedDescription],
(long)[error code]];
UIAlertView *wifiLostAlert = [[UIAlertView alloc]
initWithTitle:#"Input Stream Error"
message:errorMessage
delegate:nil
cancelButtonTitle:#"Continue"
otherButtonTitles:nil];
[wifiLostAlert show];
break;
}
case NSStreamEventHasBytesAvailable:
{
uint8_t buf[1024];
int read = 0;
while ([_inputStream hasBytesAvailable])
{
read = [(NSInputStream *)_inputStream read : buf maxLength : 1024];
if (read > 0)
{
NSLog(#"%d bytes read from the input stream.", read);
[_recvBuf appendBytes:(const void *)buf length:read];
int processedBytes = 0;
do
{
processedBytes = [self processPacket];
}
while (processedBytes > 0);
}
else
{
NSLog(#"End of the stream reached, or a socket error. Disconnecting.");
[self disconnect];
}
}
break;
}
case NSStreamEventOpenCompleted:
{
break;
}
}
}
My problem:
So, for the inputStream, I read the received data through the callbacks. This does not happen all the time. When ever the server responds back, it's not reading the result. The input stream seems to work ONLY when I send some data through the output stream, and then it reads the input stream values for the previous communication.
In terms of logical sequence...
What I expect is this.
1 --> 1'
2 --> 2'
3 --> 3'
This is reality
1 -->
2 --> 1'
3 --> 2'
Turns out it was a server-side issue. Code is good.

How to init NSInputStream with a part of file?

I was woking on YouTube Resumable Uploads: https://developers.google.com/youtube/2.0/developers_guide_protocol_resumable_uploads#Sending_a_Resumable_Upload_API_Request
I use ASIHttpRequest to upload the videos.
For a direct uploading, I can use this method.
- (void)appendPostDataFromFile:(NSString *)file
But for an resume uploading, I can't append post data from the whole video file.
Maybe I can write a method like this:
- (void)appendPostDataFromFile:(NSString *)file offset:(long long)offset
But I don't know how to make it work. Any help will be appreciated!
And here is the code from ASIHttpRequest:
- (void)appendPostDataFromFile:(NSString *)file
{
[self setupPostBody];
NSInputStream *stream = [[[NSInputStream alloc] initWithFileAtPath:file] autorelease];
[stream open];
NSUInteger bytesRead;
while ( !_isTryCanceled && [stream hasBytesAvailable] ) {
unsigned char buffer[1024*256];
bytesRead = [stream read:buffer maxLength:sizeof(buffer)];
if (bytesRead == 0) {
break;
}
if ([self shouldStreamPostDataFromDisk]) {
[[self postBodyWriteStream] write:buffer maxLength:bytesRead];
} else {
[[self postBody] appendData:[NSData dataWithBytes:buffer length:bytesRead]];
}
}
[stream close];
}
As the code show above, if I can get a seekable NSInputStream, the problem will be solved. Is it possible to do so?

CFReadStreamCopyError report error on iPad device

I am running into an issue. Same code running OK on iPhone (iOS 5) and iPhone/iPad simulator. But it does not work on an iPad (iOS 5). I'd appreciate any help.
Here is read port code:
//Code for read port.
CFIndex bytesRead = CFReadStreamRead(inputStream, bufferPoint, 1024);
if (bytesRead < 0) {
NSLog(#"bytesRead < 0");
CFErrorRef error = CFReadStreamCopyError(inputStream);
//reportError(error);
DEBUGLOG(#"readResponse error \n")
Before above, there is connection part code.
//prevent to release before relocate
if ((inputStream != nil) && (outputStream != nil)) {
[inputStream release];
inputStream = nil;
[outputStream release];
outputStream = nil;
}
[NSStream getStreamsToHostNamed:relayHost port:relayPort inputStream:&inputStream outputStream:&outputStream];
//[self lgetStreamsToHostNamed:relayHost port:relayPort inputStream:&inputStream outputStream:&outputStream];
if ((inputStream != nil) && (outputStream != nil))
{
sendState = kIMAPConnecting;
isSecure = NO;
[inputStream retain];
[outputStream retain];
[inputStream setDelegate:self];
[outputStream setDelegate:self];
result = [inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
DEBUGLOG(#"inputStream setProperty result: %d", result);
result =[ outputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
DEBUGLOG(#"outputStream setProperty result: %d", result);
if (!CFReadStreamOpen(inputStream)) {
DEBUGLOG(#"inputStream open failed");
return NO;
}
if (!CFWriteStreamOpen(outputStream)) {
DEBUGLOG(#"outputStream open failed");
return NO;
}
self.inputString = [NSMutableString string];
DEBUGLOG(#"SCRIMAPMessage startToConnect end with YES\n");
return YES;
}
The following is not available via iOS:
[NSStream getStreamsToHostNamed:relayHost port:relayPort inputStream:&inputStream outputStream:&outputStream];
I really do not know how it can work on iOS for the iPhone.
Your options are fairly simple...
A) Create a category on NSStream as described in this technical note from Apple: here
B) Use CFStreamCreatePairWithSocketToHost() and simply bridge CFReadStreamRef/CFWriteStreamRef
I recommend (B) as it will give you the best option for flexibility. More specifically you can create your own StreamObject class to handle this and the stream delegate all in one.
Happy coding!

A memory leak happen when doing ftp request in the background thread

I am developing an app which can download image from the server through ftp request and then display them. The ftp request is running in the background. When the download finished, a update message will be sent to the view.
I encountered a memory leak when doing the ftp request which has the following features:
1. The memory leak do not happen every time. May be 1 / 7.
2. If I do the ftp request on the main thread, everything is OK.
3. If I do the ftp request on simulator, everything is OK.
I used SIMPLEFTP to do the ftp job and I have done some modification to fix my request.
In the FtpListService.m, This file is used to request a document list information (name, size, modification date). Memory leak is happened here (I highlight the line with "####").
//This is the method to start a ftp request
- (void)_startReceive
// Starts a connection to download the current URL.
{
BOOL success;
NSURL * url;
CFReadStreamRef ftpStream;
//don't tap receive twice in a row!
assert(self.networkStream == nil);
// First get and check the URL.
self.InputUrl = [self.InputUrl stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
url = [FtpUtil smartURLForString:self.InputUrl];
success = (url != nil);
// If the URL is bogus, let the user know. Otherwise kick off the connection.
if (!success) {
DLog(#"Bad ftp url.");
} else {
// Create the mutable data into which we will receive the listing.
assert(self.listData != nil);
// Open a CFFTPStream for the URL.
ftpStream = CFReadStreamCreateWithFTPURL(NULL, (CFURLRef) url);
assert(ftpStream != NULL);
self.networkStream = (NSInputStream *) ftpStream;
self.networkStream.delegate = self;
[self.networkStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:RUNLOOPMODEL];
//This timer will be called to terminate the request which is blocked for a customed time.
NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:TIMEOUTFTPLIST target:self
selector:#selector(listdealTimeOut:) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:RUNLOOPMODEL];
[self.networkStream open];
CFRelease(ftpStream);
}
}
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
// An NSStream delegate callback that's called when events happen on our
// network stream.
{
connected = #"connected";
switch (eventCode) { //####################### EXC_BAD_ACCESS
case NSStreamEventOpenCompleted: {
//NSLog(#"NSStreamEventOpenCompleted");
} break;
case NSStreamEventHasBytesAvailable: {
NSInteger bytesRead;
uint8_t buffer[LISTDOCBUFFER];
// Pull some data off the network.
bytesRead = [self.networkStream read:buffer maxLength:sizeof(buffer)];
if (bytesRead == -1) {
[self _stopReceiveWithStatus:#"Network read error"];
} else if (bytesRead == 0) {
[self _stopReceiveWithStatus:#"no more data"];
} else {
assert(self.listData != nil);
// Append the data to our listing buffer.
[self.listData appendBytes:buffer length:bytesRead];
[self _parseListData];
}
} break;
case NSStreamEventHasSpaceAvailable: {
//NSLog(#"NSStreamEventHasSpaceAvailable");
assert(NO); // should never happen for the output stream
} break;
case NSStreamEventErrorOccurred: {
DLog(#"NSStreamEventErrorOccurred");
[self _stopReceiveWithStatus:#"Stream open error"];
} break;
case NSStreamEventEndEncountered: {
DLog(#"NSStreamEventEndEncountered");
// ignore
} break;
default: {
DLog(#"default");
assert(NO);
} break;
}
}
In the FtpService.m. Here I counld specify the address and trytime to do ftp request:
- (NSArray *)requstServerListInfo:(NSString *)filename tryTime:(int)tryTime
{
NSArray *result = nil;
//Create the request ftp path
NSString* tm = [NSString stringWithFormat:FTPURL];
if(filename != nil)
tm = [NSString stringWithFormat:#"%#%#/",FTPURL,filename];
while (tryTime-- > 0) {
FtpListService *listService = [[FtpListService alloc] initWithUrl:tm];
[listService _startReceive];
//isReceiving will be NO only when : connect error, time out, correctly done job
//I do not really understand the loop, I just know this will cause the request job to begin
while (listService.isReceiving) {
[[NSRunLoop currentRunLoop] runMode:RUNLOOPMODEL beforeDate:[NSDate distantFuture]];
}
//if correctly request, dirArray != nil
if(listService.dirArray == nil) {
[listService release];
continue;
} else {
result = listService.dirArray;
[listService release];
break;
}
}
return result;
}
The ftp job start from PGNetConductor.m which is a singleton:
pm = [[PGDataManagement alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
result = [pm startNetWork];
}
PGDataManagement is owned by PGNetConductor : #property (nonatomic, unsafe_unretained) PGDataManagement *pm;
I try a lot but failed to solve the problem. Hope someone could give me some advice. If you need the code or more infomation , tell me. Thanks!

NSPOSIXErrorDomain Code=12 "Cannot allocate memory" in 3G network

I'm trying to send a file on FTP server in my iPhone application.
Everything seems to be okay in WiFi and GSM:EDGE network, but in 3G network an error appears (not always, but very often):
Error Domain=NSPOSIXErrorDomain
Code=12 "The operation couldn’t be
completed. Cannot allocate memory"
Below the code where the error appears:
- (void)stream:(NSStream*)aStream handleEvent:(NSStreamEvent)eventCode {
switch( eventCode ) {
case NSStreamEventHasSpaceAvailable: {
if( _readDataOffset == _readDataLimit ) {
NSInteger readDataLen = [_readStream read:[_readData mutableBytes] maxLength:kReadDataLength];
NSLog(#"readDataLen is %d",readDataLen);
if( -1 == readDataLen ) {
_error = [[_readStream streamError] retain];
_keepRunning = NO;
} else if( 0 == readDataLen ) {
_keepRunning = NO;
} else {
_readDataOffset = 0;
_readDataLimit = readDataLen;
}
}
if( _readDataOffset != _readDataLimit ) {
NSOutputStream* writeStream = (NSOutputStream*)aStream;
uint8_t *buffer = (void *)[_readData bytes];
// vvvv and here the value of writtenDataLen is often -1 (but only on 3G network)
NSInteger writtenDataLen = [writeStream write:&buffer[_readDataOffset] maxLength:_readDataLimit - _readDataOffset];
if( writtenDataLen > 0 ) {
_readDataOffset += writtenDataLen;
_writtenDataLen += writtenDataLen;
[self ftpPutDidWriteInternal];
} else if( -1 == writtenDataLen ) {
_error = [[writeStream streamError] retain];
_keepRunning = NO;
}
}
} break;
case NSStreamEventErrorOccurred: {
_error = [aStream.streamError retain];
_keepRunning = NO;
} break;
}
}
What can be important, the whole sending is done in separate thread which has it's own NSAutoreleasePool.
Is there anyone who got the issue? Any suggestion? I would be appreciate.
UPDATE:
I've just checked that popular iPhone application "FTP On The Go" has got the same (?) issue during sending a file in 3G network! There is no error handled, but the transfer stops.
UPDATE 2:
I can't believe it, but it's true: SimpleFTPSample from Apple is also affected with the issue.
And here it is - the solution (or rather workaround):
you should set property of writeStream to false, to switch off default persistant connection
CFWriteStreamSetProperty( (CFWriteStreamRef)writeStreamRef, kCFStreamPropertyFTPAttemptPersistentConnection, kCFBooleanFalse ) ;
Have resolved this error with using operation for request (NSMutableUrlConnection) with #autorelease{} for main function
- (void)main
NSURLConnection* connection;
#autoreleasepool //urgently needed for 3G upload
{
self.currentRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:#"test.php"]];
[self.currentRequest setHTTPMethod:#"PUT"];
[self.currentRequest setHTTPBody:self.data];//inpustStream doesn't work
connection = [NSURLConnection connectionWithRequest:self.currentRequest delegate:self];
[connection start];
}//end autorelease pool
do
{
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]];
if ([self isCancelled])
{
connection = nil;
isFailed = YES;
break;
}
self.status(statusUpdateMessage);
}
while (!isFailed && !isCompleted);
[timer invalidate];//test
timer = nil;
//corresponding of status via blocks
self.completed(!isFailed);
self.status(isFailed ? errorMessage : #"Completed");
if (isFailed)
{
self.failed(errorMessage != nil ? errorMessage : #"Undefined error");
}
self.data = nil;
self.currentRequest = nil;
connection = nil;
}