iOS 网络编程


iOS 网络编程

本章内容概述

  • 检测网络状态
  • 使用NSURLConnection从网络获取数据
  • 使用NSMutableURLRequest向服务器发送数据
  • JSON数据解析
  • XML数据解析
  • 使用开源框架ASIHttpRequest实现网络编程

24.1 检测网络状态

客户端程序完全依赖网络,如果没有网络基本上程序就无法运行。因为数据都在服务器上,客户端只是数据的展现,这样就需要在程序启动时检测网络状态,甚至在程序运行时实时检测网络状态。

苹果官方帮助文档中提供了一个例程,该程序详细演示了如何检测网络状态。该项目的名称是Reachability,在本书的源码可以找到。在项目中的核心类是Reachability.h和Reachability.m,在我们自己的项目中使用时,直接添加这两个文件,并添加SystemConfiguration.framework框架即可。

Reachability类的内容如下:

#import <Foundation/Foundation.h>

#import <SystemConfiguration/SystemConfiguration.h>

typedef enum {

 NotReachable = 0,

 ReachableViaWiFi,

 ReachableViaWWAN

} NetworkStatus;

#define kReachabilityChangedNotification @"kNetworkReachabilityChangedNotification"

@interface Reachability: NSObject

{

 BOOL localWiFiRef;

 SCNetworkReachabilityRef reachabilityRef;

}

//reachabilityWithHostName- Use to check the reachability of a particular host name.

+ (Reachability*) reachabilityWithHostName: (NSString*) hostName;

//reachabilityWithAddress- Use to check the reachability of a particular IP address.

+ (Reachability*) reachabilityWithAddress: (const struct sockaddr\_in*) hostAddress;

//reachabilityForInternetConnection- checks whether the default route is available.

//  Should be used by applications that do not connect to a particular host

+ (Reachability*) reachabilityForInternetConnection;

//reachabilityForLocalWiFi- checks whether a local wifi connection is available.

+ (Reachability*) reachabilityForLocalWiFi;

//Start listening for reachability notifications on the current run loop

- (BOOL) startNotifier;

- (void) stopNotifier;

- (NetworkStatus) currentReachabilityStatus;

//WWAN may be available, but not active until a connection has been established.

//WiFi may require a connection for VPN on Demand.

- (BOOL) connectionRequired;

@end

使用该类的步骤是:

  1. 使用+ (Reachability) reachabilityWithHostName: (NSString) hostName;方法检测某个网络URL是否可以连接。例如,http://www.baidu.com。或者使用+ (Reachability) reachabilityForLocalWiFi;方法检测wifi的连接状况,或者使用+ (Reachability) reachabilityForInternetConnection;方法检测内网状态。

  2. 根据这些方法的返回值,调用- (NetworkStatus) currentReachabilityStatus;方法,判断网络状态,网络状态值有三个:NotReachable = 0,不能连接,ReachableViaWiFi,可以连接wifi,ReachableViaWWAN,可以通过移动网络连接,例如,GSM、3G等。

  3. 要实时检测网络状态需要向通知中心添加一个通知,然后调用startNotifier启动通知。

[[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(reachabilityChanged:) name: kReachabilityChangedNotification object: nil];

[hostReach startNotifier];

24.2 使用NSURLConnection从网络获取数据

可以使用NSURLConnection类从网络获取数据,通过该类可以发出同步请求和可以发出异步请求。发出的请求被封装在NSURLRequest对象中,而构建该对象还需要NSURL对象。下面通过一个案例来演示如何发出一个异步请求从网络上获得数据。该项目在xib界面上添加一个按钮和一个UIWebView组件,点击按钮发出请求,将请求数据显示在UIWebView中,这里我们获得苹果官方网站的首页面。地址是 http://www.apple.com 现步骤如下:

  1. 创建一个项目,在xib中添加一个按钮和一个UIWebView组件,并添加按钮的点击事件方法和UIWebView属性。
#import <UIKit/UIKit.h>

@interface AmakerViewController : UIViewController

// UIWebView属性

@property (strong, nonatomic) IBOutlet UIWebView *myWebView;

// 按钮点击方法

- (IBAction)get:(id)sender;

@end
  1. 实现按钮点击事件方法,定义要访问的网站地址,根据该地址创建NSURL对象,根据该对象再创建NSURLRequest对象,创建一个NSOperationQueue对象。使用NSURLConnection的静态方法+ (void)sendAsynchronousRequest:(NSURLRequest )request queue:(NSOperationQueue )queue completionHandler:(void (^)(NSURLResponse, NSData, NSError*))handler方法发出异步请求,在Block回调方法中获得返回的数据,并显示在UIWebView中。
- (IBAction)get:(id)sender {

    // 访问网络地址

    NSString *str = @"http://www.apple.com/";

    // 实例化NSURL对象

    NSURL *url = [NSURL URLWithString:str];

    // 实例化NSURLRequest对象

    NSURLRequest *request = [NSURLRequest requestWithURL:url];

    // 实例化操作队列

    NSOperationQueue *queue = [[NSOperationQueue alloc]init];

    // 发出异步请求

    [NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData * data, NSError *error) {

        // 获得网络抓取的数据

        if ([data length] >0 &amp;&amp; error == nil){

            // 将NSData转换为NSString

            NSString *html = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];

            // 使用UIWebView显示网页

            [self.myWebView loadHTMLString:html baseURL:nil];

            NSLog(@"HTML = %@", html);

        }

    }];

}
  1. 程序运行结果如下所示。

