表单(form) post 方式提交时的编码与乱码(下)

探讨了表单以 post 方式,enctype 为 multipart/form-data 提交时数据所使用的字符集编码(包含缺省使用页面编码及设置了 accept-charset 时两种情形),包括了上传文件及使用中文文件名时的情况,以及后台的接收处理。

目录
[隐藏]

在上一篇中提到,post 方式按 enctype 的不同,分成两种情况,一种是 application/x-www-form-urlencoded,前面已经分析过了,这一篇则讨论剩下的那种:multipart/form-data。

为什么需要 multipart/form-data 类型?

虽然在很多的情况下,都可以使用缺省的 application/x-www-form-urlencoded  方式,但它是有缺点的,比如它编码的效率不高,因为它采用了低效的转义表示法。

举个例子来说,“你好”两字用 gbk 编码只需要 4 个字节:C4 E3 BA C3。如下:

你好 gbk 编码 十六进制

而如果表示成转义的形式呢?是这样的“%C4%E3%BA%C3”,这一串整整有 12 个 ASCII 字符,一个 ASCII 字符就要用一个字节来表示,所以转义形式需要 12 字节,是非转义形式的三倍:

你好 gbk urlencoded 转义编码 十六进制

可以看到光是百分号(%,ASCII 为 0x25)本身就占了 4 个字节了。

另外的情况是,有时表单上还有上传文件的需求,这时候 HTML 规范要求只能使用 multipart/form-data 形式。

post 方式以 multipart/form-data 类型提交的一个示例

和之前类似,通过具体的示例来探究 multipart 时的编码。先构建一个这样的表单:

form post enctype multipart/form-data type file code

页面本身编码为 utf-8,表单 method 为 post,enctype 为 multipart/form-data,表单中含有一个类型为 file 的 input 项用作为文件上传,提交到后台的一个叫 upload 的 servlet 上。

准备的上传文件就是上面提到的那个,名为 “你好.txt”,文件内容就是两个简单的汉字“你好”,文件本身用 gbk 编码。

你好.txt

所以文件本身大小为 4 字节,字节内容具体为:C4 E3 BA C3。

以 multipart/form-data 类型提交时的编码

将以上程序部署并在浏览器打开,选中要上传的文件“你好.txt”,准备提交:

form post enctype multipart/form-data type file code 浏览器页面

在提交之前,打开“开发者工具”,然后点击提交以截获其具体的请求,在 header 中的 Request Payload 下可以看到提交的具体情况:

Request Payload form post enctype multipart/form-data type file 中文字段 中文文件名 中文内容

从中能够发现一些细节,比如中文没有进行转义的 urlencoded(不过因此也看不出来它到底是采用何种字符集编码,因为直接显示为字符);另外这里只显示了文件名,文件本身的内容它没有显示出来(文中蓝色框部分)。

当然可以通过前面提到的 Fiddler 工具来截获请求的内容:

Fiddler Inspectors TextView form post enctype multipart/form-data type file 中文字段 中文文件名 中文内容

在 TextView 下可以看到结果是类似的,文件内容还能看到三个乱码字符。转到 HexView 查看十六进制数据:

Fiddler Inspectors HexView form post enctype multipart/form-data type file 中文字段 中文文件名 中文内容

找到中文出现的几处关键地方(图中用黄色高亮标注),不难发现:

  • 表单中的普通中文 input 项“chinese”(具体值为“你好”)的编码为:E4 BD A0 E5 A5 BD,编码很显然为 utf-8,并且是原生的形式,没有转义。
  • 表单中上传“文件名”(具体值为“你好.txt”)的中文部分的编码为:E4 BD A0 E5 A5 BD,编码同样为 utf-8,并且是原生的形式,没有转义。
  • 表单中上传文件的“文件内容”部分(具体值同样为“你好”)的编码为:C4 E3 BA C3.就是文件本身的字节序列。(具体编码为 gbk)

通过以上观察不难得出一个初步的结论:multipart 形式的上传使用原生的编码,而这一编码的来源不难猜到就是页面文档本身的编码。

上传的文件的内容则忠实于文件本身。文件内容的上传本身仅是一个字节流的拷贝过程而已,不会涉及到字符集编码。

