上篇 分析了接口層的兩個(gè)類AFHttpSessionManager && AFURLSessionManager,他們的主要負(fù)責(zé)對(duì)外暴露接口供框架的使用者調(diào)用,具體的構(gòu)造請(qǐng)求和解析響應(yīng)頭則放在了序列化這塊。本篇主要講解請(qǐng)求的和響應(yīng)的序列化枚抵。
http基礎(chǔ)姿勢(shì)
1. http常用方法
http常用的請(qǐng)求方法有:get,post,head欺嗤,put,delete卫枝。其中post和put請(qǐng)求會(huì)將參數(shù)轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)后然后放入body中煎饼,get,head和delete方法則會(huì)將參數(shù)直接拼接在url中作為查詢參數(shù)校赤。比如對(duì)于接口https://xxx.com/v1/userinfo吆玖,有兩個(gè)參數(shù)userName和password,那么get方法和post方法最終的請(qǐng)求是:url:https://xxx.com/v1/userinfo?userName='terry'&password='123' 和url:https://xxx.com/v1/userinfo httpbody:userName='terry'&password='123'马篮。對(duì)于post方法沾乘,如果除了參數(shù)以外還有其他數(shù)據(jù),比如上傳一張圖片浑测,那個(gè)圖片數(shù)據(jù)將會(huì)和參數(shù)數(shù)據(jù)一樣轉(zhuǎn)換成二進(jìn)制數(shù)據(jù)后寫入httpbody里翅阵。
2. http請(qǐng)求的上傳多個(gè)文件具體原理
如果一個(gè)http請(qǐng)求要上傳多個(gè)文件歪玲,那么首先要告訴服務(wù)器要做什么,多個(gè)文件之間怎么分隔的掷匠,文件大小多大等滥崩。這部分參考了這篇博文HTTP協(xié)議下實(shí)現(xiàn)上傳文件。至于http頭部的其他信息暫時(shí)忽略讹语,重點(diǎn)介紹幾個(gè)頭部:
Content-Type:表示數(shù)據(jù)類型钙皮。如果是文件上傳必須是multipart/form-data; boundary=--xxxx 這些協(xié)議規(guī)定的,后面表示多文件之間的分隔符顽决。
Content-Length:表示總的數(shù)據(jù)包大小短条,包括分隔符以及前后換行+正文數(shù)據(jù)。
3. http請(qǐng)求的緩存處理
有時(shí)候?qū)τ谝恍┎怀W兊臄?shù)據(jù)才菠,服務(wù)端為了節(jié)省帶寬或者資源會(huì)讓客戶端使用上次請(qǐng)求數(shù)據(jù)(也就是使用緩存)茸时。會(huì)返回304狀態(tài)碼(Not Modified),需要服務(wù)端在http的響應(yīng)里寫入一些數(shù)據(jù)last-modify鸠儿,etag等信息屹蚊,不然客戶端怎么知道要不要緩存請(qǐng)求,而且怎么判斷請(qǐng)求的資源有沒(méi)有發(fā)生變化进每⌒谠粒客戶端拿到這個(gè)狀態(tài)碼之后就不會(huì)去讀取當(dāng)前響應(yīng)的body(body里其實(shí)也木有內(nèi)容),而是取上次緩存的響應(yīng)頭田晚。
知道這些就夠了嘱兼。下面進(jìn)入正題:
序列化
1. 請(qǐng)求序列化(直白的說(shuō)就是把請(qǐng)求里各種參數(shù),文件數(shù)據(jù)轉(zhuǎn)換成二進(jìn)制數(shù)據(jù))
在請(qǐng)求序列化的過(guò)程中主要涉及類和各功能如下圖:
先看下請(qǐng)求序列化類的基類頭文件
@interface AFHTTPRequestSerializer : NSObject <AFURLRequestSerialization>
/**
The string encoding used to serialize parameters. `NSUTF8StringEncoding` by default.
*/
@property (nonatomic, assign) NSStringEncoding stringEncoding;
/**
Whether created requests can use the device’s cellular radio (if present). `YES` by default.
@see NSMutableURLRequest -setAllowsCellularAccess:
*/
@property (nonatomic, assign) BOOL allowsCellularAccess;
/**
The cache policy of created requests. `NSURLRequestUseProtocolCachePolicy` by default.
@see NSMutableURLRequest -setCachePolicy:
*/
@property (nonatomic, assign) NSURLRequestCachePolicy cachePolicy;
/**
Whether created requests should use the default cookie handling. `YES` by default.
@see NSMutableURLRequest -setHTTPShouldHandleCookies:
*/
@property (nonatomic, assign) BOOL HTTPShouldHandleCookies;
/**
Whether created requests can continue transmitting data before receiving a response from an earlier transmission. `NO` by default
@see NSMutableURLRequest -setHTTPShouldUsePipelining:
*/
@property (nonatomic, assign) BOOL HTTPShouldUsePipelining;
/**
The network service type for created requests. `NSURLNetworkServiceTypeDefault` by default.
@see NSMutableURLRequest -setNetworkServiceType:
*/
@property (nonatomic, assign) NSURLRequestNetworkServiceType networkServiceType;
/**
The timeout interval, in seconds, for created requests. The default timeout interval is 60 seconds.
@see NSMutableURLRequest -setTimeoutInterval:
*/
@property (nonatomic, assign) NSTimeInterval timeoutInterval;
///---------------------------------------
/// @name Configuring HTTP Request Headers
///---------------------------------------
/**
Default HTTP header field values to be applied to serialized requests. By default, these include the following:
- `Accept-Language` with the contents of `NSLocale +preferredLanguages`
- `User-Agent` with the contents of various bundle identifiers and OS designations
@discussion To add or remove default request headers, use `setValue:forHTTPHeaderField:`.
*/
@property (readonly, nonatomic, strong) NSDictionary <NSString *, NSString *> *HTTPRequestHeaders;
/**
Creates and returns a serializer with default configuration.
*/
+ (instancetype)serializer;
/**
Sets the value for the HTTP headers set in request objects made by the HTTP client. If `nil`, removes the existing value for that header.
@param field The HTTP header to set a default value for
@param value The value set as default for the specified header, or `nil`
*/
- (void)setValue:(nullable NSString *)value
forHTTPHeaderField:(NSString *)field;
/**
Returns the value for the HTTP headers set in the request serializer.
@param field The HTTP header to retrieve the default value for
@return The value set as default for the specified header, or `nil`
*/
- (nullable NSString *)valueForHTTPHeaderField:(NSString *)field;
/**
Sets the "Authorization" HTTP header set in request objects made by the HTTP client to a basic authentication value with Base64-encoded username and password. This overwrites any existing value for this header.
@param username The HTTP basic auth username
@param password The HTTP basic auth password
*/
- (void)setAuthorizationHeaderFieldWithUsername:(NSString *)username
password:(NSString *)password;
/**
Clears any existing value for the "Authorization" HTTP header.
*/
- (void)clearAuthorizationHeader;
///-------------------------------------------------------
/// @name Configuring Query String Parameter Serialization
///-------------------------------------------------------
/**
HTTP methods for which serialized requests will encode parameters as a query string. `GET`, `HEAD`, and `DELETE` by default.
*/
@property (nonatomic, strong) NSSet <NSString *> *HTTPMethodsEncodingParametersInURI;
/**
Set the method of query string serialization according to one of the pre-defined styles.
@param style The serialization style.
@see AFHTTPRequestQueryStringSerializationStyle
*/
- (void)setQueryStringSerializationWithStyle:(AFHTTPRequestQueryStringSerializationStyle)style;
/**
Set the a custom method of query string serialization according to the specified block.
@param block A block that defines a process of encoding parameters into a query string. This block returns the query string and takes three arguments: the request, the parameters to encode, and the error that occurred when attempting to encode parameters for the given request.
*/
- (void)setQueryStringSerializationWithBlock:(nullable NSString * (^)(NSURLRequest *request, id parameters, NSError * __autoreleasing *error))block;
///-------------------------------
/// @name Creating Request Objects
///-------------------------------
/**
Creates an `NSMutableURLRequest` object with the specified HTTP method and URL string.
If the HTTP method is `GET`, `HEAD`, or `DELETE`, the parameters will be used to construct a url-encoded query string that is appended to the request's URL. Otherwise, the parameters will be encoded according to the value of the `parameterEncoding` property, and set as the request body.
@param method The HTTP method for the request, such as `GET`, `POST`, `PUT`, or `DELETE`. This parameter must not be `nil`.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be either set as a query string for `GET` requests, or the request HTTP body.
@param error The error that occurred while constructing the request.
@return An `NSMutableURLRequest` object.
*/
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(nullable id)parameters
error:(NSError * _Nullable __autoreleasing *)error;
/**
Creates an `NSMutableURLRequest` object with the specified HTTP method and URLString, and constructs a `multipart/form-data` HTTP body, using the specified parameters and multipart form data block. See http://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.2
Multipart form requests are automatically streamed, reading files directly from disk along with in-memory data in a single HTTP body. The resulting `NSMutableURLRequest` object has an `HTTPBodyStream` property, so refrain from setting `HTTPBodyStream` or `HTTPBody` on this request object, as it will clear out the multipart form body stream.
@param method The HTTP method for the request. This parameter must not be `GET` or `HEAD`, or `nil`.
@param URLString The URL string used to create the request URL.
@param parameters The parameters to be encoded and set in the request HTTP body.
@param block A block that takes a single argument and appends data to the HTTP body. The block argument is an object adopting the `AFMultipartFormData` protocol.
@param error The error that occurred while constructing the request.
@return An `NSMutableURLRequest` object
*/
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(nullable NSDictionary <NSString *, id> *)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
error:(NSError * _Nullable __autoreleasing *)error;
/**
Creates an `NSMutableURLRequest` by removing the `HTTPBodyStream` from a request, and asynchronously writing its contents into the specified file, invoking the completion handler when finished.
@param request The multipart form request. The `HTTPBodyStream` property of `request` must not be `nil`.
@param fileURL The file URL to write multipart form contents to.
@param handler A handler block to execute.
@discussion There is a bug in `NSURLSessionTask` that causes requests to not send a `Content-Length` header when streaming contents from an HTTP body, which is notably problematic when interacting with the Amazon S3 webservice. As a workaround, this method takes a request constructed with `multipartFormRequestWithMethod:URLString:parameters:constructingBodyWithBlock:error:`, or any other request with an `HTTPBodyStream`, writes the contents to the specified file and returns a copy of the original request with the `HTTPBodyStream` property set to `nil`. From here, the file can either be passed to `AFURLSessionManager -uploadTaskWithRequest:fromFile:progress:completionHandler:`, or have its contents read into an `NSData` that's assigned to the `HTTPBody` property of the request.
@see https://github.com/AFNetworking/AFNetworking/issues/1398
*/
- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request
writingStreamContentsToFile:(NSURL *)fileURL
completionHandler:(nullable void (^)(NSError * _Nullable error))handler;
@end
stringEncoding:對(duì)于參數(shù)使用的編碼贤徒,默認(rèn)是NSUTF8StringEncoding
allowsCellularAccess:是否允許使用蜂窩網(wǎng)絡(luò)芹壕,默認(rèn)允許
cachePolicy:緩存策略。默認(rèn)NSURLRequestUseProtocolCachePolicy接奈,也就是協(xié)議的緩存策略踢涌,至于協(xié)議怎么實(shí)現(xiàn)了就是iOS系統(tǒng)的事了。在實(shí)際測(cè)試過(guò)程中序宦,iOS是遵循標(biāo)準(zhǔn)http緩存協(xié)議的睁壁,也就是對(duì)304狀態(tài)碼可以正確處理。
HTTPShouldHandleCookies:是否處理cookie互捌,默認(rèn)YES潘明,具體怎么處理iOS系統(tǒng)自己的事了。
HTTPShouldUsePipelining:是否使用http管道技術(shù)秕噪,默認(rèn)不使用钳降。也就是在同一個(gè)連接上,并行多個(gè)請(qǐng)求腌巾,服務(wù)端和客戶端依然可以正常工作遂填。需要客戶端和服務(wù)端同時(shí)支持铲觉。具體的http管道技術(shù)可以參考博文HTTP的長(zhǎng)連接和短連接的第六部分。
HTTPRequestHeaders:http頭部字段
- (NSMutableURLRequest *)requestWithMethod:(NSString *)methodURLString:(NSString *)URLStringparameters:(nullable id)parameterserror:(NSError * _Nullable __autoreleasing *)error
這個(gè)方法用于構(gòu)造get城菊,head或者delete請(qǐng)求备燃,他們的參數(shù)將放在url中以key=value的方式編碼。
-(NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)methodURLString:(NSString *)URLStringparameters:(nullable NSDictionary <NSString *, id> *)parametersconstructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))blockerror:(NSError * _Nullable __autoreleasing *)error
這個(gè)方法用于構(gòu)造post或者put請(qǐng)求凌唬,第四個(gè)參數(shù)用于在上傳文件的時(shí)候?qū)?shù)據(jù)拼接在formData里,可以拼接多個(gè)文件數(shù)據(jù)漏麦。
-(NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)requestwritingStreamContentsToFile:(NSURL *)fileURLcompletionHandler:(nullable void (^)(NSError * _Nullable error))handler;
這個(gè)方法和上面的方法類似客税,只是多了一個(gè)將流內(nèi)容異步寫入指定文件的功能。
對(duì)于請(qǐng)求的序列化比較復(fù)雜的情況是帶參數(shù)撕贞,帶文件的post上傳請(qǐng)求更耻。下面主要跟著這個(gè)方法
-(NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)methodURLString:(NSString *)URLStringparameters:(nullable NSDictionary <NSString *, id> *)parametersconstructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))blockerror:(NSError * _Nullable __autoreleasing *)error
它的實(shí)現(xiàn)如下:
- (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"]);//請(qǐng)求方法不能為為GET或者HEAD
NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];//構(gòu)造一個(gè)不帶參數(shù)的請(qǐng)求對(duì)象
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];//http最終上傳的數(shù)據(jù)包括參數(shù),以及要上傳的文件數(shù)據(jù)都放這里捏膨,同時(shí)會(huì)持有這個(gè)request對(duì)象
if (parameters) {//如果參數(shù)不空則將參數(shù)放入以key=value的方式放入formdata的bodyparts數(shù)組中
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]];
}
}
}
if (block) {//執(zhí)行調(diào)用方上傳文件block秧均,也是通過(guò)調(diào)用類似 [formData appendPartWithFormData:data name:[pair.field description]];這樣的接口放入formdata的bodyparts數(shù)組中,這步完后我們要上傳的參數(shù)以及文件數(shù)據(jù)都構(gòu)造完成号涯。
block(formData);
}
return [formData requestByFinalizingMultipartFormData];//設(shè)置request的httpbodyStream 設(shè)置為formData中的輸入流目胡。同時(shí)設(shè)置一下頭部信息比如Content-Length,Content-Type等链快。
}
下面來(lái)看看怎么將輸入formData(AFStreamingMultipartFormData)的數(shù)據(jù)寫入到文件流的誉己。先看下
以及
AFMultipartBodyStream輸入流類定義了怎么讀取數(shù)據(jù)。
在看具體怎么讀取數(shù)據(jù)之前域蜗,先了解下iOS的輸入流(NSInputStream)巨双。iOS關(guān)于NSInputStream說(shuō)明
NSInputStream is an abstract superclass of a class cluster consisting of concrete subclasses of NSStream that provide standard read-only access to stream data. Although NSInputStream is probably sufficient for most situations requiring access to stream data, you can create a subclass of NSInputStream if you want more specialized behavior (for example, you want to record statistics on the data in a stream).
Methods to Override
To create a subclass of NSInputStream you may have to implement initializers for the type of stream data supported and suitably re-implement existing initializers. You must also provide complete implementations of the following methods:
read:maxLength: From the current read index, take up to the number of bytes specified in the second parameter from the stream and place them in the client-supplied buffer (first parameter). The buffer must be of the size specified by the second parameter. Return the actual number of bytes placed in the buffer; if there is nothing left in the stream, return 0. Reset the index into the stream for the next read operation.
getBuffer:length: Return in 0(1) a pointer to the subclass-allocated buffer (first parameter). Return by reference in the second parameter the number of bytes actually put into the buffer. The buffer’s contents are valid only until the next stream operation. Return NO if you cannot access data in the buffer; otherwise, return YES. If this method is not appropriate for your type of stream, you may return NO.
hasBytesAvailable Return YES if there is more data to read in the stream, NO if there is not. If you want to be semantically compatible with NSInputStream, return YES if a read must be attempted to determine if bytes are available.
那既然是AFMultipartBodyStream顯然也實(shí)現(xiàn)了這幾個(gè)方法,在AF里的實(shí)現(xiàn)代碼如下:
可以看到最終的序列化是調(diào)用AFHTTPBodyPart的read:maxLength方法把一個(gè)個(gè)AFHTTPBodyPart序列化的霉祸。那再進(jìn)一步跟蹤下AFHTTPBodyPart又是怎么把數(shù)據(jù)單元序列化的筑累。
這個(gè)地方可能會(huì)有疑問(wèn)是怎么將上圖中的文件數(shù)據(jù)寫入緩沖中的也就是下面這句:
numberOfBytesRead = [self.inputStream read:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
可以看下AFHTTPBodyPart 構(gòu)造過(guò)程:
再看看AFHTTPBodyPart里的InpustStream里生成:
就是把body數(shù)據(jù)讀入,其實(shí)這里作者也給我們演示了NSInputStream的兩種使用方法丝蹭。
說(shuō)到這里基本上整個(gè)請(qǐng)求序列化就說(shuō)完了慢宗,至于AFJSONRequestSerializer和AFPropertyListRequestSerializer就是把http頭部字段取不同值而已Content-Type。說(shuō)了這么多半夷,還是建議好好看著源碼在體會(huì)下比較婆廊。