图24.1 使用NSURLConnection从网络异步获得数据

另外,也可以使用NSURLConnection的同步方法+ (NSData )sendSynchronousRequest:(NSURLRequest )request returningResponse:(NSURLResponse )response error:(NSError )error向服务器发出同步请求。使用同步请求的时候不多,如果真的使用同步请求也是在单独的一个线程中使用,这样可以避免程序的阻塞。下面代码是一个在另外线程中发出同步请求的代码片段。

// 使用GCD

dispatch\_queue\_t queue= dispatch\_get\_global\_queue(DISPATCH\_QUEUE\_PRIORITY\_DEFAULT, 0);

dispatch\_async(queue, ^{

    NSString *str  = @"http://www.apple.com";

    NSURL *url = [NSURL URLWithString:str];

    NSURLRequest *request = [NSURLRequest requestWithURL:url];

    NSURLResponse *response;

    NSError *error;

    NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&amp;response error:&amp;error];

    if ([data length]>0&amp;&amp;error==nil) {

        NSString *str = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];

        NSLog(@"%@",str);

    }

});

24.3 使用NSMutableURLRequest向服务器发送数据

NSMutableURLRequest根据名称可以明白,该请求是一个可变请求,可以附加更多请求信息。例如,向服务器发送数据,例如,在登陆中需要向服务器发送用户名称和密码。下面通过一个案例来演示NSMutableURLRequest的用法,这里是获得新浪的天气预报信息,使用NSMutableURLRequest的setURL方法设置url、使用setTimeoutInterval方法设置请求超时时间、使用setHTTPMethod设置请求方法、使用setHTTPBody设置请求参数。实现步骤如下:

  1. 创建一个项目,在xib文件中添加一个按钮和一个UIWebView组件,并在头文件中添加按钮单击实现方法和UIWebView属性。
@interface AmakerViewController : UIViewController

// 请求丰富

- (IBAction)get:(id)sender;

// UIWebView 属性

@property (strong, nonatomic) IBOutlet UIWebView *myWebView;

@end
  1. 实现按钮点击事件方法,创建另外一个线程,在Block中异步获得网络数据,创建NSMutableURLRequest对象,并设置请求方法、body、url等属性。并将网络返回结果在UIWebView中展示。
- (IBAction)get:(id)sender {

    // 创建另外一个线程

    dispatch\_queue\_t queue = dispatch\_get\_global\_queue(DISPATCH\_QUEUE\_PRIORITY\_DEFAULT, 0);

    // 执行异步任务

    dispatch\_async(queue, ^(void){

        // http://php.weather.sina.com.cn/xml.php?city=%B1%B1%BE%A9&amp;password=DJOYnieT8234jlsK&amp;day=0

        // 在Bock中从网络获取数据

        NSString *str = @"http://php.weather.sina.com.cn/xml.php";

        // 创建NSURL对象

        // ?w=2151330&amp;u=c

        NSURL *url = [NSURL URLWithString:str];

        // 可变URL请求

        NSMutableURLRequest *mRequest = [[NSMutableURLRequest alloc]init];

        //为请求设置URL

        [mRequest setURL:url];

        // 设置请求超时时间

        [mRequest setTimeoutInterval:10];

        // 设置请求方法为post

        [mRequest setHTTPMethod:@"POST"];

        // body 内容

        NSString *body = @"city=%B1%B1%BE%A9&amp;password=DJOYnieT8234jlsK&amp;day=0";

        // 设置body

        [mRequest setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];

       // NSLog(@"test=%@",[mRequest]);

        // 获得响应

        NSURLResponse *response;

        NSError *error;

        // 获得返回数据

        NSData *data = [NSURLConnection sendSynchronousRequest: mRequest returningResponse:&amp;response error:&amp;error];

        // 将NSData转换为字符串

        NSString *content = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];

        // 使用UIWebView加载数据

        [self.myWebView loadHTMLString:content baseURL:nil];

        // 输出结果

        NSLog(@"content=%@",content);

    });

}
  1. 程序运行结果如下图所示。

