上一篇的 HTTP协议详解 主要偏向理论和入门,这篇文章会比较细地介绍其中的相关内容,主要涉及首部字段、请求与响应体等。
Content-Type
在做HTTP请求的时候,我们通常会添加首部字段Content-Type,服务端则通过Content-Type 字段来获知请求中的消息主体是用何种方式编码,再对主体进行解析。以下是容易混淆的几个字段:
- multipart/form-data
在AFNetworking 中经常会使用下面的方法上传文件:
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
使用multipart/form-data时会把请求体分成多个块,多个块之间依赖于boundary值去做分割,AFNetworking 生成boundary 的代码如下:
static NSString * AFCreateMultipartFormBoundary() {
return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()];
}
在生成的boundary的时候,要求它足够长,不能在字节流中重复出现,否则就会导致错误的传输。分块中如果某个块是文件,还要包含文件名和文件类型信息。消息主体最后以 --boundary-- 标示结束。主体信息生成之后,还会在首部字段里设置Content-Type的类型为multipart/form-data,以及本次请求的boundary内容。
以下是用AFNetworking上传文件的代码和抓包,乱码部分为图片二进制数据:
[manager POST:uploadURL parameters:nil constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
NSURL *url = [[NSBundle mainBundle] URLForResource:@"1.jpg" withExtension:nil];
[formData appendPartWithFileURL:url name:@"file" error:nil];
} progress:^(NSProgress * _Nonnull uploadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"%@", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"%@", [error localizedDescription]);
}];
可以看出boundary=Boundary+B21C39B63D7ABC44,主题报文的只有一个分块,分块的文件类型为Content-Type: image/jpeg。
- application/x-www-form-urlencoded
在AFNetworking中,如果我们用下面的方法上传文件:
- (nullable NSURLSessionDataTask *)POST:(NSString *)URLString
parameters:(nullable id)parameters
progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgress
success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
那么最终就会以 application/x-www-form-urlencoded 的方式提交数据,它最大的特点就是会使用urlencoded对body内容编码。在AFNetworking中源码如下:
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
withParameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(request);
NSMutableURLRequest *mutableRequest = [request mutableCopy];
[self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
if (![request valueForHTTPHeaderField:field]) {
[mutableRequest setValue:value forHTTPHeaderField:field];
}
}];
NSString *query = nil;
if (parameters) {
if (self.queryStringSerialization) {
NSError *serializationError;
query = self.queryStringSerialization(request, parameters, &serializationError);
if (serializationError) {
if (error) {
*error = serializationError;
}
return nil;
}
} else {
switch (self.queryStringSerializationStyle) {
case AFHTTPRequestQueryStringDefaultStyle:
query = AFQueryStringFromParameters(parameters);
break;
}
}
}
if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
if (query && query.length > 0) {
mutableRequest.URL = [NSURL URLWithString:[[mutableRequest.URL absoluteString] stringByAppendingFormat:mutableRequest.URL.query ? @"&%@" : @"?%@", query]];
}
} else {
// #2864: an empty string is a valid x-www-form-urlencoded payload
if (!query) {
query = @"";
}
if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
[mutableRequest setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
}
[mutableRequest setHTTPBody:[query dataUsingEncoding:self.stringEncoding]];
}
return mutableRequest;
}
以下是用AFNetworking上传文件的代码和抓包,乱码部分为图片二进制数据:
NSURL *fileURL = [[NSBundle mainBundle] URLForResource:@"1.jpg" withExtension:nil];
[manager POST:uploadURL parameters:@{@"file":[NSData dataWithContentsOfURL:fileURL]} progress:^(NSProgress * _Nonnull uploadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"%@", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"%@", [error localizedDescription]);
}];
可以看见file字段进行了URL编码,这样在上传文件的时候,因为字节流中有许多非ASCII码,文件的长度会变至原本的2-3倍。
以下是一张图片经过x-www-form-urlencoded编码后的抓包数据,原图像91KB,编码上传的时候变为251KB。因此这种方式上传大文件的时候,流量会加大许多。
- application/json
application/json 这个Content-Type作为响应头比较简单。它用来告诉服务端消息主体是序列化后的JSON字符串。
以下是用AFNetworking上传文件的代码和抓包,乱码部分为图片二进制数据:
manager.requestSerializer = [AFJSONRequestSerializer serializer];
[manager POST:uploadURL parameters:@{@"file":@"I am file"} progress:^(NSProgress * _Nonnull uploadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"%@", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"%@", [error localizedDescription]);
}];
- application/octet-stream
application/octet-stream 这个在AFNetworking中一般是指二进制数据,当一个文件的类型不能被识别的时候,一般会使用application/octet-stream。在AFNetworking中一般会配合multipart/form-data使用,因为使用multipart/form-data可以传输多个文件,每个文件在body分块由boundary分割,如果该分块是文件,需要指定文件名和文件类型信息,当一个文件的类型不能被识别的时候,就会使用application/octet-stream。
static inline NSString * AFContentTypeForPathExtension(NSString *extension) {
NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL);
NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
if (!contentType) {
return @"application/octet-stream";
} else {
return contentType;
}
}
以下是上传1.data时的主题报文的的Content-Type:
在上传大文件时,如果使用application/x-www-form-urlencoded作为Content-Type,文件的长度会变至原本的2-3倍,这时最好使用multipart/form-data。在上传少量数据时,如果使用multipart/form-data由于存在boundary会带来额外的流量,这时最好使用application/x-www-form-urlencoded。至于使用哪个还是根据实际情况考虑。
Range
Range 属性在做断点续传的时候使用的比较多,表示请求资源的部分内容(不包括响应头的大小), 单位是byte,即字节,从0开始。如果服务器能够正常响应的话,服务器会返回206 Partial Content的状态码及说明。如果不能处理这种Range的话,就会返回整个资源以及响应状态码为200 OK。
请求 Range。
Range: bytes=100- 第100个字节及最后个字节的数据
Range: bytes=200-1000 第20个字节到第1000个字节之间的数据.
Range: bytes=-500 表示最后500个字节
Range: bytes=500- 表示500字节以后的范围
Range: bytes=0-0,-1 第一个和最后一个字节
Range: bytes=500-600,601-999 同时指定几个范围
响应 Range。
Content-Range: bytes 0-100/4000 服务器响应了前(0-100)个字节的数据,该资源一共有(4000)个字节大小。
ETag
被请求变量的实体值,用来判断当前请求资源是否改变,主要为了解决 Last-Modified 无法解决的一些问题,Last-Modified和If-Modified-Since只判断资源的最后修改时间,而ETags和If-None-Match可以是资源任何的任何属性,类似于资源的MD5。
- If-None-Match
在HTTP Response中添加ETags信息,当客户端再次请求该资源时,将在HTTP Request中加入If-None-Match信息(ETags的值)。如果服务器验证资源的ETags没有改变(该资源没有改变),将返回一个304状态;否则服务器将返回200状态,并返回该资源和新的ETags。
- If-Match
在HTTP Request中加入If-Match信息(ETags的值)。如果服务器验证资源的ETags与If-Match的值一致,表示文件没有被修改,则开始继续传送文件,服务器返回 200 OK;2. 如果文件被修改,则不传输,服务器返回 412 Precondition failed
以下是对简书请求的响应报文:
我们可以拿到 ETags再去请求:
If-None-Match 请求:
GET / HTTP/1.1
If-None-Match: W/"46897b97f1ec60f7e9234128a807a4da"
Content-Type: application/json; charset=utf-8
Cookie: signin_redirect=http%3A%2F%2Fwww.jianshu.com%2F; _maleskine_session=OTlTVXFmSkFUWkwvckpsL1NQZzRCb3d2WjZKVUd0cjg2bmswNXRCZ2lSYll5NXUwNm9PM3crK1RyVlhLOWdBTGFLYW5uUjJzZ2M5N25OeVMwUWZzNWo3NzBKYnNCaEprN0szMERXc29hYzIvQ3ZLRGgrR21Dc1pGSXYwakt3dytPc3lEdUNqT0VxNm9ObWQzNjd2TllnT1Y1Q2xmeE90OTN5TitiSzgyb1I0ZnVSRzYydko3Yk1Kbk91YmZvYWdEajJBVVZqZnhObkx3TzZxTC9RV3lMVTM2azFsdjVjenU3VElxeFNSV2FsZ2trM3Y0NXV4UW0zSDFVUFp3QzVEbC0tWG5OR0YyWTZ4VzZlZzYrRHZ1ZU9RZz09--f0b6a705d154b0689d3b7dd1c372bacb85bea348
Host: www.jianshu.com
Connection: close
User-Agent: Paw/3.1.4 (Macintosh; OS X/10.12.6) GCDHTTPRequest
If-None-Match 响应 :
HTTP/1.1 304 Not Modified
Date: Tue, 17 Oct 2017 13:06:41 GMT
Server: Tengine
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Security-Policy: script-src 'self' 'unsafe-inline' 'unsafe-eval' *.jianshu.io api.geetest.com static.geetest.com dn-staticdown.qbox.me zz.bdstatic.com *.google-analytics.com hm.baidu.com push.zhanzhang.baidu.com res.wx.qq.com qzonestyle.gtimg.cn as.alipayobjects.com ;style-src 'self' 'unsafe-inline' *.jianshu.io api.geetest.com static.geetest.com ;
ETag: W/"46897b97f1ec60f7e9234128a807a4da"
Cache-Control: max-age=0, private, must-revalidate
Set-Cookie: _maleskine_session=OTlTVXFmSkFUWkwvckpsL1NQZzRCb3d2WjZKVUd0cjg2bmswNXRCZ2lSYll5NXUwNm9PM3crK1RyVlhLOWdBTGFLYW5uUjJzZ2M5N25OeVMwUWZzNWo3NzBKYnNCaEprN0szMERXc29hYzIvQ3ZLRGgrR21Dc1pGSXYwakt3dytPc3lEdUNqT0VxNm9ObWQzNjd2TllnT1Y1Q2xmeE90OTN5TitiSzgyb1I0ZnVSRzYydko3Yk1Kbk91YmZvYWdEajJBVVZqZnhObkx3TzZxTC9RV3lMVTM2azFsdjVjenU3VElxeFNSV2FsZ2trM3Y0NXV4UW0zSDFVUFp3QzVEbC0tWG5OR0YyWTZ4VzZlZzYrRHZ1ZU9RZz09--f0b6a705d154b0689d3b7dd1c372bacb85bea348; path=/; HttpOnly
X-Request-Id: b4e0dee3-4a91-4cc3-84ce-9538ea4d29c5
X-Runtime: 0.037953
X-Via: 1.1 zhouwtong132:2 (Cdn Cache Server V2.0)
Connection: close
可以看出返回码为 304 Not Modified,这时body没有内容。这样在网络上传输的数据就会大大减少,同时也减轻了服务器的负担。
If-Match 请求:
GET / HTTP/1.1
If-Match: W/"46897b97f1ec60f7e9234128a807a4da"
Content-Type: application/json; charset=utf-8
Cookie: signin_redirect=http%3A%2F%2Fwww.jianshu.com%2F; _maleskine_session=VDUzOFExbmhvUnVLMkhmVDFYQ09vbXJ2Z0EwVXNFQkl1aVg5L0hLb1liR1BnTWhnU2RubFBRTEcwWWZ1TWxabndxTFJ1cTkxQzZVZjYwdXNmM3hOckJoQndVOG81eURUOC9HNHRyZWJ3VnF6NEJXbUo0UW4vR0pqMHRMV0RpaWhLNHhaQWdqc3ZJSnhIWWgzd3hNeDZUOXhFajY4SFdSUnl2UE9LVGVlM3dkekx1WnVXWEZ2Q0NQSWhMMDg0OS91ODQ1NE1kS0NHRGpxTzM2aEZuRDVIUi8xclVYMDg4Q3lNZnF1K3dLcFdPWTB1V0pLNVpqSy8vZVdiWWNZTEQwcS0tMmlZWUdwMjkrZHNhM24vMGJpbzk1UT09--0ef0ba03bd4f031c21a0c3206e640769ccec2cc7
Host: www.jianshu.com
Connection: close
User-Agent: Paw/3.1.4 (Macintosh; OS X/10.12.6) GCDHTTPRequest
If-Match 响应:
HTTP/1.1 200 OK
Date: Tue, 17 Oct 2017 13:08:54 GMT
Server: Tengine
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Security-Policy: script-src 'self' 'unsafe-inline' 'unsafe-eval' *.jianshu.io api.geetest.com static.geetest.com dn-staticdown.qbox.me zz.bdstatic.com *.google-analytics.com hm.baidu.com push.zhanzhang.baidu.com res.wx.qq.com qzonestyle.gtimg.cn as.alipayobjects.com ;style-src 'self' 'unsafe-inline' *.jianshu.io api.geetest.com static.geetest.com ;
ETag: W/"46897b97f1ec60f7e9234128a807a4da"
Cache-Control: max-age=0, private, must-revalidate
Set-Cookie: _maleskine_session=VDUzOFExbmhvUnVLMkhmVDFYQ09vbXJ2Z0EwVXNFQkl1aVg5L0hLb1liR1BnTWhnU2RubFBRTEcwWWZ1TWxabndxTFJ1cTkxQzZVZjYwdXNmM3hOckJoQndVOG81eURUOC9HNHRyZWJ3VnF6NEJXbUo0UW4vR0pqMHRMV0RpaWhLNHhaQWdqc3ZJSnhIWWgzd3hNeDZUOXhFajY4SFdSUnl2UE9LVGVlM3dkekx1WnVXWEZ2Q0NQSWhMMDg0OS91ODQ1NE1kS0NHRGpxTzM2aEZuRDVIUi8xclVYMDg4Q3lNZnF1K3dLcFdPWTB1V0pLNVpqSy8vZVdiWWNZTEQwcS0tMmlZWUdwMjkrZHNhM24vMGJpbzk1UT09--0ef0ba03bd4f031c21a0c3206e640769ccec2cc7; path=/; HttpOnly
X-Request-Id: c9cf0f89-fe3e-46b3-bac4-c7e519660434
X-Runtime: 0.007676
X-Via: 1.1 zhouwtong132:2 (Cdn Cache Server V2.0)
Connection: close
<!DOCTYPE html>
<!--[if IE 6]><html class="ie lt-ie8"><![endif]-->
<!--[if IE 7]><html class="ie lt-ie8"><![endif]-->
<!--[if IE 8]><html class="ie ie8"><![endif]-->
<!--[if IE 9]><html class="ie ie9"><![endif]-->
<!--[if !IE]><!--> <html> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-widt
...
</html>
表示服务器验证资源的ETags与If-Match的值一致,表示文件没有被修改,则开始继续传送文件,服务器返回 200 OK。
Modified
字段名 | 说明 | 用法 |
---|---|---|
Last-Modified | 由服务器往客户端发送的 HTTP 头 | 表示资源的最后修改日期时间 |
If-Modified-Since | 1. 如果文件被修改,就开始传输, 服务器返回 200 OK;2. 如果文件没有被修改,就无需传输, 服务器返回 304 Not Modified. | 客户端下载文件时,如果文件没有修改,就不用重新下载 |
If-Unmodified-Since | 1. 如果文件没有被修改,则开始继续传送文件,服务器返回 200 OK;2. 如果文件被修改,则不传输,服务器返回 412 Precondition failed | 断点续传,判断文件时候被修改 |
(待续)