AFNetWorking源码之AFHTTPSessionManager

Posted by 黄成都 on 2017-04-20
Words 3.2k and Reading Time 14 Minutes
Viewed Times

1 概述

AFHTTPSessionManagerAFURLSessionManager的子类。我们可以通过这个类做HTTP请求。其实整个AFHTTPSessionManager逻辑很简单,只是用HTTP的方式拼接了请求,并且调用父类的方式做处理。我会通过AFHTTPSessionManagerapi来讲一下POST上传数据的几种基本格式,然后我再随便分析一下AFHTTPSessionManager

2 POST请求的常用格式

HTTP/1.1协议规定的HTTP请求方法有OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 这几种。其中POST一般用来向服务端提交数据,接下来要讨论POST提交数据的几种方式。协议规定POST提交的数据必须放在消息主体中,但协议并没有规定数据必须使用什么编码方式。实际上,开发者完全可以自己决定消息主体的格式,只要最后发送的 HTTP 请求满足上面的格式就可以。

但是,数据发送出去,还要服务端解析成功才有意义。一般服务端语言如php、python等,以及它们的framework,都内置了自动解析常见数据格式的功能。服务端通常是根据请求头(headers)中的Content-Type字段来获知请求中的消息主体是用何种方式编码,再对主体进行解析。所以说到POST提交数据方案,包含了Content-Type和消息主体编码方式两部分。

2.1 application/x-www-form-urlencoded格式的POST请求

这应该是最常见的 POST 提交数据的方式了。浏览器的原生表单,如果不设置enctype属性,那么最终就会以application/x-www-form-urlencoded方式提交数据。Content-Type被指定为application/x-www-form-urlencoded,提交的数据按照 key1=val1&key2=val2的方式进行编码,key和val都进行了URL转码。

下面这个请求是简书进入一篇文章页面的时候,会自动往服务器POST一个请求,估计是统计文章被阅读的次数等功能。具体看下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//发送的请求,删除了cookie相关的部分
POST /notes/e15592ce40ae/mark_viewed.json HTTP/1.1
Host: www.jianshu.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
X-CSRF-Token: vJvptva4Tqou/V3dd3nFCrcvRsb78FReHuIYZke5PVAnfR/tIAAMCfuaB2Z2/gaEohIZAsiEksUYyPqzg3DpSA==
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: http://www.jianshu.com/p/e15592ce40ae
Content-Length: 98
Connection: keep-alive
Cache-Control: max-age=0
//请求体

uuid=4e3abc0f-1824-4a5d-982f-7d9dee92d9cd&referrer=http%3A%2F%2Fwww.jianshu.com%2Fu%2Fad726ba6935d

AFHTTPSessionManager实现上面这个application/x-www-form-urlencoded请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc]initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSDictionary *params = @{
@"uuid":@"4e3abc0f-1824-4a5d-982f-7d9dee92d9cd",
@"referrer":@"http://www.jianshu.com/p/e15592ce40ae"
};
NSURLSessionDataTask *task = [manager POST:@"http://www.jianshu.com//notes/e15592ce40ae/mark_viewed.json" parameters:params progress:^(NSProgress * _Nonnull uploadProgress) {
NSLog(@"进度更新");
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"返回数据:%@",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"返回错误:%@",error);
}];
[task resume];

2.2 multipart/form-data格式的POST请求

Multipart/form-data的基础方法是POST , 也就是说是由POST方法来组合实现的.
Multipart/form-data与POST方法的不同之处在于请求头和请求体.
Multipart/form-data的请求头必须包含一个特殊的头信息 : Content-Type , 且其值也必须规定为multipart/form-data , 同时还需要规定一个内容分割符用于分割请求体中的多个POST的内容 , 如文件内容和文本内容自然需要分割开来 , 不然接收方就无法正常解析和还原这个文件了.
Multipart/form-data的请求体也是一个字符串 , 不过和post的请求体不同的是它的构造方式 , post是简单的name=value值连接 , 而Multipart/form-data则是添加了分隔符等内容的构造体.

请求的头部信息如下:

1
2
//其中xxxxx是我自定义的分隔符,每个人都可以选择自己的分隔符
Content-Type: multipart/form-data; boundary=xxxxx

