中文乱码深刻分析

几种常见的编码格式html

为何要编码java

不知道你们有没有想过一个问题,那就是为何要编码?咱们能不能不编码?要回答这个问题必需要回到计算机是如何表示咱们人类可以理解的符号的,这些符号也就是咱们人类使用的语言。因为人类的语言有太多,于是表示这些语言的符号太多,没法用计算机中一个基本的存储单元—— byte 来表示,于是必需要通过拆分或一些翻译工做,才能让计算机能理解。咱们能够把计算机可以理解的语言假定为英语,其它语言要可以在计算机中使用必须通过一次翻译,把它翻译成英语。这个翻译的过程就是编码。因此能够想象只要不是说英语的国家要可以使用计算机就必需要通过编码。这看起来有些霸道,可是这就是现状,这也和咱们国家如今在大力推广汉语同样,但愿其它国家都会说汉语,之后其它的语言都翻译成汉语,咱们能够把计算机中存储信息的最小单位改为汉字,这样咱们就不存在编码问题了。mysql

因此总的来讲,编码的缘由能够总结为:web

1, 计算机中存储信息的最小单元是一个字节即 8 个 bit,因此能表示的字符范围是 0~255 个算法

2, 人类要表示的符号太多,没法用一个字节来彻底表示sql

3, 要解决这个矛盾必须须要一个新的数据结构 char,从 char 到 byte 必须编码数据库

如何翻译apache

明白了各类语言须要交流,通过翻译是必要的,那又如何来翻译呢?计算中提拱了多种翻译方式,常见的有 ASCII、ISO-8859-一、GB23十二、GBK、UTF-八、UTF-16 等。它们均可以被看做为字典,它们规定了转化的规则,按照这个规则就可让计算机正确的表示咱们的字符。目前的编码格式不少,例如 GB23十二、GBK、UTF-八、UTF-16 这几种格式均可以表示一个汉字,那咱们到底选择哪一种编码格式来存储汉字呢?这就要考虑到其它因素了,是存储空间重要仍是编码的效率重要。根据这些因素来正确选择编码格式,下面简要介绍一下这几种编码格式。windows

  • ASCII 码

学过计算机的人都知道 ASCII 码,总共有 128 个,用一个字节的低 7 位表示,0~31 是控制字符如换行回车删除等;32~126 是打印字符,能够经过键盘输入而且可以显示出来。数组

  • ISO-8859-1

128 个字符显然是不够用的,因而 ISO 组织在 ASCII 码基础上又制定了一些列标准用来扩展 ASCII 编码,它们是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵盖了大多数西欧语言字符,全部应用的最普遍。ISO-8859-1 仍然是单字节编码,它总共能表示 256 个字符。

  • GB2312

它的全称是《信息交换用汉字编码字符集 基本集》,它是双字节编码,总的编码范围是 A1-F7,其中从 A1-A9 是符号区,总共包含 682 个符号,从 B0-F7 是汉字区,包含 6763 个汉字。

  • GBK

全称叫《汉字内码扩展规范》,是国家技术监督局为 windows95 所制定的新的汉字内码规范,它的出现是为了扩展 GB2312,加入更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字能够用 GBK 来解码,而且不会有乱码。

  • GB18030

全称是《信息交换用汉字编码字符集》,是我国的强制标准,它多是单字节、双字节或者四字节编码,它的编码与 GB2312 编码兼容,这个虽然是国家标准,可是实际应用系统中使用的并不普遍。

  • UTF-16

说到 UTF 必需要提到 Unicode(Universal Code 统一码),ISO 试图想建立一个全新的超语言字典,世界上全部的语言均可以经过这本字典来相互翻译。可想而知这个字典是多么的复杂,关于 Unicode 的详细规范能够参考相应文档。Unicode 是 Java 和 XML 的基础,下面详细介绍 Unicode 在计算机中的存储形式。

UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用两个字节来表示 Unicode 转化格式,这个是定长的表示方法,不论什么字符均可以用两个字节表示,两个字节是 16 个 bit,因此叫 UTF-16。UTF-16 表示字符很是方便,每两个字节表示一个字符,这个在字符串操做时就大大简化了操做,这也是 Java 以 UTF-16 做为内存的字符存储格式的一个很重要的缘由。

  • UTF-8

