最近在做一个文件上传的开放接口,用到Content-Type: multipart/form-data这种请求类型,特地做了一些研究和记录。
在最初的 http协议中,并没有上传文件方面的功能。RFC1867为 http协议添加了这个能力。常见的浏览器,如 Microsoft IE, Mozila, Opera, Chrome,Safari等都已经支持。按照此规范将用户指定的文件发送到服务器。服务器端的程序如 java等,可以按照此规范解析出用户发送来的文件。
RFC1867定义的请求格式如下示例:
------ZcyOpenBoundaryEEpIo3GVWKVCPrX8Content-Disposition: form-data; name="_data_"Content-Type: text/plain; charset=UTF-8Content-Transfer-Encoding: 8bit{"fileName":"redis-use.png","bizCode":"1071"}------ZcyOpenBoundaryEEpIo3GVWKVCPrX8Content-Disposition: form-data; name="file"; filename="deaultFilename"Content-Type: application/octet-streamContent-Transfer-Encoding: binaryÿØÿàJFIFÿÛC $.' ",#(7),01444'9=82<.>342ÿÛC ÿÀ22"ÿÄÿÄ3!1Aa"Qq¡Á2BR#3¢±ÿÄÿÄ !13QAÿÚ?ÇÛ{çZ3ãçjãêµrÖåcñ÷·µO åÃKë¥Tv®ÊhíI§~2,ºFT4É÷åò©Ëë¹¢Y¼9 etúÍD=ÞØâ¡1Mº :î~þõStY)vè°l¦t¶îðâHùË=E>ÿ¤R3µ³/îEæÞb¿¸Í §\6£OJ#4Ý÷åFÀÕh_E5âw¢§ßg®÷1V¯/Å·Ô³nDÞ=9ÏÒªi ,xïS^2Ûx¦ÊF²åÐåHûÒ¬±}K;h×ZóøÂîïÝÐàx®Z4]©¦àr_Ç-yç½q4Ó2FVÎÀáïåì\Ó¯á×%½6[=Pë9lëÔcJcæ;-²½Ø'ÒPÝÈË5ÉÎ=´©%¶·ÜFc«1%±Û'SL?´íã8¢¶y@zc^]»Tm8·Ss"É1æQwGË'þÔÁ¢ÊQçYµmÁÐýimclä4§H35ÕÛlÅp4=¥,·(íA~xçO~]êjWÒªçókgëæ%¼lª&27ër[áL¤ñÌr7¦II?¹4©`Q^Í,$¤gw9î(¬ÃùÝÆSþTØÿ쯨]?WÞ«¾%b¯C]EVÊÎoþÂ:Ùê¦î,¨>¡Ìw¢Ô0K7£ô.¬:TÒ${B0ª 1E¢æUãÉéôïEQ ÿÙ...省略部分内容...------ZcyOpenBoundaryEEpIo3GVWKVCPrX8--
这里的"----ZcyOpenBoundaryEEpIo3GVWKVCPrX8"是规范中定义的boundary。http传输的内容通过boundary进行了分割,以--${boundary}开始,并以${boundary}--结尾。
明白了以上内容,我们再来看如何使用multipart/form-data进行文件上传。以HttpClient为例进行说明,其他工具大同小异。首先想到的就是要配置 http请求头信息中的Content-Type字段,没错,我们来看如何进行设置:
httpPost.addHeader("Content-Type", "multipart/form-data; boundary=----ZcyOpenBoundaryEEpIo3GVWKVCPrX8");
注意,这里multipart/form-data 后面要跟上boundary。当然,我们也可以不进行Content-Type设置,一般工具都会为我们自动生成规范的Content-Type,自动生成过程不在本次讨论范围内,读者可以自行阅读代码。
继续,我们设置了请求头中的boundary以后还要确保与代码片段1中的boundary保持一致,否则服务端无法读取到请求体信息。服务端正常情况下收到的请求是下面的样子:
当然,上图是以Spring框架为例,其他框架或语言亦大同小异。
那么怎么保证请求头中的boundary与代码片段1中的boundary一致呢?一种办法是模拟http请求手写拼接报文:
String BOUNDARY = "----ZcyOpenBoundaryEEpIo3GVWKVCPrX8";StringBuffer sb = new StringBuffer();// 发送字段for(int i=0; i sb = sb.append("--"); sb = sb.append(BOUNDARY); sb = sb.append("\r\n"); sb = sb.append("Content-Disposition: form-data; name=\""+ props[i] + "\"\r\n\r\n"); sb = sb.append(URLEncoder.encode(values[i])); sb = sb.append("\r\n");}// 发送文件:sb = sb.append("--");sb = sb.append(BOUNDARY);sb = sb.append("\r\n");sb = sb.append("Content-Disposition: form-data; name=\"1\"; filename=\"1.txt\"\r\n");sb = sb.append("Content-Type: application/octet-stream\r\n\r\n");byte[] data = sb.toString().getBytes();byte[] end_data = ("\r\n--" + BOUNDARY + "--\r\n").getBytes();// 设置HTTP头hc.setRequestProperty("Content-Type", MULTIPART_FORM_DATA + "; boundary=" + BOUNDARY);hc.setRequestProperty("Content-Length", String.valueOf(data.length + file.length + end_data.length));// 输出output = client.openOutputStream();output.write(data);output.write(file);output.write(end_data);......
当然以上方式比较原始,容易出错,我们更喜欢用高级语言。下面还是以HttpClient为例:
String result = "";String boundary ="----ZcyOpenBoundaryEEpIo3GVWKVCPrX8";try (CloseableHttpClient httpClient = HttpClients.createDefault()){ String fileName = file.getName(); HttpPost httpPost = new HttpPost(url); //设置请求头 httpPost.setHeader("Content-Type","multipart/form-data; boundary="+boundary); MultipartEntity multipartEntity = new MultipartEntity(HttpMultipartMode.STRICT, boundary, Charset.defaultCharset()); ...省略内容... httpPost.setEntity(multipartEntity); // 执行提交 HttpResponse response = httpClient.execute(httpPost); if (response.getStatusLine().getStatusCode() == 200) { //响应 HttpEntity responseEntity = response.getEntity(); if (responseEntity != null) { // 将响应内容转换为字符串 result = EntityUtils.toString(responseEntity, Charset.forName("UTF-8")); } } } catch (IOException e) { e.printStackTrace();} catch (Exception e) { e.printStackTrace();} System.out.println("result=" + result);
注意,上述代码中除了设置header头中的boundary外,还要同时设置MultipartEntity对象中的boundary,这样就保持一致啦。
至此,服务端已经可以获取到期待已久的文件流信息了。