AFNetworking 全解析之 AFURLRequestSerialization

概览

RequestSerilization 是AFNetwroking中对网络请求中request这个概率的封装。它的原型其实是NSURLRequest,将NSURLRequest进行第二次封装,将许多诸如请求头,请求参数格式化, multipar/form data文件上传等进行了简化处理。
总结来说,使用AFURLRequestSerializer有以下几个优点:
1、自动处理的请求参数转义,以及对不同请求方式自动对请求参数进行格式化。
2、实现了multipart/form-data方式的请求。
3、自动处理了User-Agent,Language等请求头。

使用方法

AFURLRequestSerializtion在AF框架中是封装请求这一部分对象的,作为AFHTTPSessionManaager的一个属性被使用。
如:

 ///    request data parse
manager.requestSerializer = [AFHTTPRequestSerializer serializer];
manager.requestSerializer.timeoutInterval = 30.f;

如果上传时使用的是json格式数据,那么使用AFJSONRequestSerializer:

 manager.requestSerializer = [AFJSONRequestSerializer serializer];

原来存在于NSURLRequest对象的属性,都可以该对象使用如:

[manager.requestSerializer setValue:@"application/json" forHTTPHeaderField:@"accept"];

AFURLRequestSerialization中所涉及的疑难知识点

URL Percent Escape (URL 百分比转议)

URL中的字符只能是ascii字符,非ascii字符如果需要出现在url中,则必须进行转义,每一个非ascii字符都会被替换成”%hh”的形式,hh为两位16进制数,对应该字符在iso-8859-1字符集里面的编码。这个过程即url转议,或者叫百分比转义, Percent Escape

在我们的ios中,通常在url中遇到有非ascii字符的情况时,一般是直接使用stringByAddingPercentEscapesUsingEncoding:方法进行转义,这时会将每个汉字都转换成相应的unicode编码对应的3个%形式。然而使用这种方法进行转义却有一些问题,因为我们的url参数很有可能包含&, ?这样的字符,而stringByAddingPercentEscapesUsingEncoding:并不会对它们进行转义,这样会导致最终获得的编码后的url与预期不符。
举个栗子:
比如urlString = @“汉字&ss”;
转义后就会变为: @“%E6%B1%89%E5%AD%97&ss"
然而显然,我们需要将&符号也进行转义掉。

那么,如何解决这个问题呢?ios7以后,stringByAddingPercentEncodingWithAllowedCharacters:方法,这个方法会对字符串进行更彻底的转义,但需要传递一个参数:一个字符集,处于这个字符集中的字符不会被转义。
如: [queryWord stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet letterCharacterSet]]
当然,如果你将url 直接使用上述方法进行转义,那么问题有来了,本来作为分隔符的&以及?也都会被转义掉。

AFNetwork 的并不会将url进行转义,而是将parameters参数中的各个组件分别进行转议,然后再进行拼接。
代码:

 - (NSString *)URLEncodedStringValue {
    if (!self.value || [self.value isEqual:[NSNull null]]) {
        return AFPercentEscapedStringFromString([self.field description]);
    } else {
        return [NSString stringWithFormat:@"%@=%@", AFPercentEscapedStringFromString([self.field description]), AFPercentEscapedStringFromString([self.value description])];
    }
}

函数AFPercentEscapedStringFromString 用于将一个字符串进行转义。static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@“;static NSString * const kAFCharactersSubDelimitersToEncode = @"!$ 定义了不需要被转义字符集,然后从URLQueryAllowedCharacterSet移除这些字符集。 实际在进行转义时,该函数并不是直接使用 string stringByAddingPercentEncodingWithAllowedCharacters 进行转义,而是使用rangeOfComposedCharacterSequencesForRange 来算出该自符串下每一个字的位置,然后对每一个字进行转义,再拼接到一起。

    range = [string rangeOfComposedCharacterSequencesForRange:range];

    NSString *substring = [string substringWithRange:range];
    NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
    [escaped appendString:encoded];

这样做的原因是,我们平常书写的字符, 并不全部都是用唯一的一个16位字符来表示, 而是有一部分用两个16位字符来表示, 这就是surrogate pairs的概念. 如果还是用上面的方法遍历字符串, 就会出现”断字”。

URL QueryStringFormat url请求的参数格式

