一、整体介绍
-
前面已经介绍了网络访问的NSURLSession、NSURLConnection,还有网页加载有关的webview,基本满足通常的网络相关的开发。
其实在网络开发中还有比较常用的就是网络状态的检测。苹果对需要联网的应用要求很高,就是必须要进行联网检查。另外,当网络发生异常时能够及时提示用户网络已断开,而不是程序问题造成卡顿;当用户观看视频或下载大文件时,提示用户当前的网络状态为移动流量或wifi下,是否继续使用,以避免在用户不知情下产生过多流量资费等等。 - 网络状态的检测有多种方法,常用的有三种
- 官方提供的Reachability下载苹果Reachability
- AFNetworking附带提供的
AFNetworkReachabilityManager
,下载AFNetworking - 专门的第三方框架,使用比较多的下载第三方框架
二、苹果Reachability使用
使用非常简单,将Reachability.h
与Reachability.m
加入项目中,在要使用的地方包含Reachability.h
头文件,示例代码:
#import "Reachability.h"
/// 在刚开始就开始监听
- (void)viewDidLoad { [super viewDidLoad]; // Reachability使用了通知,当网络状态发生变化时发送通知kReachabilityChangedNotification [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appReachabilityChanged:) name:kReachabilityChangedNotification object:nil]; // 检测指定服务器是否可达 NSString *remoteHostName = @"www.bing.com"; self.hostReachability = [Reachability reachabilityWithHostName:remoteHostName]; [self.hostReachability startNotifier]; // 检测默认路由是否可达 self.routerReachability = [Reachability reachabilityForInternetConnection]; [self.routerReachability startNotifier]; } /// 当网络状态发生变化时调用 - (void)appReachabilityChanged:(NSNotification *)notification{ Reachability *reach = [notification object]; if([reach isKindOfClass:[Reachability class]]){ NetworkStatus status = [reach currentReachabilityStatus]; // 两种检测:路由与服务器是否可达 三种状态:手机流量联网、WiFi联网、没有联网 if (reach == self.routerReachability) { if (status == NotReachable) { NSLog(@"routerReachability NotReachable"); } else if (status == ReachableViaWiFi) { NSLog(@"routerReachability ReachableViaWiFi"); } else if (status == ReachableViaWWAN) { NSLog(@"routerReachability ReachableViaWWAN"); } } if (reach == self.hostReachability) { NSLog(@"hostReachability"); if ([reach currentReachabilityStatus] == NotReachable) { NSLog(@"hostReachability failed"); } else if (status == ReachableViaWiFi) { NSLog(@"hostReachability ReachableViaWiFi"); } else if (status == ReachableViaWWAN) { NSLog(@"hostReachability ReachableViaWWAN"); } } } } /// 取消通知 - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:kReachabilityChangedNotification object:nil]; }
代码中两种检测:默认路由是否可达、服务器是否可达。有很多人可能有疑问,检测是否联网就可以了,怎么还要检测是否服务器可达?默认路由可达?
其实虽然联网了,也不一定能访问外网(通常说的互联网)。比如连了一个路由器,但是路由器没有联网,那么也是不能联网的。还有就是网络数据包在网际层传递时,一个路由传到另一个路由称为一跳,当达到255跳(大部分路由设置为255)还没有传到目的地时,网络数据包则丢弃。
路由器有一套算法来保证路径最优,还有路由表(保存路径表),如果一个数据包在路由表中没有匹配的路径的话,那么路由器就将此数据包发送到默认路由,这里的默认路由就是上面检测的默认路由是否可达。(里面相当复杂,就此打住)
令人崩溃的是:Reachability并不能检测到服务器是否真的可达,只能检测设备是否连接到局域网,以及用的WiFi还是WWAN。即:把设备网络关了,立马检测出NotReachable,连接到路由器立马检测出是ReachableViaWiFi、、、
代码中使用了通知,则释放对象时一定要在dealloc
中取消通知。我们知道,通知不能在进程间通信,在哪个线程发送通知则在哪个线程执行。如果想在其它线程监听,则在其它线程调用startNotifier
函数,新开启的线程默认都没有开启runloop消息循环,因此还要开启runloop,如下:
// 被通知函数运行的线程应该由startNotifier函数执行的线程决定
typeof(self) weakSelf = self; dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSString *remoteHostName = @"www.bing.com"; weakSelf.hostReachability = [Reachability reachabilityWithHostName:remoteHostName]; [weakSelf.hostReachability startNotifier]; weakSelf.routerReachability = [Reachability reachabilityForInternetConnection]; [weakSelf.routerReachability startNotifier]; // 开启当前线程消息循环 [[NSRunLoop currentRunLoop] run]; });
最后,如果想取消检测,调用stopNotifier方法即可
[self.hostReachability stopNotifier];
[self.routerReachability stopNotifier];
三、AFNetworkReachabilityManager使用
- 直接使用
使用CocoaPods
或者直接将AFNetwork下载并添加进项目。如果只是使用AFNetworkReachabilityManager
而不适用其它网络功能则只将其.m和.h添加进项目即可。AFNetworkReachabilityManager
使用了block的方式,当网络状态发生变化就会调用,且block的调用AFN已经将其限定在主线程下。下面介绍直接使用
#import "AFNetworkReachabilityManager.h"
- (void)afnReachabilityTest {
[[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
// 一共有四种状态 switch (status) { case AFNetworkReachabilityStatusNotReachable: NSLog(@"AFNetworkReachability Not Reachable"); break; case AFNetworkReachabilityStatusReachableViaWWAN: NSLog(@"AFNetworkReachability Reachable via WWAN"); break; case AFNetworkReachabilityStatusReachableViaWiFi: NSLog(@"AFNetworkReachability Reachable via WiFi"); break; case AFNetworkReachabilityStatusUnknown: default: NSLog(@"AFNetworkReachability Unknown"); break; } }]; [[AFNetworkReachabilityManager sharedManager] startMonitoring]; }
- 使用AFHTTPSessionManager
当使用AFN网络框架时,大多情况下,我们使用AFNetwork时会创建一个网络中间单例类,以防止换网络框架时要改动太多,比如替换之前用的多的ASI,如果有个中间类的话,替换就很简单,只需要修改中间类即可。使用时调用[NetworkTools sharedManager];
即可
/// 头文件
#import "AFHTTPSessionManager.h"
@interface NetworkTools : AFHTTPSessionManager + (instancetype)sharedManager; @end --------------------------------------------------------------------------------- /// .m文件 #import "NetworkTools.h" @implementation NetworkTools + (instancetype)sharedManager { static dispatch_once_t once; static id instance; dispatch_once(&once, ^{ //#warning 基地址 // instance = [[self alloc] initWithBaseURL:[NSURL URLWithString:@"http://www.bing.com"]]; instance = [[self alloc] init]; }); return instance; } - (instancetype)init { if ((self = [super init])) { // 设置超时时间,afn默认是60s self.requestSerializer.timeoutInterval = 30; // 响应格式添加text/plain self.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/plain", nil]; // 监听网络状态,每当网络状态发生变化就会调用此block [self.reachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { switch (status) { case AFNetworkReachabilityStatusNotReachable: // 无连线 NSLog(@"AFNetworkReachability Not Reachable"); break; case AFNetworkReachabilityStatusReachableViaWWAN: // 手机自带网络 NSLog(@"AFNetworkReachability Reachable via WWAN"); break; case AFNetworkReachabilityStatusReachableViaWiFi: // WiFi NSLog(@"AFNetworkReachability Reachable via WiFi"); break; case AFNetworkReachabilityStatusUnknown: // 未知网络 default: NSLog(@"AFNetworkReachability Unknown"); break; } }]; // 开始监听 [self.reachabilityManager startMonitoring]; } return self; } @end
四、第三方框架使用
这个使用会更方便一点,有block和通知两种方式,且支持多线程,这里不再详细介绍,README.md有使用方法:
- (void)viewDidLoad {
[super viewDidLoad];
// Allocate a reachability object
Reachability* reach = [Reachability reachabilityWithHostname:@"www.bing.com"]; // Set the blocks reach.reachableBlock = ^(Reachability*reach) { // keep in mind this is called on a background thread // and if you are updating the UI it needs to happen // on the main thread, like this: dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"REACHABLE!"); }); }; reach.unreachableBlock = ^(Reachability*reach) { NSLog(@"UNREACHABLE!"); }; // Start the notifier, which will cause the reachability object to retain itself! [reach startNotifier]; }
问题解决
三种方式差不多,它们在检测设备是否连接局域网和连接方式时很灵敏,但是不能检测服务器是否可达。因为它们底层都是使用了SCNetworkReachability
,SCNetworkReachability
发送网络数据包到服务器,但它并不会确认服务器真的收到了此数据包。所以,如果我们想确认是否服务器可达,则需要发送一个真实的网络请求。或者我们使用socket编程,建立一个tcp链接来检测(三次握手成功),只要链接成功则服务器可达。这样只会发送tcpip的报头,数据量最小。如果网络环境差,connect
函数会阻塞,所以最后不要在主线程下,调用示例代码,示例如下:
#import <arpa/inet.h>
/// 服务器可达返回true
- (BOOL)socketReachabilityTest { // 客户端 AF_INET:ipv4 SOCK_STREAM:TCP链接 int socketNumber = socket(AF_INET, SOCK_STREAM, 0); // 配置服务器端套接字 struct sockaddr_in serverAddress; // 设置服务器ipv4 serverAddress.sin_family = AF_INET; // 百度的ip serverAddress.sin_addr.s_addr = inet_addr("202.108.22.5"); // 设置端口号,HTTP默认80端口 serverAddress.sin_port = htons(80); if (connect(socketNumber, (const struct sockaddr *)&serverAddress, sizeof(serverAddress)) == 0) { close(socketNumber); return true; } close(socketNumber);; return false; }