UTF-16 统一采用两个字节表示一个字符,虽然在表示上很是简单方便,可是也有其缺点,有很大一部分字符用一个字节就能够表示的如今要两个字节表示,存储空间放大了一倍,在如今的网络带宽还很是有限的今天,这样会增大网络传输的流量,并且也不必。而 UTF-8 采用了一种变长技术,每一个编码区域有不一样的字码长度。不一样类型的字符能够是由 1~6 个字节组成。

UTF-8 有如下编码规则:

1, 若是一个字节,最高位(第 8 位)为 0,表示这是一个 ASCII 字符(00 - 7F)。可见,全部 ASCII 编码已是 UTF-8 了。

2, 若是一个字节,以 11 开头,连续的 1 的个数暗示这个字符的字节数,例如:110xxxxx 表明它是双字节 UTF-8 字符的首字节。

3, 若是一个字节,以 10 开始,表示它不是首字节,须要向前查找才能获得当前字符的首字节

Java 中须要编码的场景

前面描述了常见的几种编码格式,下面将介绍 Java 中如何处理对编码的支持,什么场合中须要编码。

I/O 操做中存在的编码

咱们知道涉及到编码的地方通常都在字符到字节或者字节到字符的转换上,而须要这种转换的场景主要是在 I/O 的时候,这个 I/O 包括磁盘 I/O 和网络 I/O,关于网络 I/O 部分在后面将主要以 Web 应用为例介绍。下图是 Java 中处理 I/O 问题的接口:

 

Reader 类是 Java I/O 中读字符的父类,而 InputStream 类是读字节的父类,InputStreamReader 类就是关联字节到字符的桥梁,它负责在 I/O 过程当中处理读取字节到字符的转换,而具体字节到字符的解码实现它由 StreamDecoder 去实现,在 StreamDecoder 解码过程当中必须由用户指定 Charset 编码格式。值得注意的是若是你没有指定 Charset,将使用本地环境中的默认字符集,例如在中文环境中将使用 GBK 编码。

写的状况也是相似,字符的父类是 Writer,字节的父类是 OutputStream,经过 OutputStreamWriter 转换字符到字节。以下图所示:

 

一样 StreamEncoder 类负责将字符编码成字节,编码格式和默认编码规则与解码是一致的。

以下面一段代码,实现了文件的读写功能:

清单 1.I/O 涉及的编码示例

 String file = "c:/stream.txt";

 String charset = "UTF-8";

 // 写字符换转成字节流

 FileOutputStream outputStream = new FileOutputStream(file);

 OutputStreamWriter writer = new OutputStreamWriter(

 outputStream, charset);

 try {

    writer.write("这是要保存的中文字符");

 } finally {

    writer.close();

 }

 // 读取字节转换成字符

 FileInputStream inputStream = new FileInputStream(file);

 InputStreamReader reader = new InputStreamReader(

 inputStream, charset);

 StringBuffer buffer = new StringBuffer();

 char[] buf = new char[64];

 int count = 0;

 try {

    while ((count = reader.read(buf)) != -1) {

        buffer.append(buffer, 0, count);

    }

 } finally {

    reader.close();

 }

在咱们的应用程序中涉及到 I/O 操做时只要注意指定统一的编解码 Charset 字符集,通常不会出现乱码问题,有些应用程序若是不注意指定字符编码,中文环境中取操做系统默认编码,若是编解码都在中文环境中,一般也没问题,可是仍是强烈的不建议使用操做系统的默认编码,由于这样,你的应用程序的编码格式就和运行环境绑定起来了,在跨环境下极可能出现乱码问题。

内存中操做中的编码

在 Java 开发中除了 I/O 涉及到编码外,最经常使用的应该就是在内存中进行字符到字节的数据类型的转换,Java 中用 String 表示字符串,因此 String 类就提供转换到字节的方法,也支持将字节转换为字符串的构造函数。以下代码示例:

 String s = "这是一段中文字符串";

 byte[] b = s.getBytes("UTF-8");

 String n = new String(b,"UTF-8");

另一个是已经被被废弃的 ByteToCharConverter 和 CharToByteConverter 类,它们分别提供了 convertAll 方法能够实现 byte[] 和 char[] 的互转。以下代码所示:

 ByteToCharConverter charConverter = ByteToCharConverter.getConverter("UTF-8");

 char c[] = charConverter.convertAll(byteArray);

 CharToByteConverter byteConverter = CharToByteConverter.getConverter("UTF-8");

 byte[] b = byteConverter.convertAll(c);