我们发送一个请求,无论是get,还是post,发送出去的请求体都是一个字符串。那么这个字符串如何表示数组、字典等格式呢?
假设我们发出去的请求参数为 {“name”: “jack”, “specialty”: [“Guitar”, “Coding”], “families”: {“wife”: “rose”, “son”: “jacky"}}
那么实际发出去的请求参数为: name=jack&specialty[]=Cuitar&specialty[]=Coding&families[wife]=rose&families[son]=jacky,
即 数组参数为: key[]=value1&key[]=value2的格式,
字典参数为: key[subkey] = value的方式
具体实现可参照 函数AFQueryStringPairsFromKeyAndValue

HTTP Basic Authentication (http基本认证)

http 认证是基于质询/回应的,即我们在NSURLSession代理中常见到的challenge。
最简单的认证方式为基本认证,它的密码是通过明文来传递的。
基本认证步骤:

 1. 客户端访问一个受http基本认证保护的资源。
 2. 服务器返回401状态,要求客户端提供用户名和密码进行认证。
       401 Unauthorized
       WWW-Authenticate: Basic realm="WallyWorld"
 3. 客户端将输入的用户名密码用Base64进行编码后,采用非加密的明文方式传送给服务器。
       Authorization: Basic xxxxxxxxxx.
 4. 如果认证成功,则返回相应的资源。如果认证失败,则仍返回401状态,要求重新进行认证。

摘要认证:服务器端以nonce进行质询,客户端以用户名,密码,nonce,HTTP方法,请求的URI等信息为基础产生的response信息进行认证的方式。
摘要认证步骤:

 1. 客户端访问一个受http摘要认证保护的资源。
 2. 服务器返回401状态以及nonce等信息,要求客户端进行认证。

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Digest
realm="[email protected]",
qop="auth,auth-int",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
opaque="5ccc069c403ebaf9f0171e9517f40e41"

 3. 客户端将以用户名,密码,nonce值,HTTP方法, 和被请求的URI为校验值基础而加密(默认为MD5算法)的摘要信息返回给服务器。
       认证必须的五个情报:

     ・ realm : 响应中包含信息
     ・ nonce : 响应中包含信息
     ・ username : 用户名
     ・ digest-uri : 请求的URI
     ・ response : 以上面四个信息加上密码信息,使用MD5算法得出的字符串。

Authorization: Digest
username="Mufasa",  ← 客户端已知信息
realm="[email protected]",   ← 服务器端质询响应信息
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",  ← 服务器端质询响应信息
uri="/dir/index.html", ← 客户端已知信息
qop=auth,   ← 服务器端质询响应信息
nc=00000001, ← 客户端计算出的信息
cnonce="0a4f113b", ← 客户端计算出的客户端nonce
response="6629fae49393a05397450978507c4ef1", ← 最终的摘要信息 ha3
opaque="5ccc069c403ebaf9f0171e9517f40e41"  ← 服务器端质询响应信息

 4. 如果认证成功,则返回相应的资源。如果认证失败,则仍返回401状态,要求重新进行认证。

HTTP Multipart Form Request

在进行http请求的时候,尤其文件上传的时候,我们常常会使用mutlipart-form-data这个请求类型,那么,什么是multipart-form-data Request呢?
根据标准的http协议,我们的请求只能是OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE这几种。http协议是以ASCII码传输,建立在tcp, ip协议之上的应用层规范,http请求被分为了三个部分:状态行、请求头、请求体。
实际上,原始的http请求是不支持什么multipart或者www-form-urlencoded的,而所有的这些类型,实际上是对http请求体的一次封装。
multipart/form-data是这样的一种请求类型:它基于post方法,它的头信息中必须包含Content-Type=multipart/form-data。同时它的请求头中包含一个分隔符,将请求体分隔开来。
具体的请求头如下:Content-Type: multipart/form-data; boundary=${bound}
这里的${bound}是一个自定义的分隔符。
multipart/form-data的请求体也是一个字符串,不过与post方法不同,post的请求体是简单的key=value值联接,而multipart/form-data则添加了分隔符来将请求体分隔成不同的部分,每一个部分均为一个请求域,如:

      --${bound}

Content-Disposition: form-data; name="Filename"

HTTP.pdf
--${bound}
Content-Disposition: form-data; name="file000"; filename="HTTP协议详解.pdf"
Content-Type: application/octet-stream

%PDF-1.5
file content
%%EOF

--${bound}
Content-Disposition: form-data; name="Upload"

Submit Query
--${bound}--
其中,${bound}为之前头信息中的分隔符。每一个域,均以—${bound}起头,最后以—${bound}—结尾。每个域均包含以下信息: Content-Disposition:form-data;name=xxx;...
以及Content-Type:...

AFURLRequestSerialization类结构图

以下为AFURLRequestSerialization及相关类的UML图。

AFNetworking 全解析之 AFURLRequestSerialization_第1张图片

从图中可以看出,该组件中的主要类为AFURLRequestSerialization,该类在运行过程中使用到的类则有AFQueryStringPair和AFStreamingMultipartFormData。
AFQueryStringPair类封装了请求过程中所用到的键值对参数,一般的post方法、Get方法,均使用这个类来组建请求参数。
AFStreamingMultipartFormData继承于接口AFMultipartFormData,用于组件multipart/form-data类型请求的参数。而AFStreamingMultipartFormData类中的主要成员则是AFMultipartBodyStream,它代表用于生成multipart/form-data请求体的一个流。该对象又使用一个mutableArray来维护一系列的AFHTTPBodyPart对象,该对象表示一个multipart/form-data请求体中的每一个请求域。

几个关键过程分析

NSURLRequest参数和HTTPHeader参数的传递

AFHTTPRequestSerializer对象实质上是对NSURLRequest对象的封装,然而实际上我们最后使用的,仍然是NSURLRequest对象。AFHTTPRequestSerializer对象将大部分NSURLRequest对象所需要用到的属性导出,并使用KVO的方式统一进行处理,最终生成NSURLRequest时,这些更改的属性会被重新赋于NSURLRequest对象中。
这段代码注册了这些属性的变动通知。

for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
    if ([self respondsToSelector:NSSelectorFromString(keyPath)]) {
        [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext];
    }
}

每当相应的属性发生变动,AFHTTPRequestSerilizer对角便宜该属性名保存起来:

- (void)observeValueForKeyPath:(NSString *)keyPath
                  ofObject:(__unused id)object
                    change:(NSDictionary *)change
                   context:(void *)context
{
    if (context == AFHTTPRequestSerializerObserverContext) {
        if ([change[NSKeyValueChangeNewKey] isEqual:[NSNull null]]) {
            [self.mutableObservedChangedKeyPaths removeObject:keyPath];
        } else {
            [self.mutableObservedChangedKeyPaths addObject:keyPath];
        }
    }
}

然后在生成NSURLRequest对象过程中,将这些变更过的属性值进行赋值:

for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
    if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
        [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
    }
}