下面我们来看一下一个我的Multipart/form-data请求体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
POST /uploadFile HTTP/1.1
Host: 这里是url,就不暴露了^_^
Content-Type: multipart/form-data; boundary=xxxxx
Connection: keep-alive
Accept: */*
User-Agent: AFNetWorking3.X%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/1 CFNetwork/808.2.16 Darwin/15.6.0
Content-Length: 32175
Accept-Language: en-us
Accept-Encoding: gzip, deflate

--xxxxx
Content-Disposition: form-data;name="file"

img.jpeg
--xxxxx
Content-Disposition: form-data;name="businessType"

CC_USER_CENTER
--xxxxx
Content-Disposition: form-data;name="fileType"

image
--xxxxx
Content-Disposition:form-data;name="file";filename="img1.jpeg"
Content-Type:image/png

这里是图片数据,太长了.我就删了

--xxxxx--

这个请求有三个参数file,businessType,fileType。比如file参数和他的值就通过如下格式传输:

1
2
3
4
--xxxxx
Content-Disposition: form-data;name="file"

img.jpeg

上面这种就是一个参数与之对应的值。协议规定的就是这个格式,没有为什么。我们可以看看图片数据部分:

1
2
3
4
5
6
7
--xxxxx
Content-Disposition:form-data;name="file";filename="img1.jpeg"
Content-Type:image/png

这里是图片数据,太长了.我就删了

--xxxxx--

其中name=”参数名” filename=”文件名” 其中参数名这个要和接收方那边相对应 正常开发中可以去问服务器那边 , 文件名是说在服务器端保存成文件的名字 , 这个参数然并卵 , 因为一般服务端会按照他们自己的要求去处理文件的存储.

下一行是指定类型 , 我这里示例中写的是PNG图片类型 , 这个可以根据你的实际需求的写。如果我们要上传多分图片或者文件,则只需要按照指定格式就可以了,比如下面就是上传两张图片的请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
POST /uploadFile HTTP/1.1
Host: 这里是url,就不暴露了^_^
Content-Type: multipart/form-data; boundary=xxxxx
Connection: keep-alive
Accept: */*
User-Agent: AFNetWorking3.X%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/1 CFNetwork/808.2.16 Darwin/15.6.0
Content-Length: 32175
Accept-Language: en-us
Accept-Encoding: gzip, deflate

--xxxxx
Content-Disposition: form-data;name="file"

img.jpeg
--xxxxx
Content-Disposition: form-data;name="businessType"

CC_USER_CENTER
--xxxxx
Content-Disposition: form-data;name="fileType"

image
--xxxxx
Content-Disposition:form-data;name="file";filename="img1.jpeg"
Content-Type:image/png

这里是图片1数据,太长了.我就删了
--xxxxx
Content-Disposition:form-data;name="file";filename="img2.jpeg"
Content-Type:image/png

这里是图片1数据,太长了.我就删了
--xxxxx--

下面是我Demo中一个multipart/form-data请求的实现代码,分别用NSRULDataTaskAFHTTPSessionManager实现,我们可以发现用第二种方法简便了很多,因为AFN已经帮我们做好了拼接工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
//方法一
- (IBAction)updatePic:(id)sender {
//请求头参数
NSDictionary *dic = @{
@"businessType":@"CC_USER_CENTER",
@"fileType":@"image",
@"file":@"img.jpeg"
};
//请求体图片数据
NSData *imageData = UIImagePNGRepresentation([UIImage imageNamed:@"1.png"]);
//创建request
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:[NSURL URLWithString:url]];
//post方法
[request setHTTPMethod:@"POST"];
// 设置请求头格式为Content-Type:multipart/form-data; boundary=xxxxx
//[request setValue:@"multipart/form-data; boundary=xxxxx" forHTTPHeaderField:@"Content-Type"];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc]initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSessionDataTask *task = [manager POST:url parameters:dic constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
//请求体里面的参数
NSDictionary *bodyDic = @{
@"Content-Disposition":@"form-data;name=\"file\";filename=\"img.jpeg\"",
@"Content-Type":@"image/png",
};
[formData appendPartWithHeaders:bodyDic body:imageData];
} progress:^(NSProgress * _Nonnull uploadProgress) {
NSLog(@"下载进度");
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"下载成功:%@",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"下载失败%@",error);
}];
[task resume];
}
//方法二
- (IBAction)multipartformPost2:(id)sender {
//参数
NSDictionary *dic = @{
@"businessType":@"CC_USER_CENTER",
@"fileType":@"image",
@"file":@"img.jpeg"
};
NSString *boundaryString = @"xxxxx";
NSMutableString *str = [NSMutableString string];
[dic enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[str appendFormat:@"--%@\r\n",boundaryString];
[str appendFormat:@"%@name=\"%@\"\r\n\r\n",@"Content-Disposition: form-data;",key];
[str appendFormat:@"%@\r\n",obj];
}];

NSMutableData *requestMutableData=[NSMutableData data];

