上篇 分析了接口层的两个类AFHttpSessionManager && AFURLSessionManager,他们的主要负责对外暴露接口供框架的使用者调用,具体的构造请求和解析响应头则放在了序列化这块。本篇主要讲解请求的和响应的序列化。
http基础姿势
1. http常用方法
http常用的请求方法有:get,post,head,put,delete。其中post和put请求会将参数转换成二进制数据后然后放入body中,get,head和delete方法则会将参数直接拼接在url中作为查询参数。比如对于接口https://xxx.com/v1/userinfo,有两个参数userName和password,那么get方法和post方法最终的请求是:url:https://xxx.com/v1/userinfo?userName='terry'&password='123' 和url:https://xxx.com/v1/userinfo httpbody:userName='terry'&password='123'。对于post方法,如果除了参数以外还有其他数据,比如上传一张图片,那个图片数据将会和参数数据一样转换成二进制数据后写入httpbody里。
2. http请求的上传多个文件具体原理
如果一个http请求要上传多个文件,那么首先要告诉服务器要做什么,多个文件之间怎么分隔的,文件大小多大等。这部分参考了这篇博文HTTP协议下实现上传文件。至于http头部的其他信息暂时忽略,重点介绍几个头部:
Content-Type:表示数据类型。如果是文件上传必须是multipart/form-data; boundary=--xxxx 这些协议规定的,后面表示多文件之间的分隔符。
Content-Length:表示总的数据包大小,包括分隔符以及前后换行+正文数据。
3. http请求的缓存处理
有时候对于一些不常变的数据,服务端为了节省带宽或者资源会让客户端使用上次请求数据(也就是使用缓存)。会返回304状态码(Not Modified),需要服务端在http的响应里写入一些数据last-modify,etag等信息,不然客户端怎么知道要不要缓存请求,而且怎么判断请求的资源有没有发生变化。客户端拿到这个状态码之后就不会去读取当前响应的body(body里其实也木有内容),而是取上次缓存的响应头。
知道这些就够了。下面进入正题:
序列化
1. 请求序列化(直白的说就是把请求里各种参数,文件数据转换成二进制数据)
在请求序列化的过程中主要涉及类和各功能如下图:
先看下请求序列化类的基类头文件
@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:对于参数使用的编码,默认是NSUTF8StringEncoding
allowsCellularAccess:是否允许使用蜂窝网络,默认允许
cachePolicy:缓存策略。默认NSURLRequestUseProtocolCachePolicy,也就是协议的缓存策略,至于协议怎么实现了就是iOS系统的事了。在实际测试过程中,iOS是遵循标准http缓存协议的,也就是对304状态码可以正确处理。
HTTPShouldHandleCookies:是否处理cookie,默认YES,具体怎么处理iOS系统自己的事了。
HTTPShouldUsePipelining:是否使用http管道技术,默认不使用。也就是在同一个连接上,并行多个请求,服务端和客户端依然可以正常工作。需要客户端和服务端同时支持。具体的http管道技术可以参考博文HTTP的长连接和短连接的第六部分。
HTTPRequestHeaders:http头部字段
- (NSMutableURLRequest *)requestWithMethod:(NSString *)methodURLString:(NSString *)URLStringparameters:(nullable id)parameterserror:(NSError * _Nullable __autoreleasing *)error
这个方法用于构造get,head或者delete请求,他们的参数将放在url中以key=value的方式编码。
-(NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)methodURLString:(NSString *)URLStringparameters:(nullable NSDictionary <NSString *, id> *)parametersconstructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))blockerror:(NSError * _Nullable __autoreleasing *)error
这个方法用于构造post或者put请求,第四个参数用于在上传文件的时候将数据拼接在formData里,可以拼接多个文件数据。
-(NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)requestwritingStreamContentsToFile:(NSURL *)fileURLcompletionHandler:(nullable void (^)(NSError * _Nullable error))handler;
这个方法和上面的方法类似,只是多了一个将流内容异步写入指定文件的功能。
对于请求的序列化比较复杂的情况是带参数,带文件的post上传请求。下面主要跟着这个方法
-(NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)methodURLString:(NSString *)URLStringparameters:(nullable NSDictionary <NSString *, id> *)parametersconstructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))blockerror:(NSError * _Nullable __autoreleasing *)error
它的实现如下:
- (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
NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];//构造一个不带参数的请求对象
__block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];//http最终上传的数据包括参数,以及要上传的文件数据都放这里,同时会持有这个request对象
if (parameters) {//如果参数不空则将参数放入以key=value的方式放入formdata的bodyparts数组中
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) {//执行调用方上传文件block,也是通过调用类似 [formData appendPartWithFormData:data name:[pair.field description]];这样的接口放入formdata的bodyparts数组中,这步完后我们要上传的参数以及文件数据都构造完成。
block(formData);
}
return [formData requestByFinalizingMultipartFormData];//设置request的httpbodyStream 设置为formData中的输入流。同时设置一下头部信息比如Content-Length,Content-Type等。
}
下面来看看怎么将输入formData(AFStreamingMultipartFormData)的数据写入到文件流的。先看下
以及
AFMultipartBodyStream输入流类定义了怎么读取数据。
在看具体怎么读取数据之前,先了解下iOS的输入流(NSInputStream)。iOS关于NSInputStream说明
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显然也实现了这几个方法,在AF里的实现代码如下:
可以看到最终的序列化是调用AFHTTPBodyPart的read:maxLength方法把一个个AFHTTPBodyPart序列化的。那再进一步跟踪下AFHTTPBodyPart又是怎么把数据单元序列化的。
这个地方可能会有疑问是怎么将上图中的文件数据写入缓冲中的也就是下面这句:
numberOfBytesRead = [self.inputStream read:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];
可以看下AFHTTPBodyPart 构造过程:
再看看AFHTTPBodyPart里的InpustStream里生成:
就是把body数据读入,其实这里作者也给我们演示了NSInputStream的两种使用方法。
说到这里基本上整个请求序列化就说完了,至于AFJSONRequestSerializer和AFPropertyListRequestSerializer就是把http头部字段取不同值而已Content-Type。说了这么多,还是建议好好看着源码在体会下比较。