iPhone锁屏网络连接关闭问题(iphone锁屏打开时请求网络失败)

一.问题描述

在开发的iPhone APP中,有个设计是:当APP从后台(Background)返回到前台(Foreground)时,会进行一些网络连接获取新的数据,网络请求是使用ASIHTTPRequest完成的。但是在测试中发现一个问题:在运行APP时进入锁屏状态,当解锁后显示时,APP会从Background状态进入到Froeground状态,按照设计此时会进行网络连接更新数据,但是ASIHTTPRequest请求却返回了错误,如下

Error Domain=ASIHTTPRequestErrorDomain Code=1 "A connection failure occurred" UserInfo=0x69f400 {NSUnderlyingError=0x6e9fc0 "The operation couldn’t be completed. Broken pipe", NSLocalizedDescription=A connection failure occurred}

返回的错误是“Broken pipe”,这个错误的意思是对端(服务器端)将连接关闭了,而客户端不知道,仍然使用了已经关闭的连接。

二.错误信息分析

通过跟踪错误产生的位置研究了下ASIHTTPRequest的代码,发现对于每一个ASIHTTPRequest请求并不是每次都新建一个连接,其内部有连接池存储之前的连接。
见ASIHTTPRequest.m

// An array of connectionInfo dictionaries.
// When attempting a persistent connection, we look here to try to find an existing connection to the same server that is currently not in use
static NSMutableArray *persistentConnectionsPool = nil;

HTTP中有个持久连接(persistent connection)的技术,也叫做HTTP keep-alive或者HTTP connection reuse,其意思时,在同一个TCP连接上,进行多次的HTTP请求/回复的发送和接收,相对于每个HTTP请求/回复都新建一个连接,这种方式可以减少系统消耗。

ASIHTTPRequest中也使用了这种技术,如果当前HTTP连接属于持久连接,则会将TCP连接保存到持久连接池(persistentConnectionsPool)中,如果之后的HTTP请求请求的是同一个服务器,则会复用连接池中的连接发送HTTP请求以及接收HTTP回复,见ASIHTTPRequest帮助文档中的Configuring persistent connections节。

By default, ASIHTTPRequest will attempt to keep connections to a server open so that they can be reused by other requests to the same server (this generally results in significant speed boost, especially if you have many small requests). Persistent connections will be used automatically when connecting to an HTTP 1.1 server, or when the server sends a keep-alive header. Persistent connections are not used if the server explicitly sends a ‘Connection: close’ header. Additionally, ASIHTTPRequest will not use persistent connections for requests that include a body (eg POST/PUT) by default (as of v1.8.1). You can force the use of persistent connections for these request by manually setting the request method, then turning persistent connections back on:

[request setRequestMethod:@"PUT"];
[request setShouldAttemptPersistentConnection:YES];

如果服务器未指定当前连接的超时时间的话,则ASIHTTPRequest则默认为60s,在+expirePersistentConnections方法中会将超时的连接从连接池中删掉。

Many servers do not provide any information in response headers on how long to keep a connection open, and may close the connection at any time after a request is finished. If the server does not send any information about how long the connection should be used, ASIHTTPRequest will keep connections to a server open for 60 seconds after any request has finished using them. Depending on your server configuration, this may be too long, or too short.

如果假定的超时时间太短,则持久连接会在仍然可用的时候被关掉,下一次HTTP请求就得再新建一个连接,只是影响性能,但不影响使用;如果假定的超时时间太长,则在持久连接已经关闭的时候其仍然在连接池中,这时候使用此连接进行HTTP请求就会出现错误,如果错误为连接已关闭的话,ASIHTTPRequest会使用其他连接重试一次此请求。

If this timeout is too long, the server may close the connection before the next request gets a chance to use it. When ASIHTTPRequest encounters an error that appears to be a closed connection, it will retry the request on a new connection.

If this timeout is too short, and the server may be happy to keep the connection open for longer, but ASIHTTPRequest will needlessly open a new connection, which will incur a performance penalty.

ASIHTTPRequest根据错误码判断该错误是否是由连接已关闭引起的,这三个错误码为POSIX socket的ENOTCONN,EPIPE错误和CFNetwork的kCFURLErrorNetworkConnectionLost(-1005)错误,见ASIHttpRequest.m中的-handleStreamError方法。

// First, check for a 'socket not connected', 'broken pipe' or 'connection lost' error
// This may occur when we've attempted to reuse a connection that should have been closed
// If we get this, we need to retry the request
// We'll only do this once - if it happens again on retry, we'll give up
// -1005 = kCFURLErrorNetworkConnectionLost - this doesn't seem to be declared on Mac OS 10.5
if (([[underlyingError domain] isEqualToString:NSPOSIXErrorDomain] && ([underlyingError code] == ENOTCONN || [underlyingError code] == EPIPE))
	|| ([[underlyingError domain] isEqualToString:(NSString *)kCFErrorDomainCFNetwork] && [underlyingError code] == -1005)) {
	if ([self retryUsingNewConnection]) {
		return;
	}
}

而我们在锁屏恢复时收到的错误是”Broken pipe”,也就是EPIPE错误,ASIHTTPRequest在第一次收到这个错误时会重试另一个连接,既然错误到我们这了,说明使用的两个连接全都被关闭了。

<3>三.测试验证

为了查清楚这个问题,我们需要抓取网络数据包,可以根据使用Mac抓取iPhone数据包中的描述建立抓包环境,在Mac电脑上使用WireShark抓包。
在ASIHTTPRequestConfig.h中有持久连接的调试开关,我们可以将其打开,以观察持久连接的使用情况。

