1. 程式人生 > >iPhone鎖屏網路連線關閉問題(iphone鎖屏開啟時請求網路失敗)

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:

<code><span style="color: rgb(0, 34, 0);">[</span>request setRequestMethod<span style="color: rgb(0, 34, 0);">:</span><span style="color: rgb(191, 29, 26);">@</span><span style="color: rgb(191, 29, 26);">"PUT"</span><span style="color: rgb(0, 34, 0);">]</span>;
<span style="color: rgb(0, 34, 0);">[</span>request setShouldAttemptPersistentConnection<span style="color: rgb(0, 34, 0);">:</span><span style="color: rgb(166, 19, 144);">YES</span><span style="color: rgb(0, 34, 0);">]</span>;</code>

如果伺服器未指定當前連線的超時時間的話,則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的ENOTCONNEPIPE錯誤和CFNetworkkCFURLErrorNetworkConnectionLost(-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也都失敗了,兩次失敗則不會重試,直接將錯誤返回給我們。

四.解決方案

那麼怎麼解決這個問題呢,問題的簡單描述為鎖屏操作會關閉所有網路連線,並進入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.
     */
}

最後再補充一下:如果使用AFNetWorking,當應用程式進入後臺或鎖屏時(ios5.0及以後鎖屏會導致網路連線中斷),如何保持持續的請求? 解決如下:

  1. AFURLConnectionOperation有一個叫setShouldExecuteAsBackgroundTaskWithExpirationHandler:的方法用於處理在應用程式進入後臺後,進行持續的請求  
  2. [self setShouldExecuteAsBackgroundTaskWithExpirationHandler:^{  
  3. }]; 

本文出自 清風徐來,水波不興 的部落格,轉載時請註明出處及相應連結。(感謝分享)

本文永久連結: http://www.winddisk.com/2012/08/27/iphone_screenlock_network_disconnection