前言
最近在做一个博客的小项目,需要用到文件上传,HttpClient又被Android给弃用了,图片框架暂时还没学。只能使用HttpURLConnection来上传。折腾了好久,今天终于顺利地跟后台完成了对接。因此,写这篇博客梳理一下知识。
理论知识
背景
最早的HTTP POST是 不支持 文件上传的,给编程开发带来很多问题。但是在1995年,ietf出台了rfc1867,也就是《RFC 1867 -Form-based File Upload in HTML》,用以支持文件上传。所以Content-Type的类型扩充了multipart/form-data用以支持向服务器发送二进制数据。因此发送post请求时候,表单属性enctype共有二个值可选,这个属性管理的是表单的MIME编码:
①application/x-www-form-urlencoded( 注:不设置enctype属性时默认为①)
②multipart/form-data
POST的报文请求分析
使用浏览器进行post请求将会发送以下数据:
//我是请求头
POST /t2/upload.do HTTP/1.1
Accept-Charset: GBK,utf-8;
Connection: keep-alive
Content-Length: 60408
Content-Type:multipart/form-data; boundary=ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC //设置内容类型为表单类型,同时定义了boundary “界限标识”
Host: w.sohu.com
//这里开始请求体的地盘啦,第一条请求体的实体数据(字符串参数)
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC //这里是"--"+boundary
Content-Disposition: form-data;name="xxx" //name="xxx", xxx为要发送的参数名
Content-Type: text/plain; charset=UTF-8 //设置内容类型为text 编码格式为utf-8
Content-Transfer-Encoding: 8bit
//这里是一个空行(不可少)
116.361545 // 我勒个去(到这里[空行之后的一行]才能写上xxx的参数值)有点坑是吧,我也觉得
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC //这一大串还是"--"+boundary
//第二条请求体的实体数据(图片文件上传)
Content-Disposition: form-data;name="pic"; filename="photo.jpg" //指定了文件
Content-Type: application/octet-stream //设置了内容类型为application/octet-stream
Content-Transfer-Encoding: binary
//还是一个空行(不可少)
[这里是图片二进制数据]
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC--
Boundary说明
根据RFC 1867定义,我们需要选择一段数据作为作为请求参数之间的“界限标识” (即boundary属性),这个“边界数据”不能在内容其他地方出现,一般来说使用一段从概率上说“几乎不可能”的数据即可。
不同浏览器的实现不同
火狐某次post的 boundary=---------------------------32404670520626,
operade某次post的 boundary=----------E4SgDZXhJMgNE8jpwNdOAX
例如参数1和参数2之间需要有一个明确的界限,这样服务器才能正确的解析到参数1和参数2。但是分隔符并不仅仅是boundary,而是下面这样的格式:–+ boundary。
如:boundary为ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC,那么参数分隔符则为:
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
不管boundary本身有没有这个”--“(前缀),这个前缀都是不能省略的。
最后--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC--为结束标识
注:以上内容整理自
Multipart/form-data POST文件上传详解
HTTP POST请求报文格式分析与Java实现文件上传
\r (回车) 与 \n (换行)
‘\r’ 回车,回到当前行的行首,而不会换到下一行,如果接着输出的话,本行以前的内容会被逐一覆盖;
‘\n’ 换行,换到当前位置的下一行,而不会回到行首;
所以在写完每一行数据之后要使用 \r\n才能达到切换至下一行行首的效果
实例
private static final int TIME_OUT = 8 * 1000; //超时时间
private static final String CHARSET = "utf-8"; //编码格式
private static final String PREFIX = "--"; //前缀
private static final String BOUNDARY = UUID.randomUUID().toString(); //边界标识 随机生成
private static final String CONTENT_TYPE = "multipart/form-data"; //内容类型
private static final String LINE_END = "\r\n"; //换行
/**
* post请求方法
* */
public static void postRequest(final Map<String, String> strParams, final Map<String, File> fileParams) {
new Thread(new Runnable() {
@Override
public void run() {
HttpURLConnection conn = null;
try {
URL url = new URL(requestUrl);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setReadTimeout(TIME_OUT);
conn.setConnectTimeout(TIME_OUT);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);//Post 请求不能使用缓存
//设置请求头参数
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("Charset", "UTF-8");
conn.setRequestProperty("Content-Type", CONTENT_TYPE+";boundary=" + BOUNDARY);
/**
* 请求体
*/
//上传参数
DataOutputStream dos = new DataOutputStream(conn.getOutputStream());
//getStrParams()为一个
dos.writeBytes( getStrParams(strParams).toString() );
dos.flush();
//文件上传
StringBuilder fileSb = new StringBuilder();
for (Map.Entry<String, File> fileEntry: fileParams.entrySet()){
fileSb.append(PREFIX)
.append(BOUNDARY)
.append(LINE_END)
/**
* 这里重点注意: name里面的值为服务端需要的key 只有这个key 才可以得到对应的文件
* filename是文件的名字,包含后缀名的 比如:abc.png
*/
.append("Content-Disposition: form-data; name=\"file\"; filename=\""
+ fileEntry.getKey() + "\"" + LINE_END)
.append("Content-Type: image/jpg" + LINE_END) //此处的ContentType不同于 请求头 中Content-Type
.append("Content-Transfer-Encoding: 8bit" + LINE_END)
.append(LINE_END);// 参数头设置完以后需要两个换行,然后才是参数内容
dos.writeBytes(fileSb.toString());
dos.flush();
InputStream is = new FileInputStream(fileEntry.getValue());
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) != -1){
dos.write(buffer,0,len);
}
is.close();
dos.writeBytes(LINE_END);
}
//请求结束标志
dos.writeBytes(PREFIX + BOUNDARY + PREFIX + LINE_END);
dos.flush();
dos.close();
Log.e(TAG, "postResponseCode() = "+conn.getResponseCode() );
//读取服务器返回信息
if (conn.getResponseCode() == 200) {
InputStream in = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line = null;
StringBuilder response = new StringBuilder();
while ((line = reader.readLine()) != null) {
response.append(line);
}
Log.e(TAG, "run: " + response);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if (conn!=null){
conn.disconnect();
}
}
}
}).start();
}
/**
* 对post参数进行编码处理
* */
private static StringBuilder getStrParams(Map<String,String> strParams){
StringBuilder strSb = new StringBuilder();
for (Map.Entry<String, String> entry : strParams.entrySet() ){
strSb.append(PREFIX)
.append(BOUNDARY)
.append(LINE_END)
.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"" + LINE_END)
.append("Content-Type: text/plain; charset=" + CHARSET + LINE_END)
.append("Content-Transfer-Encoding: 8bit" + LINE_END)
.append(LINE_END)// 参数头设置完以后需要两个换行,然后才是参数内容
.append(entry.getValue())
.append(LINE_END);
}
return strSb;
}