[str appendFormat:@"--%@\r\n",boundaryString];
[str appendFormat:@"%@:%@",@"Content-Disposition",@"form-data;"];
[str appendFormat:@"%@=\"%@\";",@"name",@"file"];
[str appendFormat:@"%@=\"%@\"\r\n",@"filename",@"img1.jpeg"];
[str appendFormat:@"%@:%@\r\n\r\n",@"Content-Type",@"image/png"];
//转换成为二进制数据
[requestMutableData appendData:[str dataUsingEncoding:NSUTF8StringEncoding]];
NSData *imageData = UIImagePNGRepresentation([UIImage imageNamed:@"1.png"]);
//文件数据部分
[requestMutableData appendData:imageData];
//添加结尾boundary
[requestMutableData appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",boundaryString] dataUsingEncoding:NSUTF8StringEncoding]];
//创建一个请求对象
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:[NSURL URLWithString:url]];
//post方法
[request setHTTPMethod:@"POST"];
// 设置请求头格式为Content-Type:multipart/form-data; boundary=xxxxx
[request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",boundaryString] forHTTPHeaderField:@"Content-Type"];
//session
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURLSessionDataTask *task = [session uploadTaskWithRequest:request fromData:requestMutableData completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSString *result = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",result);
}];
[task resume];
}

