0

Following precisely Apple's docs here: https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Streams/Streams.html - I cannot get any network NSOutputStream to ever write data. It always returns a number that Apple's docs seem to say shouldn't be returned (0 - Apple claims this is for fixed-size streams, not network streams??).

All of Apple's sample code ignores the return code (seems to assume it's positive - A lot of samples I found on the web use unsigned type! Which is very wrong :( ).

I've successfully:

  1. CFSocketCreate() with kCFSocketAcceptCallBack and a pointer to my handleConnect function
  2. CFSocketSetAddress() with "any" and a hand-chosen port
  3. CFRunLoopAddSource() to start listening on port

... all this works, confirmed by telnetting to the IP address + port ...


...but then it goes horribly wrong, simply by following the Apple docs:

void handleConnect (
                 CFSocketRef s,
                 CFSocketCallBackType callbackType,
                 CFDataRef address,
                 const void *data,
                 void *info
                 )
{
    NSLog(@"handleConnect called with callbackType = %li", callbackType);

    (myclass) server = (myclass*) info;

    BOOL canAccept = callbackType & kCFSocketAcceptCallBack;
    BOOL canWrite = callbackType & kCFSocketWriteCallBack;
    BOOL canRead = callbackType & kCFSocketReadCallBack;
    NSLog(@" ... acceptable? %@  .. readable? %@ .. writeable? %@", canAccept?@"yes":@"no", canRead?@"yes":@"no", canWrite?@"yes":@"no" );

    if( canAccept)
    {
        NSLog(@"[%@] Accepted a socket connection from remote host. Address = %@", [server class], addressString );
        /**
         "which means that a new connection has been accepted. In this case, the data parameter of the callback is a pointer to a CFSocketNativeHandle value (an integer socket number) representing the socket.

         To handle the new incoming connections, you can use the CFStream, NSStream, or CFSocket APIs. The stream-based APIs are strongly recommended."
         */

        // "1. Create read and write streams for the socket with the CFStreamCreatePairWithSocket function."
        CFReadStreamRef clientInput = NULL;
        CFWriteStreamRef clientOutput = NULL;

        // for an AcceptCallBack, the data parameter is a pointer to a CFSocketNativeHandle
        CFSocketNativeHandle nativeSocketHandle = *(CFSocketNativeHandle *)data;

        CFStreamCreatePairWithSocket(kCFAllocatorDefault, nativeSocketHandle, &clientInput, &clientOutput);

        // "2. Cast the streams to an NSInputStream object and an NSOutputStream object if you are working in Cocoa."

        // "3. Use the streams as described in “Writing a TCP-Based Client.”
        self.outputBuffer = [NSMutableData dataWithData:[@"Hello" dataWithEncoding:NSUTF8StringEncoding]];
        self.outputBytesWrittenRecently = 0;
        ((NSOutputStream*)output).delegate = self;

        [((NSOutputStream*)output) scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [((NSOutputStream*)output) open]; // MUST go last, undocumented Apple bug
    }
}

...and the delegate methods:

-(void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
    BOOL streamOpened = eventCode & NSStreamEventOpenCompleted;
    BOOL streamReadyToWrite = eventCode & NSStreamEventHasSpaceAvailable;
    BOOL streamReadyToRead = eventCode & NSStreamEventHasBytesAvailable;
    BOOL streamEnded = eventCode & NSStreamEventEndEncountered;
    BOOL streamErrored = eventCode & NSStreamEventErrorOccurred;

    if( streamReadyToWrite )
#define C_BUFFER_IN_MEMORY_SIZE_WRITE ( 4096 ) // Apple vaguely recommends 4k, but I tried numbers down to 100 with no effect
    uint8_t extraCBufferNSStreamSucks[ C_BUFFER_IN_MEMORY_SIZE_WRITE ];
    NSRange rangeOfBytesToAttemptToWriteNext = { self.outputBytesWrittenRecently, MIN( C_BUFFER_IN_MEMORY_SIZE_WRITE, self.outputBuffer.length - self.outputBytesWrittenRecently) };
    [self.outputBuffer getBytes:&extraCBufferNSStreamSucks range:rangeOfBytesToAttemptToWriteNext];

    NSLog(@"About to write data to stream, this might block...");
    NSInteger amountWritten = [self.outputS write:extraCBufferNSStreamSucks maxLength:C_BUFFER_IN_MEMORY_SIZE_WRITE];
    NSLog(@"...finished write data to stream, it might have blocked");
    if( amountWritten > 0 )
    {
        self.outputBytesWrittenRecently += amountWritten;

        NSRange totalWrittenRange = { 0, self.outputBytesWrittenRecently };
        NSLog(@"Written %i bytes: %@", amountWritten, [[[NSString alloc] initWithData:[self.outputBuffer subdataWithRange:totalWrittenRange] encoding:NSUTF8StringEncoding] autorelease] );

        if( self.outputBytesWrittenRecently == self.outputBuffer.length )
        {
            NSLog(@"Finished writing data!");

        }
    }
    else
        NSLog(@"ERROR: failed to write bytes to stream (bytes written = %i)", amountWritten);
}
Adam
  • 32,900
  • 16
  • 126
  • 153
  • What is your question? – xpereta Jun 17 '13 at 15:30
  • Why does it return 0? what is wrong? what is missing? why doesn't the API work as advertised? what mistake have I made? I have no idea what's causing the described behaviour - I simply followed Apple's docs - so I can't be more precise :( – Adam Jun 17 '13 at 15:42

1 Answers1

1

Ah! Undocumented, but ... if your NSStream instance ever becomes nil, instead of returning an error, Apple returns success - but then returns 0 for number of bytes written.

In my case, a typo in an init method meant I was overwriting my freshly-captured NSOutputStream and NSInputStream with nil.

ARGH.

Adam
  • 32,900
  • 16
  • 126
  • 153
  • I saw your flag. I think this is worth keeping around just for the undocumented NSStream issue. – Bill the Lizard Jun 19 '13 at 16:23
  • Agreed - my mistake that lead to this was simply stupid, but the underlying problem (Apple's poorly documented return code of 0) is valid. And ... thanks to this question, there's now a google hit for the behaviour of Apple's API/ :). – Adam Jun 20 '13 at 09:53
  • Just for the record, in objective-c sending messages to nil is completely ok, it will do nothing. So, if your `self.outputS` is nil there is no chance for returning errors. nothing will be executed. Regarding returning 0, IMO, this doesn't look like an undocumented thing. https://developer.apple.com/library/ios/documentation/cocoa/reference/foundation/Classes/NSOutputStream_Class/Reference/Reference.html#//apple_ref/occ/instm/NSOutputStream/write:maxLength: – nacho4d Aug 26 '13 at 04:29
  • @nacho4d yes, you're right about the "sending to nil". It's confusing because the docs you quoted say "If there was an error writing to the stream, return -1." - but this is not what happens. The docs were written according to the underlying C API I expect, and don't work so well for an Objective-C API. – Adam Aug 26 '13 at 13:42
  • @nacho4d Those docs say zero if "is of a fixed length and has reached its capacity". Since a network stream is by definition NOT fixed length ... you don't expect the method to return zero. – Adam Aug 26 '13 at 13:43