上个世纪60年代,美国制订了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。
这被称为ASCII码,一贯沿用至今。
ASCII码一共规定了128个字符的编码,比如空格\"大众SPACE\"大众是32(二进制00100000),大写的字母A是65(二进制01000001)。
这128个符号(包括32个不能打印出来的掌握符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。
0~31 是掌握字符如换行回车删除等,32~126 是打印字符,可以通过键盘输入并且能够显示出来。

英语用128个符号编码就够了,但是用来表示其他措辞,128个符号是不足的。
比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。
于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。
比如,法语中的é的编码为130(二进制10000010)。
这样一来,这些欧洲国家利用的编码体系,可以表示最多256个符号。

但是,这里又涌现了新的问题。
不同的国家有不同的字母,因此,哪怕它们都利用256个符号的编码办法,代表的字母却不一样。
比如,130在法语编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。
但是不管若何,所有这些编码办法中,0—127表示的符号是一样的,不一样的只是128—255的这一段。

jsp不能使用全角字符让编码问题不再迷惑你 JavaScript

至于亚洲国家的笔墨,利用的符号就更多了,汉字就多达10万旁边。
一个字节只能表示256种符号,肯定是不足的,就必须利用多个字节表达一个符号。
比如,简体中文常见的编码办法是GB2312,利用两个字节表示一个汉字,以是理论上最多可以表示65536个符号。

2. Unicode编码#

可以想象,如果有一种编码,将天下上所有的符号都纳入个中。
每一个符号都给予一个独一无二的编码,那么就不会涌现上面的问题。
Unicode编码便是这样一种编码。

Unicode是一个很大的字符凑集,现在的规模可以容纳100多万个符号。
每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。

须要把稳的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码该当如何存储。
这就造成了两个问题:

第一个问题是,如何才能差异unicode和ascii?打算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英笔墨母只用一个字节表示就够了,如果unicode统一规定,每个符号用三个或四个字节表示,那么每个英笔墨母前都一定有二到三个字节是0,这对付存储来说是极大的摧残浪费蹂躏,文本文件的大小会因此大出二三倍,这是无法接管的。

记住,Unicode只是一个用来映射字符和数字的标准。
它对支持字符的数量没有限定,也不哀求字符必须占两个、三个或者其它任意数量的字节。
Unicode字符是若何被编码成内存中的字节这是其余的话题,它是被UTF(Unicode Transformation Formats)定义的。

3. UTF-8编码#

互联网的遍及,强烈哀求涌现一种统一的编码办法。
UTF-8便是在互联网上利用最广的一种unicode的实现办法。
其他实现办法还包括UTF-16和UTF-32,不过在互联网上基本不用。
重复一遍,这里的关系是,UTF-8是Unicode的实现办法之一。

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,又称万国码。
由Ken Thompson于1992年创建。
现在已经标准化为RFC 3629。
UTF-8用1到4个字节编码Unicode字符。
用在网页上可以统一页面显示中文简体繁体及其它措辞(如英文,日文,韩文)。

UTF-8最大的一个特点,便是它是一种变长的编码办法。
它可以利用1~4个字节表示一个符号,根据不同的符号而变革字节长度(UTF-8编码可以容纳2^21个字符,统共200多万个字符)。

UTF-8的编码规则很大略,只有二条:

对付单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。
因此对付英语字母,UTF-8编码和ASCII码是相同的。
对付n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。
剩下的没有提及的二进制位,全部为这个符号的unicode码。

下表总结了编码规则,字母x表示可用编码的位。

Unicode符号范围 | UTF-8编码办法

UTF字节数 (十六进制) | (二进制)

--------------------+---------------------------------------------一个字节 0000 0000-0000 007F | 0xxxxxxx

两个字节 0000 0080-0000 07FF | 110xxxxx 10xxxxxx

三个字节 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx

四个字节 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

下面, 还是以汉字“严”为例,演示如何实现UTF-8编码。

已知“严”的unicode是4E25(100111000100101),根据上表,可以创造4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码须要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。
然后,从“严”的末了一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。
这样就得到了,“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制便是E4B8A5。

4. UTF8、UTF16和UTF32之间的差异#

首先我们要确定一个观点便是Unicode是一个字符集,这个字符集天下上所有的字符定义了一个唯一编码。
其仅仅规定了每个符号的二进制代码,没有制订细化的存储规则。
UTF-8、UTF-16、UTF-32才是Unicode的存储格式定义。
(拿一个通信中的列子做个比拟,一个旗子暗记(类比成Unicode编码指),通过不同的编码办法,会被编码身分歧的高低旗子暗记)

4.1 UCS-2和UCS-4#

Unicode是为整合全天下的所有措辞笔墨而出身的。
任何笔墨在Unicode中都对应一个值, 这个值称为代码点(code point)。
代码点的值常日写成 U+ABCD 的格式。
而笔墨和代码点之间的对应关系便是UCS-2(Universal Character Set coded in 2 octets)。
顾名思义,UCS-2是用两个字节来表示代码点,其取值范围为 U+0000~U+FFFF。

为了能表示更多的笔墨,人们又提出了UCS-4,即用四个字节表示代码点。
它的范围为 U+00000000~U+7FFFFFFF,个中 U+00000000~U+0000FFFF和UCS-2是一样的。

要把稳,UCS-2和UCS-4只规定了代码点和笔墨之间的对应关系,并没有规定代码点在打算机中如何存储。
规定存储办法的称为UTF(Unicode Transformation Format),个中运用较多的便是UTF-16和UTF-8了。

4.2 UTF-16#

UTF-16由RFC2781规定,它利用两个字节来表示一个代码点。
不难猜到,UTF-16是完备对应于UCS-2的,即把UCS-2规定的代码点通过Big Endian或Little Endian办法直接保存下来。
UTF-16包括三种:UTF-16,UTF-16BE(Big Endian),UTF-16LE(Little Endian)。
UTF-16BE和UTF-16LE不难明得,而UTF-16就须要通过在文件开头以名为BOM(Byte Order Mark)的字符来表明文件是Big Endian还是Little Endian。
BOM为U+FEFF这个字符。
实在BOM是个小聪明的想法。
由于UCS-2没有定义U+FEFF,因此只要涌现 FF FE 或者 FE FF 这样的字节序列,就可以认为它是U+FEFF,并且可以判断出是Big Endian还是Little Endian。

BOM(Byte Order Mark)用来放在文档的开头见告阅读器该文档的字节序。
UTF-8不须要BOM来表明字节顺序,但可以用BOM来表明编码办法。
字符\公众ZERO WIDTH NO-BREAK SPACE\"大众的UTF-8编码是EF BB BF。
以是如果吸收者收到以EF BB BF开头的字节流,就知道这是UTF-8编码了。
UTF-16才须要加bom。
由于它是按unicode顺序编码,在BMP范围内是二字节,须要识别是大或小字节序。

低字节序(Little Endian)和高字节序(Big Endian)

低字节序和高字节序只是一个关于在内存中存储和读取一段字节(被称作words)的约定。
这意味着当你让打算机用UTF-16把字母A(占两个字节)存在内存中时,利用哪种字节序方案决定了你把第一个字节放在第二个字节的前面还是后面。
这么说有点不太随意马虎懂,让我们来看一个例子:当你利用UTF-16存下某段内容时,在不同的系统中它的后半部分可能是这样的:

00 68 00 65 00 6C 00 6C 00 6F(高字节序,高位字节被存在前面)

68 00 65 00 6C 00 6C 00 6F 00(低字节序,低位字节被存在前面)

字节序方案只是一个微处理器架构设计者的偏好问题,例如,Intel利用低字节序,Motorola利用高字节序。

举个例子。
“ABC”这三个字符用各种办法编码后的结果如下:

4.3 UTF-32#

UTF-32用四个字节表示代码点,这样就可以完备表示UCS-4的所有代码点,而无需像UTF-16那样利用繁芜的算法。
与UTF-16类似,UTF-32也包括UTF-32、UTF-32BE、UTF-32LE三种编码,UTF-32也同样须要BOM字符。

4.4 文本编辑器怎么知道文本的编码#

当一个软件打开一个文本时,它要做的第一件事是决定这个文本究竟是利用哪种字符集的哪种编码保存的。
软件一样平常采取三种办法来决定文本的字符集和编码:

检测文件头标识(BOM)EF BB BF UTF-8 FE FF UTF-16/UCS-2, big endian FF FE UTF-16/UCS-2, little endian FF FE 00 00 UTF-32/UCS-4, little endian. 00 00 FE FF UTF-32/UCS-4, big-endian.软件自己根据编码规则预测当前文件的编码提示用户自己输入当前文件的编码

5. GBK、GB2312和GB18030之间的差异#

GB2312是对ASCll码的扩展,占用两个字节。
一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。
在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这便是常说的\公众全角\"大众字符,而原来在127号以下的那些就叫\公众半角\"大众字符了。

GB2312能表示的字符还是不足用,于是GBK涌现了。
GBK是对GB1212的扩展,也是占用2个字节,GBK不再哀求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。
结果扩展之后的编码方案被称为 GBK 标准,GBK 包括了 GB2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。

GB18030采取变长编码,可以是1个字节、2个字节和4个字节。
是对GB2312和GBK的扩展,完备兼容两者。

经由上面先容,我们可以看出Unicode是一个天下标准,针对天下上所有措辞符号制订编码表,而GBK、GB2312等则紧张是针对中国的字符进行编码。

6. Java中的编码问题#

我们知道涉及到编码的地方一样平常都在字符到字节或者字节到字符的转换上,而须要这种转换的场景紧张是在 I/O 的时候,这个 I/O 包括磁盘 I/O 和网络 I/O。
而大部分 I/O 引起的乱码都是网络 I/O。

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

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

URL 的编解码

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

Port 对应在 Tomcat 的 中配置,而 Context Path 在 中配置,Servlet Path 在 Web 运用的 web.xml 中的

<servlet-mapping> <servlet-name>junshanExample</servlet-name> <url-pattern>/servlets/servlet/</url-pattern> </servlet-mapping>

中配置,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 的测试结果:

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

从上面测试结果可知浏览器对 PathInfo 和 QueryString 的编码是不一样的,不同浏览器对 PathInfo 也可能不一样,这就对做事器的解码造成很大的困难,下面我们以 Tomcat 为例看一下,Tomcat 接管到这个 URL 是如何解码的。
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的 中定义的,如果没有定义,那么将以默认编码 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 的 中的 useBodyEncodingForURI 设置为 true。
这个配置项的名字有点让人产生稠浊,它并不是对全体 URI 都采取 BodyEncoding 进行解码而仅仅是对 QueryString 利用 BodyEncoding 解码,这一点还要特殊把稳。

从上面的 URL 编码和解码过程来看,比较繁芜,而且编码和解码并不是我们在运用程序中能完备掌握的,以是在我们的运用程序中该当只管即便避免在 URL 中利用非 ASCII 字符,不然很可能会碰到乱码问题,当然在我们的做事器端最好设置 中的 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 的 中的 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\"大众

8. 乱码问题剖析#

下面看一下,当我们碰到一些乱码时,该当怎么处理这些问题?涌现乱码问题唯一的缘故原由都是在 char 到 byte 或 byte 到 char 转换中编码和解码的字符集不一致导致的,由于每每一次操作涉及到多次编解码,以是涌现乱码时很难查找到底是哪个环节涌现了问题。
根据自己的履历,每每从最源头开始一步步查缘故原由是最快的。

9. 参考#

编码博客

为什么Java最多只能标识65535个字符

Unicode本身只是一个标准,不是详细实现,并没有限定字节数。
目前用于实用的 Unicode 版本对应于 UCS-2,利用16位的编码空间,因此最大能表示65535个字符。
Unicode是发展的,6万个确实不足,事实上现在的Unicode已经支持超过10万个字符(第10万个于2005年被采纳,为马来亚拉姆语。
当前的Unicode版本为6.3,2013年9月30日制订。
Java中利用的仍是UCS-2。