理解Android客户端POST请求参数
我们都知道,我们的客户端通过HTTP向服务器发送的post请求实质都是在拼接一个form表单。我们一般会使用下面几种方式进行post
1. 提交参数
2. 提交文件
3. 即提交参数也提交文件
本文也将就这三种方式的请求进行分析
提交参数的请求
如我们使用OkHttp发起一个post请求,我们需要自己构建一个FormBody表单。使用方法如下:
FormBody.Builder builder = new FormBody.Builder();
builder.add("uid", "10059");
builder.add("token", "J8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-");
FormBody body = builder.build();
Request request = new Request.Builder()
.post(body)
.url(URL_BASE)
.build();
mClient.newCall(request).enqueue(new Callback() {..}
其中不重要的回调部分已经省略。可以看出我们简单拼接了一个uid和一个token的Params参数。我们通过拦截器获得请求长这个样子:
D/HttpLogInfo: --> POST https://beta.goldenalpha.com.cn/fundworks/sys/getBanners http/1.1
D/HttpLogInfo: Content-Type: application/x-www-form-urlencoded
D/HttpLogInfo: Content-Length: 48
D/HttpLogInfo: uid=10059&token=J8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-
这样的一个请求表现成form表单格式如下
<form method="post"action="https://beta.goldenalpha.com.cn/fundworks/sys/getBanners" enctype="text/plain">
<inputtype="text" name="uid" value=“10059” >
<inputtype="text" name="uid" value=“J8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-” >
</form>
----- 下面是发出去的请求的样子
POST / HTTP/1.1
Content-Type:application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
Content-Length: 48
Connection: Keep-Alive
Cache-Control: no-cache
uid=10059&token=J8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-
对于普通的Form POST请求,头信息每行一条,空行之后便是请求体 也就是我们添加的Params
Content-Length注明内容长度。
Content-Type是application/x-www-form-urlencoded,这意味着消息内容会经过URL编码,就像在GET请 求时URL里的QueryString那样。
Accept-Encoding表示是浏览器发给服务器,声明浏览器支持的编码类型也就是服务返回的时候的编码格式必须是client支持的不然就会出现乱码等情况。常见的是gzip格式。参考Accept-Encoding
Cache-Control 是缓存处理字段,服务器将会根据该字段判断此次请求是否需要进行缓存,OkHttp也可以配置该字段。但是由于Android的缓存大部分是在本地做的,所以这里也不研究这个字段, 该字段常见的取值有 private、no-cache、max-age、must-revalidate等,默认为private。 参考Cache-control
看完了请求我们再来看看请求结果是什么样的
Connection →keep-alive
Content-Encoding →gzip
Content-Type →application/json;charset=utf-8
Date →Thu, 19 Jan 2017 08:08:12 GMT
Transfer-Encoding →chunked
Vary →Accept-Encoding
{"status":200,"message":"获取banner成功","debug":null,"attachment":{"banners":[{"qbid":1,"title":"test","summary":"test","url":"","rank":1,"status":1,"image":"http://source.goldenalpha.com.cn/S0XsJR7GHaeIpz","createTime":1482407860000}]}}
可以看到响应和请求一样也包括响应头和响应体,响应头包含的信息有
Content-Encoding 响应采用的编码形式,该形式是在请求中Accept-Encoding的内容选择一种编码方式进行编码的。客户端拿到这个响应将通过该方式进行解码 参考HTTP 协议中的 Content-Encoding
Content-Type 指定请求和响应的HTTP内容类型。如果未指定 ContentType,默认为text/html。application/json是指响应体内是一个json,charset表示的是该json的编码方式为 utf-8
Transfer-Encoding →chunked 表示分块传输编码,通常,HTTP应答消息中发送的数据是整个发送的,Content-Length消息头字段表示数据的长度。然而,使用分块传输编码,数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。通常数据块的大小是一致的,但也不总是这种情况。如果一个HTTP消息(请求消息或应答消息)的Transfer-Encoding消息头的值为chunked,那么,消息体由数量未定的块组成,并以最后一个大小为0的块为结束。参考维基百科分块传输编码
空一行以后就是响应体的真正内容了
Content-Type enctype MediaType等概念
通过上述的post提交参数请求分析,我们梳理了一个请求到响应的具体过程。这样方便我们梳理更复杂的提交文件以及混合提交方式。 需要关注的是Content-Type和enctype这两个字段,因为在这3种请求中这两个字段会有所差异。其实在web中是通过enctype来规定请求表单数据的编码格式,而在Android中是通过setContentType来设置的。这两种形式在HTTP中都表现为Content-type.
这里的Content-type类型遵循了MIME协议对的传输类型。也就是我们的参数类型MediaType多媒体类型:
常见的MediaType有
1. URLencoded: application/x-www-form-urlencoded
2. Multipart: multipart/form-data
3. JSON: application/json
4. XML: text/xml
5. 纯文本: text/plain
6. 二进制流数据 application/octet-stream
参考HTTP 表单编码 enctype
一个完整的Content-type表示方式为:Content-Type: [type]/[subtype]; parameter
也就是我们在响应头中看到的样式。如果我们不已完整的形式填写。则不同的客户端会有不同的默认值。
type有下面的形式
Text:用于标准化地表示的文本信息,文本消息可以是多种字符集和或者多种格式的;
Multipart:用于连接消息体的多个部分构成一个消息,这些部分可以是不同类型的数据;
Application:用于传输应用程序数据或者二进制数据;
Message:用于包装一个E-mail消息;
Image:用于传输静态图片数据;
Audio:用于传输音频或者音声数据;
Video:用于传输动态影像数据,可以是与音频编辑在一起的视频数据格式。
subtype用于指定type的具体形式,MIME使用Internet Assigned Numbers Authority (IANA)作为中心的注册机制来管理这些值。 如果想了解到底有哪些组合方式请参考Media Types
parameter 常用于指定附加信息,一般是用于来指定文本的编码格式如UTF-8
MIME根据type制定了默认的subtype,当客户端不能确定消息的subtype的情况下,消息被看作默认的subtype进行处理。Text默认是text/plain,Application默认是application/octet-stream而Multipart默认情况下被看作multipart/mixed。
事实上我们在Android中的上传下载操作设置的contenttype多为application/octet-stream 即为二进制流。
对于multipart的请求方式,还可以单独设置part的content-type的。稍后再上传文件的时候回详细讲。
post请求提交文件
这里为什么要把上传文件但单独列出来,是因为post上传文件可以有两种方式一种是使用FileBody 一种是是使用MultipartBody 两者的不同之处是请求体中的数据类型,前者是只有文件,后者是还可以包含一些text参数。
同样贴出File请求的办法
File mfile = new File("/storage/sdcard0/alpha/image/1484209275141.jpg");
String sha1_ = MD5Util.sha1(mfile);
String size = String.valueOf(mfile.length());
String uid = "10060";
String token = "8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-";
String url = "......falphaupload/upload/upload.json";
url = url + "?" + "sha1" + "=" + sha1_ + "&" + "size" + "=" + size + "&" + "uid" + "=" + uid + "&" + "token" + "=" + token
+ "&" + "vinfo" + "=" + "AA_samsung_001_30000" + "&" + "suffix" + "=" + "jpg" + "&" + "type" + "=" + "1" + "&" + "plat" + "=" + "0";
RequestBody requestBody = RequestBody.create(MediaType.parse("application/octet-stream"), mfile);
Request request = new Request.Builder()
.post(requestBody)
.url(url)
.build();
mClient.newCall(request).enqueue(new Callback() {
Log显示:
请求
D/HttpLogInfo: --> POST https://beta.goldenalpha.com.cn/falphaupload/upload/upload.json?sha1=1F179D204C4D89533240E51EF8CCCA5D6E08A0F5&size=27748&uid=10060&token=8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-&vinfo=AA_samsung_001_30000&suffix=jpg&type=1&plat=0 http/1.1
D/HttpLogInfo: Content-Type: application/octet-stream
D/HttpLogInfo: Content-Length: 27748
D/HttpLogInfo: --> END POST (binary 27748-byte body omitted)
响应
D/HttpLogInfo: Server: nginx/1.0.15
D/HttpLogInfo: Date: Thu, 19 Jan 2017 09:54:12 GMT
D/HttpLogInfo: Content-Type: application/json;charset=UTF-8
D/HttpLogInfo: Transfer-Encoding: chunked
D/HttpLogInfo: Connection: keep-alive
D/HttpLogInfo: Vary: Accept-Encoding
D/HttpLogInfo: {"status":200,"message":"成功","debug":null,"attachment":{"mid":8426}}
D/HttpLogInfo: <-- END HTTP (72-byte body)
由log可以看出请求的Content-Type 为application/octet-stream意思就是请求体内包含的信息为二进制流数据。也就是说我们的文件被编码为二进制的形式发送给服务器。
值得注意的是,我们这里RequestBody内就添加了一个file,而没有其他的key value,然后我们使用MediaType.parse("application/octet-stream")
设置我们请求体的Content-Type。我们可以尝试把该字段改成其他的类型如image/jpeg,但是请求不会成功。其他的Content—Type类型可以参考Content—Type对照表
Post上传文件及参数
上述请求的时候我们的请求体只携带了一个文件,而且只能携带一个。如果我们单纯的想上传一个图片或者文件的时候我们可以通过上述方法构建一个请求体。但是日常的开发中我们这样的需求并不能满足我们的需求,而且我们也不希望手动拼接如上的url这么多参数。那么采用multipart/form-data
的请求方式就此诞生了。
我们发送一个multipart/form-data
的表单格式大体如下:
<form method="post"action="url" enctype=”multipart/form-data”>
<inputtype="text" name="desc">
<inputtype="file" name="pic">
</form>
其中的enctype就是我们上述所说的content-type属性。那么我们通过拦截器拦截请求后大体会看出该请求的数据为:
POST HTTP/1.1
Accept-Language: zh-cn,zh;q=0.5
Accept-Charset: GBK,utf-8
Connection: keep-alive
Content-Length: 60408
Content-Type:multipart/form-data; boundary=ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
Content-Disposition: form-data;name="desc"
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[......][......][......][......]...........................
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
Content-Disposition: form-data;name="pic"; filename="photo.jpg"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
[图片二进制数据]
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC--
我们来分析一下上述数据的内容:
我们可以看multipart的请求体有多块数据构成,每一块都有他自己的Content-Type,Content-Transfer-Encoding,Content-Disposition。而且在请求头中我们也发现了boundary这个字段。--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC--
看起来像分隔线的东西就好像是boundary的值。
这个boundary根据RFC 1867定义,是一段数据的“分割边界”,这个“边界数据”不能在内容其他地方出现,一般来说使用一段从概率上说“几乎不可能”的数据即可。不同的浏览器及webkite实现的方式不太相同,但是这个数据是webkit随机生成的。而不是遍历请求体后生成的,虽然是随机生成,但是绝大多数条件下他也是不能和请求体中的字节重复的。如果你发现有重复的话,那么请去买彩票。
选择了这个边界之后,webkite便把它放在Content-Type 里面传递给服务器,服务器根据此边界解析数据。下面的数据便根据boundary划分段,每一段便是一项数据。
每个field被分成小部分,而且包含一个value是”form-data”的”Content-Disposition”的头部;一个”name”属性对应field的名称,文件的话还会多出一个filename的属性。
接下来我们看下在OkHttp中该请求应该如何构造:
File mFile = new File("/storage/sdcard0/alpha/image/1484209275141.jpg");
String sha1_ = MD5Util.sha1(mFile);
String size = String.valueOf(mFile.length());
String uid = "10060";
String token = "8ihCfw6JniZnUsxpiUPM9iKtymuZ-p-";
String url = "..../falphaupload/upload/upload.json";
MultipartBody.Builder builder = new MultipartBody.Builder();
builder.addFormDataPart("sha1",sha1_);
builder.addFormDataPart("size",size);
builder.addFormDataPart("suffix","jpg");
builder.addFormDataPart("type","1");
builder.addFormDataPart("uid",uid);
builder.addFormDataPart("plat","0");
builder.addFormDataPart("token",token);
builder.addFormDataPart("vinfo","AA_samsung_001_30000");
builder.addFormDataPart("temp.jpg",mFile.getAbsolutePath(),RequestBody.create(MediaType.parse("application/octet-stream"),mFile));
MultipartBody multipartBody = builder.build();
Request request = new Request.Builder()
.post(multipartBody)
.url(url)
.build();
mClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
}
});
我们看到OkHttp提供给你么MultipartBody的构造器Builder,我们通过构造器可以调用addFormDataPart方法来构建我们的表单。该方法有如下两个重载方法。
/** Add a form data part to the body. */
public Builder addFormDataPart(String name, String value) {
return addPart(Part.createFormData(name, value));
}
/** Add a form data part to the body. */
public Builder addFormDataPart(String name, String filename, RequestBody body) {
return addPart(Part.createFormData(name, filename, body));
}
接下来Part.createFormData应该就是构造我们对应的上述表单数据中的每一段数据了,进入源一看果真不出意外,form-data
name
Content-Disposition
这几个熟悉的字样就映入眼帘了。至于具体怎么拼接的有兴趣的可以自己在深入研究,毕竟我们已经知道他最终的样子。
public static Part createFormData(String name, String filename, RequestBody body) {
if (name == null) {
throw new NullPointerException("name == null");
}
StringBuilder disposition = new StringBuilder("form-data; name=");
appendQuotedString(disposition, name);
if (filename != null) {
disposition.append("; filename=");
appendQuotedString(disposition, filename);
}
return create(Headers.of("Content-Disposition", disposition.toString()), body);
}
为什么会写这篇文章
至于为什么要整理这篇文章,是因为最近询问我使用OkHttp上传图片或者文件的小伙伴有点多,因为现在的项目的框架,还是我们公司的老大自己搭的HttpClient的框架,对于OkHttp的了解也不多,所以就研究了一下,以后也可能使用这个框架来替代掉公司的项目框架。
其中有一个同学也问了我使用Client上传图片和使用OkHttp上传图片的区别。那么就以他们公司的上传框架代码分析下如何从HttpClient 转到 OkHttp
下面是使用HttpClient构造请求的方法:
HttpClient包中各种各样的body: FileBody StringBody MultiBody 其实就是对应了常用的三种请求方式。而这几个类在OkHttp中也能找到他的身影,我们知道OkHttp的RequestBody有三种构造参数:
public static RequestBody create(MediaType contentType, String content) {}
— Stringbodypublic static RequestBody create(final MediaType contentType, final File file) {}
— FileBody而MultiBody 在OkHttp3.0以后单独出来一个RequestBody的子类,它的
public Builder addFormDataPart(String name, String filename, RequestBody body) {}
以及public Builder addFormDataPart(String name, String value) {}
通过这两个方法 我们基本上可以构造任何请求体。
下面来看下具体实现:
HttpEntity entity = null;
HttpPost httpPostUpLoadFile = new HttpPost(uri);
//这里拼接了一个Http请求的请求头 就是我们上边分析的 Accept-Charset: GBK,utf-8 Content-Type:multipart/form-data; boundary=
httpPostUpLoadFile.addHeader("Accept", "application/json");
// boundary可以是任意字符,但是必须和MultipartEntity的boundary相同,否则就会报错
httpPostUpLoadFile.addHeader("Content-Type", "multipart/form-data;boundary=-");
try {
// 与header的boundary一致,否则报错
MultipartEntity multiEntity = new MultipartEntity(HttpMultipartMode.STRICT, "-", Charset.forName(HTTP.UTF_8));
//拼接params参数 在Client包中称作StringBody请求体 其实就是Content—Type为text/plan的字段 new Stringbody对应的请求头内容
//Content-Disposition: form-data;name="desc"
//Content-Type: text/plain; charset=UTF-8
for (Iterator<String> iter = params.keySet().iterator(); iter.hasNext(); ) {
String name = iter.next();
String value = params.get(name);
multiEntity.addPart(name, new StringBody(value, Charset.forName(HTTP.UTF_8)));
}
//这里是上传文件
if (files != null) {
for (int i = 0; i < files.size(); i++) {
File file = new File(files.get(i));
//拼接一个文件的请求体 在client中被称为FileBody 也就是Content-Type为application/octet-stream的请求体
//Content-Disposition: form-data;name="pic"; filename="photo.jpg"
//Content-Type: application/octet-stream
//Content-Transfer-Encoding: binary
ContentBody cbFile = new FileBody(file);
//addpart的两个参数一个是filename 一个是file 请求体
multiEntity.addPart("fjList" + "[" + i + "].file", cbFile);
}
}
if (images != null) {
// 图片参数 图片的话求file是相同的了唯一不同的就是FileBody的Content—Type可以更进一步的指定为image/jpeg 其实实质也是而二进制传输,
// 但是这个字段需要跟服务器约定好,如果服务器解析的image/jpeg你传application/octet-stream服务器可能解析不到
for (int i = 0; i < images.size(); i++) {
File f = new File(images.get(i));
if (f.exists()) {
FileBody fp = new FileBody(f, "image/jpeg");
multiEntity.addPart("fjList" + "[" + i + "].file", fp);
}
}
}
//接下来就是发送请求 这里不再赘述了
httpPostUpLoadFile.setEntity(multiEntity);
// 创建客户端
HttpClient httpClient = getNewHttpClient();
// 执行请求获得响应
HttpResponse response = httpClient.execute(httpPostUpLoadFile);
// 解析响应对象
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
entity = response.getEntity();
String json = "";
if (entity != null) {
json = EntityUtils.toString(entity, "utf-8");
System.out.println(json);
return json;
}
}
} catch (Exception e) {
System.out.println(e.toString());
}
return uri;
这里是修改为OkHttp后的请求操作,主要区别就是在与请求体的封装方法。
public static String getEntity2(String uri, Map<String, String> params, ArrayList<String> images, ArrayList<String> files) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
OkHttpClient okHttpClient = builder.build();
MultipartBody.Builder multiBuilder = new MultipartBody.Builder("-");
//添加key value 请求体
for (Iterator<String> iter = params.keySet().iterator(); iter.hasNext(); ) {
String name = iter.next();
String value = params.get(name);
multiBuilder.addPart(RequestBody.create(MediaType.parse("text/plain"), value)); //对于text/plain okhttp默认的Charset就是UTF-8所以不用手动设置
//multiBuilder.addFormDataPart(name,value); 这种方式也可以 为了方便理解写成上边的添加请求体方式
}
//这里是上传文件
if (files != null) {
for (int i = 0; i < files.size(); i++) {
File file = new File(files.get(i));
//这两种方式都可以,但是服务器对于file的名字有要求的时候就需要用第一种了
multiBuilder.addFormDataPart("fjList" + "[" + i + "].file", file.getAbsolutePath(), RequestBody.create(MediaType.parse("application/octet-stream"), file));
//multiBuilder.addPart(RequestBody.create(MediaType.parse("application/octet-stream"),file));
}
}
if (images != null) {
for (int i = 0; i < images.size(); i++) {
File f = new File(images.get(i));
if (f.exists()) {
multiBuilder.addFormDataPart("fjList" + "[" + i + "].file", f.getAbsolutePath(), RequestBody.create(MediaType.parse("image/jpeg"), f));
}
}
}
//构造请求体完成
MultipartBody multipartBody = multiBuilder.build();
Request.Builder requestBuilder = new Request.Builder();
// okHttp的请求头是在Request.Builder添加的
requestBuilder.addHeader("Accept", "application/json");
requestBuilder.addHeader("Content-Type", "multipart/form-data;boundary=-");
Request request = requestBuilder
.post(multipartBody)
.url(uri)
.build();
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
//响应失败操作
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//响应完成操作
}
});
return null;
}