24.2 使用NSMutableURLRequest向服务器发送数据

24.4 JSON数据解析

在网络编程中,从服务器获取的数据格式大致分为两种:JSON和XML。其中JSON作为一种轻量级的数据交换格式,正在逐步取代xml,成为网络数据的通用格式。

从IOS5开始,APPLE提供了对json的原生支持(NSJSONSerialization),但是为了兼容以前的ios版本,可以使用第三方库来解析Json。

第三方的库有如下几种:

TouchJson包下载: http://toxicsoftware.com/cocoajson/

SBJson 包下载: http://superloopy.io/json-framework/

JSONKit包下载:https://github.com/johnezang/JSONKit

本节我们通过使用SBJson库和Apple自带的NSJSONSerialization来演示JSON的解析过程。首先使用Apple自带的NSJSONSerialization来解析JSON。这里我们调用的是一个天气预报api,http://m.weather.com.cn/data/101010100.html,该api返回的结果是:

{"weatherinfo":{"city":"北京","city\_en":"beijing","date\_y":"2013年4月19日","date":"","week":"星期五","fchh":"11","cityid":"101010100","temp1":"6℃~3℃","temp2":"16℃~5℃","temp3":"18℃~7℃","temp4":"20℃~10℃","temp5":"23℃~11℃","temp6":"23℃~9℃","tempF1":"42.8℉~37.4℉","tempF2":"60.8℉~41℉","tempF3":"64.4℉~44.6℉","tempF4":"68℉~50℉","tempF5":"73.4℉~51.8℉","tempF6":"73.4℉~48.2℉","weather1":"小雨转阴","weather2":"多云转晴","weather3":"晴转多云","weather4":"多云","weather5":"多云","weather6":"多云转晴","img1":"7","img2":"2","img3":"1","img4":"0","img5":"0","img6":"1","img7":"1","img8":"99","img9":"1","img10":"99","img11":"1","img12":"0","img\_single":"7","img\_title1":"小雨","img\_title2":"阴","img\_title3":"多云","img\_title4":"晴","img\_title5":"晴","img\_title6":"多云","img\_title7":"多云","img\_title8":"多云","img\_title9":"多云","img\_title10":"多云","img\_title11":"多云","img\_title12":"晴","img\_title\_single":"小雨","wind1":"微风","wind2":"微风","wind3":"微风","wind4":"微风","wind5":"微风","wind6":"微风","fx1":"微风","fx2":"微风","fl1":"小于3级","fl2":"小于3级","fl3":"小于3级","fl4":"小于3级","fl5":"小于3级","fl6":"小于3级","index":"冷","index\_d":"天气冷,建议着棉服、羽绒服、皮夹克加羊毛衫等冬季服装。年老体弱者宜着厚棉衣、冬大衣或厚羽绒服。","index48":"冷","index48\_d":"天气冷,建议着棉服、羽绒服、皮夹克加羊毛衫等冬季服装。年老体弱者宜着厚棉衣、冬大衣或厚羽绒服。","index\_uv":"最弱","index48\_uv":"弱","index\_xc":"不宜","index\_tr":"适宜","index\_co":"较舒适","st1":"2","st2":"1","st3":"17","st4":"6","st5":"18","st6":"7","index\_cl":"较不宜","index\_ls":"不宜","index\_ag":"易发"}}

通过返回结果我们看到,一般JSON返回的是一个数组,数组中嵌套一个字典,我们可以遍历数组,获得NSDictionary,再通过key获得value。下面我们看一下创建该案例的步骤:

  1. 创建一个项目,在界面上添加一个按钮,并添加按钮的单击事件方法。
