Java Web 之编解码分析
所谓编码,就是将字符转换成字节,所谓解码,就是将字节转换为字符。而编解码中存在的问题主要是由编码和解码所用字符集不匹配导致的。本文主要从以下三个方面分析 Java Web 中存在的字符编解码问题:
- 几种常见的编码格式
- Java 中存在编码和解码的场景分析
- Java Web 中存在编码和解码的过程分析
常见编码格式
ASCII 码:单字节编码,其中最高位用作奇偶校验,实际上是由低 7 位来进行编码,总共有 127 个编码。其中 0 - 31 是控制字符,而 32 - 126 是打印字符。此外,IBM 指定了 127 - 255 的扩展编码,但是这些编码并非标准 ASCII 码。
IOS-8859-1:单字节编码,总共能表示 256 个字符,其中 0 - 126 的编码与 ASCII 编码相同。
GB2312:双字节编码,分为符号区和汉字区。
GBK:双字节编码,与 GB2312 兼容,可以表示更多的汉字。
GB18030:不定长编码(可能为单字节、双字节或四字节),国家标准但不流行,兼容 GB2312。
UTF-16:双字节编码,Java 中字符的内存存储格式。
UTF-8:不定长编码,不同的字符可由 1- 6 个字节组成。
此处需要注意,UTF-16 存在以下几个值得注意的问题:
- UTF-16 由于每个字符都需要两字节编码(包括西欧字符),因此在网络传输中可能会带来不必要的传输消耗。
- UTF-16 采用固定两编码方式,单个字节的丢失可能会导致后面编码全部出错。
- 由于 UTF-16 将字符的高位和地位拆分为两个字节,低位和高位的顺序影响编码的结果,因此需要发送和接受双方协商大小端,同时大小端的转换会带来不必要的消耗。
Java 中存在编码和解码的场景分析
Java 提供了 Reader 和 Writer 用于读取字符和写入字符,提供了 InputStream 和 OutputStream 用于读取字节流和写入字节流,提供了 InputStreamReader 和 OutputStreamWriter 用于从字节流中读取字符和将字符写成字节流。
InputStreamReader 和 OutputStreamWriter 内部使用 StreamDecoder 和 StreamEncoder 来完成字节与字符之间的转换。StreamDecoder 和 StreamEncoder 是使用 Charset 构造的,Charset 用于指定字符的编码集,如果未指定 Charset,则使用系统语言默认的编码集,中文环境将使用 GBK。
需要注意的是:InputStreamReader 和 OutputStreamWriter 使用了使用了适配器模式将 Reader 和 Writer 与 InputStream 和 OutputStream 适配。适配器模式的细节本文不作展开。
Java Web 中存在编码和解码的过程分析
当客户端向服务器发起一次 HTTP 请求的过程中,需要将 HTTP 请求中包含的 URL、Cookie、Parameter 发送给服务器,此处涉及到了编码,服务器在收到请求字节后,需要将字节解析为字符,此处涉及到了解码。HTTP 回复过程与之类似,本文不再赘述。
URL 编解码
URL 过程的编解码需要注意的是不同浏览器对于 PathInfo 和 QueryString 的编码不一样,而 PathInfo 与 QueryString 的解码过程也分为两部,PathInfo 是 Tomcat 服务器负责,需要在 URIEncoding 参数设定字符集,而 QueryString 是保存在请求参数中的,其解码所用字符集要么为 Header 中 ContentType 指定的字符集要么为 ISO-8859-1,使用 Content-Type 字符集需要设置 useBodyEncodingForURI 参数为 true。
Header 编解码
Header 中的用户设置如 Cookie 和 Parameter 也需要编码和解码,默认是使用 ISO-8859-1 编码。
POST 表单和 BODY
这两部分都是使用 Content-Type 设置的字符集编解码,因此只要客户端和服务器使用的是同一个 Content-Type,那么编解码过程中不会出现问题。Header 中的 Content-Type 是通过 request.setCharacterEncoding 设置的。
注意返回的 BODY 部分的编码和解码与 POST 表单不同,由于是 HTML,如果 Header 中的 Content-Type 没有指定字符集,会使用 HTML 头部设置的字符集,如果 HTML 头部没有设置 Content-Type 才会使用系统默认字符集。