// When set to 1, ASIHTTPRequests will print information about persistent connections to the console
#ifndef DEBUG_PERSISTENT_CONNECTIONS
#define DEBUG_PERSISTENT_CONNECTIONS 1
#endif

在抓包中发现,当我们进行锁屏操作时,所有的网络连接的通过FIN/ACK的流程正常关闭了,如下

当前有4个与服务器的连接,而4个连接在锁屏时都通过了FIN/ACK的流程关闭了,但此时在ASIHttpRequest的连接池(persistentConnectionsPool)中应该仍然存在这4个连接,那么在锁屏恢复进入Foreground时仍然会去尝试使用这4个已经关闭的连接。那么在锁屏恢复时,可以看到如下打印

[CONNECTION] Request #15 will use connection #4
[CONNECTION] Request #16 will use connection #3
[CONNECTION] Request attempted to use connection #4, but it has been closed - will retry with a new connection
[CONNECTION] Request #15 will use connection #2
[CONNECTION] Request attempted to use connection #3, but it has been closed - will retry with a new connection
[CONNECTION] Request #16 will use connection #1
[CONNECTION] Request attempted to use connection #2, but it has been closed - we have already retried with a new connection, so we must give up
[CONNECTION] Request #15 failed and will invalidate connection #2
[CONNECTION] Request attempted to use connection #1, but it has been closed - we have already retried with a new connection, so we must give up
[CONNECTION] Request #16 failed and will invalidate connection #1

从上述打印可以看出Request #15尝试使用connection #4,connection #2都失败了,Request #16尝试使用connection #3,connection #1也都失败了,两次失败则不会重试,直接将错误返回给我们。

那么为什么会这样呢,在网络上搜索到了下面的信息
iphone app network connection disconnect after screen locking with new ios sdk 5.0
Locking iPhone disconnects sockets on iOS 5 only
从链接中的信息可知,在iOS5.0+的版本中锁屏会导致网络连接断开,而之前的版本不会,但并没有找到Apple的官方文档对此改变的说明。

四.解决方案

那么怎么解决这个问题呢,问题的简单描述为锁屏操作会关闭所有网络连接,并进入Background模式,而ASIHttpRequst中的连接池没有随着网络连接的关闭而更新,导致恢复到Foreground时复用网络连接时出现错误。
那么解决方法就是在锁屏关闭网络连接时同时将ASIHttpRequst持久连接池中的连接清掉。但我们在APP中并没有办法得知是否发生了锁屏,而只知道何时进入Background。锁屏必然进入Background模式,而进入Background模式并不一定是因为锁屏,还可以是手动按Home键,被电话、短信等中断引起的。这样我们可以在进入Background时手工将ASIHttpRequst中的连接池中的连接清掉,虽然在不是因为锁屏进入Background的情况下会影响性能,但保证了锁屏情况下的正确性,所以还是值得的。
ASIHttpRequst中只有清除过期持久连接的方法,而没有清除所有持久连接的方法,所有我们要仿照+expirePersistentConnections方法自己写一个

清除过期持久连接方法为

+ (void)expirePersistentConnections
{
	[connectionsLock lock];
	NSUInteger i;
	for (i=0; i<[persistentConnectionsPool count]; i++) {
		NSDictionary *existingConnection = [persistentConnectionsPool objectAtIndex:i];
		if (![existingConnection objectForKey:@"request"] && [[existingConnection objectForKey:@"expires"] timeIntervalSinceNow] <= 0) {
#if DEBUG_PERSISTENT_CONNECTIONS
			ASI_DEBUG_LOG(@"[CONNECTION] Closing connection #%i because it has expired",[[existingConnection objectForKey:@"id"] intValue]);
#endif
			NSInputStream *stream = [existingConnection objectForKey:@"stream"];
			if (stream) {
				[stream close];
			}
			[persistentConnectionsPool removeObject:existingConnection];
			i--;
		}
	}
	[connectionsLock unlock];
}

自己写的清除所有持久连接方法为

+ (void)clearPersistentConnections
{
	[connectionsLock lock];
	NSUInteger i;
	for (i=0; i<[persistentConnectionsPool count]; i++) {
		NSDictionary *existingConnection = [persistentConnectionsPool objectAtIndex:i];
		if (![existingConnection objectForKey:@"request"]) {
#if DEBUG_PERSISTENT_CONNECTIONS
			ASI_DEBUG_LOG(@"[CONNECTION] Closing connection #%i manualy",[[existingConnection objectForKey:@"id"] intValue]);
#endif
			NSInputStream *stream = [existingConnection objectForKey:@"stream"];
			if (stream) {
				[stream close];
			}
			[persistentConnectionsPool removeObject:existingConnection];
			i--;
		}
	}
	[connectionsLock unlock];
}

在进入Background模式时,即AppDelegate的-applicationDidEnterBackground:方法中清空连接池

- (void)applicationDidEnterBackground:(UIApplication *)application {
    [ASIHTTPRequest clearPersistentConnections];
    ...

    /*
     Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
     If your application supports background execution, called instead of applicationWillTerminate: when the user quits.
     */
}

参考:
ASIHTTPRequest documentation
HTTP persistent connection
iphone app network connection disconnect after screen locking with new ios sdk 5.0
Locking iPhone disconnects sockets on iOS 5 only

本文出自 清风徐来,水波不兴 的博客,转载时请注明出处及相应链接。(感谢分享)

本文永久链接: http://www.winddisk.com/2012/08/27/iphone_screenlock_network_disconnection


你可能感兴趣的:(ios开发)