AFNetworking3.0源码解析2(请求序列化)

上篇 分析了接口层的两个类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. 请求序列化(直白的说就是把请求里各种参数,文件数据转换成二进制数据)

在请求序列化的过程中主要涉及类和各功能如下图:

AFNetworking请求序列化相关类.png

先看下请求序列化类的基类头文件

@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)的数据写入到文件流的。先看下

AFStreamingMultipartFormData类.png

以及


AFMultipartBodyStream类.png

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里的实现代码如下:

AFMultipartBodyStream的接口实现.png

可以看到最终的序列化是调用AFHTTPBodyPart的read:maxLength方法把一个个AFHTTPBodyPart序列化的。那再进一步跟踪下AFHTTPBodyPart又是怎么把数据单元序列化的。

AFHTTPBodyPart写数据到缓冲.png

这个地方可能会有疑问是怎么将上图中的文件数据写入缓冲中的也就是下面这句:

numberOfBytesRead = [self.inputStream read:&buffer[totalNumberOfBytesRead] maxLength:(length - (NSUInteger)totalNumberOfBytesRead)];

可以看下AFHTTPBodyPart 构造过程:

AFHTTPBodyPart构造.png

再看看AFHTTPBodyPart里的InpustStream里生成:

AFHTTPBodyPart的InpustStream.png

就是把body数据读入,其实这里作者也给我们演示了NSInputStream的两种使用方法。
说到这里基本上整个请求序列化就说完了,至于AFJSONRequestSerializer和AFPropertyListRequestSerializer就是把http头部字段取不同值而已Content-Type。说了这么多,还是建议好好看着源码在体会下比较。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容