#import <UIKit/UIKit.h>

#define weatherInfo @"weatherinfo"

@interface AmakerViewController : UIViewController

// 单击事件方法

- (IBAction)parse:(id)sender;

// 线程方法

-(void)action1;

@end
  1. 在按钮点击事件中另外启动一个线程,在该线程中调用action1方法,在该方法中获得网络数据,将网络数据NSData数据格式通过NSJSONSerialization的JSONObjectWithData方法转换为字典。并遍历字典。
// 使用系统自带库

- (IBAction)parse:(id)sender {

    // 创建另外一个线程

    NSThread *t = [[NSThread alloc]initWithTarget:self selector:@selector(action1) object:nil];

    // 启动线程

    [t start];

}

-(void)action1{

    // 天气预报地址

    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://m.weather.com.cn/data/101010100.html"]];

    // 获得数据

    NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];

    // 将NSData转换为NSString

    NSString *str = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];

    NSLog(@"str=%@",str);

    // 将NSData转换为NSDictionary

    NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];

    // 获得可以为weatherinfo的字典

    NSDictionary *dic2 = [dic objectForKey:weatherInfo];

    // 遍历字典

    for (NSString *key in dic2) {

        NSLog(@"%@:%@",key,[dic2 objectForKey:key]);

    }

}
  1. 程序运行结果如下图所示。

24.3 使用NSJSONSerialization解析JSON数据

另外,可以使用第三方的库来实现JSON的解析,这里使用SBJson。实现步骤如下:

  1. 下载SBJson,并添加到项目中。下载地址为:https://github.com/stig/json-framework/tags

  2. 在界面上添加一个按钮,并添加单击事件方法,在另外一个线程中,从网络获得数据,实例化SBJsonParser,调用objectWithString方法解析数据。

// 启动另外一个线程

- (IBAction)parse2:(id)sender {

    NSThread *t = [[NSThread alloc]initWithTarget:self selector:@selector(action2) object:nil];

    [t start];

}

-(void)action2{

    // 天气预报地址

    NSURL *url = [NSURL URLWithString:@"http://m.weather.com.cn/data/101010100.html"];

    // 通过url直接获得字符串内容

    NSString *jsonString = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil];

    // 实例化json解析类SBJsonParser

    SBJsonParser *parser2 = [[SBJsonParser alloc]init];

    // 调用解析方法,转换为字典

    NSDictionary *dic = [parser2 objectWithString:jsonString];

    // 解析

    NSDictionary *dic2 = [dic objectForKey:weatherInfo];

    // 遍历

    for (NSString *key in dic2) {

        NSLog(@"%@:%@",key,[dic2 objectForKey:key]);

    }

}

24.5 XML数据解析

在上一节我讲述了JSON数据格式的解析,本节将讲述另外一种常用的数据格式XML文件。可以通过苹果自带的API NSXMLParser和其代理类NSXMLParserDelegate来解析XML。

其中NSXMLParserDelegate代理类中有一组方法来检测文档的读取状态,例如,文档开始、文档结束、元素开始、元素结束、内容开始等。解析文件的基本思路是,在元素开始时创建一个空对象,找到内容时,读取内容并为对象的属性赋值,元素结束时将对象添加到集合当中。

下面通过一个案例来演示如何解析XML文件。步骤如下:

  1. 在项目中创建一个xml文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>

<customers>

    <customer>

        <id>1</id>

        <name>tom</name>

        <age>20</age>

    </customer>

    <customer>

        <id>2</id>

        <name>kite</name>

        <age>21</age>

    </customer>

    <customer>

        <id>3</id>

        <name>rose</name>

        <age>22</age>

    </customer>

</customers>
  1. 创建一个Customer类。
@interface Customer : NSObject

// 客户id

@property(nonatomic)int cid;

// 客户姓名

@property(nonatomic,retain)NSString *name;

// 客户年龄

@property(nonatomic)int age;

@end
  1. 在界面上添加一个按钮,并在头文件中,添加单击事件方法、Customer属性、NSXMLParser属性、NSMutableArray属性并实现NSXMLParserDelegate代理协议。
#import <UIKit/UIKit.h>

#import "Customer.h"

@interface AmakerViewController : UIViewController<NSXMLParserDelegate>

- (IBAction)parse:(id)sender;

// 客户实例

@property(nonatomic,retain)Customer *customer;