这两个类已经被 Charset 类取代,Charset 提供 encode 与 decode 分别对应 char[] 到 byte[] 的编码和 byte[] 到 char[] 的解码。以下代码所示:

 Charset charset = Charset.forName("UTF-8");

 ByteBuffer byteBuffer = charset.encode(string);

 CharBuffer charBuffer = charset.decode(byteBuffer);

编码与解码都在一个类中完成,经过 forName 设置编解码字符集,这样更容易统一编码格式,比 ByteToCharConverter 和 CharToByteConverter 类更方便。

Java 中还有一个 ByteBuffer 类,它提供一种 char 和 byte 之间的软转换,它们之间转换不须要编码与解码,只是把一个 16bit 的 char 格式,拆分红为 2 个 8bit 的 byte 表示,它们的实际值并无被修改,仅仅是数据的类型作了转换。以下代码因此:

 ByteBuffer heapByteBuffer = ByteBuffer.allocate(1024);

 ByteBuffer byteBuffer = heapByteBuffer.putChar(c);

以上这些提供字符和字节之间的相互转换只要咱们设置编解码格式统一通常都不会出现问题。

回页首

Java 中如何编解码

前面介绍了几种常见的编码格式,这里将以实际例子介绍 Java 中如何实现编码及解码,下面咱们以“I am 君山”这个字符串为例介绍 Java 中如何把它以 ISO-8859-一、GB23十二、GBK、UTF-1六、UTF-8 编码格式进行编码的。

清单 2.String 编码

 public static void encode() {

        String name = "I am 君山";

        toHex(name.toCharArray());

        try {

            byte[] iso8859 = name.getBytes("ISO-8859-1");

            toHex(iso8859);

            byte[] gb2312 = name.getBytes("GB2312");

            toHex(gb2312);

            byte[] gbk = name.getBytes("GBK");

            toHex(gbk);

            byte[] utf16 = name.getBytes("UTF-16");

            toHex(utf16);

            byte[] utf8 = name.getBytes("UTF-8");

            toHex(utf8);

        } catch (UnsupportedEncodingException e) {

            e.printStackTrace();

        }

 }

咱们把 name 字符串按照前面说的几种编码格式进行编码转化成 byte 数组,而后以 16 进制输出,咱们先看一下 Java 是如何进行编码的。

下面是 Java 中编码须要用到的类图

1. Java 编码类图

 

首先根据指定的 charsetName 经过 Charset.forName(charsetName) 设置 Charset 类,而后根据 Charset 建立 CharsetEncoder 对象,再调用 CharsetEncoder.encode 对字符串进行编码,不一样的编码类型都会对应到一个类中,实际的编码过程是在这些类中完成的。下面是 String. getBytes(charsetName) 编码过程的时序图

2.Java 编码时序图

 

从上图能够看出根据 charsetName 找到 Charset 类,而后根据这个字符集编码生成 CharsetEncoder,这个类是全部字符编码的父类,针对不一样的字符编码集在其子类中定义了如何实现编码,有了 CharsetEncoder 对象后就能够调用 encode 方法去实现编码了。这个是 String.getBytes 编码方法,其它的如 StreamEncoder 中也是相似的方式。下面看看不一样的字符集是如何将前面的字符串编码成 byte 数组的?

如字符串“I am 君山”的 char 数组为 49 20 61 6d 20 541b 5c71,下面把它按照不一样的编码格式转化成相应的字节。

按照 ISO-8859-1 编码

字符串“I am 君山”用 ISO-8859-1 编码,下面是编码结果:

 

从上图看出 7 char 字符通过 ISO-8859-1 编码转变成 7 byte 数组,ISO-8859-1 是单字节编码,中文君山被转化成值是 3f byte3f 也就是字符,因此常常会出现中文变成极可能就是错误的使用了 ISO-8859-1 这个编码致使的。中文字符通过 ISO-8859-1 编码会丢失信息,一般咱们称之为黑洞,它会把不认识的字符吸取掉。因为如今大部分基础的 Java 框架或系统默认的字符集编码都是 ISO-8859-1,因此很容易出现乱码问题,后面将会分析不一样的乱码形式是怎么出现的。

按照 GB2312 编码

字符串“I am 君山”用 GB2312 编码,下面是编码结果:

 

GB2312 对应的 Charset sun.nio.cs.ext. EUC_CN 而对应的 CharsetDecoder 编码类是 sun.nio.cs.ext. DoubleByteGB2312 字符集有一个 char byte 的码表,不一样的字符编码就是查这个码表找到与每一个字符的对应的字节,而后拼装成 byte 数组。查表的规则以下:

 c2b[c2bIndex[char >> 8] + (char & 0xff)]