此外,请求的header值也会在生成NSURLRequest对象时被赋值

[self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
    if (![request valueForHTTPHeaderField:field]) {
        [mutableRequest setValue:value forHTTPHeaderField:field];
    }
}];

GET、POST请求参数的过滤及组建

我们使用AFHTTPRequestSerializer对象时,请求参数均是通过requestWithMethod:URLString:parameters:方法传递过来,然后在requestBySerializingRequest:withParameters:error:方法中进行组建。
AFURLRequestSerializer对象首先提供了一个queryStringSerialization回调,用于将请求参数组合成一个查询字符串。

if (self.queryStringSerialization) {
        NSError *serializationError;
        query = self.queryStringSerialization(request, parameters, &serializationError);

        if (serializationError) {
            if (error) {
                *error = serializationError;
            }

            return nil;
        }
    }

如果没有这个回调,那么该对象会使用函数AFQueryStringFromParameters(NSDictionary *parameters)来组件这个查询字符串。

NSString * AFQueryStringFromParameters(NSDictionary *parameters) {
    NSMutableArray *mutablePairs = [NSMutableArray array];
    for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
    [mutablePairs addObject:[pair URLEncodedStringValue]];
    }

    return [mutablePairs componentsJoinedByString:@"&"];
}

最终这个查询字符串query会被传递给request对象,或者放入请求体中,或者拼接到url之后。

当然,如果是JSON请求格式,即使用的是AFJSONRequestSerializer,那么会直接将请求参数转为JSON格式并放入请求体中。

你可能感兴趣的:(afnetworking)