1、背景
最近在使用一个okhttp网络框架时,在拦截器中打印post数据发现数据均经过了编码,打印出来的是类似如下的字符
loginName%22%3A%22hello14%22%2C%22mymac%22%3B%2294%3Abe%3A46%3A15%3A15%3A20%22%2C%22pId%22%3A%225602923243413443840%22%2C%22
对比原数据发现,只有其中部分数据被编码过了。 为什么会这样?
2、Content-Type与数据编码
后来查看其源码发现有如下代码:
requestContentType = "application/x-www-form-urlencoded; charset=UTF-8"
if (Util.isNotEmptyMap(apiRequest.getFormParams())) {
requestBody = RequestBody.create(MediaType.parse(requestContentType), buildParamString(apiRequest.getFormParams()));
}
private static String buildParamString(Map<String, String> params) {
StringBuilder res = new StringBuilder();
if (null != params && params.size() > 0) {
String key;
try {
// 改写一下
Iterator<String> it = params.keySet().iterator();
while (it.hasNext()) {
key = (String)it.next();
res.append(key).append("=").append(URLEncoder.encode((String)params.get(key), "UTF-8"));
res.append("&");
}
} catch (UnsupportedEncodingException ex) {
}
}
int len = res.toString().length();
return res.toString().substring(0, len - 1);
}
代码中指定了Content-Type类型为 application/x-www-form-urlencoded。
上面的代码主要作用是将参数用key=value&的方式连接,其中value经过了URLEncoder.encode()方法进行编码。
URLEncoder.encode()方法的说明如下:
Translates a string into {@code application/x-www-form-urlencoded}
format using a specific encoding scheme. This method uses the
supplied encoding scheme to obtain the bytes for unsafe
characters.<p> <em><strong>Note:</strong> The <a href= "http://www.w3.org/TR/html40/appendix/notes.html#non-ascii-chars"> World Wide Web Consortium Recommendation</a> states that
UTF-8 should be used. Not doing so may introduce
incompatibilities.</em>
该方法说明了会将字符串转化成application/x-www-form-urlencoded格式的。所以上面的编码是因为设置了http的Content-Type请求头而进行的格式转换。
在以表单形式提交数据时,默认的数据格式是 application/x-www-form-urlencoded 。
3、关于URL编码
主要的规则是将非ascii码编码为ascii码。具体的编码规则参见维基百科以及阮一峰的《关于URL编码》。
部分引用如下:
当HTML表单中的数据被提交时,表单的域名与值被编码并通过HTTP的GET或者POST方法甚至更古远的email[2]把请求发送给服务器。这里的编码方法采用了一个非常早期的通用的URI百分号编码方法,并且有很多小的修改如换行规范化以及把空格符的编码"%20"替换为"+" 。按这套方法编码的数据的MIME类型是application/x-www-form-urlencoded,当前仍用于(虽然非常过时了)HTML与XForms规范中。此外,CGI规范包括了web服务器如何解码这类数据、利用这类数据的内容。
如果发送的是HTTP GET请求,application/x-www-form-urlencoded数据包含在所请求URI的查询成分中。如果发送的是HTTP POST请求或通过email,数据被放置在消息体中,媒体类型的名字被包含在消息的Content-Type头内部。
4、Okhttp的FormBody提交方式分析
在okhttp中提供了FormBody类来提交表单数据。 基本用法如下:
public void testPostFormData() {
// 先定义请求体,是FormBody类型
RequestBody body = new FormBody.Builder().add("name", "张三丰")
.add("age", "99")
.build();
Request request = new Request.Builder()
.url("http://httpbin.org/post")
.post(body)
.build();
mOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
Log.i(TAG, "onFailure: " + e.getMessage());
}
@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
Log.i(TAG, "onResponse: " + response.body().string());
}
});
}
在FormBody类中定义了Content-Type,如下图:
既然申明了数据类型,那么提交的数据就要转化为URL编码的。
继续查看FormBody.Builder()类的add方法源码,果然将数据进行了URL编码,如下:
// 其中在String上的扩展函数canonicalize实现了url规则编码
fun add(name: String, value: String) = apply {
names += name.canonicalize(
encodeSet = FORM_ENCODE_SET,
plusIsSpace = true,
charset = charset
)
values += value.canonicalize(
encodeSet = FORM_ENCODE_SET,
plusIsSpace = true,
charset = charset
)
}
故可以看出,okhttp的FormBody的Content-Type是application/x-www-form-urlencoded 且数据经过了URL编码。只是okhttp中的编码没有使用java中的URLEncoder类,而是框架自已处理的。
5、http的Content-Type类型
对于每个http请求都会有一个Content-Type,来告知服务端数据的类型。通过打印请求头我们也可以发现这一点。这个是http协议的规范内容。但是一般Content-Type的可取值有哪些呢?
根据数据类型的不同,Content-Type的取值也不同。 如提交一个纯文本文件时,值为text/plain。而提交图片时,则是image/jpeg等。
从该博客摘取部分类型如下,具体可以参考此篇文章。
6、常见的提交方式
我们可以结合使用postman和charles查看不同提交方式的请求头及请求体相关的信息来加深理解。 在postman中提供了多种方式提交数据,如下:
如果选中以x-www-form-urlencoded方式提交数据,那么在header列会自动设置成 application/x-www-form-urlencoded。 如下:
使用 application/x-www-form-urlencoded 提交数据时请求信息分别如下:
通过修改提交类型,可以方便查看到不同的Content-Type类型。
7、参考
感谢作者分享。
1、http字段头参考
2、post使用form-data和x-www-form-urlencoded的本质区别
3、四种常见的 POST 提交数据方式
4、stackoverflow