若是查到的码位值大于 oxff 则是双字节,不然是单字节。双字节高 8 位做为第一个字节,低 8 位做为第二个字节,以下代码所示:

 if (bb > 0xff) {    // DoubleByte

            if (dl - dp < 2)

                return CoderResult.OVERFLOW;

            da[dp++] = (byte) (bb >> 8);

            da[dp++] = (byte) bb;

 } else {                      // SingleByte

            if (dl - dp < 1)

                return CoderResult.OVERFLOW;

            da[dp++] = (byte) bb;

 }

从上图能够看出前 5 个字符通过编码后仍然是 5 个字节,而汉字被编码成双字节,在第一节中介绍到 GB2312 只支持 6763 个汉字,因此并非全部汉字都可以用 GB2312 编码。

按照 GBK 编码

字符串“I am 君山”用 GBK 编码,下面是编码结果:

 

你可能已经发现上图与 GB2312 编码的结果是同样的,没错 GBK GB2312 编码结果是同样的,由此能够得出 GBK 编码是兼容 GB2312 编码的,它们的编码算法也是同样的。不一样的是它们的码表长度不同,GBK 包含的汉字字符更多。因此只要是通过 GB2312 编码的汉字均可以用 GBK 进行解码,反过来则否则。

按照 UTF-16 编码

字符串“I am 君山”用 UTF-16 编码,下面是编码结果:

 

UTF-16 编码将 char 数组放大了一倍,单字节范围内的字符,在高位补 0 变成两个字节,中文字符也变成两个字节。从 UTF-16 编码规则来看,仅仅将字符的高位和地位进行拆分变成两个字节。特色是编码效率很是高,规则很简单,因为不一样处理器对 2 字节处理方式不一样,Big-endian(高位字节在前,低位字节在后)或 Little-endian(低位字节在前,高位字节在后)编码,因此在对一串字符串进行编码是须要指明究竟是 Big-endian 仍是 Little-endian,因此前面有两个字节用来保存 BYTE_ORDER_MARK 值,UTF-16 是用定长 16 位(2 字节)来表示的 UCS-2 Unicode 转换格式,经过代理对来访问 BMP 以外的字符编码。

按照 UTF-8 编码

字符串“I am 君山”用 UTF-8 编码,下面是编码结果:

 

UTF-16 虽然编码效率很高,可是对单字节范围内字符也放大了一倍,这无形也浪费了存储空间,另外 UTF-16 采用顺序编码,不能对单个字符的编码值进行校验,若是中间的一个字符码值损坏,后面的全部码值都将受影响。而 UTF-8 这些问题都不存在,UTF-8 对单字节范围内字符仍然用一个字节表示,对汉字采用三个字节表示。它的编码规则以下:

清单 3.UTF-8 编码代码片断

 private CoderResult encodeArrayLoop(CharBuffer src,

 ByteBuffer dst){

            char[] sa = src.array();

            int sp = src.arrayOffset() + src.position();

            int sl = src.arrayOffset() + src.limit();

            byte[] da = dst.array();

            int dp = dst.arrayOffset() + dst.position();

            int dl = dst.arrayOffset() + dst.limit();

            int dlASCII = dp + Math.min(sl - sp, dl - dp);

            // ASCII only loop

            while (dp < dlASCII && sa[sp] < '\u0080')

                da[dp++] = (byte) sa[sp++];

            while (sp < sl) {

                char c = sa[sp];

                if (c < 0x80) {

                    // Have at most seven bits

                    if (dp >= dl)

                        return overflow(src, sp, dst, dp);

                    da[dp++] = (byte)c;

                } else if (c < 0x800) {

                    // 2 bytes, 11 bits

                    if (dl - dp < 2)

                        return overflow(src, sp, dst, dp);

                    da[dp++] = (byte)(0xc0 | (c >> 6));

                    da[dp++] = (byte)(0x80 | (c & 0x3f));

                } else if (Character.isSurrogate(c)) {

                    // Have a surrogate pair

                    if (sgp == null)

                        sgp = new Surrogate.Parser();

                    int uc = sgp.parse(c, sa, sp, sl);

                    if (uc < 0) {

                        updatePositions(src, sp, dst, dp);

                        return sgp.error();

                    }

                    if (dl - dp < 4)

                        return overflow(src, sp, dst, dp);

                    da[dp++] = (byte)(0xf0 | ((uc >> 18)));

                    da[dp++] = (byte)(0x80 | ((uc >> 12) & 0x3f));

                    da[dp++] = (byte)(0x80 | ((uc >>  6) & 0x3f));

                    da[dp++] = (byte)(0x80 | (uc & 0x3f));

                    sp++;  // 2 chars

                } else {

                    // 3 bytes, 16 bits

                    if (dl - dp < 3)

                        return overflow(src, sp, dst, dp);

                    da[dp++] = (byte)(0xe0 | ((c >> 12)));

                    da[dp++] = (byte)(0x80 | ((c >>  6) & 0x3f));

                    da[dp++] = (byte)(0x80 | (c & 0x3f));

                }

                sp++;

            }

            updatePositions(src, sp, dst, dp);

            return CoderResult.UNDERFLOW;

 }

