Multipart/form-data

前言

在iOS开发中很多时候需要会涉及到上传图片等服务器交互的操作 , 这基本上全部都会使用Multipart/form-data的请求方式来完成上传 , 这需要我们去严格按照规范的格式来组装请求体 , 每一个换行每一个空格都是不可忽略的 , 虽然这些操作很繁琐 但平时我们使用的第三方网络库都会自动帮助我们完成这些处理 , 那这里为什么还要去讲如何去做呢? 答案很简单 , 因为我们不可能在任何时候都有第三方网络库去使用 , 比如你需要写一个静态库.

简介

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则是添加了分隔符等内容的构造体.

请求的头部信息如下:

Content-Type: multipart/form-data; boundary=你的自定义boundary

下面我们来大致看一下Multipart/form-data请求体的格式:

--LEE你好帅
Content-Disposition: form-data; name="UserID"  
  
lee1994
--LEE你好帅
Content-Disposition: form-data; name="imageName"; filename="imageName.png"  
Content-Type: image/png  

...contents of image.png...  
--LEE你好帅--  

其主要格式就是这样子的 , 我来讲一下每一部分都是什么含义.

我们先说一下请求头部的信息 boundary这个参数是分界线的意思 , 也就是说你在请求头中指定分界线为: LEE你好帅 , 那么请求体中凡是LEE你好帅这样的字段都会被视为分界线 , 这个分界线参数具体是什么你可以随意自定义 , 我这里只是举个例子 ⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄ 你如果要用也可以 , 建议设置的复杂一些 避免和请求体中其他字段发生重复的现象.

--+分隔符(boundary) = --LEE你好帅 :分界线 (分界线后要有换行)

接着往下看

Content-Disposition: form-data; name="UserID"  

lee1994

其中name="你的参数名" 参数名设置好后下面就是参数的值了 , 也就是lee1994 , 这里有一点非常要注意 参数名这行后面一定要有2个换行 然后才是参数的值 , 这点非常重要 一定要严格按照这个格式去写 , 别问我为什么 , 官方规定.

上面所设置的参数为文本类型 , 下面我们讲一下文件类型如何去写 这里我用图片文件举例.

分界线不用说了 每个参数前面都要有分界线

Content-Disposition: form-data; name="imageName"; filename="imageName.png"  
Content-Type: image/png  

...contents of image.png...

其中name="参数名" filename="文件名" 其中参数名这个要和接收方那边相对应 正常开发中可以去问服务器那边 , 文件名是说在服务器端保存成文件的名字 , 这个参数然并卵 , 因为一般服务端会按照他们自己的要求去处理文件的存储.

下一行是指定类型 , 我这里示例中写的是PNG图片类型 , 这个可以根据你的实际需求的写.

再往下就是设置参数值了 , 和文本类型一样 这里同样要有2个换行

...contents of image.png... 这里是image.png的二进制内容 , 如:

<ffd8ffe0 00104a46 49460001 01000048 00480000 ffe1004c 45786966 00004d4d 002a0000 00080002 01120003 00000001 00010000 87690004 00000001 00000026 00000000 0002a002 00040000 00010000 0280a003......

整个请求体拼装完成后 , 最后会以--分隔符--结尾 , 表示请求体结束 , 也就是--LEE你好帅--.

请求体格式纯注释参照:

--分隔符(boundary)[换行]
Content-Disposition: form-data; name="参数名"[换行] 
[换行]
参数值[换行]
--分隔符(boundary)[换行]
Content-Disposition: form-data; name="图片名"; filename="图片文件名"[换行]
Content-Type: 类型[换行]
[换行]
图片文件的二进制内容[换行]
--分隔符(boundary)--

多参数多文件参照:

--分隔符(boundary)
Content-Disposition: form-data; name="参数名1"  

参数值1
--分隔符(boundary)
Content-Disposition: form-data; name="参数名2"  

参数值2
--分隔符(boundary)
Content-Disposition: form-data; name="参数名3"  

参数值3
--分隔符(boundary)
Content-Disposition: form-data; name="图片名1"; filename="图片文件名1"  
Content-Type: 类型  

图片文件的二进制内容1
--分隔符(boundary)
Content-Disposition: form-data; name="图片名2"; filename="图片文件名2"  
Content-Type: 类型  

图片文件的二进制内容2
--分隔符(boundary)
Content-Disposition: form-data; name="图片名3"; filename="图片文件名3"  
Content-Type: 类型  

图片文件的二进制内容3
--分隔符(boundary)--

看到这里你大概已经了解了我们需要组装一个怎样的请求参数结构体 , 最后我附上一段Objective - C 的上传图片代码段:

//初始化请求

NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30.0f];

//================组装Body=================

//分界线的标识符

NSString *Boundary = [NSString stringWithFormat:@"LEE%u%u" , arc4random() , arc4random()];

//分界线 --LEE

NSString *LEEboundary = [[NSString alloc]initWithFormat:@"--%@" , Boundary];

//结束符 LEE--

NSString *endLEEboundary = [[NSString alloc]initWithFormat:@"%@--" , LEEboundary];

//得到图片的data

NSData* imagData = [NSData dataWithData:image];

//HTTP body的字符串

NSMutableString *body=[[NSMutableString alloc]init];

//参数的集合的所有key的集合

NSArray *keys= [parameters allKeys];

//遍历keys 组装文本类型参数

for (NSString *key in keys) {
    
    //添加分界线,换行
    
    [body appendFormat:@"%@\r\n",LEEboundary];
    
    //添加字段名称,换2行
    
    [body appendFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n",key];
    
    //添加字段的值
    
    [body appendFormat:@"%@\r\n",[parameters objectForKey:key]];

}

//添加分界线,换行

[body appendFormat:@"%@\r\n",LEEboundary];

//组装文件类型参数

[body appendFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n" , name , fileName];

//声明上传文件的格式 换2行

[body appendFormat:@"Content-Type: image/jpeg\r\n\r\n"];

//声明结束符:--LEE--

NSString *end=[[NSString alloc]initWithFormat:@"\r\n%@",endLEEboundary];

//声明requestData,用来放入http body

NSMutableData *requestData=[NSMutableData data];

//将body字符串转化为UTF8格式的二进制

[requestData appendData:[body dataUsingEncoding:NSUTF8StringEncoding]];

//将image的data加入

[requestData appendData:imagData];

//加入结束符--LEE--

[requestData appendData:[end dataUsingEncoding:NSUTF8StringEncoding]];

//设置HTTP body

[request setHTTPBody:requestData];

//================END组装Body=================

//设置HTTPHeader中Content-Type的值

NSString *content=[[NSString alloc]initWithFormat:@"multipart/form-data; boundary=%@" , Boundary];

//设置HTTPHeader

[request setValue:content forHTTPHeaderField:@"Content-Type"];

//设置Content-Length

[request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)[requestData length]] forHTTPHeaderField:@"Content-Length"];

//设置请求方式

[request setHTTPMethod:@"POST"];

/**
 NSURLSessionConfiguration(会话配置)
 
 defaultSessionConfiguration;       // 磁盘缓存,适用于大的文件上传下载
 ephemeralSessionConfiguration;     // 内存缓存,以用于小的文件交互,GET一个头像
 backgroundSessionConfiguration:(NSString *)identifier; // 后台上传和下载
 */

NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];

NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:self.upLoadQueue];

[[session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {

     //请求完成的处理巴拉巴拉..
    
}] resume];

注意:

  • 文件类型参数中 name="参数名" 一定要和服务端对应 , 开发的时候 , 可以问服务端人员.

  • 上传文件的数据部分使用二进制数据 (NSData)拼接.

  • 上边界部分和下边界部分的字符串 , 最后都要转换成二进制数据(NSData) , 和文件部分的二进制数据拼接在一起 , 作为请求体发送给服务器(完整的请求体格式上面说过).

  • 每一行末尾需要有一定的 \r\n

  • 有些服务器可以直接使用 \n , 但是新浪微博如果使用 \n 上传文件 , 服务器会返回"没有权限"的错误~ 所以还是建议使用 \r\n.

  • NSURLSession做上传请求时可以通过代码方法去获取上传的进度.

    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                                    didSendBodyData:(int64_t)bytesSent
                                     totalBytesSent:(int64_t)totalBytesSent
                           totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend;
    
    -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend{
    
        // bytesSent                 totalBytesSent                totalBytesExpectedToSend
    
        // 发送字节(本次发送的字节数)    总发送字节数(已经上传的字节数)     总希望要发送的字节(文件大小)
    
        NSLog(@"上传进度: \n发送字节:%lld - 总发送字节数:%lld - 总大小:%lld ", bytesSent, totalBytesSent, totalBytesExpectedToSend);
    
    }
    

总结:

上面所讲的请求方式虽然不常用 但我还是要说 , 第三方的库往往会给我带来方便 , 但不要过渡依赖于第三方 , 往往了解一些原始的东西 会给你的开发生涯带来意向不到的效果 .

我是LEE , 一枚有信仰的果粉Coder , 如果喜欢记得关注哦 亲 ~ 么了个哒 

推荐阅读更多精彩内容