很多时候上传的“文件内容”根本就不是文本文件,比如很多时候会上传的图片文件,因此这一过程是跟字符集编码无关的,跟字符集编码有关的仅仅是“文件名”部分。

后端接收 multipart 时的处理

说完了提交的部分,再看后台接收处理的方面,具体用一个 servlet 来处理,对于 multipart 形式的提交,需要引入一个注解 @MultipartConfig,这样 request.getParameter 才能取到值:

servlet 文件上传 @MultipartConfig getPart getSubmittedFileName

有几点要注意的。一是 request.setCharacterEncoding(“utf-8”),跟之前是一样的,如果不设置,servlet 缺省按 iso-8859-1 去解码,将导致乱码。

注:不过对于 part.getSubmittedFileName 获取上传文件名而言,虽然它也受到 setCharacterEncoding 方法的影响,但它的缺省是 utf-8 而不是 iso-8859-1,至少我本地实验时发现它是如此的。

getSubmittedFileName 方法在 servlet 3.1 标准后引入的。

二是获得上传“文件内容”本身使用 getPart 方法,会返回一个 Part 接口的实现,可以通过其 getInputStream 获得上传的字节流。然后构建 reader 时传入的编码为 gbk,因为它是这段字节流真正的编码。

如果以上设置均 OK,结果页面将是正常的:

form post enctype multipart/form-data type file 中文字段 中文文件名 中文内容 响应页面

使用 accept-charset 属性时的编码

与前面类似,使用 multipart 时表单同样可以使用 accept-charset 指定一个编码,可以与页面文档本身的编码不同,而且表单此时也优先采纳 accept-charset 指定的值。比如下面就改变了前面的表单:

form post enctype multipart/form-data type file 中文字段 中文文件名 中文内容 accept-charset

这样之后,提交的数据将采用 gbk 编码而不是文档本身的 utf-8:

Fiddler form post multipart 中文 accept-charset HexView

可以看到,高亮的前两处地方编码都是 gbk 了。

当然,最后的“文件内容”本身该怎样还是怎样,不受这里调整影响。(因它本身就是 gbk,所以这里三处的值都一样了)

相应的,后台部分的 request.setCharacterEncoding 也要调整,否则中文字段与中文文件名都将出现乱码:

form post multipart 中文 乱码

注:“文件内容”本身是独立的,将不受这里的影响,只与 new InputStreamReader 时传入的编码参数有关,所以这里显示还是 OK 的。

关于这方面,可以参考前面的 Java 字节流与字符流系列中的介绍。

调整为 setCharacterEncoding(“gbk”)后结果将显示正常,具体截图从略,读者可自行实验。

总之,后台解码要与前台编码的一致即可。当发生乱码时,首先要检查前台提交过来的数据,确定它使用的真实编码,可以通过浏览器的“开发者工具”或者一些抓包工具(比如这里提到的 Fiddler,其它的还有比如 wiresharp 之类的),之后,在后台作相应更改。

如果你用的不是 Java servlet 平台,或者 server 用的不是 tomcat 之类的,某些具体的处理过程可能会有差异,但基本上可以说是大同小异,因为从根本上讲,都是对 html,http,uri 等规范的实现,这些统一的规范适用于所有的语言与平台。

总结

下面对 multipart 形式的提交做个总结,其实总的来说,跟前面的那些 get 方式以及 urlencoded 的 post 方式是差不多的:

  • 没有用 accept-charset 指定,就用文档本身的编码,但不会转义;
  • 设置了 accept-charset 的值,就用它设置的值,但不会转义;
  • “文件名”跟表单字段使用相同的编码,同样也不会转义;
  • “文件内容”本身按其原样字节上传,不涉及编码问题。

可以说,不管是 get,还是 urlencoded 的 post,还是这里 multipart 的 post,基本逻辑都是差不多的。

示例代码(git)与参考

前面很多的示例代码基本都是用图片的方式给出的,如果你想拷贝一些代码亲自实验,可以到我的 gitee 共享工程下看到这些示例代码:

https://gitee.com/goldenshaw/java_code_complete/tree/master/jcc-web/src/main/webapp/demo/encoding/form/post

另外,写作的过程中主要参考是:

关于 post 方式以 multipart/form-data 类型提交的介绍就到这里,关于整个表单提交时的编码与乱码的主题也介绍完了。

发表评论

电子邮件地址不会被公开。