前一篇文章讲解了requestSerialization中关于参数格式化,http headers设置,timeout等属性设置的内容。本文来剖析请求序列化过程中另一个重要的部分: multipart post 请求。multipart所有的内容都是围绕着AFStreamingMultipartFormData类,AFMultipartBodyStream类,AFMultipartFormData协议。
0 涉及到类的关系
在bang's blog中他梳理的很清楚了:
通过constructingBodyWithBlock向使用者提供了一个AFStreamingMultipartFormData对象,调这个对象的几种append方法就可以添加不同类型的数据,包括FileURL/NSData/NSInputStream,AFStreamingMultipartFormData内部把这些append的数据转成不同类型的AFHTTPBodyPart,添加到自定义的AFMultipartBodyStream里。最后把AFMultipartBodyStream赋给原来NSMutableURLRequest的bodyStream。NSURLConnection发送请求时会读取这个bodyStream,在读取数据时会调用这个bodyStream的-read:maxLength:方法,AFMultipartBodyStream重写了这个方法,不断读取之前append进来的AFHTTPBodyPart数据直到读完。
AFHTTPBodyPart封装了各部分数据的组装和读取,一个AFHTTPBodyPart就是一个数据块。实际上三种类型(FileURL/NSData/NSInputStream)的数据在AFHTTPBodyPart都转成NSInputStream,读取数据时只需读这个inputStream。inputStream只保存了数据的实体,没有包括分隔符和头部,AFHTTPBodyPart是边读取变拼接数据,用一个状态机确定现在数据读取到哪一部份,以及保存这个状态下已被读取的字节数,以此定位要读的数据位置,详见AFHTTPBodyPart的-read:maxLength:方法。
AFMultipartBodyStream封装了整个multipart数据的读取,主要是根据读取的位置确定现在要读哪一个AFHTTPBodyPart。AFStreamingMultipartFormData对外提供友好的append接口,并把构造好的AFMultipartBodyStream赋回给NSMutableURLRequest,关系大致如下图:
1 multipart请求基础知识
requestSerializer中另外一个重要的部分是构建multipart请求,关于multipart的基础知识可以参考:http://stackoverflow.com/questions/16958448/what-is-http-multipart-request
简单来说,multipart请求有如下几个特点:
- multipart/form-data的基础方法是post,也就是说是由post方法来组合实现的
- multipart/form-data与post方法的不同之处:请求头,请求体。
- multipart/form-data的请求头必须包含一个特殊的头信息:Content-Type,且其值也必须规定为multipart/form-data,同时还需要规定一个内容分割符用于分割请求体中的多个post的内容。例如:
Content-Type: multipart/form-data; boundary=XXXXXX}
引用stackoverflow上面的一个实例:
POST /cgi-bin/qtest HTTP/1.1
Host: aram
User-Agent: Mozilla/5.0 Gecko/2009042316 Firefox/3.0.10
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Referer: http://aram/~martind/banner.htm
Content-Type: multipart/form-data; boundary=----------287032381131322
Content-Length: 514
------------287032381131322
Content-Disposition: form-data; name="name"
brownfeng
------------287032381131322
Content-Disposition: form-data; name="gifpic"; filename="g.gif"
Content-Type: image/gif
GIF87a.............,...........D..;
------------287032381131322
Content-Disposition: form-data; name="textfile"; filename="text.txt"
Content-Type: text/plain
… contents of text.txt …;
------------287032381131322--
以上表示数据name=brown,和一个gif图片以及一个txt文件,filename是文件名, … contents of text.txt …
以及GIF87a.............,...........D..
是文件实体内容。分隔符----------287032381131322
是可以自定义的,写在HTTP头部里:Content-type: multipart/form-data, boundary=----------287032381131322
每一个部分都有自己的头部,表明这部分的数据类型以及其他一些参数,例如文件名,普通字段的key,content-Type等。最后一个分隔符会多加两横,表示数据已经结束:------------287032381131322--
一般而言,构造Multipart里的数据,按照上述格式拼接数据。写入NSURLRequest的http body字段即可,但是当发送文件过大,这种方式就可使用了。第二种方法是在临时文件拼接,加入格式化的内容,然后将零时文件的file stream设置成request 的http body stream。
AFNetworking中,并非使用上述两种方法,是边拼数据边上传数据。
2 multipart post的调用栈
AFNetwoking官方用例中,调用方法如下:
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSDictionary *parameters = @{@"foo": @"bar"};
NSURL *filePath = [NSURL fileURLWithPath:@"file://path/to/image.png"];
[manager POST:@"http://example.com/resources.json" parameters:parameters constructingBodyWithBlock:^(id formData) {
[formData appendPartWithFileURL:filePath name:@"image" error:nil];
} success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"Success: %@", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"Error: %@", error);
}];
在constructingBodyWithBlock中,将只需要传入filePathurl,以及name即可。manager的POST方法中,会调用requestSerialization的以下方法创建request。其中会通过AFStreamingMultipartFormData以及其配套方法完成multi part body 的组装,并设置到request中。
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(NSDictionary *)parameters
constructingBodyWithBlock:(void (^)(id formData))block
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(method);
NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]);
NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];
//传入request参数,创建AFStreamingMultipartFormData对象
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
//如果有parameters值,则转化成data,并写入formData
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];
}
//向formData中加入普通参数
if (data) {
[formData appendPartWithFormData:data name:[pair.field description]];
}
}
}
//向formData中添加其他的内容-appendPartWithFileURL:name:error: 等方法
if (block) {
block(formData);
}
//返回最终完成的以后的request
return [formData requestByFinalizingMultipartFormData];
}
3 AFStreamingMultipartFormData类以及AFMultipartFormData protocol
所有关于multipart的方法都与核心类AFStreamingMultipartFormData有关,它最重要的属性就是body part的分隔符boundary,以及AFMultipartBodyStream *bodyStream
,同时它会遵守AFMultipartFormData协议,该协议是为AFHTTPRequestSerializer -multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:
方法中的block内的添加data准备的,传入到formdata的输出可以包括FileURL/NSData/NSInputStream。协议里面的appendXXXXX方法将传入的formdata数据转成不同类型的部分(AFNetworking用AFHTTPBodyPart类来表示)。
@interface AFStreamingMultipartFormData : NSObject
- (instancetype)initWithURLRequest:(NSMutableURLRequest *)urlRequest
stringEncoding:(NSStringEncoding)encoding;
- (NSMutableURLRequest *)requestByFinalizingMultipartFormData;
@end
@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
在AFStreamingMultipartFormData的init方法中,会随机生成一定格式的multipart boundary属性,另一个重点是初始化bodyStream属性。
- (id)initWithURLRequest:(NSMutableURLRequest *)urlRequest
stringEncoding:(NSStringEncoding)encoding
{
...
self.request = urlRequest;
self.stringEncoding = encoding;
self.boundary = AFCreateMultipartFormBoundary();
//初始化bodyStream
self.bodyStream = [[AFMultipartBodyStream alloc] initWithStringEncoding:encoding];
return self;
}
//生成boundary的函数
static NSString * AFCreateMultipartFormBoundary() {
return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()];
}
协议的内容是将NSData,NSFile等数据添加到AFStreamingMultipartFormData的bodyStream中。
@protocol AFMultipartFormData
//Appends the HTTP header `Content-Disposition: file; filename=#{generated filename}; name=#{name}"` and `Content-Type: #{generated mimeType}`, followed by the encoded file data and the multipart form boundary.
//The filename and MIME type for this data in the form will be automatically generated, using the last path component of the `fileURL` and system associated MIME type for the `fileURL` extension, respectively.
- (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);
//一定要是fileURL
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) {//reachable
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;
}
//向header dict中加入[Content-Disposition:form-data; name="%@"; filename="%@"],注意转意
NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
[mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
//向header dict中加入[Content-Type:mimeType]
[mutableHeaders setValue:mimeType forKey:@"Content-Type"];
//创建AFHTTPBodyPart对象,代表一个bodypart,并设置其重要属性
AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
bodyPart.stringEncoding = self.stringEncoding;
bodyPart.headers = mutableHeaders;
bodyPart.boundary = self.boundary;
bodyPart.body = fileURL;
bodyPart.bodyContentLength = [fileAttributes[NSFileSize] unsignedLongLongValue];
//加入到body stream中
[self.bodyStream appendHTTPBodyPart:bodyPart];
return YES;
}
对AFStreamingMultipartFormData以及AFMultipartFormData protocol的总结:
- init AFStreamingMultipartFormData时候传入request,encoding,boundary以及bodyStream
- 调用AFMultipartFormData协议实现appendXXX方法:
- 根据传入的类型--file,data等等--添加headers的Content-Disposition、MIME-Type.
- 创建AFHTTPBodyPart临时对象
- 将AFHTTPBodyPart临时对象加入到AFStreamingMultipartFormData对象的bodyStream中
- 调用block中完成同样的事情,将part内容加入Formdata bodystream
- 调用 [formData requestByFinalizingMultipartFormData]返回组装完成的request
4 AFMultipartBodyStream的调用使用
对于iOS中流概念理解比较薄弱可以先看 apple 官方文档: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Streams/Streams.html
直接引用 bang's blog:
NSURLRequest的setHTTPBodyStream接受的是一个NSInputStream*参数,那我们要自定义inputStream的话,创建一个NSInputStream的子类传给它是不是就可以了?实际上不行,这样做后用NSURLRequest发出请求会导致crash,提示[xx _scheduleInCFRunLoop:forMode:]: unrecognized selector。
这是因为NSURLRequest实际上接受的不是NSInputStream对象,而是CoreFoundation的CFReadStreamRef对象,因为CFReadStreamRef和NSInputStream是toll-free bridged,可以自由转换,但CFReadStreamRef会用到CFStreamScheduleWithRunLoop这个方法,当它调用到这个方法时,object-c的toll-free bridging机制会调用object-c对象NSInputStream的相应函数,这里就调用到了_scheduleInCFRunLoop:forMode:,若不实现这个方法就会crash。详见这篇文章。
5 Request 中bodyStream内容设置完成以后的工作
在前面multipartFormRequstWithMethod...方法最后会调用return [formData requestByFinalizingMultipartFormData]
, 这个方法的源码如下
- (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
if ([self.bodyStream isEmpty]) {
return self.request;
}
// 设置bodyStream中的初始和结束boundaries,设置 request的body stream
[self.bodyStream setInitialAndFinalBoundaries];
[self.request setHTTPBodyStream:self.bodyStream];
//设置request header 'Content-Type' 和 'Content-Length'字段
[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;
}
最后完整的代码解析
可以查看 bangs blog: http://blog.cnbang.net/tech/2371/
·