在上一篇博客中,我们分析了AFHTTPSessionManager
,以及它是如何实现GET/HEAD/PATCH/DELETE
相关接口的。
我们还剩下POST
相关接口没有分析,在这篇博客里面,我们就来分析一下POST
相关接口是如何实现的。
multipart/form-data请求
在继续理解POST接口之前,我们先来了解一下HTTP协议中和POST相关的multipart/form-data
请求。
关于multipart/form-data
请求的内容,大部分都来源于这篇博客:HTTP协议之multipart/form-data请求分析。
我们知道,根据HTTP 1.1的协议规定,我们的请求类型可以是GET, HEAD, PATCH, POST, DELETE, OPTIONS, TRANCE。 那么,什么是multipart/form-data请求
呢?
http协议大家都知道是规定了以ASCII码传输
,建立在tcp、ip
协议之上的应用层规范
。http协议的格式我们在上一篇博客中已经提及,分为 请求行
,请求头
,请求体
三部分。并且在我们发送请求时,可以附加上相关的参数
。对于GET/PUT
等方法,参数都是按照"key=vaule"
的格式附加到URL
中的,其中key
和vaule
都是ASCII的字符串。而当我们调用POST方法,将这些"key=vaule"
参数添加加到body中时,则需要在请求头
中指明Content-Type
是application/x-www-form-urlencoded
,并在body中写入这些参数,而不是附加在URL
中。
到目前为止,我所说的参数类型都是key=value格式
的,但如果我们想向服务器上传一个文件,那么这种key=value格式
显然是不太合适的。
为了解决向服务器上传文件及其他信息的需求,人们对POST请求
作出扩展:在POST请求
中,支持Content-Type:multipart/form-data
的请求。 为了区别与其他类型的POST请求,我们在这里可以先将这类POST请求称作:
multipart/form-data请求
。
multipart/form-data请求
:
- 请求行上与其他
POST请求
一致,需要写明POST方法,URL,协议类型。 - 但是在请求头中,需要加上下面的头信息:
Content-Type: multipart/form-data; boundary=${bound}
首先,它声明了请求体 body内容是符合multipart/form-data格式
。之后,指明body将会用到的分隔符boundary=${bound}
,${bound}
是我们指定的分隔符,用来分隔body的内容。 这个分隔符是可以任意自定义的,但是为了区别与body中的内容,我们都会将其定义为一个比较复杂的字符串,如--------------------56423498738365
。
- 设置完请求头后,接下来就是设置
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}--
上面是一个典型的multipart/form-data格式
的请求体。
其中${bound}为之前头信息中的分隔符
,如果头信息中规定为123,那么这里也要为123。
这个请求体是多个部分组成的:每一个部分都是以--分隔符
开始的,然后是该部分内容的描述信息Content-Disposition:
,如果传送的内容是一个文件的话,那么还会包含文件名信息,以及文件内容的类型。上面的第二个小部分其实是一个文件体的结构然后一个回车,然后是描述信息的具体内容
;
最后会以--分隔符--
结尾,表示请求体结束。
以上就是关于multipart/form-data请求
的概要知识。我们需要重点记忆的是form-data的body格式,在下面的代码分析中,我们会了解到,AFHTTPRequestSerializer是如何组装multipart/form-data请求
的body的。
AFHTTPSessionManager & POST
我们先来看一下AFHTTPSessionManager
提供的关于POST的接口:
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure DEPRECATED_ATTRIBUTE;
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure DEPRECATED_ATTRIBUTE;
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
headers:(nullable NSDictionary <NSString *, NSString *> *)headers
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure DEPRECATED_ATTRIBUTE;
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure DEPRECATED_ATTRIBUTE;
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
headers:(nullable NSDictionary <NSString *, NSString *> *)headers
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
接口比较多,一共6个,但其中的4个AF已经声明为废弃了。剩下的2个,才是AF所提供的POST接口。其余4个最终都会调用到这2个POST接口之一:
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
headers:(nullable NSDictionary <NSString *, NSString *> *)headers
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
headers:(nullable NSDictionary <NSString *, NSString *> *)headers
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
这两个接口的区别在于是否存在constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block参数
。
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
这个block参数,我们可以理解为AF的一个block回调,当AF在组装POST的form-data的body时,会回调到这个block,用户可以通过设置符合AFMultipartFormData协议
的formData
,将自己要上传的文件信息附加到formData
中,AF会将文件data添加的request 的body中。 具体是怎么做的,我们稍后会看到。
这样就是说,上面两个POST接口,一个不需要上传文件,而另一个需要上传文件。是否需要上传文件的POST接口的实现是不一样的。
我们先来看一下不需要上传文件的POST接口:
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
headers:(nullable NSDictionary <NSString *, NSString *> *)headers
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure
{
NSURLSessionDataTask *dataTask = [self dataTaskWithHTTPMethod:@"POST" URLString:URLString parameters:parameters headers:headers uploadProgress:uploadProgress downloadProgress:nil success:success failure:failure];
[dataTask resume];
return dataTask;
}
可以发现,它最终还是会调用AFHTTPSessionManager
的dataTaskWithHTTPMethod
方法。这和我们上一篇中介绍的GET/PUT
等方法的实现是一样的。沿着我们上一篇中介绍的脉络,就可以理解其实现。需要注意的是,与GET
等方法不同,最终POST的参数是写在body中的,其Content-Type: application/x-www-form-urlencoded
。这里就不再冗述。
我们重点来看一下上传文件版本的POST接口
的实现:
- (NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(id)parameters
headers:(NSDictionary<NSString *,NSString *> *)headers
constructingBodyWithBlock:(void (^)(id<AFMultipartFormData> _Nonnull))block
progress:(void (^)(NSProgress * _Nonnull))uploadProgress
success:(void (^)(NSURLSessionDataTask * _Nonnull, id _Nullable))success failure:(void (^)(NSURLSessionDataTask * _Nullable, NSError * _Nonnull))failure
{
NSError *serializationError = nil;
// 1. 用 AFHTTPRequestSerializer组装 multipart-Form 的body及相关的header,同时返回request
NSMutableURLRequest *request = [self.requestSerializer multipartFormRequestWithMethod:@"POST" URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters constructingBodyWithBlock:block error:&serializationError];
for (NSString *headerField in headers.keyEnumerator) {
[request addValue:headers[headerField] forHTTPHeaderField:headerField];
}
if (serializationError) {
if (failure) {
dispatch_async(self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(nil, serializationError);
});
}
return nil;
}
// 2. 对于multi-part form, 调用父类的upload task 方法,返回upload task
__block NSURLSessionDataTask *task = [self uploadTaskWithStreamedRequest:request progress:uploadProgress completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *error) {
if (error) {
if (failure) {
failure(task, error);
}
} else {
if (success) {
success(task, responseObject);
}
}
}];
[task resume];
// 3. 返回task
return task;
}
可以看到,POST接口
与之前介绍过的GET/PUT
等接口的实现类似,均是用三步来提供task:
- 调用
AFHTTPRequestSerializer
的相关接口来组装request
- 调用父类
AFURLSessionManager
的方法来返回task
- 调用
task resume
启动任务,并向外返回该task
与之前介绍的接口的不同之处在于2点,
- 对于
AFHTTPRequestSerializer
, POST请求调用的是multipartFormRequestWithMethod
而不是之前的requestWithMethod
方法。 - 调用的父类方法,不是返回的
NSURLSessionDataTask
,而是调用uploadTaskWithStreamedRequest
接口。从这里也可以看出,带有block回调的POST接口,是设计用来向服务器上传文件的。
multipartFormRequestWithMethod
让我们把目光移到AFHTTPSessionManager
的HTTP请求组装器AFHTTPRequestSerializer
中,来看一下它是怎么组装POST request的。
AFHTTPRequestSerializer
会调用multipartFormRequestWithMethod
来组装上传文件的POST请求:
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(method);
NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]); // 肯定不是GET 和 HEAD方法
// 1. 先获取request。 由于POST方法的parameters要用Form格式放在 body中,所以这里的 parameters 参数填写nil
NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];
// 2. 生成AFStreamingMultipartFormData对象,用来存储将会添加到POST body中的parameters
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
if (parameters) {
for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
NSData *data = nil;
if ([pair.value isKindOfClass:[NSData class]]) {
data = pair.value;
} else if ([pair.value isEqual:[NSNull null]]) {
data = [NSData data];
} else {
data = [[pair.value description] dataUsingEncoding:self.stringEncoding];
}
if (data) {
[formData appendPartWithFormData:data name:[pair.field description]];
}
}
}
// 如果有用户传入的 construct Body block, 则会调用。这里主要是向用户提供回调时机,让用户可以传入要添加到POST body的file data
if (block) {
block(formData);
}
// 3. 将formData 真正附加到request 的body中 并返回
return [formData requestByFinalizingMultipartFormData];
}
multipartFormRequestWithMethod
方法会分3个步骤来组装POST请求:
- 用
requestWithMethod
方法,来返回对应的POST request
。 - 生成
AFStreamingMultipartFormData
对象form data
,来存储要添加到POST request中的body 数据。 - 调用
form data
的requestByFinalizingMultipartFormData
方法,将form data附加到POST request中。
关于第1个步骤,我们在上一篇中已经分析过,不再多说。重点是第2,3步骤,POST的body data是如何生成的,body data又是如何附加到POST request中的。
Generate body data
AFHTTPRequestSerializer
是利用AFStreamingMultipartFormData
对象来生成body data的。从类的命名就可以看出,AFStreamingMultipartFormData
是通过数据流
来提供body data的(主要是要上传的file data)。
关于生成body data的代码摘抄出来如下:
// 2. 生成AFStreamingMultipartFormData对象,用来存储将会添加到POST body中的parameters
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
if (parameters) {
for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
NSData *data = nil;
if ([pair.value isKindOfClass:[NSData class]]) {
data = pair.value;
} else if ([pair.value isEqual:[NSNull null]]) {
data = [NSData data];
} else {
data = [[pair.value description] dataUsingEncoding:self.stringEncoding];
}
if (data) {
[formData appendPartWithFormData:data name:[pair.field description]];
}
}
}
// 如果有用户传入的 construct Body block, 则会调用。这里主要是向用户提供回调时机,让用户可以传入要添加到POST body的file data
if (block) {
block(formData);
}
上面的内容可以分为两部分:
(1) 生成AFStreamingMultipartFormData对象
,并传入parameters
(2)调用block
回调,让AFStreamingMultipartFormData对象
接受来自用户的文件data
。
我们先来看一下AFStreamingMultipartFormData对象
是如何生成的:
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
@interface AFStreamingMultipartFormData ()
@property (readwrite, nonatomic, copy) NSMutableURLRequest *request;
@property (readwrite, nonatomic, assign) NSStringEncoding stringEncoding;
@property (readwrite, nonatomic, copy) NSString *boundary;
@property (readwrite, nonatomic, strong) AFMultipartBodyStream *bodyStream;
@end
- (instancetype)initWithURLRequest:(NSMutableURLRequest *)urlRequest
stringEncoding:(NSStringEncoding)encoding
{
self = [super init];
if (!self) {
return nil;
}
self.request = urlRequest;
self.stringEncoding = encoding;
self.boundary = AFCreateMultipartFormBoundary();
self.bodyStream = [[AFMultipartBodyStream alloc] initWithStringEncoding:encoding];
return self;
}
form data对象
的初始化函数很简单,就是记录了从外界传入的参数:urlRequest和encoding
类型。同时,初始化了其成员AFMultipartBodyStream对象
。
其中,boundary
成员是form 的分隔符,是由AFCreateMultipartFormBoundary()
函数生成的一个随机字符串。
而AFMultipartBodyStream* bodyStream
,则用来记录POST的body data
。关于它是如何记录的,我们稍后会做分析。
知道了AFStreamingMultipartFormData form data对象
是如何生成的后,我们回过头来看一下参数是如何附加到form data中的:
if (parameters) {
for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
NSData *data = nil;
if ([pair.value isKindOfClass:[NSData class]]) {
data = pair.value;
} else if ([pair.value isEqual:[NSNull null]]) {
data = [NSData data];
} else {
data = [[pair.value description] dataUsingEncoding:self.stringEncoding];
}
if (data) {
[formData appendPartWithFormData:data name:[pair.field description]];
}
}
}
这里用到了上一篇博客中提到的AFQueryStringPairsFromDictionary
方法以及AFQueryStringPair
类型。然后,AF会将AFQueryStringPair
中存储的value转换为NSData
类型。
将vaule转换为NSData
类型后,调用form data的:
- (void)appendPartWithFormData:(NSData *)data
name:(NSString *)name
将data和其对应的key附加到form data中。
我们来看一下AFStreamingMultipartFormData
的appendPartWithFormData:name:
方法是如何实现的:
- (void)appendPartWithFormData:(NSData *)data
name:(NSString *)name
{
NSParameterAssert(name);
NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
[mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"", name] forKey:@"Content-Disposition"];
// 附加一个AFHTTPBodyPart
[self appendPartWithHeaders:mutableHeaders body:data];
}
form 首先会生成一个表示该data节点头的字典mutableHeaders,然后存入如下内容:
key: @"Content-Disposition" value:@"form-data; name=\"%@\"", name
然后,将header 和 data 组合起来,存储到AFHTTPBodyPart
中。
// 附加一个AFHTTPBodyPart
[self appendPartWithHeaders:mutableHeaders body:data];
- (void)appendPartWithHeaders:(NSDictionary *)headers
body:(NSData *)body
{
NSParameterAssert(body);
AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
bodyPart.stringEncoding = self.stringEncoding;
bodyPart.headers = headers;
bodyPart.boundary = self.boundary;
bodyPart.bodyContentLength = [body length];
bodyPart.body = body;
[self.bodyStream appendHTTPBodyPart:bodyPart];
}
传入的参数被form data转换为了对应的AFHTTPBodyPart
,然后,AFHTTPBodyPart
会被AFMultipartBodyStream *bodyStream
添加到其HTTPBodyPart
中。
OK,到这里,我们已经涉及到了好几个类的关系。我们现在先暂停一下,总结一下上面AFHTTPSessionManager
是如何为POST请求生成form data body的。上面涉及到的几个类的关系如下图:
通过上图,这几个类之间的关系会清楚许多。首先,我们要生成form data类型
的POST请求
,需要调用AFHTTPRequestSerializer
的相关方法,利用AFHTTPRequestSerializer
来生成对应的request,这个和其他请求GET/PUT
等是一样的。
而在AFHTTPRequestSerializer
, 会生成一个AFStreamingMultipartFormData
对象来存储所有POST 请求的body data
。
在AFStreamingMultipartFormData
的内部实现中,会针对每一个POST 请求
参数,生成一个AFHTTPBodyPart
对象,然后这些对象又会存储到其成员变量AFMultipartBodyStream对象
中。
如果细心的话,可以注意到,用于存储参数的AFMultipartBodyStream类
是继承自NSInputStream
的,这也就暗示了,最终将这些参数附加到request中时,是通过流的方式进行的,这对于上传大的文件,很有帮助。
对于各个类的实现细节,我们暂不去管,首先从整体上把握类之间的关系。在稍后的部分中,我们将会进一步分析类实现的细节。
上面是关于parameter的存储方式,如果用户需要上传文件的话,AF会调用block回调来给用户上传文件的时机:
// 如果有用户传入的 construct Body block, 则会调用。这里主要是向用户提供回调时机,让用户可以传入要添加到POST body的file data
if (block) {
block(formData);
}
这里的block定义是:
(void (^)(id <AFMultipartFormData> formData))block
这里的block会传入一个符合AFMultipartFormData协议
的对象, 这里传入的是AFStreamingMultipartFormData
对象。
这里有个小思考,为什么block的参数是一个协议类型,而不是具体的AFStreamingMultipartFormData
类型? 其实我们之间将block定义改写为:
(void (^)(AFStreamingMultipartFormData *formData))block
在逻辑上也是完全行得通的。但是,这会对外部对象过多的暴露AF
的实现细节,或者说和用户需求功能不相干的细节,也暴露给了用户,这样就对代码的误用留下了隐患,同时,对于用户的使用也造成了不必要的麻烦。
AF
在这里的处理是向外暴露一个AFMultipartFormData协议
,该协议只有和用户上传文件相关的接口,而屏蔽了AFStreamingMultipartFormData
中如boundary
,request
等无关的属性。
这就是通过协议
向外提供了一个窄接口
,屏蔽了无关的实现。这也是我们可以借鉴的一个面向对象编程的技巧。
我们来看一下AFMultipartFormData协议
都定义了那些接口:
@protocol AFMultipartFormData
// 将file data附加到form data中
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
error:(NSError * _Nullable __autoreleasing *)error;
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
error:(NSError * _Nullable __autoreleasing *)error;
- (void)appendPartWithInputStream:(nullable NSInputStream *)inputStream
name:(NSString *)name
fileName:(NSString *)fileName
length:(int64_t)length
mimeType:(NSString *)mimeType;
- (void)appendPartWithFileData:(NSData *)data
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType;
- (void)appendPartWithFormData:(NSData *)data
name:(NSString *)name;
// 将headers信息添加到form data中,并跟一个body data
- (void)appendPartWithHeaders:(nullable NSDictionary <NSString *, NSString *> *)headers
body:(NSData *)body;
// 考虑到3G带宽的限制,文件流可能会报错:"request body stream exhausted"。因此AF提供了一个可以设置包大小和延迟时间的接口。
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
delay:(NSTimeInterval)delay;
@end
可以看到,AFMultipartFormData协议
主要是提供了三个功能:
(1)用户上传文件data
(2)添加form 的headers
(3)设置form data的流 包大小和延迟时间。
我们先来看用户上传data相关的接口在AFStreamingMultipartFormData
中是如何实现的:
那其中一个接口做例子:
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
name:(NSString *)name
fileName:(NSString *)fileName
mimeType:(NSString *)mimeType
error:(NSError * __autoreleasing *)error
{
NSParameterAssert(fileURL);
NSParameterAssert(name);
NSParameterAssert(fileName);
NSParameterAssert(mimeType);
// 检测文件的相关属性,如果有错误,直接返回NO及error
if (![fileURL isFileURL]) {
NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"Expected URL to be a file URL", @"AFNetworking", nil)};
if (error) {
*error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo];
}
return NO;
} else if ([fileURL checkResourceIsReachableAndReturnError:error] == NO) {
NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"File URL not reachable.", @"AFNetworking", nil)};
if (error) {
*error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo];
}
return NO;
}
NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:error];
if (!fileAttributes) {
return NO;
}
// 组装form data form data中关于file的节点的头信息
NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
[mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
[mutableHeaders setValue:mimeType forKey:@"Content-Type"];
// 将头信息以及file data的相关信息存储为AFHTTPBodyPart.
AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
bodyPart.stringEncoding = self.stringEncoding;
bodyPart.headers = mutableHeaders;
bodyPart.boundary = self.boundary;
bodyPart.body = fileURL; // 注意AFHTTPBodyPart的body属性,是id类型,可以直接存储file URL,file data, 或NSInputStream
bodyPart.bodyContentLength = [fileAttributes[NSFileSize] unsignedLongLongValue];
[self.bodyStream appendHTTPBodyPart:bodyPart];
return YES;
}
其余的接口都大同小异,读者可以自行分析。
我们再来看一下AFStreamingMultipartFormData
中又如何设置包的大小和延迟时间,这里只是简单的记录下来:
- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
delay:(NSTimeInterval)delay
{
self.bodyStream.numberOfBytesInPacket = numberOfBytes;
self.bodyStream.delay = delay;
}
Append body data to POST Request
通过上面的分析,我们知道,form data是如何存储传入POST body的headers和file data信息的。这其中涉及到三个类: AFStreamingMultipartFormData
,AFMultipartBodyStream
,AFHTTPBodyPart
。
对于multi form data中的每一个节(被分隔符分割),都是对应一个AFHTTPBodyPart
,而所有的这些节,都被AFStreamingMultipartFormData
统一append 到AFMultipartBodyStream
中。
到目前为止,AFStreamingMultipartFormData
和POST request
还没有发生实质性关系,AFStreamingMultipartFormData
中仅是存储了相关body data,但还未将这些body data附加到request中。
当调用AFStreamingMultipartFormData
的requestByFinalizingMultipartFormData
时,会设置POST request,并将其中存储的POST form data信息附加到request 上:
return [formData requestByFinalizingMultipartFormData];
- (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
if ([self.bodyStream isEmpty]) {
return self.request;
}
// Reset the initial and final boundaries to ensure correct Content-Length
[self.bodyStream setInitialAndFinalBoundaries];
// 将request的body stream设置为self.bodyStream。使得POST request的body和self.bodyStream建立关联(AFMultipartBodyStream)
[self.request setHTTPBodyStream:self.bodyStream];
// 设置request 的请求头为:Content-Type:multipart/form-data; boundary=self.boundary, 表明request body是multi-part form类型
[self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary] forHTTPHeaderField:@"Content-Type"];
[self.request setValue:[NSString stringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"];
return self.request;
}
上面的逻辑比较好理解,重点是设置request的bodyStream:
[self.request setHTTPBodyStream:self.bodyStream];
当把self.bodyStream(AFMultipartBodyStream
)设置为request的body stream后,当request请求被发送时,会自动调用read方法:
- (NSInteger)read:(uint8_t *)buffer
maxLength:(NSUInteger)length
- (NSInteger)read:(uint8_t *)buffer
maxLength:(NSUInteger)length
{
// 如果当前的stream 没有打开,则直接返回0
if ([self streamStatus] == NSStreamStatusClosed) {
return 0;
}
NSInteger totalNumberOfBytesRead = 0;
while ((NSUInteger)totalNumberOfBytesRead < MIN(length, self.numberOfBytesInPacket)) { // 读取iOS stream指定的大小 或 用户设置的最小包大小 (以min为准)
if (!self.currentHTTPBodyPart || ![self.currentHTTPBodyPart hasBytesAvailable]) {
if (!(self.currentHTTPBodyPart = [self.HTTPBodyPartEnumerator nextObject])) { // update currentHTTPBodyPart
break;
}
} else {
NSUInteger maxLength = MIN(length, self.numberOfBytesInPacket) - (NSUInteger)totalNumberOfBytesRead; // 本次while循环可以读取的buffer 大小: 本次read:maxLength允许读取的最大字节数 - 已经读取的字节数
NSInteger numberOfBytesRead = [self.currentHTTPBodyPart read:&buffer[totalNumberOfBytesRead] maxLength:maxLength]; //读取数据到buffer中,并返回读取到的字节数
if (numberOfBytesRead == -1) { // 读取字节数等于-1 表示读取失败,退出循环
self.streamError = self.currentHTTPBodyPart.inputStream.streamError;
break;
} else {
totalNumberOfBytesRead += numberOfBytesRead; // 更新总的字节数
if (self.delay > 0.0f) { // 根据用户设置的延迟时间 ,休息一下
[NSThread sleepForTimeInterval:self.delay];
}
}
}
}
return totalNumberOfBytesRead; // 返回读取的总数据
}
在AFMultipartBodyStream
的read:maxLength
方法中,会依次遍历其存储的AFHTTPBodyPart
,并调用AFHTTPBodyPart
的read:maxLength
方法:
// AFHTTPBodyPart
- (NSInteger)read:(uint8_t *)buffer
maxLength:(NSUInteger)length
{
// 读取buffer, 并更新phase
NSInteger totalNumberOfBytesRead = 0;
// AFHTTPBodyPart 读取stream 时,会分为三个/四个阶段 对应了multipart form data的结构
// phase 1. 开头的分隔符
if (_phase == AFEncapsulationBoundaryPhase) {
NSData *encapsulationBoundaryData = [([self hasInitialBoundary] ? AFMultipartFormInitialBoundary(self.boundary) : AFMultipartFormEncapsulationBoundary(self.boundary)) dataUsingEncoding:self.stringEncoding];
totalNumberOfBytesRead += [self readData:encapsulationBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
}
// phase 2. form 节点的header信息
if (_phase == AFHeaderPhase) {
NSData *headersData = [[self stringForHeaders] dataUsingEncoding:self.stringEncoding];
totalNumberOfBytesRead += [self readData:headersData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
}
// phase 3. form body
if (_phase == AFBodyPhase) {
NSInteger numberOfBytesRead = 0;
numberOfBytesRead = [self.inputStream read:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
if (numberOfBytesRead == -1) {
return -1;
} else {
totalNumberOfBytesRead += numberOfBytesRead;
if ([self.inputStream streamStatus] >= NSStreamStatusAtEnd) {
[self transitionToNextPhase];
}
}
}
// phase 4. form 的结束符(如果是最后一个 form 节点才会有这个阶段)
if (_phase == AFFinalBoundaryPhase) {
NSData *closingBoundaryData = ([self hasFinalBoundary] ? [AFMultipartFormFinalBoundary(self.boundary) dataUsingEncoding:self.stringEncoding] : [NSData data]);
totalNumberOfBytesRead += [self readData:closingBoundaryData intoBuffer:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
}
return totalNumberOfBytesRead;
}
因为iOS每次input stream
读取的字节流大小并不能预知,因此,AFHTTPBodyPart
会用变量_phase
来记录body data
已经读取到了哪一个部分。当下次input stream
再来读取数据时,会按照当前的_phase
来读取AFHTTPBodyPart
的不同内容。
对于AFHTTPBodyPart
的内容读取,会调用AFHTTPBodyPart
的
- (NSInteger)readData:(NSData *)data intoBuffer:(uint8_t *)buffer maxLength:(NSUInteger)length
方法:
- (NSInteger)readData:(NSData *)data
intoBuffer:(uint8_t *)buffer
maxLength:(NSUInteger)length
{
NSRange range = NSMakeRange((NSUInteger)_phaseReadOffset, MIN([data length] - ((NSUInteger)_phaseReadOffset), length));
[data getBytes:buffer range:range];
_phaseReadOffset += range.length;
if (((NSUInteger)_phaseReadOffset) >= [data length]) {
[self transitionToNextPhase];
}
return (NSInteger)range.length;
}
在readData
方法中,会记录在当前阶段已经读取数据的偏移值_phaseReadOffset
。在下次读取时,会从偏移值的地方开始,读取data
剩余的数据。如果_phaseReadOffset >= [data length]
,则说明当前阶段的data
已经读取完毕,调用transitionToNextPhase
转入到下一个阶段。
当然,对于AFHTTPBodyPart
的 AFBodyPhase阶段
所对应的inputStream属性data,因为本身就是流,因此不用记录_phaseReadOffset
,而直接调用NSInputStream
的read:maxLength
方法即可。在AFBodyPhase阶段
,需要手动判断流状态,来转入到下一个阶段:
if ([self.inputStream streamStatus] >= NSStreamStatusAtEnd) {
[self transitionToNextPhase];
}
然我们在看一下,AFHTTPBodyPart
是如何转入到下一个阶段的:
- (BOOL)transitionToNextPhase {
if (![[NSThread currentThread] isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self transitionToNextPhase]; // transitionToNextPhase 必须在主线程调用
});
return YES;
}
// 根据当前阶段,转换到下一个阶段
switch (_phase) {
case AFEncapsulationBoundaryPhase:
_phase = AFHeaderPhase;
break;
case AFHeaderPhase:
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.inputStream open];
_phase = AFBodyPhase;
break;
case AFBodyPhase:
[self.inputStream close];
_phase = AFFinalBoundaryPhase;
break;
case AFFinalBoundaryPhase:
default:
_phase = AFEncapsulationBoundaryPhase; // 默认或初始化为 AFEncapsulationBoundaryPhase(包装分隔符) 阶段
break;
}
_phaseReadOffset = 0;
return YES;
}
在transitionToNextPhase方法中,转换阶段要做的事情多数时间很简单:
- 修改当前的_phase等于下一个阶段
_phase =nextPhase
- 清空当前阶段已经读取的偏移量
_phaseReadOffset = 0
;
这里需要注意的是两点,第一,transitionToNextPhase
会保证在main线程
调用:
if (![[NSThread currentThread] isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self transitionToNextPhase]; // transitionToNextPhase 必须在主线程调用
});
return YES;
}
第二,对于AFBodyPhase
,会将input stream
附加到main 线程的runloop
中,并打开流:
case AFHeaderPhase:
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.inputStream open];
_phase = AFBodyPhase;
break;
当AFBodyPhase
阶段结束,转入AFFinalBoundaryPhase
阶段前,需要将流关闭:
case AFBodyPhase:
[self.inputStream close];
_phase = AFFinalBoundaryPhase;
break;
上面就是POST 请求的multipart form data的组装过程。我们要留意的是NSInputStream的使用方式,即必须附加到runloop上:
[self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
这样做可以避免在没有数据可读时阻塞代理对象的操作。
总结
在这一篇博客中,我们了解了AFHTTPSessionManager
中关于POST接口的实现。同时,我们也了解了HTTP协议中,multipart form data的body格式。
至此,对于AFHTTPSessionManager
的分析也就告一段落。同时,我们也了解了AFHTTPRequestSerializer的大部分接口。
接下来,我们将会对AFHTTPRequestSerializer
剩下的接口进行分析,同时会了解reponse的解析类AFHTTPResponseSerializer
和AFJSONResponseSerializer
。