Multipart/form-data格式的POST请求总结:

  • 文件类型参数中name="参数名"一定要和服务端对应, 开发的时候 , 可以问服务端人员,我这里是file
  • 上传文件的数据部分使用二进制数据(NSData)拼接。
  • 上边界部分和下边界部分的字符串 , 最后都要转换成二进制数据(NSData) , 和文件部分的二进制数据拼接在一起 , 作为请求体发送给服务器。
  • 每一行末尾需要有一定的`\r\n·。

2.3 application/json格式的POST请求

接下来我将常使用NSURLSessionDataTask做一个application/json的POST请求。并且请求体数据我存储在一个test.txt文件中,从文件中读取出来然后上传。

1
2
//test.txt文件内容
{"name":"huang","phone":"124"}

通过抓包软件我的请求如下,和其他POST请求原理一样,只是拼接请求体的方式不一样,并且更具不同格式的请求体,设置不同的Content-Type

1
2
3
4
5
6
7
8
9
10
11
POST /posts HTTP/1.1
Host: jsonplaceholder.typicode.com
Content-Type: application/json
Connection: keep-alive
Accept: application/json
User-Agent: AFNetWorking3.X%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/1 CFNetwork/808.2.16 Darwin/15.6.0
Content-Length: 31
Accept-Language: en-us
Accept-Encoding: gzip, deflate

{"name":"huang","phone":"124"}

下面是我Demo的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (IBAction)applicationjsonPOST2:(id)sender {
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://jsonplaceholder.typicode.com/posts"]];
//指请求体的类型。由于我们test.txt里面的文件是json格式的字符串。所以我这里指定为`application/json`
[request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request addValue:@"application/json" forHTTPHeaderField:@"Accept"];
[request setHTTPMethod:@"POST"];
[request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
[request setTimeoutInterval:20];
NSString *path = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"txt"];
NSURL *url = [NSURL URLWithString:[path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
//使用Block来处理返回数据
NSURLSessionDataTask *task = [session uploadTaskWithRequest:request fromFile:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSString *result = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",result);
}];
[task resume];
}

AFHTTPSessionManager分析

上面主要讲了对POST请求的分析,主要是AFHTTPSessionManager并没有多少逻辑,他主要是调用AFURLSessionManager的实现。另外就是通过baseURL改变了url的拼接过程。下面我就抽出他们的不同点分析一下:

1 首先多了一个属性

1
@property (nonatomic, strong) AFHTTPRequestSerializer <AFURLRequestSerialization> * requestSerializer;

这个属性的主要作用就是帮我们拼接请求头和请求体,从上面的Demo我们发现很多请求的拼接工作都通过requestSerializer处理了。如果我们不手动设置,默认是一个AFHTTPRequestSerializer对象。具体可以去初始化方法里面看到。

2 重写了securityPolicy这个属性的setter方法,增加对于SSLPinningMode的异常处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)setSecurityPolicy:(AFSecurityPolicy *)securityPolicy {
//增加对于SSLPinningMode的异常处理。
if (securityPolicy.SSLPinningMode != AFSSLPinningModeNone && ![self.baseURL.scheme isEqualToString:@"https"]) {
NSString *pinningMode = @"Unknown Pinning Mode";
switch (securityPolicy.SSLPinningMode) {
case AFSSLPinningModeNone: pinningMode = @"AFSSLPinningModeNone"; break;
case AFSSLPinningModeCertificate: pinningMode = @"AFSSLPinningModeCertificate"; break;
case AFSSLPinningModePublicKey: pinningMode = @"AFSSLPinningModePublicKey"; break;
}
NSString *reason = [NSString stringWithFormat:@"A security policy configured with `%@` can only be applied on a manager with a secure base URL (i.e. https)", pinningMode];
@throw [NSException exceptionWithName:@"Invalid Security Policy" reason:reason userInfo:nil];
}
//调用`AFURLSessionManager`的`securityPolicy`属性的setter方法。
[super setSecurityPolicy:securityPolicy];
}

3 NSCopying和NSSecureCoding协议的实现过程

NSCopyingNSSecureCoding协议的实现过程添加了对requestSerializer,responseSerializer,securityPolicy这三个属性的复制。也就是说,用copy方法复制的manager,这三个属性的配置跟着一起复制。而父类AFURSSessionManager只实现了对configuration的复制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
+ (BOOL)supportsSecureCoding {
return YES;
}
- (instancetype)initWithCoder:(NSCoder *)decoder {
NSURL *baseURL = [decoder decodeObjectOfClass:[NSURL class] forKey:NSStringFromSelector(@selector(baseURL))];
//获取当前manager的NSURLSessionConfiguration
NSURLSessionConfiguration *configuration = [decoder decodeObjectOfClass:[NSURLSessionConfiguration class] forKey:@"sessionConfiguration"];
if (!configuration) {
NSString *configurationIdentifier = [decoder decodeObjectOfClass:[NSString class] forKey:@"identifier"];
if (configurationIdentifier) {
//iOS7和iOS8初始化NSURLSessionConfiguration方法不一样。所以要分开处理
#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 1100)
configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:configurationIdentifier];
#else
configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:configurationIdentifier];
#endif
}
}
//初始化一个新的manager
self = [self initWithBaseURL:baseURL sessionConfiguration:configuration];
if (!self) {
return nil;
}
//添加了对`requestSerializer`,`responseSerializer`,`securityPolicy`这三个属性的接档。
self.requestSerializer = [decoder decodeObjectOfClass:[AFHTTPRequestSerializer class] forKey:NSStringFromSelector(@selector(requestSerializer))];
self.responseSerializer = [decoder decodeObjectOfClass:[AFHTTPResponseSerializer class] forKey:NSStringFromSelector(@selector(responseSerializer))];
AFSecurityPolicy *decodedPolicy = [decoder decodeObjectOfClass:[AFSecurityPolicy class] forKey:NSStringFromSelector(@selector(securityPolicy))];
if (decodedPolicy) {
self.securityPolicy = decodedPolicy;
}
return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
[super encodeWithCoder:coder];
//添加对baseURL属性的归档
[coder encodeObject:self.baseURL forKey:NSStringFromSelector(@selector(baseURL))];
if ([self.session.configuration conformsToProtocol:@protocol(NSCoding)]) {
[coder encodeObject:self.session.configuration forKey:@"sessionConfiguration"];
} else {
[coder encodeObject:self.session.configuration.identifier forKey:@"identifier"];
}
//添加了对`requestSerializer`,`responseSerializer`,`securityPolicy`这三个属性的归档。
[coder encodeObject:self.requestSerializer forKey:NSStringFromSelector(@selector(requestSerializer))];
[coder encodeObject:self.responseSerializer forKey:NSStringFromSelector(@selector(responseSerializer))];
[coder encodeObject:self.securityPolicy forKey:NSStringFromSelector(@selector(securityPolicy))];
}

#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone *)zone {
AFHTTPSessionManager *HTTPClient = [[[self class] allocWithZone:zone] initWithBaseURL:self.baseURL sessionConfiguration:self.session.configuration];
//添加了对`requestSerializer`,`responseSerializer`,`securityPolicy`这三个属性的复制。
HTTPClient.requestSerializer = [self.requestSerializer copyWithZone:zone];
HTTPClient.responseSerializer = [self.responseSerializer copyWithZone:zone];
HTTPClient.securityPolicy = [self.securityPolicy copyWithZone:zone];
return HTTPClient;
}

4 HEAD和PUT等方法的实现

我在这里不准备深入讲这两个方法是如何实现的,因为AFHTTPSessionManager主要通过他的requestSerializer属性来实现对HEADPUT等请求的拼接。我准备分析AFHTTPRequestSerializer的时候再看这一块是如何实现的。

1
2
 //通过requestSerializer属性来拼接request对象。
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];

最后原文地址,demo地址


...

...

00:00
00:00