UTF-8 编码与 GBK 和 GB2312 不一样,不用查码表,因此在编码效率上 UTF-8 的效率会更好,因此在存储中文字符时 UTF-8 编码比较理想。

几种编码格式的比较

对中文字符后面四种编码格式都能处理,GB2312 与 GBK 编码规则相似,可是 GBK 范围更大,它能处理全部汉字字符,因此 GB2312 与 GBK 比较应该选择 GBK。UTF-16 与 UTF-8 都是处理 Unicode 编码,它们的编码规则不太相同,相对来讲 UTF-16 编码效率最高,字符到字节相互转换更简单,进行字符串操做也更好。它适合在本地磁盘和内存之间使用,能够进行字符和字节之间快速切换,如 Java 的内存编码就是采用 UTF-16 编码。可是它不适合在网络之间传输,由于网络传输容易损坏字节流,一旦字节流损坏将很难恢复,想比较而言 UTF-8 更适合网络传输,对 ASCII 字符采用单字节存储,另外单个字符损坏也不会影响后面其它字符,在编码效率上介于 GBK 和 UTF-16 之间,因此 UTF-8 在编码效率上和编码安全性上作了平衡,是理想的中文编码方式。

回页首

Java Web 涉及到的编码

对于使用中文来讲,有 I/O 的地方就会涉及到编码,前面已经提到了 I/O 操做会引发编码,而大部分 I/O 引发的乱码都是网络 I/O,由于如今几乎全部的应用程序都涉及到网络操做,而数据通过网络传输都是以字节为单位的,因此全部的数据都必须可以被序列化为字节。在 Java 中数据被序列化必须继承 Serializable 接口。

这里有一个问题,你是否定真考虑过一段文本它的实际大小应该怎么计算,我曾经碰到过一个问题:就是要想办法压缩 Cookie 大小,减小网络传输量,当时有选择不一样的压缩算法,发现压缩后字符数是减小了,可是并无减小字节数。所谓的压缩只是将多个单字节字符经过编码转变成一个多字节字符。减小的是 String.length(),而并无减小最终的字节数。例如将“ab”两个字符经过某种编码转变成一个奇怪的字符,虽然字符数从两个变成一个,可是若是采用 UTF-8 编码这个奇怪的字符最后通过编码可能又会变成三个或更多的字节。一样的道理好比整型数字 1234567 若是当成字符来存储,采用 UTF-8 来编码占用 7 个 byte,采用 UTF-16 编码将会占用 14 个 byte,可是把它当成 int 型数字来存储只须要 4 个 byte 来存储。因此看一段文本的大小,看字符自己的长度是没有意义的,即便是同样的字符采用不一样的编码最终存储的大小也会不一样,因此从字符到字节必定要看编码类型。

另一个问题,你是否考虑过,当咱们在电脑中某个文本编辑器里输入某个汉字时,它究竟是怎么表示的?咱们知道,计算机里全部的信息都是以 01 表示的,那么一个汉字,它究竟是多少个 0 和 1 呢?咱们可以看到的汉字都是以字符形式出现的,例如在 Java 中“淘宝”两个字符,它在计算机中的数值 10 进制是 28120 和 23453,16 进制是 6bd8 和 5d9d,也就是这两个字符是由这两个数字惟一表示的。Java 中一个 char 是 16 个 bit 至关于两个字节,因此两个汉字用 char 表示在内存中占用至关于四个字节的空间。

这两个问题搞清楚后,咱们看一下 Java Web 中那些地方可能会存在编码转换?

