您的位置:首页 > 理论基础 > 计算机网络

iOS开源库解析之ASIHTTPRequest

2015-11-26 13:49 627 查看
坚持 成长 每日一篇

前言

ASIHTTPRequest框架作者已经停更,今日公司ASIHTTPRequest从项目移除需要重构使用了ASIHTTPRequest的代码,要想重构ASIHTTPRequest最好对ASIHTTPRequest有所了解。今日花了2天时间看了ASIHTTPRequest源码,以此博客作为一些理解纪录

内容

ASIHTTPRequest和ASIFormDataRequest的关系

FormDataRequest是继承了httpRequest,他主要是针HTTP协议四种常见的POST方式的multipart/form-data请求和application/x-www-form-urlencoded请求。四种常见的请求详情http://www.aikaiyuan.com/6324.html

对于ASIHTTPRequest大家并不陌生,类似于iOS系统自带的request,这里主要介绍ASIFormDataRequest原理

ASIFormDataRequest特有属性

postData:用来保存非文件的数据

fileData:用来保存文件的数据

debugBodyString:只是用于纪录组件HttpPostBody过程错误日志信息,没有其他作用,设置Body时候并没有用他

postFormat:这是一个枚举属性,决定post是以form-data格式还是url encode格式上传数据

stringEncoding:对于DebugBodyString转data时候采用的是何种编码。

ASIFormDataRequest.m实现文件里面的主要方法解释

三种设置PostData情况

普通文本类型

//普通文本类型最终会在这里加入所有的数据
- (void)addPostValue:(id <NSObject>)value forKey:(NSString *)key
{
if (!key) {
return;
}
if (![self postData]) {
[self setPostData:[NSMutableArray array]];
}
NSMutableDictionary *keyValuePair = [NSMutableDictionary dictionaryWithCapacity:2];
[keyValuePair setValue:key forKey:@"key"];
[keyValuePair setValue:[value description] forKey:@"value"];
[[self postData] addObject:keyValuePair];
}
//此方法最终调用上面的addPostValue:forKey:来给postData添加字典,postData在构建PostBody时候会被用到
- (void)setPostValue:(id <NSObject>)value forKey:(NSString *)key
{
// Remove any existing value
NSUInteger i;
for (i=0; i<[[self postData] count]; i++) {
NSDictionary *val = [[self postData] objectAtIndex:i];
if ([[val objectForKey:@"key"] isEqualToString:key]) {
[[self postData] removeObjectAtIndex:i];
i--;
}
}
[self addPostValue:value forKey:key];
}


本地文件路径,最终添加是数据到fileData

//无论你是addFileValue还是setFileValue最终都会调用这个方法来组织文件数据
- (void)addFile:(NSString *)filePath withFileName:(NSString *)fileName andContentType:(NSString *)contentType forKey:(NSString *)key
{
BOOL isDirectory = NO;
BOOL fileExists = [[[[NSFileManager alloc] init] autorelease] fileExistsAtPath:filePath isDirectory:&isDirectory];
if (!fileExists || isDirectory) {
[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIInternalErrorWhileBuildingRequestType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"No file exists at %@",filePath],NSLocalizedDescriptionKey,nil]]];
}

// If the caller didn't specify a custom file name, we'll use the file name of the file we were passed
if (!fileName) {
fileName = [filePath lastPathComponent];
}

// If we were given the path to a file, and the user didn't specify a mime type, we can detect it from the file extension
if (!contentType) {
contentType = [ASIHTTPRequest mimeTypeForFileAtPath:filePath];
}
[self addData:filePath withFileName:fileName andContentType:contentType forKey:key];
}


最后一种就是用于如果你手中已经有了文件数据,可以通过下列方法组织到fileData里面,包括上面介绍的file方法最终也是屌用此方法来

- (void)addData:(id)data withFileName:(NSString *)fileName andContentType:(NSString *)contentType forKey:(NSString *)key
{
if (![self fileData]) {
[self setFileData:[NSMutableArray array]];
}
if (!contentType) {
contentType = @"application/octet-stream";
}

NSMutableDictionary *fileInfo = [NSMutableDictionary dictionaryWithCapacity:4];
[fileInfo setValue:key forKey:@"key"];
[fileInfo setValue:fileName forKey:@"fileName"];
[fileInfo setValue:contentType forKey:@"contentType"];
[fileInfo setValue:data forKey:@"data"];

[[self fileData] addObject:fileInfo];
}


