Recovering a lost connection using sockets

2019-08-21 10:31发布

问题:

I'm trying to dig deeper by using this example of an iPhone Chart Server and all is working as expected.

What I wanted to learn next is how to recover when the connection to the server is lost (for whatever reason) while the app is running.

First issue is that the app tries to open the input & output streams concurrently thus if I implement an alert, I get it two times. I managed to resolve this by closing the streams if there is an error.

The second is that if I'm another view controller and the connection is lost I can't seem to be able to recover it.I call the sendSocketMessage and if there is the error flat to try to use the initNetworkCommunication but I get a fatal error.

This 2nd issue is troubling me. I've added an All Exceptions breakpoint but got nothing. How I try to 'test' this is by making sure the server works and the app loads and connects. Then I shut-down the server, try a few clicks on the app and I get the alert. I switch-on the server and try to click again, I get the message sent to the server but then the app crashes with no info!

@implementation ViewController

bool connectionError = true;

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


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

    CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (CFStringRef)@"192.168.1.1", 6035, &readStream, &writeStream);

    inputStream = (__bridge NSInputStream *)readStream;
    outputStream = (__bridge NSOutputStream *)writeStream;

    [inputStream setDelegate:self];
    [outputStream setDelegate:self];

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

    [inputStream open];
    [outputStream open];

}

- (void)closeAll {
    NSLog(@"Closing streams.");

    [inputStream close];
    [outputStream close];

    [inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    [inputStream setDelegate:nil];
    [outputStream setDelegate:nil];

    inputStream = nil;
    outputStream = nil;
}

- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent {

    switch (streamEvent) {

    case NSStreamEventOpenCompleted:
        NSLog(@"Stream opened");
        break;

    case NSStreamEventHasBytesAvailable:

        connectionError = false;
        if (theStream == inputStream) {

        uint8_t buffer[1024];
        long len;

        NSMutableData *currentMessage;
        currentMessage = [NSMutableData dataWithBytes:"" length:strlen("")];

        while ([inputStream hasBytesAvailable]) {
            len = [inputStream read:buffer maxLength:sizeof(buffer)];
            [currentMessage appendBytes:buffer length:len];
        }
        NSData * nullByte = [NSMutableData dataWithLength:1];
        len = currentMessage.length;
        NSRange searchRange = {0, len};
        for (NSRange r; (r = [currentMessage rangeOfData:nullByte options:0 range:searchRange]).length; ) {
            NSString * message = [[NSString alloc] initWithBytes:currentMessage.bytes length:r.location encoding:NSUTF8StringEncoding];
            searchRange.location = r.location+r.length;
            searchRange.length = len - searchRange.location;
            [self messageReceived:message];
        }
        [currentMessage replaceBytesInRange:(NSRange){0, searchRange.location} withBytes:NULL length:0];

        }
        break;

    case NSStreamEventErrorOccurred:
        NSLog(@"Can not connect to the host!");
        connectionError = true;

        [self closeAll];

        [self connectionLost];

        break;


    case NSStreamEventEndEncountered:
        break;

    default:
        NSLog(@"Unknown event");
    }

}

- (void) connectionLost {

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Alert!"
                            message:@"Connection to the server lost!"
                           delegate:nil
                      cancelButtonTitle:@"OK"
                      otherButtonTitles:nil];

    [alert show];

}

- (void) messageReceived:(NSString *)message {

    [messages addObject:message];

    if (inputStream.streamStatus == NSStreamStatusClosed || inputStream.streamStatus == NSStreamStatusError || inputStream.streamStatus == NSStreamStatusNotOpen) {
    [self closeAll];
    [self initNetworkCommunication];
    }

    // do things with the message...

}

- (void) initConnection {

    [self initNetworkCommunication];

    messages = [[NSMutableArray alloc] init];

}

- (IBAction)joinChat:(id)sender {

    [self initConnection];

    [self sendSocketMessage: @"iam:" message: _inputNameField.text];

}


- (void) sendSocketMessage:(NSString*) sendCommand message:(NSString*) sendMessage
{

    if (outputStream.streamStatus == NSStreamStatusClosed || outputStream.streamStatus == NSStreamStatusError || outputStream.streamStatus == NSStreamStatusNotOpen) {
    [self closeAll];
    [self initNetworkCommunication];
    }

    NSString *response  = [NSString stringWithFormat: @"%@%@", sendCommand, sendMessage];
    NSData *data = [[NSData alloc] initWithData:[response dataUsingEncoding:NSASCIIStringEncoding]];
    [outputStream write:[data bytes] maxLength:[data length]];
    NSLog(@"clint sent: %@", response);

}



@end

The error I get is this screen grab:

int main(int argc, char * argv[]) {
    @autoreleasepool {
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

Thread 1: EXC_BAD_ACCESS(code=1, address=0x0)

Any advice?

回答1:

First issue:

I would use a strategy that involves pairing your alerts to connection objects. Since there is no built-in connection object in the API for dealing with CF streams, you'll have to create your own. Typically, a useful connection object would have properties for the associated NSInputStream and NSOutputStream. This approach should eliminate the problem of getting two alerts, since you will be showing an alert for the one connection object, instead of it's associated IO streams. This also encapsulates properties and operations neatly into one connection object, which you can separate from your view controller. Another benefit to using a connection object is that you will have more flexibility in your view controllers' object graph, since you will be using the connection object through composition, instead of inheritance in view controller subclasses.

Second issue:

The main function is shown when you get the error, not because it is causing the problem, but rather it is the function at the end of the call stack--when an exception is thrown, the system exits each call in the stack until it is caught, or until it reaches the end of the call stack, which is typically the main function in Objective-C.

I would first go your breakpoints panel, and add a general breakpoint, by tapping on the + sign in the bottom left corner, and selecting Add Exception Breakpoint. This usually helps me, when it successfully shows the point in my code that initially threw an exception, instead of showing the main function, which isn't helpful. You don't need to edit this breakpoint once it is added. Sometimes it causes the debugger to stop unnecessarily, if there are other exceptions that are thrown and caught in the system. If this is the case, then you can simply disable the exception break point until you arrive at the point in question, and then manually re-enable the break point before initiating an operation.

Edit

Another approach that you can use, if the catch-all Exception break point fails, is to use an exception handler. You would typically apply this inside your application:didLaunchWithOptions: method, like this:

NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);

And, implement your handler to print out some useful information:

void uncaughtExceptionHandler(NSException *exception) {
    debugLog(@"CRASH: %@", exception);
    debugLog(@"Stack trace: %@", [exception callStackSymbols]);
}