NSStream and Sockets, NSStreamDelegate methods not being called
Asked Answered
S

2

12

I've followed the guide Setting Up Socket Streams and have effectively duplicated that code in my class. No matter what I try the delegate methods just don't seem to get called.

In the header file I have (basically):

@interface myClass : NSObject <NSStreamDelegate> {
    NSInputStream *inputStream;
    NSOutputStream *outputStream;
}
- (void)connect;
@end;

The connect method:

- (void)connect {
    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;

    CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (CFStringRef)@"host.example.com", 1234, &readStream, &writeStream);

    inputStream = (NSInputStream *)readStream;
    outputStream = (NSOutputStream *)writeStream;
    [inputStream setDelegate:self];
    [outputStream setDelegate:self];
    [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [inputStream open];
    [outputStream open];
}

Also tried using CFStreamCreatePairWithSocketToCFHost() and [NSStream getStreamsToHost:port:inputStream:outputStream: - all with exactly the same result.

I've set a breakpoint at the beginning of the connect method, stepped through every line and every pointer is valid and seems to point to the correct object.

In GDB, after the setDelegate calls, po [inputStream delegate] prints <myClass: 0x136380> as expected, so it has set the delegate correctly.

For the life of me I can't work out why it refuses to call the stream:handleEvent: method on my class:

- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
    NSLog(@"got an event");
}

Hopefully I've missed something really simple and obvious and a second pair of eyes can spot my mistake.

Thanks in advance to anyone who has the patience and taken the time to read this far!

Shipmaster answered 8/2, 2011 at 8:31 Comment(0)
S
29

In the lines like this:

[inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

Instead of using [NSRunLoop currentRunLoop] I changed it to [NSRunLoop mainRunLoop].

EDIT 2011-05-30:

The reason this did not work is because I was setting up the sockets in a background thread via +[NSThread detachNewThreadSelector:toTarget:withObject:].

Doing it that way created a new run loop, which after reading the run loop developer documentation I discovered that you need to tell the NSRunLoop to run manually.

Running it in the same run loop as the main thread was fine on performance, though I was able to squeeze a bit more performance out by writing a wrapper class and running all network I/O on a background thread.

Shipmaster answered 8/2, 2011 at 23:46 Comment(6)
@user102008 Edited my response to explain.Shipmaster
could u potentially post the ending code? For some reason, I changed the [NSRunLoop currentRunLoop] to [NSRunLoop mainRunLoop] and the connection doesn't fails shortly after it initiatesRiggs
you nailed it! after removing my socket client from the main thread, i stopped listening to the inputStream (even though my outputStream was still working, as i could monitor on the socket server). i just changed the inputstream`s run loop and bingo! UPVOTED :)Simplicity
both your question and answer helped me to know where to go, but im not sure how to start and connect using a simple button??Gensler
Thanks a lot. I had the same problem using Monotouch and opening the socket in a Task. Using Main loop instead of current (Task's thread) solved the problem.Bankable
one year later, this answer helped me again.. funny reading comments and discovering you commented on this answer before!Gensler
J
9

That solution will work if & only if you don't have blocking work on thread 0. This is often OK, but a better solution is to create a new thread (i.e. using a class method to create the thread on demand) and then enqueue on that thread. i.e.

+ (NSThread *)networkThread {
    static NSThread *networkThread = nil;
    static dispatch_once_t oncePredicate;

    dispatch_once(&oncePredicate, ^{
        networkThread =
             [[NSThread alloc] initWithTarget:self
                                     selector:@selector(networkThreadMain:)
                                       object:nil];
        [networkThread start];
    });

    return networkThread;
}

+ (void)networkThreadMain:(id)unused {
    do {
        @autoreleasepool {
            [[NSRunLoop currentRunLoop] run];
        }
    } while (YES);
}

- (void)scheduleInCurrentThread
{
    [inputstream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                           forMode:NSRunLoopCommonModes];
}

With this, you can schedule the input stream using:

[self performSelector:@selector(scheduleInCurrentThread)
             onThread:[[self class] networkThread]
           withObject:nil
        waitUntilDone:YES];

This will allow you to run your network operations without worrying about deadlocks anymore.

Janel answered 10/4, 2012 at 17:51 Comment(3)
Thanks for the tip. I have another related problem. When stream is being written onto, user may go ahead and move the application in background. I would prefer to keep the stream running even in background. I tried using beginBackgroundTask... to achieve that but no matter what I do, I cant make my picture uploading work when app is in background. Do you this this will be at all possible?Mauldon
One of our tutorial apps does this. If you're still looking for help, you might want to check it out at parse.com/tutorials/anypic#post Check out the bit about 'saving the PFObject.' Though our framework hides it, the bit inside the background task makes a network call.Janel
Does this code adequately drain the autorelease pool regularly? The documentation for -[NSRunLoop run] says that it will repeatedly invoke runMode:beforeDate:. That means there is no chance for the autorelease pool to be drained in between those invocations.Karat

© 2022 - 2024 — McMap. All rights reserved.