用户从浏览器端发起一个 HTTP 请求,须要存在编码的地方是 URL、Cookie、Parameter。服务器端接受到 HTTP 请求后要解析 HTTP 协议,其中 URI、Cookie 和 POST 表单参数须要解码,服务器端可能还须要读取数据库中的数据,本地或网络中其它地方的文本文件,这些数据均可能存在编码问题,当 Servlet 处理完全部请求的数据后,须要将这些数据再编码经过 Socket 发送到用户请求的浏览器里,再通过浏览器解码成为文本。这些过程以下图所示:

3. 一次 HTTP 请求的编码示例(查看大图

 

如上图所示一次 HTTP 请求设计到不少地方须要编解码,它们编解码的规则是什么?下面将会重点阐述一下:

URL 的编解码

用户提交一个 URL,这个 URL 中可能存在中文,所以须要编码,如何对这个 URL 进行编码?根据什么规则来编码?有如何来解码?以下图一个 URL:

4.URL 的几个组成部分

 

上图中以 Tomcat 做为 Servlet Engine 为例,它们分别对应到下面这些配置文件中:

Port 对应在 Tomcat 的 <Connector port="8080"/> 中配置,而 Context Path 在 <Context path="/examples"/> 中配置,Servlet Path 在 Web 应用的 web.xml 中的

 <servlet-mapping>

        <servlet-name>junshanExample</servlet-name>

        <url-pattern>/servlets/servlet/*</url-pattern>

 </servlet-mapping>

<url-pattern> 中配置,PathInfo 是咱们请求的具体的 Servlet,QueryString 是要传递的参数,注意这里是在浏览器里直接输入 URL 因此是经过 Get 方法请求的,若是是 POST 方法请求的话,QueryString 将经过表单方式提交到服务器端,这个将在后面再介绍。

上图中 PathInfo 和 QueryString 出现了中文,当咱们在浏览器中直接输入这个 URL 时,在浏览器端和服务端会如何编码和解析这个 URL 呢?为了验证浏览器是怎么编码 URL 的咱们选择 FireFox 浏览器并经过 HTTPFox 插件观察咱们请求的 URL 的实际的内容,如下是 URL:HTTP://localhost:8080/examples/servlets/servlet/ 君山 ?author= 君山在中文 FireFox3.6.12 的测试结果

5. HTTPFox 的测试结果

 

君山的编码结果分别是:e5 90 9b e5 b1 b1,be fd c9 bd,查阅上一届的编码可知,PathInfo 是 UTF-8 编码而 QueryString 是通过 GBK 编码,至于为何会有“%”?查阅 URL 的编码规范 RFC3986 可知浏览器编码 URL 是将非 ASCII 字符按照某种编码格式编码成 16 进制数字而后将每一个 16 进制表示的字节前加上“%”,因此最终的 URL 就成了上图的格式了。

默认状况下中文 IE 最终的编码结果也是同样的,不过 IE 浏览器能够修改 URL 的编码格式在选项 -> 高级 -> 国际里面的发送 UTF-8 URL 选项能够取消。

从上面测试结果可知浏览器对 PathInfo 和 QueryString 的编码是不同的,不一样浏览器对 PathInfo 也可能不同,这就对服务器的解码形成很大的困难,下面咱们以 Tomcat 为例看一下,Tomcat 接受到这个 URL 是如何解码的。

解析请求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBuffer 的 parseRequestLine 方法中,这个方法把传过来的 URL 的 byte[] 设置到 org.apache.coyote.Request 的相应的属性中。这里的 URL 仍然是 byte 格式,转成 char 是在 org.apache.catalina.connector.CoyoteAdapter 的 convertURI 方法中完成的:

 protected void convertURI(MessageBytes uri, Request request)

 throws Exception {

        ByteChunk bc = uri.getByteChunk();

        int length = bc.getLength();

        CharChunk cc = uri.getCharChunk();

        cc.allocate(length, -1);

        String enc = connector.getURIEncoding();

        if (enc != null) {

            B2CConverter conv = request.getURIConverter();

            try {

                if (conv == null) {

                    conv = new B2CConverter(enc);

                    request.setURIConverter(conv);

                }

            } catch (IOException e) {...}

            if (conv != null) {

                try {

                    conv.convert(bc, cc, cc.getBuffer().length -

 cc.getEnd());

                    uri.setChars(cc.getBuffer(), cc.getStart(),

 cc.getLength());

                    return;

                } catch (IOException e) {...}

            }

        }

        // Default encoding: fast conversion

        byte[] bbuf = bc.getBuffer();

        char[] cbuf = cc.getBuffer();

        int start = bc.getStart();

        for (int i = 0; i < length; i++) {

            cbuf[i] = (char) (bbuf[i + start] & 0xff);

        }

        uri.setChars(cbuf, 0, length);

 }

从上面的代码中能够知道对 URL 的 URI 部分进行解码的字符集是在 connector 的 <Connector URIEncoding=”UTF-8”/> 中定义的,若是没有定义,那么将以默认编码 ISO-8859-1 解析。因此若是有中文 URL 时最好把 URIEncoding 设置成 UTF-8 编码。

QueryString 又如何解析? GET 方式 HTTP 请求的 QueryString 与 POST 方式 HTTP 请求的表单参数都是做为 Parameters 保存,都是经过 request.getParameter 获取参数值。对它们的解码是在 request.getParameter 方法第一次被调用时进行的。request.getParameter 方法被调用时将会调用 org.apache.catalina.connector.Request 的 parseParameters 方法。这个方法将会对 GET 和 POST 方式传递的参数进行解码,可是它们的解码字符集有可能不同。POST 表单的解码将在后面介绍,QueryString 的解码字符集是在哪定义的呢?它自己是经过 HTTP 的 Header 传到服务端的,而且也在 URL 中,是否和 URI 的解码字符集同样呢?从前面浏览器对 PathInfo 和 QueryString 的编码采起不一样的编码格式不一样能够猜想到解码字符集确定也不会是一致的。的确是这样 QueryString 的解码字符集要么是 Header 中 ContentType 中定义的 Charset 要么就是默认的 ISO-8859-1,要使用 ContentType 中定义的编码就要设置 connector 的 <Connector URIEncoding=”UTF-8” useBodyEncodingForURI=”true”/> 中的 useBodyEncodingForURI 设置为 true。这个配置项的名字有点让人产生混淆,它并非对整个 URI 都采用 BodyEncoding 进行解码而仅仅是对 QueryString 使用 BodyEncoding 解码,这一点还要特别注意。

从上面的 URL 编码和解码过程来看,比较复杂,并且编码和解码并非咱们在应用程序中能彻底控制的,因此在咱们的应用程序中应该尽可能避免在 URL 中使用非 ASCII 字符,否则极可能会碰到乱码问题,固然在咱们的服务器端最好设置 <Connector/> 中的 URIEncoding 和 useBodyEncodingForURI 两个参数。

HTTP Header 的编解码

当客户端发起一个 HTTP 请求除了上面的 URL 外还可能会在 Header 中传递其它参数如 Cookie、redirectPath 等,这些用户设置的值极可能也会存在编码问题,Tomcat 对它们又是怎么解码的呢?

对 Header 中的项进行解码也是在调用 request.getHeader 是进行的,若是请求的 Header 项没有解码则调用 MessageBytes 的 toString 方法,这个方法将从 byte 到 char 的转化使用的默认编码也是 ISO-8859-1,而咱们也不能设置 Header 的其它解码格式,因此若是你设置 Header 中有非 ASCII 字符解码确定会有乱码。

咱们在添加 Header 时也是一样的道理,不要在 Header 中传递非 ASCII 字符,若是必定要传递的话,咱们能够先将这些字符用 org.apache.catalina.util.URLEncoder 编码而后再添加到 Header 中,这样在浏览器到服务器的传递过程当中就不会丢失信息了,若是咱们要访问这些项时再按照相应的字符集解码就行了。

POST 表单的编解码

在前面提到了 POST 表单提交的参数的解码是在第一次调用 request.getParameter 发生的,POST 表单参数传递方式与 QueryString 不一样,它是经过 HTTP 的 BODY 传递到服务端的。当咱们在页面上点击 submit 按钮时浏览器首先将根据 ContentType 的 Charset 编码格式对表单填的参数进行编码而后提交到服务器端,在服务器端一样也是用 ContentType 中字符集进行解码。因此经过 POST 表单提交的参数通常不会出现问题,并且这个字符集编码是咱们本身设置的,能够经过 request.setCharacterEncoding(charset) 来设置。

另外针对 multipart/form-data 类型的参数,也就是上传的文件编码一样也是使用 ContentType 定义的字符集编码,值得注意的地方是上传文件是用字节流的方式传输到服务器的本地临时目录,这个过程并无涉及到字符编码,而真正编码是在将文件内容添加到 parameters 中,若是用这个编码不能编码时将会用默认编码 ISO-8859-1 来编码。

HTTP BODY 的编解码

当用户请求的资源已经成功获取后,这些内容将经过 Response 返回给客户端浏览器,这个过程先要通过编码再到浏览器进行解码。这个过程的编解码字符集能够经过 response.setCharacterEncoding 来设置,它将会覆盖 request.getCharacterEncoding 的值,而且经过 Header 的 Content-Type 返回客户端,浏览器接受到返回的 socket 流时将经过 Content-Type 的 charset 来解码,若是返回的 HTTP Header 中 Content-Type 没有设置 charset,那么浏览器将根据 Html 的 <meta HTTP-equiv="Content-Type" content="text/html; charset=GBK" /> 中的 charset 来解码。若是也没有定义的话,那么浏览器将使用默认的编码来解码。

其它须要编码的地方

除了 URL 和参数编码问题外,在服务端还有不少地方可能存在编码,如可能须要读取 xml、velocity 模版引擎、JSP 或者从数据库读取数据等。

xml 文件能够经过设置头来制定编码格式

 <?xml version="1.0" encoding="UTF-8"?>

Velocity 模版设置编码格式:

 services.VelocityService.input.encoding=UTF-8

JSP 设置编码格式:

 <%@page contentType="text/html; charset=UTF-8"%>

访问数据库都是经过客户端 JDBC 驱动来完成,用 JDBC 来存取数据要和数据的内置编码保持一致,能够经过设置 JDBC URL 来制定如 MySQL:url="jdbc:mysql://localhost:3306/DB?useUnicode=true&characterEncoding=GBK"。 

回页首

常见问题分析

在了解了 Java Web 中可能须要编码的地方后,下面看一下,当咱们碰到一些乱码时,应该怎么处理这些问题?出现乱码问题惟一的缘由都是在 char 到 byte 或 byte 到 char 转换中编码和解码的字符集不一致致使的,因为每每一次操做涉及到屡次编解码,因此出现乱码时很难查找究竟是哪一个环节出现了问题,下面就几种常见的现象进行分析。

中文变成了看不懂的字符

例如,字符串“淘!我喜欢!”变成了“Ì Ô £ ¡Î Ò Ï²»¶ £ ¡”编码过程以下图所示

 

字符串在解码时所用的字符集与编码字符集不一致致使汉字变成了看不懂的乱码,并且是一个汉字字符变成两个乱码字符。

一个汉字变成一个问号

例如,字符串“淘!我喜欢!”变成了“??????”编码过程以下图所示

 

将中文和中文符号通过不支持中文的 ISO-8859-1 编码后,全部字符变成了,这是由于用 ISO-8859-1 进行编解码时遇到不在码值范围内的字符时统一用 3f 表示,这也就是一般所说的黑洞,全部 ISO-8859-1 不认识的字符都变成了

一个汉字变成两个问号

例如,字符串“淘!我喜欢!”变成了“????????????”编码过程以下图所示

 

这种状况比较复杂,中文通过屡次编码,可是其中有一次编码或者解码不对仍然会出现中文字符变成现象,出现这种状况要仔细查看中间的编码环节,找出出现编码错误的地方。

一种不正常的正确编码

还有一种状况是在咱们经过 request.getParameter 获取参数值时,当咱们直接调用

 String value = request.getParameter(name);

会出现乱码,可是若是用下面的方式

 String value = String(request.getParameter(name).getBytes("

 ISO-8859-1"), "GBK"); 

解析时取得的 value 会是正确的汉字字符,这种状况是怎么形成的呢?

看下如所示:

 

这种状况是这样的,ISO-8859-1 字符集的编码范围是 0000-00FF,正好和一个字节的编码范围相对应。这种特性保证了使用 ISO-8859-1 进行编码和解码能够保持编码数值不变。虽然中文字符在通过网络传输时,被错误地成了两个欧洲字符,但因为输出时也是用 ISO-8859-1,结果被开的中文字的两半又被合并在一块儿,从而又恰好组成了一个正确的汉字。虽然最终能取得正确的汉字,可是仍是不建议用这种不正常的方式取得参数值,由于这中间增长了一次额外的编码与解码,这种状况出现乱码时由于 Tomcat 的配置文件中 useBodyEncodingForURI 配置项没有设置为”true”,从而形成第一次解析式用 ISO-8859-1 来解析才形成乱码的。

相关文章
相关标签/搜索