AFNetWorking源碼之AFHTTPSessionManager

1 概述

AFHTTPSessionManagerAFURLSessionManager的子類。咱們能夠經過這個類作HTTP請求。其實整個AFHTTPSessionManager邏輯很簡單,只是用HTTP的方式拼接了請求,而且調用父類的方式作處理。我會經過AFHTTPSessionManagerapi來說一下POST上傳數據的幾種基本格式,而後我再隨便分析一下AFHTTPSessionManagerphp

2 POST請求的經常使用格式

HTTP/1.1協議規定的HTTP請求方法有OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 這幾種。其中POST通常用來向服務端提交數據,接下來要討論POST提交數據的幾種方式。協議規定POST提交的數據必須放在消息主體中,但協議並無規定數據必須使用什麼編碼方式。實際上,開發者徹底能夠本身決定消息主體的格式,只要最後發送的 HTTP 請求知足上面的格式就能夠。html

可是,數據發送出去,還要服務端解析成功纔有意義。通常服務端語言如php、python等,以及它們的framework,都內置了自動解析常見數據格式的功能。服務端一般是根據請求頭(headers)中的Content-Type字段來獲知請求中的消息主體是用何種方式編碼,再對主體進行解析。因此說到POST提交數據方案,包含了Content-Type和消息主體編碼方式兩部分。python

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轉碼。git

下面這個請求是簡書進入一篇文章頁面的時候,會自動往服務器POST一個請求,估計是統計文章被閱讀的次數等功能。具體看下面:github

//發送的請求,刪除了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請求。json

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則是添加了分隔符等內容的構造體.api

請求的頭部信息以下:瀏覽器

//其中xxxxx是我自定義的分隔符,每一個人均可以選擇本身的分隔符
Content-Type: multipart/form-data; boundary=xxxxx

下面咱們來看一下一個個人Multipart/form-data請求體:服務器

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參數和他的值就經過以下格式傳輸:cookie

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

img.jpeg

上面這種就是一個參數與之對應的值。協議規定的就是這個格式,沒有爲何。咱們能夠看看圖片數據部分:

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

這裏是圖片數據,太長了.我就刪了

--xxxxx--

其中name="參數名" filename="文件名" 其中參數名這個要和接收方那邊相對應 正常開發中能夠去問服務器那邊 , 文件名是說在服務器端保存成文件的名字 , 這個參數然並卵 , 由於通常服務端會按照他們本身的要求去處理文件的存儲.

下一行是指定類型 , 我這裏示例中寫的是PNG圖片類型 , 這個能夠根據你的實際需求的寫。若是咱們要上傳多分圖片或者文件,則只須要按照指定格式就能夠了,好比下面就是上傳兩張圖片的請求:

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已經幫咱們作好了拼接工做:

//方法一
- (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) , 和文件部分的二進制數據拼接在一塊兒 , 做爲請求體發送給服務器。

  • 每一行末尾須要有必定的`rn·。

2.3 application/json格式的POST請求

接下來我將常使用NSURLSessionDataTask作一個application/json的POST請求。而且請求體數據我存儲在一個test.txt文件中,從文件中讀取出來而後上傳。

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

經過抓包軟件個人請求以下,和其餘POST請求原理同樣,只是拼接請求體的方式不同,而且更具不一樣格式的請求體,設置不一樣的Content-Type

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的具體實現

- (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 首先多了一個屬性

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

這個屬性的主要做用就是幫咱們拼接請求頭和請求體,從上面的Demo咱們發現不少請求的拼接工做都經過requestSerializer處理了。若是咱們不手動設置,默認是一個AFHTTPRequestSerializer對象。具體能夠去初始化方法裏面看到。

2 重寫了securityPolicy這個屬性的setter方法,增長對於SSLPinningMode的異常處理。

- (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的複製。

+ (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的時候再看這一塊是如何實現的。

//經過requestSerializer屬性來拼接request對象。
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];

最後原文地址,demo地址

相關文章
相關標籤/搜索