// xml解析器

@property(nonatomic,retain)NSXMLParser *parser;

// 可变数组

@property(nonatomic,retain)NSMutableArray *array;

// 当前元素

@property(nonatomic,strong)NSString *currentElement;

@end
  1. 在viewDidLoad方法中,初始化数组,用来容纳客户对象,获得客户xml文件url路径,将客户xml转换为NSData,实例化XML解析器,并为其设置代理。
- (void)viewDidLoad

{

    [super viewDidLoad];

    // 初始化数组,用来容纳客户对象

    self.array = [NSMutableArray arrayWithCapacity:10];

    // 获得客户xml文件url路径

    NSString  *str = [[NSBundle mainBundle]pathForResource:@"customers" ofType:@"xml"];

    // 将客户xml转换为NSData

    NSData *data = [[NSData alloc]initWithContentsOfFile:str];

    // 实例化XML解析器

    self.parser = [[NSXMLParser alloc]initWithData:data];

    // 并设置代理

    self.parser.delegate = self;

}
  1. 实现协议的开始读文档和结束读文档方法。
// 开始文档

- (void)parserDidStartDocument:(NSXMLParser *)parser{

    NSLog(@"parserDidStartDocument...");

}

// 结束文档

- (void)parserDidEndDocument:(NSXMLParser *)parser{

    NSLog(@"parserDidEndDocument...");

}
  1. 实现协议的开始读取元素方法,创建客户实例。
// 开始元素

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict{

    NSLog(@"didStartElement...%@",elementName);

    self.currentElement = elementName;

    if ([self.currentElement isEqualToString:@"customer"]) {

         self.customer = [[Customer alloc]init];

    }

}
  1. 在协议的找到内容的方法中,获得各个元素内容,并为对象属性赋值。
// 找到内容

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{

     NSLog(@"foundCharacters...%@",string);

    if ([self.currentElement isEqualToString:@"id"]) {

        int cid = [string integerValue];

        [customer setCid:cid];

    }else if([self.currentElement isEqualToString:@"name"]){

        [customer setName:string];

    }else if([self.currentElement isEqualToString:@"age"]){

        int age = [string integerValue];

        [customer setAge:age];

    }

}
  1. 在协议的结束元素中,将客户对象添加到数组中。
// 结束元素

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName{

    NSLog(@"didEndElement...");

    if ([elementName isEqualToString:@"customer"]) {

        [self.array addObject:customer];

    }

 self.currentElement=nil;

}
  1. 实现解析方法parse,删除数组中的所有元素,调用parse方法开始解析。
- (IBAction)parse:(id)sender {

    // 删除数组中的所有数据

    [self.array removeAllObjects];

    // 开始解析

    [self.parser parse];

    // 获得数组大小

    NSInteger count = [self.array count];

    NSLog(@"count=%d",count);

}
  1. 程序运行结果如下。

24.4 解析XML数据

24.6 使用开源框架ASIHttpRequest实现网络编程

ASIHttpRequest是iOS网络开发中最优秀的框架,虽然我们可以使用Apple自己的网络api但是功能很限,不够强大。相反ASIHttpRequest框架简单易用,而且功能强大,例如,可以使用同步请求、异步请求、使用队列、可以实现文件的上传和下载以及显示上传下载进度等。

ASIHttpRequest的官方站点是:http://allseeing-i.com/ASIHTTPRequest/ 。这里包括了下载地址、项目搭建和开发文档。ASIHttpRequest框架的下载地址是:http://github.com/pokeb/asi-http-request/tarball/master 。点击该连接可以直接下载该框架。

下面我们来看如何搭建开发框架,步骤如下:

  1. 创建一个项目,添加如下框架,CFNetwork, SystemConfiguration, MobileCoreServices, CoreGraphics 和libz。如下图所示。

24.5 搭建ASIHttpRequest框架,添加其他框架

  1. 从下载的框架中添加如下文件到自己的项目中。

24.6 搭建ASIHttpRequest框架,添加需要的文件

  1. 当前框架并不支持arc,如果我们的项目使用了arc,则需要设置如下编译选项,设置这些类不使用arc。

24.7 搭建ASIHttpRequest框架,设置编译选项

  1. 重新编译整个项目,如果没有报错,则项目配置成功。

该框架的更多详细用法可以通过如下连接获得帮助:http://allseeing-i.com/ASIHTTPRequest/How-to-use