小结:上面介绍的三种方法都是收集一些组建PostData时候用的数据,并没有真正二进制数据存在内存,只有我们掉了ASIFormDataRequest.m的- (void)buildPostBody方法时候才真正开始组织数据

//这是一个重写了父类及ASIHTTPRequest类的buildPostBody
- (void)buildPostBody
{
if ([self haveBuiltPostBody]) {
return;
}

#if DEBUG_FORM_DATA_REQUEST
[self setDebugBodyString:@""];
#endif
//如果用户都没有设置postData和fileData则调用父类的buildPostBody
//注意父类的build方法会根据是否含有postBodydata来设置http的Method(请求方式是post还是get)
if (![self postData] && ![self fileData]) {
[super buildPostBody];
return;
}
//如果要上传的数据存在文件数据,则设置“需要从磁盘读取数据的标志”为YES
if ([[self fileData] count] > 0) {
[self setShouldStreamPostDataFromDisk:YES];
}

//判断以何种post方式组织bodyData,只有multipart和Application两种的一种,默认采用的是Application
if ([self postFormat] == ASIURLEncodedPostFormat) {
//下面会细看buildURLEncodedPostBody和buildMultipartFormDataPostBody
[self buildURLEncodedPostBody];
} else {
[self buildMultipartFormDataPostBody];
}

//调用父类及ASIHTTPRequest类的buildPostBody
[super buildPostBody];

#if DEBUG_FORM_DATA_REQUEST
ASI_DEBUG_LOG(@"%@",[self debugBodyString]);
[self setDebugBodyString:nil];
#endif
}


buildURLEncodedPostBody和buildMultipartFormDataPostBody方法:

buildURLEncodedPostBody源码如下:

- (void)buildURLEncodedPostBody
{

// We can't post binary data using application/x-www-form-urlencoded
/*这里如果存在fileData会强制转换为multipart的post格式*/
if ([[self fileData] count] > 0) {
[self setPostFormat:ASIMultipartFormDataPostFormat];
[self buildMultipartFormDataPostBody];
return;
}

#if DEBUG_FORM_DATA_REQUEST
[self addToDebugBody:@"\r\n==== Building an application/x-www-form-urlencoded body ====\r\n"];
#endif

NSString *charset = (NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding([self stringEncoding]));

[self addRequestHeader:@"Content-Type" value:[NSString stringWithFormat:@"application/x-www-form-urlencoded; charset=%@",charset]];

NSUInteger i=0;
NSUInteger count = [[self postData] count]-1;
for (NSDictionary *val in [self postData]) {
NSString *data = [NSString stringWithFormat:@"%@=%@%@", [self encodeURL:[val objectForKey:@"key"]], [self encodeURL:[val objectForKey:@"value"]],(i<count ?  @"&" : @"")];
[self appendPostString:data];
i++;
}
#if DEBUG_FORM_DATA_REQUEST
[self addToDebugBody:@"\r\n==== End of application/x-www-form-urlencoded body ====\r\n"];
#endif
}


buildMultipartFormDataPostBody源码如下:

- (void)buildMultipartFormDataPostBody
{
#if DEBUG_FORM_DATA_REQUEST
[self addToDebugBody:@"\r\n==== Building a multipart/form-data body ====\r\n"];
#endif

NSString *charset = (NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding([self stringEncoding]));

// We don't bother to check if post data contains the boundary, since it's pretty unlikely that it does.
CFUUIDRef uuid = CFUUIDCreate(nil);
NSString *uuidString = [(NSString*)CFUUIDCreateString(nil, uuid) autorelease];
CFRelease(uuid);
NSString *stringBoundary = [NSString stringWithFormat:@"0xKhTmLbOuNdArY-%@",uuidString];

[self addRequestHeader:@"Content-Type" value:[NSString stringWithFormat:@"multipart/form-data; charset=%@; boundary=%@", charset, stringBoundary]];

[self appendPostString:[NSString stringWithFormat:@"--%@\r\n",stringBoundary]];

// Adds post data
NSString *endItemBoundary = [NSString stringWithFormat:@"\r\n--%@\r\n",stringBoundary];
NSUInteger i=0;
for (NSDictionary *val in [self postData]) {
[self appendPostString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n",[val objectForKey:@"key"]]];
[self appendPostString:[val objectForKey:@"value"]];
i++;
if (i != [[self postData] count] || [[self fileData] count] > 0) { //Only add the boundary if this is not the last item in the post body
[self appendPostString:endItemBoundary];
}
}

// Adds files to upload
i=0;
for (NSDictionary *val in [self fileData]) {

[self appendPostString:[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n", [val objectForKey:@"key"], [val objectForKey:@"fileName"]]];
[self appendPostString:[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", [val objectForKey:@"contentType"]]];

id data = [val objectForKey:@"data"];
if ([data isKindOfClass:[NSString class]]) {
[self appendPostDataFromFile:data];
} else {
[self appendPostData:data];
}
i++;
// Only add the boundary if this is not the last item in the post body
if (i != [[self fileData] count]) {
[self appendPostString:endItemBoundary];
}
}

[self appendPostString:[NSString stringWithFormat:@"\r\n--%@--\r\n",stringBoundary]];

#if DEBUG_FORM_DATA_REQUEST
[self addToDebugBody:@"==== End of multipart/form-data body ====\r\n"];
#endif
}


小结:对于以上两个方法源码需要注意的是appendPostString和addToDebugBody两个方法。

addToDebugBody

- (void)addToDebugBody:(NSString *)string
{
if (string) {
[self setDebugBodyString:[[self debugBodyString] stringByAppendingString:string]];
}
}


- (void)appendPostString:(NSString *)string
{
#if DEBUG_FORM_DATA_REQUEST
[self addToDebugBody:string];
#endif
[super appendPostData:[string dataUsingEncoding:[self stringEncoding]]];
}


这两个方法可以看出当我们掉用一次appendPostData时候其实是给BodyData加入一行数据而已,并纪录到DebugBody,所以DebugBody不参与任何转换,只是用于纪录与appendPostString类似的方法还有

- (void)appendPostData:(NSData *)data
{
[self addToDebugBody:[NSString stringWithFormat:@"[%lu bytes of data]",(unsigned long)[data length]]];
[super appendPostData:data];
}


- (void)appendPostDataFromFile:(NSString *)file
{
NSError *err = nil;
unsigned long long fileSize = [[[[[[NSFileManager alloc] init] autorelease] attributesOfItemAtPath:file error:&err] objectForKey:NSFileSize] unsignedLongLongValue];
if (err) {
[self addToDebugBody:[NSString stringWithFormat:@"[Error: Failed to obtain the size of the file at '%@']",file]];
} else {
[self addToDebugBody:[NSString stringWithFormat:@"[%llu bytes of data from file '%@']",fileSize,file]];
}

[super appendPostDataFromFile:file];
}


等等,这里就不多解释了!注意所有的append动作都会在DebugString里面有纪录噢!

父类的方法

看完了ASIFormDataRequest.m的实现文件我们发现子类回调父类的方法有append和build等方法

下面重点看几个方法

-(void)main(详细看源码),由于HTTPRequest是对NSOperation的实例,所以main方法是发起网络请求的入口。组建网络请求参数从这里开始真正组建。

//父类的build方法在结束时候会把haveBuiltPostBody设置为YES
- (void)buildPostBody
{
//如果haveBuiltPostBody为YES返回,这样就保证了一个Request只构建一次BodyData
if ([self haveBuiltPostBody]) {
return;
}

// Are we submitting the request body from a file on disk
if ([self postBodyFilePath]) {
// If we were writing to the post body via appendPostData or appendPostDataFromFile, close the write stream
if ([self postBodyWriteStream]) {
[[self postBodyWriteStream] close];
[self setPostBodyWriteStream:nil];
}

NSString *path;
if ([self shouldCompressRequestBody]) {
if (![self compressedPostBodyFilePath]) {
[self setCompressedPostBodyFilePath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];

NSError *err = nil;
if (![ASIDataCompressor compressDataFromFile:[self postBodyFilePath] toFile:[self compressedPostBodyFilePath] error:&err]) {
[self failWithError:err];
return;
}
}
path = [self compressedPostBodyFilePath];
} else {
path = [self postBodyFilePath];
}

NSError *err = nil;
[self setPostLength:[[[[[NSFileManager alloc] init] autorelease] attributesOfItemAtPath:path error:&err] fileSize]];
if (err) {
[self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIFileManagementError userInfo:[NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"Failed to get attributes for file at path '%@'",path],NSLocalizedDescriptionKey,error,NSUnderlyingErrorKey,nil]]];
return;
}

// Otherwise, we have an in-memory request body
} else {
//涉及到是否对要上传的数据进行压缩
if ([self shouldCompressRequestBody]) {
NSError *err = nil;
NSData *compressedBody = [ASIDataCompressor compressData:[self postBody] error:&err];
if (err) {
[self failWithError:err];
return;
}
[self setCompressedPostBody:compressedBody];
[self setPostLength:[[self compressedPostBody] length]];
} else {
[self setPostLength:[[self postBody] length]];
}
}

//如果有Post数据我们强制HttpMethod为POST模式
if ([self postLength] > 0) {
if ([requestMethod isEqualToString:@"GET"] || [requestMethod isEqualToString:@"DELETE"] || [requestMethod isEqualToString:@"HEAD"]) {
[self setRequestMethod:@"POST"];
}
[self addRequestHeader:@"Content-Length" value:[NSString stringWithFormat:@"%llu",[self postLength]]];
}

[self setHaveBuiltPostBody:YES];
}


这里有两个坑爹的设计

setupPostBody方法必须通过下面的amped方法才会叼到,如果没有调此方法postData是为空的,append方法是必须通过子类才回调得到,也就是说ASIHTPPRequest这个类是不能自己调动这两个方法,我们发起post请求只有直接设置postBody这个属性了。也许这么设计是为了延伸更多的Request类型,但是看到这里时候也是很疑惑了蛮久的。

- (void)setupPostBody
{
if ([self shouldStreamPostDataFromDisk]) {
if (![self postBodyFilePath]) {
[self setPostBodyFilePath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];
[self setDidCreateTemporaryPostDataFile:YES];
}
if (![self postBodyWriteStream]) {
[self setPostBodyWriteStream:[[[NSOutputStream alloc] initToFileAtPath:[self postBodyFilePath] append:NO] autorelease]];
[[self postBodyWriteStream] open];
}
} else {
if (![self postBody]) {
[self setPostBody:[[[NSMutableData alloc] init] autorelease]];
}
}
}


- (void)appendPostData:(NSData *)data
{
[self setupPostBody];
if ([data length] == 0) {
return;
}
if ([self shouldStreamPostDataFromDisk]) {
[[self postBodyWriteStream] write:[data bytes] maxLength:[data length]];
} else {
[[self postBody] appendData:data];
}
}


- (void)appendPostDataFromFile:(NSString *)file
{
[self setupPostBody];
NSInputStream *stream = [[[NSInputStream alloc] initWithFileAtPath:file] autorelease];
[stream open];
NSUInteger bytesRead;
while ([stream hasBytesAvailable]) {

unsigned char buffer[1024*256];
bytesRead = [stream read:buffer maxLength:sizeof(buffer)];
if (bytesRead == 0) {
break;
}
if ([self shouldStreamPostDataFromDisk]) {
[[self postBodyWriteStream] write:buffer maxLength:bytesRead];
} else {
[[self postBody] appendData:[NSData dataWithBytes:buffer length:bytesRead]];
}
}
[stream close];
}


差不多了解ASIHTTPRequest大致的逻辑,开始重构项目代码了!项目由于是用ASIFormDataRequest居多,所以从ASIFormDataRequest入手了解框架其他部分由于项目也没怎么使用到先不做了解,赶项目要紧。。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: