Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Close connection when NSOutputStream has finished

How can a connection be closed when the NSOutputStream has finished sending data?

After searching around i have found that the event NSStreamEventEndEncountered is only called if the server drops the connection. not if the OutputStream has finished the data to send.

StreamStatus is Always returning 0 (connection closed) or 2 (connection open) but never 4 (writing data).

since both methods mentioned above are not telling me enough about the write process i am not able to find a way do determine if the Stream is still writing or if it has finished and i can close the connection now.

After 5 days of googleling and trying i am totally out of ideas... Any help appreciated. Thanks

EDIT ADDED CODE AS REQUESTED:

- (void)startSend:(NSString *)filePath

{

BOOL                    success;

NSURL *                 url;



assert(filePath != nil);

assert([[NSFileManager defaultManager] fileExistsAtPath:filePath]);

assert( [filePath.pathExtension isEqual:@"png"] || [filePath.pathExtension isEqual:@"jpg"] );



assert(self.networkStream == nil);      // don't tap send twice in a row!

assert(self.fileStream == nil);         // ditto



// First get and check the URL.

...
....
.....


// If the URL is bogus, let the user know.  Otherwise kick off the connection.

...
....
.....


if ( ! success) {

    self.statusLabel.text = @"Invalid URL";

} else {



    // Open a stream for the file we're going to send.  We do not open this stream; 

    // NSURLConnection will do it for us.



    self.fileStream = [NSInputStream inputStreamWithFileAtPath:filePath];

    assert(self.fileStream != nil);



    [self.fileStream open];



    // Open a CFFTPStream for the URL.



    self.networkStream = CFBridgingRelease(

        CFWriteStreamCreateWithFTPURL(NULL, (__bridge CFURLRef) url)

    );

    assert(self.networkStream != nil);



    if ([self.usernameText.text length] != 0) {

        success = [self.networkStream setProperty:self.usernameText.text forKey:(id)kCFStreamPropertyFTPUserName];

        assert(success);

        success = [self.networkStream setProperty:self.passwordText.text forKey:(id)kCFStreamPropertyFTPPassword];

        assert(success);

    }



    self.networkStream.delegate = self;

    [self.networkStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    ///////******** LINE ADDED BY ME TO DISONNECT FROM FTP AFTER CLOSING CONNECTION *********////////////

    [self.networkStream setProperty:(id)kCFBooleanFalse forKey:(id)kCFStreamPropertyFTPAttemptPersistentConnection];

    ///////******** END LINE ADDED BY ME *********//////////// 

    [self.networkStream open];



    // Tell the UI we're sending.



    [self sendDidStart];

}

}



- (void)stopSendWithStatus:(NSString *)statusString

{

if (self.networkStream != nil) {

    [self.networkStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    self.networkStream.delegate = nil;

    [self.networkStream close];

    self.networkStream = nil;

}

if (self.fileStream != nil) {

    [self.fileStream close];

    self.fileStream = nil;

}

[self sendDidStopWithStatus:statusString];

}



- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode

// An NSStream delegate callback that's called when events happen on our 

// network stream.

{

#pragma unused(aStream)

assert(aStream == self.networkStream);



switch (eventCode) {

    case NSStreamEventOpenCompleted: {

        [self updateStatus:@"Opened connection"];

    } break;

    case NSStreamEventHasBytesAvailable: {

        assert(NO);     // should never happen for the output stream

    } break;

    case NSStreamEventHasSpaceAvailable: {

        [self updateStatus:@"Sending"];



        // If we don't have any data buffered, go read the next chunk of data.



        if (self.bufferOffset == self.bufferLimit) {

            NSInteger   bytesRead;



            bytesRead = [self.fileStream read:self.buffer maxLength:kSendBufferSize];



            if (bytesRead == -1) {

                [self stopSendWithStatus:@"File read error"];

            } else if (bytesRead == 0) {

                [self stopSendWithStatus:nil];

            } else {

                self.bufferOffset = 0;

                self.bufferLimit  = bytesRead;

            }

        }



        // If we're not out of data completely, send the next chunk.



        if (self.bufferOffset != self.bufferLimit) {

            NSInteger   bytesWritten;

            bytesWritten = [self.networkStream write:&self.buffer[self.bufferOffset] maxLength:self.bufferLimit - self.bufferOffset];

            assert(bytesWritten != 0);

            if (bytesWritten == -1) {

                [self stopSendWithStatus:@"Network write error"];

            } else {

                self.bufferOffset += bytesWritten;

            }

        }

    } break;

    case NSStreamEventErrorOccurred: {

        [self stopSendWithStatus:@"Stream open error"];

    } break;

    case NSStreamEventEndEncountered: {

        // FOR WHATEVER REASON THIS IS NEVER CALLED!!!!

    } break;

    default: {

        assert(NO);

    } break;

}

}
like image 959
sharkyenergy Avatar asked Apr 28 '13 19:04

sharkyenergy


1 Answers

There can be two interpretations to your question. If what you are asking is "I have a NSOutputStream and I'm finished writing to it how do I signal this?" then the answer is as simple as call the close method on it.

Alternately, If what you are really saying is "I have a NSInputStream and I want to know when I've reached the end-of-stream" then you can look at hasBytesAvailable or streamStatus == NSStreamStatusAtEnd.

For your information, to actually get the status NSStreamStatusWriting you would need to be calling the streamStatus method from another thread while this thread is calling write:maxLength:.

--- Edit: Code Suggestion

The reason you would never get notified is that an output stream is never finished (unless it's a fixed size stream, which an FTP stream is not). It's the input stream that gets "finished" at which point you can close your output stream. That's the answer to your original question.

As a further suggestion, I would skip run loop scheduling and the "event processing" except for handling errors on the output stream. Then I would put the read/write code into a NSOperation subclass and send it off into a NSOperationQueue. By keeping a reference to the NSOperations in that queue you would be able to cancel them easily and even show a progress bar by adding a percentComplete property. I've tested the code below and it works. Replace my memory output stream with your FTP output stream. You will notice that I have skipped the validations, which you should keep of course. They should probably be done outside the NSOperation to make it easier to query the user.

@interface NSSendFileOperation : NSOperation<NSStreamDelegate> {

    NSInputStream  *_inputStream;
    NSOutputStream *_outputStream;

    uint8_t *_buffer;
}

@property (copy) NSString* sourceFilePath;
@property (copy) NSString* targetFilePath;
@property (copy) NSString* username;
@property (copy) NSString* password;

@end


@implementation NSSendFileOperation

- (void) main
{
    static int kBufferSize = 4096;

    _inputStream  = [NSInputStream inputStreamWithFileAtPath:self.sourceFilePath];
    _outputStream = [NSOutputStream outputStreamToMemory];
    _outputStream.delegate = self;

    [_inputStream open];
    [_outputStream open];

    _buffer = calloc(1, kBufferSize);

    while (_inputStream.hasBytesAvailable) {
        NSInteger bytesRead = [_inputStream read:_buffer maxLength:kBufferSize];
        if (bytesRead > 0) {
            [_outputStream write:_buffer maxLength:bytesRead];
            NSLog(@"Wrote %ld bytes to output stream",bytesRead);
        }
    }

    NSData *outputData = [_outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey];
    NSLog(@"Wrote a total of %lu bytes to output stream.", outputData.length);

    free(_buffer);
    _buffer = NULL;

    [_outputStream close];
    [_inputStream close];
}

- (void) stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    // Handle output stream errors such as disconnections here
}

@end


int main (int argc, const char * argv[])
{
    @autoreleasepool {

        NSOperationQueue *sendQueue = [[NSOperationQueue alloc] init];

        NSSendFileOperation *sendOp = [[NSSendFileOperation alloc] init];
        sendOp.username = @"test";
        sendOp.password = @"test";
        sendOp.sourceFilePath = @"/Users/eric/bin/data/english-words.txt";
        sendOp.targetFilePath = @"/Users/eric/Desktop/english-words.txt";

        [sendQueue addOperation:sendOp];
        [sendQueue waitUntilAllOperationsAreFinished];
    }
    return 0;
}
like image 81
aLevelOfIndirection Avatar answered Oct 16 '22 18:10

aLevelOfIndirection