1.1 为什么要编码
在打算机中存储信息的最小单元是 1 个字节,即 8 个 bit, 以是能表示的字符范围是 0 ~ 255 个。
要表示的符号太多,无法用 1 个字节来完备表示。
1.2 如何翻译
打算机中供应多种翻译办法,常见的有 ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16等。这些都规定了转化的规则,按照这个规则就可以让打算机精确的表示我们的字符。下面先容这几种编码格式:
ASCII 码
统共有 128 个,用 1 个字节的低 7 位表示, 0 ~ 31 是掌握字符如换行、回车、删除等,32 ~ 126 是打印字符,可以通过键盘输入并且能够显示出来。
ISO-8859-1
128 个字符显然是不足用的,以是 ISO 组织在 ASCII 的根本上扩展,他们是 ISO-8859-1 至 ISO-8859-15,前者涵盖大多数字符,运用最广。ISO-8859-1 仍是单字节编码,它总归能表示 256 个字符。
GB2312
它是双字节编码,总的编码范围是 A1 ~ F7,个中 A1 ~ A9 是符号区,统共包含 682 个符号;B0 ~ F7 是汉字区,包含 6763 个汉字。
GBk
GBK 为《汉字内码扩展规范》,为 GB2312 的扩展,它的编码范围是 8140 ~ FEFE(去掉XX7F),统共有 23940 个码位,能表示 21003 个汉字,和 GB2312的编码兼容,不会有乱码。
UTF-16
它详细定义了 Unicode 字符在打算机中的存取方法。UTF-16 用两个字节来表示 Unicode 的转化格式,它采取定长的表示方法,即不论什么字符用两个字节表示。两个字节是 16 个 bit,以是叫 UTF-16。它表示字符非常方便,没两个字节表示一个字符,这就大大简化了字符串操作。
UTF-8
虽说 UTF-16 统一采取两个字节表示一个字符很大略方便,但是很大一部分字符用一个字节就可以表示,如果用两个字节表示,存储空间放大了一倍,在网络带宽有限的情形下会增加网络传输的流量。UTF-8 采取了一种变长技能,每个编码区域有不同的字码长度不同类型的字符可以由 1 ~ 6 个字节组成。
UTF-8 有以下编码规则:
如果是 1 个字节,最高位(第 8 位)为 0,则表示这是一个 ASCII 字符(00 ~ 7F)
如果是 1 个字节,以 11 开头,则连续的 1 的个数暗示这个字符的字节数
如果是 1 个字节,以 10 开头,表示它不是首字节,则须要向前查找才能得到当前字符的首字节
2、在 Java 中须要编码的场景
2.1 在 I/O 操作中存在的编码
如上图:Reader 类是在 Java 的 I/O 中读取符的父类,而 InputStream 类是读字节的父类, InputStreamReader 类便是关联字节到字符的桥梁,它卖力在 I/O 过程中处理读取字节到字符的转换,而对详细字节到字符的解码实现,它又委托 StreamDecoder 去做,在 StreamDecoder 解码过程中必须由用户指定 Charset 编码格式。值得把稳的是,如果你没有指定 Charset,则将利用本地环境中默认的字符集,如在中文环境中将利用 GBK 编码。
如下面一段代码,实现了文件的读写功能:
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 字符集,一样平常不会涌现乱码问题。
2.2 在内存操作中的编码
在内存中进行从字符到字节的数据类型转换。
1、String 类供应字符串转换到字节的方法,也支持将字节转换成字符串的布局函数。
String s = \"大众字符串\"大众;
byte[] b = s.getBytes(\"大众UTF-8\"大众);
String n = new String(b, \"大众UTF-8\"大众);
2、Charset 供应 encode 与 decode,分别对应 char[] 到 byte[] 的编码 和 byte[] 到 char[] 的解码。
Charset charset = Charset.forName(\"大众UTF-8\"大众);
ByteBuffer byteBuffer = charset.encode(string);
CharBuffer charBuffer = charset.decode(byteBuffer);
…
3、在 Java 中如何编解码
Java 编码类图
首先根据指定的 charsetName 通过 Charset.forName(charsetName) 设置 Charset 类,然后根据 Charset 创建 CharsetEncoder 工具,再调用 CharsetEncoder.encode 对字符串进行编码,不同的编码类型都会对应到一个类中,实际的编码过程是在这些类中完成的。下面是 String. getBytes(charsetName) 编码过程的时序图
Java 编码时序图
从上图可以看出根据 charsetName 找到 Charset 类,然后根据这个字符集编码天生 CharsetEncoder,这个类是所有字符编码的父类,针对不同的字符编码集在其子类中定义了如何实现编码,有了 CharsetEncoder 工具后就可以调用 encode 方法去实现编码了。这个是 String.getBytes 编码方法,其它的如 StreamEncoder 中也是类似的办法。
常常会涌现中文变成“?”很可能便是缺点的利用了 ISO-8859-1 这个编码导致的。中笔墨符经由 ISO-8859-1 编码会丢失信息,常日我们称之为“黑洞”,它会把不认识的字符接管掉。由于现在大部分根本的 Java 框架或系统默认的字符集编码都是 ISO-8859-1,以是很随意马虎涌现乱码问题,后面将会剖析不同的乱码形式是怎么涌现的。
几种编码格式的比较
对中笔墨符后面四种编码格式都能处理,GB2312 与 GBK 编码规则类似,但是 GBK 范围更大,它能处理所有汉字字符,以是 GB2312 与 GBK 比较该当选择 GBK。UTF-16 与 UTF-8 都是处理 Unicode 编码,它们的编码规则不太相同,相对来说 UTF-16 编码效率最高,字符到字节相互转换更大略,进行字符串操作也更好。它适宜在本地磁盘和内存之间利用,可以进行字符和字节之间快速切换,如 Java 的内存编码便是采取 UTF-16 编码。但是它不适宜在网络之间传输,由于网络传输随意马虎破坏字节流,一旦字节流破坏将很难规复,想比较而言 UTF-8 更适宜网络传输,对 ASCII 字符采取单字节存储,其余单个字符破坏也不会影响后面其它字符,在编码效率上介于 GBK 和 UTF-16 之间,以是 UTF-8 在编码效率上和编码安全性上做了平衡,是空想的中文编码办法。
4、在 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 来存储。以是看一段文本的大小,看字符本身的长度是没故意义的,纵然是一样的字符采取不同的编码终极存储的大小也会不同,以是从字符到字节一定要看编码类型。
我们能够看到的汉字都因此字符形式涌现的,例如在 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 发送到用户要求的浏览器里,再经由浏览器解码成为文本。这些过程如下图所示:
一次 HTTP 要求的编码示例
4.1 URL 的编解码
用户提交一个 URL,这个 URL 中可能存在中文,因此须要编码,如何对这个 URL 进行编码?根据什么规则来编码?有如何来解码?如下图一个 URL:
上图中以 Tomcat 作为 Servlet Engine 为例,它们分别对应到下面这些配置文件中:
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 的我选择的是360极速浏览器并通过 Postman 插件不雅观察我们要求的 URL 的实际的内容,以下是 URL:
君山的编码结果是:e5 90 9b e5 b1 b1,和《深入剖析 Java Web 技能底细》中的结果不一样,这是由于我利用的浏览器和插件和原作者是有差异的,那么这些浏览器之间的默认编码是不一样的,原文中的结果是:
君山的编码结果分别是: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 是如何解码的。
解析要求的 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 的 中定义的,如果没有定义,那么将以默认编码 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 两个参数。
4.2 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 中,这样在浏览器到做事器的通报过程中就不会丢失信息了,如果我们要访问这些项时再按照相应的字符集解码就好了。
4.3 POST 表单的编解码
在前面提到了 POST 表单提交的参数的解码是在第一次调用 request.getParameter 发生的,POST 表单参数通报办法与 QueryString 不同,它是通过 HTTP 的 BODY 通报到做事真个。当我们在页面上点击 submit 按钮时浏览器首先将根据 ContentType 的 Charset 编码格式对表单填的参数进行编码然后提交到做事器端,在做事器端同样也是用 ContentType 中字符集进行解码。以是通过 POST 表单提交的参数一样平常不会涌现问题,而且这个字符集编码是我们自己设置的,可以通过 request.setCharacterEncoding(charset) 来设置。
其余针对 multipart/form-data 类型的参数,也便是上传的文件编码同样也是利用 ContentType 定义的字符集编码,值得把稳的地方是上传文件是用字节流的办法传输到做事器确当地临时目录,这个过程并没有涉及到字符编码,而真正编码是在将文件内容添加到 parameters 中,如果用这个编码不能编码时将会用默认编码 ISO-8859-1 来编码。
4.4 HTTP BODY 的编解码
当用户要求的资源已经成功获取后,这些内容将通过 Response 返回给客户端浏览器,这个过程先要经由编码再到浏览器进行解码。这个过程的编解码字符集可以通过 response.setCharacterEncoding 来设置,它将会覆盖 request.getCharacterEncoding 的值,并且通过 Header 的 Content-Type 返回客户端,浏览器接管到返回的 socket 流时将通过 Content-Type 的 charset 来解码,如果返回的 HTTP Header 中 Content-Type 没有设置 charset,那么浏览器将根据 Html 的 中的 charset 来解码。如果也没有定义的话,那么浏览器将利用默认的编码来解码。
4.5 其它须要编码的地方
除了 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”。
5、常见问题剖析
下面看一下,当我们碰到一些乱码时,该当怎么处理这些问题?涌现乱码问题唯一的缘故原由都是在 char 到 byte 或 byte 到 char 转换中编码和解码的字符集不一致导致的,由于每每一次操作涉及到多次编解码,以是涌现乱码时很难查找到底是哪个环节涌现了问题,下面就几种常见的征象进行剖析。
5.1 中文变成了看不懂的字符
例如,字符串“淘!
我喜好!
”变成了“Ì Ô £ ¡Î Ò Ï²»¶ £ ¡”编码过程如下图所示:
字符串在解码时所用的字符集与编码字符集不一致导致汉字变成了看不懂的乱码,而且是一个汉字字符变成两个乱码字符。
5.2 一个汉字变成一个问号
例如,字符串“淘!
我喜好!
”变成了“??????”编码过程如下图所示:
将中文和中文符号经由不支持中文的 ISO-8859-1 编码后,所有字符变成了“?”,这是由于用 ISO-8859-1 进行编解码时碰着不在码值范围内的字符时统一用 3f 表示,这也便是常日所说的“黑洞”,所有 ISO-8859-1 不认识的字符都变成了“?”。
5.3 一个汉字变成两个问号
例如,字符串“淘!
我喜好!
”变成了“????????????”编码过程如下图所示:
这种情形比较繁芜,中文经由多次编码,但是个中有一次编码或者解码不对仍旧会涌现中笔墨符变成“?”征象,涌现这种情形要仔细查看中间的编码环节,找出涌现编码缺点的地方。
5.4 一种不正常的精确编码
还有一种情形是在我们通过 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 来解析才造成乱码的。
6、总结
本文首先总结了几种常见编码格式的差异,然后先容了支持中文的几种编码格式,并比较了它们的利用场景。接着先容了 Java 那些地方会涉及到编码问题,已经 Java 中如何对编码的支持。并以网络 I/O 为例重点先容了 HTTP 要求中的存在编码的地方,以及 Tomcat 对 HTTP 协议的解析,末了剖析了我们平常碰着的乱码问题涌现的缘故原由。
综上所述,要办理中文问题,首先要搞清楚哪些地方会引起字符到字节的编码以及字节到字符的解码,最常见的地方便是读取会存储数据到磁盘,或者数据要经由网络传输。然后针对这些地方搞清楚操作这些数据的框架的或系统是如何掌握编码的,精确设置编码格式,避免利用软件默认的或者是操作系统平台默认的编码格式。、
还有一句 IG牛逼 哈哈哈