在前面说完了 URL 中的编码与乱码( 上 , 下 ), 也为本章节谈论的主题, 关于表单(form)以 get 方式提交时的编码与乱码问题打下了一个良好的基础.
事实上, 表单以 get 方式提交就是把表单中的数据拼凑在 url 中提交到后台, 也因此与 url 中的编码有着非常紧密的关系, 可以说这两种方式是极为类似的.
get 方式提交的表单与 urlencoded
依旧是从一个简单的例子开始去探讨. 假如有以下表单:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>表单 get 提交页(页面编码 utf8)</title>
</head>
<body>
<h2>get 提交, 页面编码: UTF-8</h2><hr>
<form action="form_get_target_default.jsp">
<input name="english" value="hi">
<input name="chinese" value="你好">
<input type="submit" value="submit">
</form>
</body>
</html>
其中有两个数据项, 一个英文, 一个中文, 为方便测试, 值直接写死在 value
中, 免得要录入.
还有一个 type
为 submit
的按钮用于提交.
action
的值设置表单要提交到的目标页面, 因为打算在目标页面取出表单提交的值并显示, 这里用了一个动态的 jsp 页面来处理, 具体逻辑如下:
<%@ page pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>表单 get 接受页(缺省)</title>
</head>
<body>
<h3>request keep default</h3>
${param.english} ${param.chinese}
</body>
</html>
就是利用 EL
表达式把表单中提交过来的值简单取出显示.
这里的 el 表达式
${param.english}
等价于 jsp 表达式 :
<%=request.getParameter("english") %>
关于 el 表达式, 如果读者不熟悉请自行查阅资料了解. 对于支持 el 表达式的容器, 这样的写法显得更为简短直接.
把以上代码部署在 tomcat8 上, 打开表单页面:
点击提交, 结果如下:
显示是正常的, 参数的值也正常取出来了. 此时请注意地址栏中的部分, 回想一下上一篇章中谈到的 url 中的查询字符串, 是不是非常类似呢?
查看其 request header:
可以看到转义字符串 "%E4%BD%A0%E5%A5%BD", 六个字节, 编码为 utf-8, 也就是页面的编码.
其实在 header 中还可以查看 查询字符串参数(Queue String Parameters):
点击右边的 "view source" 或 "view URL encoded":
可以看到原始的值(source), 其实也就是 URL encoded 后的值.
表单的 method 和 enctype 属性
具体来说, 表单提交其实有两种方式, get
和 post
, 可以通过 method
属性指定. 比如下面是一个 post 方式提交的表单:
<form action="xxx.jsp" method="post">...</form>
而这样则是 get 方式:
<form action="xxx.jsp" method="get">...</form>
你可能注意到前面的表单是没有指定 method 属性, 那么则使用缺省方式, 在 html 规范中, 缺省即为 get 方式.
具体见 html 4.0.1 规范关于 form 表单属性 method 的描述:
method = get|post
This attribute specifies which HTTP method will be used to submit the form data set.
Possible (case-insensitive) values are "get" (the default) and "post".
See the section on form submission for usage information.
参考: https://www.w3.org/TR/html401/interact/forms.html#adef-method
其实表单中还有另外一个重要属性, 也就是 enctype
, 它是 encoding type 的缩写, 意思即为"编码类型"(或"内容类型(content type)").
具体见 html 4.0.1 规范关于 form 表单属性 enctype 的描述:
enctype= content-type
This attribute specifies the content type used to submit the form to the server (when the value of method is "post").
The default value for this attribute is "application/x-www-form-urlencoded".
The value "multipart/form-data" should be used in combination with the
INPUT
element, type="file".参考: https://www.w3.org/TR/html401/interact/forms.html#adef-enctype
具体也有两个值:
application/x-www-form-urlencoded
multipart/form-data
缺省即为 application/x-www-form-urlencoded
. 所以, 前面例子中的表单等价为:
<form action="form_get_target_default.jsp" method="get" enctype="application/x-www-form-urlencoded">...</form>
这个所谓的 urlencoded 的方式正是考虑到 get 方式提交时的特殊要求而设计的.
注意: get 方式只使用 application/x-www-form-urlencoded 方式, 指定为 multipart/form-data 无效;
multipart 方式只用于 post 方式, 在有文件上传的情况下使用(form 中包含 input type 为 file 的情况).
综上, 表单 get 方式的提交就是使用 urlencoded 把表单数据以 url 查询字符串的形式放到 url 末尾发送到服务端.
关于 application/x-www-form-urlencoded 方式的一个更为详细的描述参见 html 规范:
application/x-www-form-urlencoded
This is the default content type. Forms submitted with this content type must be encoded as follows:
Control names and values are escaped. Space characters are replaced by
+
, and then reserved characters are escaped as described in [RFC1738], section 2.2: Non-alphanumeric characters are replaced by%HH
, a percent sign and two hexadecimal digits representing the ASCII code of the character. Line breaks are represented as "CR LF" pairs (i.e.,%0D%0A
).The control names/values are listed in the order they appear in the document. The name is separated from the value by
=
and name/value pairs are separated from each other by&
.参考: https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1
具体内容这里也不一一翻译了, 跟上一篇章中说到的 uri 中对 query component 编码的方式是类似的.
简单地说, 就是:
- "不是字母数字的字符(Non-alphanumeric character)" 就要转义成
%HH
的形式, - 键值对用
=
分隔 - 多个键值对间用
&
分隔.
有些细节方面稍微提下, 比如:
- 表单中的空格用加号
+
代替, 而 uri 中对空格是用转义的%20
来表示的;- 换行用
CR LF
表示因为这些跟这里讨论的编码与乱码主题关系不大, 这里也只是稍微提下.
提交过程的详情参考 html 规范中关于 get 部分的描述:
Step four: Submit the encoded form data set
Finally, the encoded data is sent to the processing agent designated by the action attribute using the protocol specified by the method attribute.
This specification does not specify all valid submission methods or content types that may be used with forms. However, HTML 4 user agents must support the established conventions in the following cases:
If the method is "get" and the action is an HTTP URI, the user agent takes the value of action, appends a
?
to it, then appends the form data set, encoded using the "application/x-www-form-urlencoded" content type. The user agent then traverses the link to this URI. In this scenario, form data are restricted to ASCII codes.If the method is "post" and the action is an HTTP URI, the user agent conducts an HTTP "post" transaction using the value of the action attribute and a message created according to the content type specified by the enctype attribute.
参考: https://www.w3.org/TR/html401/interact/forms.html#h-17.13.3.4
内容较多, 这里也不打算一一去翻译了, 简单讲就是将上一步骤得到的值接在 uri 后面, 以问号分隔符"?"隔开. 这样一来就得到了在前面请求头中看到的最终效果:
http://localhost:8080/jcc-web/demo/encoding/form/get/form_get_target_default.jsp?english=hi&chinese=%E4%BD%A0%E5%A5%BD
表单中的 accept-charset 属性
那么, 就字符集编码这个问题来说, 是否就可以得出结论, 表单 get 提交就是使用页面本身的编码呢? 其实还没有那么简单.
表单中还有一个属性, 名为 accept-charset
, 意为 "接受的字符集".
在 html 规范中对它的描述这样的:
accept-charset
= charset listThis attribute specifies the list of character encodings for input data that is accepted by the server processing this form.
The value is a space- and/or comma-delimited list of charset values.
The client must interpret this list as an exclusive-or list, i.e., the server is able to accept any single character encoding per entity received.
The default value for this attribute is the reserved string "UNKNOWN". User agents may interpret this value as the character encoding that was used to transmit the document containing this FORM element.
参考: https://www.w3.org/TR/html401/interact/forms.html#adef-accept-charset
简单地说, 如果在这里指定了一个字符集, 那么表单优先采用它来编码要提交的数据;没有的话, 才用文档本身的编码.
比如, 如下指定表单使用 gbk 作为提交数据的编码:
<form action="xxx.jsp" method="get" accept-charset="gbk">...</form>
哪怕这个页面本身的编码不是 gbk 而是 utf-8, 表单本身依旧使用 gbk 编码提交的数据.
比如, 将前面的例子中增加这一属性:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>表单 get 提交页(页面编码 utf8)</title>
</head>
<body>
<h2>get 提交, 页面编码: UTF-8</h2><hr>
<form action="form_get_target_default.jsp" accept-charset="gbk">
<input name="english" value="hi">
<input name="chinese" value="你好">
<input type="submit" value="submit">
</form>
</body>
</html>
再提交表单, 发现乱码了:
此时再看地址栏, 发现中文转义后的编码是 "%C4%E3%BA%C3", 四个字节, 明显是 gbk 的编码.
注: 当转义编码不是 utf-8 时, chrome 浏览器地址栏中不会将它转换成中文字符显示.
而前面说过, tomcat8 默认用 utf-8 去解码整个接收到的 uri, 与 uri 实际所用的编码不一致, 所以就乱码.
假如你打算在表单中使用 accept-charset="gbk"
, 那么相应的, 就要把 port 为 8080 的 connector 处的 URIEncoding 的值也调整为 gbk:
<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443" URIEncoding="GBK"/>
具体调整位置见 前面篇章 中的介绍.
这样再测试, 结果就 OK 了:
可以看到地址栏处的编码确实为 gbk 的, 而 el 表达式所取出的值也是正常的.
多个 accept-charset 值的情况
如果仔细阅读上述提到的规范, 可以知道 accept-charset
里允许指定多个值供浏览器选择, 以防止某些指定的编码浏览器不支持, 多个值之间使用空格或逗号隔开. 比如下面指定了两个编码:
<form action="xxx.jsp" method="get" accept-charset="gbk,utf-8">...</form>
意思就是说, 如果你浏览器支持 gbk 编码, 你就用 gbk 编码;假如不支持 gbk, 就用 utf-8 编码;假设有更多编码选项, 依次类推.
当然, 这样就会给后台服务端的处理提出了难题, 因为你不清楚提交过来的到底是那种编码, 可能两种都有, 你还要在服务端去检测那些字节流, 猜测它的可能编码, 这其实挺困难的.
如果是这样, 以 tomcat 为例, 就不能简单的设置 URIEncoding 为某个值了. 一种 hack 的方式是你可以把它设置成 iso-8859-1, 先确保它按原样接收上来, 交到你的程序里再去处理, 但即使这样, 也还是不好办, 主要是猜测编码还是不好弄的.
这里也不具体介绍这种 hack 的形式, 想了解的同学可以参考这篇 文本在内存中的编码(3) 中的一些介绍.
所以, 总的来说, 还是简单处理好一点, 别搞出太多选项来. 如果有可能, 就在各个环节统一使用 utf-8 编码.
总结
总结一下表单以 get 方式提交时的编码与乱码问题:
- 表单的缺省提交方式是 get, 或者使用 method="get" 显式指定为 get 方式.
- get 的方式以 application/x-www-form-urlencoded 形式编码, 表单数据以查询字符串键值对的方式接在 uri 后面提交.
- urlencoded 编码数据时使用的字符集由 accept-charset 属性指定, 假如没有指定, 就用页面本身的编码.
- 服务端解码 uri 时所用的编码要与浏览器端编码时所用的一致, 否则, 将发生乱码.
关于表单以 get 方式提交时的编码与乱码问题的介绍就到这里, 下一篇, 将接着介绍以 post 方式提交时的编码与乱码问题.