编码问题总结

转自:《深入分析 Java Web技术内幕》

 

为什么要编码

把计算机能理解的语言假定为英语,其他语言能够在计算机中使用必须经过一次翻译,把它翻译成英语,这个翻译的过程就是编码。

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

(2) 人类要表示的符号太多,无法用一个字节来完全表示。

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

 

几种编码格式

ASCII码:总共128个,用一个字节的低7位表示。

ISO-8859-1:涵盖大多数西欧语言字符,总共256个,单字节编码。

    ISO组织在ASCII码基础上又制定了一系列标准用来扩展ASCII编码,它们是ISO-8859-1~ISO-8859-15。

GB2312:双字节编码,包含682个字符,6763个汉字

GBK:扩展了GB2312,包含21003个汉字。用GB2312编码的汉字可以用GBK来解码,并且不会有乱码。

GB18030:编码与GB2312兼容,可能是单字节、双字节或四字节编码。

UTF-16:Unicode是Java和XML的基础。UTF-16具体定义了Unicode字符在计算机中的存取方法。UTF-16用两个字节来表示Unicode转化格式。UTF-16表示字符非常方便,每两个字节表示一个字符,就大大简化了字符串操作,这也是Java以UTF-16作为内存的字符存储格式的一个很重要的原因。

缺点:很大一部分字符用一个字节就可以表示的现在要用两个字节表示,存储空间放大了一倍。

UTF-8:采用变长技术,每个编码区域有不同的字码长度。不同类型的字符可以由1~6字节组成。

(1)如果一个字节,最高位为0,表示这是一个ASCII字符,可见,所有ASCII编码已经是UTF-8了。

(2)如果一个字节,以11开头,连续的1的个数暗示这个字符的字节数。

(3)如果 一个字节,以10开始,表示它不是首字节,需要向前查找才能得到当前字符的首字节。

 

具体说来,Java在内存中的使用的编码格式是UTF-16,而在字节码(class)文件中存储时,使用的编码格式是UTF-8。

 

I/O操作中存在的编码

涉及编码的地方一般都在字符到字节或者字节到字符的转换上,需要这种转换的场景主要是I/O,这个I/O包括磁盘I/O或网络I/O。

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

写的情况也类似,字符的父类是Writer,字节的父类是OutputStream,通过OutputStreamWriter转换字符到字节,StreamEncoder类负责将字符编码成字节。

应用程序中涉及I/O操作时,只要注意指定统一的编解码Charset字符集,一般不会出现乱码问题。

强烈建议不要使用或依赖于操作系统的默认编码,这样跨环境时很可能出现乱码问题。

 

内存操作中的编码

String类提供了转换到字节的方法,也支持将字节转换为字符串的构造方法。

已被废弃的ByteToCharConverter和CharToByteConverter类分别提供了convertAll方法可实现byte[]和char[]的互转。这两个类已经被Charset类取代,Charset类提供encode与decode方法分别对应char[]到byte[]的编码和byte[]到char[]的解码。

Charset charset = Charset.forName("UTF-8");
ByteBuffer byteBuffer = charset.encode("字符转字节");
CharBuffer charBuffer = charset.decode(byteBuffer);

ByteBuffer类提供一种char和byte之间的软转换,它们之间转换不需要编码与解码,只是把一个16bit的char拆分成2个8bit的byte表示,它们的实际值并没有被修改,仅仅是数据的类型做了转换。

char c = 'a';

ByteBuffer heapByteBuffer = ByteBuffer.allocate(1024);

ByteBuffer byteBuffer = heapByteBuffer.putChar(c);

 

按照ISO-8859-1编码

3f也就是"?"字符,经常会出现中文变成“?”,很可能就是错误地使用了ISO-8859-1这个编码导致的,中文字符经过ISO-8859-1编码会丢失信息,通常我们称之为“黑洞”,它会把不认识的字符吸收掉。现在大部分 基础的Java框架或系统默认的字符集编码都是ISO-8859-1,所以很容易出现乱码问题。

 

按照GB2312编码

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

GB2312只支持6763个汉字,所以并不是所有汉字都能用GB2312编码。

 

按照GBK编码

GBK编码兼容GB2312编码,但它们的码表长度不一样,GBK包含的汉字更多。所以只要是经过GB2312编码的汉字都可以用GBK解码,反之则不成立。

 

按照UTF-16编码

从UTF-16的编码规则来看,仅仅将字符的高位和低位拆分变成两个字节,编码效率非常高,规则很简单。由于不同处理器对2字节处理方式不同,Big-endian或Little-endian编码,在对字符串进行编码时需要指明到底是Big-endian还是Little-endian,所以前面有两个字节用来保存BYTE_ORDER_MARK值,UTF-16是定长16位(2字节)来表示的UCS-2或Unicode转换格式,通过代理来访问BMP之外的字符编码。

 

按照UTF-8编码

UTF16的缺点:

1. UTF-16虽然编码效率非常高,但是对单字节范围内的字符放大了一倍,浪费了存储空间。

2. UTF-16采用的是顺序编码,不能对单个字符的编码值进行校验,如果中间的一个字符码值损坏,后面的所有码值都将受影响。

UTF-8不存在这些问题,UTF-8对单节范围内的字符仍然采用一个字节表示,对汉字采用三个字节表示。

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

UTF-16效率高,用于内存和系统进行数据交换,UTF-8更适合网络传输。

 

Java Web中涉及的编解码

看一段文本的大小,看字符本身的长度是没有意义的,即使是一样的字符采用不同的编码最终存储的大小也会不同,所以从字符到字节一定要看编码类型。

计算机中一个字符是由一个数字唯一表示的。Java中一个char是16个bit,相当于两个字节。

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

 

URL的编解码

URL的几个组成部分:

URL

http://localhost:8050/examples/servlets/servlet/君山?author=君

在windows的默认字符集为GBK(936)或者UTF-8(65001)的环境下:

在Chrome浏览器中被编码为:

http://localhost:8050/examples/servlets/servlet/%E5%90%9B%E5%B1%B1?author=%E5%90%9B%E5%B1%B1

在火狐浏览器中被编码为:

http://localhost:8050/examples/servlets/servlet/%E5%90%9B%E5%B1%B1?author=%BE%FD%C9%BD

而在IE中访问时,没有显示编码……

火狐浏览器中,PathInfo是UTF-8编码,QueryString是GBK编码,浏览器编码URL将非ASCII字符按照某种编码格式编码成16进制数字后将每个16进制表示的字节前加上“%”。

默认IE浏览器的编码结果也一样,不过IE可以修改URL的编码格式,在“选项”——>"高级"——>"国际"里面的“发送UTF-8 URL”选项可以取消。

从上面的测试可知,浏览器对PathInfo和QueryString的编码是不一样的,不同浏览器对PathInfo的编码也可能不一样,这对服务器的解码造成了很大的困难。

Tomcat对URL的URI部分进行解码的字符集是在connector的<Connector URIEncoding="UTF-8"/>中定义的,如果没有定义,将以默认编码ISO-8859-1解析。所以如果有中文时最好把URIEncoding设置成UTF-8编码。(大部分浏览器对PathInfo采用UTF-8编码)

GET方式HTTP请求的QueryString与POST方式HTTP请求的表单参数都是作为Parameters保存的,都通过request.getParameter获取参数值。对它们的解码是在request.getParameter方法第一次被调用时进行的。request.getParameter方法被调用时将会调用org.apache.catalina.connector.Request的parseParameters方法,这个方法会对GET和POST方式传递的参数进行解码,但它们的解码字符集有可能不一样。

QueryString的本身是通过HTTP的Header传到服务器的,并且也在URL中,但和PathInfo的解码字符集不一样。QueryString的解码字符集要么是Header中ContentType定义的Charset,要么是默认的ISO-88590-1,要使用ContentType中定义的编码就要将connector的<Connector URIEncoding="UTF-8" useBodyEncodingForURI="true"/>中的useBodyEncodingForURI设为true,它并不是对整个URI都采用UTF-8解码,而仅仅是对QueryString使用BodyEncoding解码。

从上面可以看出,编码和解码并不是我们在应用程序中能完全控制的,所以在应用程序中应尽量避免在URL中使用非ASCII字符。

 

HTTP Header的编解码

对Header中的项进行解码是在调用request.getHeader时进行的,默认编码是ISO-8859-1,而我们也不能设置其他解码格式,所以如果Header中有非ASCII字符解码肯定会有乱码。所以,不要在Header中传递非ASCII字符,如果要传递,可以先将这些字符用org.apache.catalina.util.URLEncoder编码,然后再添加到Header中。

 

POST表单的编解码

POST表单参数的解码也是在第一次调用request.getParameter时(同GET),但POST表单参数的传递方式与QueryString不同,它是通过HTTP的BODY传递到服务端的。提交POST表单时,浏览器会根据ContentType的Charset编码格式对表单填的参数进行编码,在服务端同样也是用ContentType中的字符集进行解码。所以通过POST表单提交的参数一般不会出现问题,而且这个字符集可以通过request.setCharacterEncoding(charset)方法来设置。

注意,一定要在第一次调用request.getParameter方法之前就设置request.setCharacterEncoding(charset),否则POST表单提交上来的数据也可能出现乱码。

Tomcat在解析Parameter参数集合前,会获取Header的content-type请求头,并检查这个content-type中的charset值,默认情况下,浏览器在提交form表单时,提交的content-type是不会含有charset信息的,所以如果没设置request.setCharacterEncoding(charset),表单提交的数据就会按系统默认编码方式解析。

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

 

HTTP BODY的编解码

返回给客户端浏览器的编解码过程中,编解码字符集可通过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来解码,如果也没有定义,浏览器将使用默认编码来解码。

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

 

JS中的编码问题

外部引入JS文件

如果在一个单独的JS文件中包含字符串输入的情况,这时如果script没有设置charset,浏览器就会以当前这个页面的默认字符集解析这个JS文件。

 

JS的URL编码

通过JS发起异步调用的URL默认编码也受浏览器影响,另外,不同的JS框架可能对URL的编码处理也不一样。

JS中处理URL编码有3个函数

1.escape()

将ASCII字母、数字、标点符号(*+-./@_)以外其他所有字符转化成Unicode编码值,并在编码值前加上"%u"。

解码通过unescape()函数。

通过将特殊字符转换成Unicode编码值可避免因为编码的字符集的不兼容而出现信息丢失问题,在服务端通过解码参数就可避免乱码问题。

注意:escape和unescape已经从ECMAScript v3标准中删除了,URL的编码可以用encodeURI和encodeURICompoment代替。

 

2.encodeURI()

JS用来对URL编码的函数,可将整个URL中的字符(除一些特殊字符,如!#$&'()*+,-./:;=?@_0-9a-zA-Z)进行UTF-8编码,在每个码值前加上%

解码通过decodeURI()函数

 

3.encodeURIComponent()

比encodeURI更彻底,除了对!'()*-._~0-9 a-z A-Z这几个字符不编码外,其他所有字符都要编码,通常用于将一个URL当做一个参数放在另一个URL中。如果不进行encodeURICompoment编码,后面URL中的"&"将会影响前面的URL的完整性。

解码通过decodeURIComponent()

 

4.Java与JS编解码问题

如果JS进行了编码,那么编码的字符串到服务端后可通过java来解码,那么java又怎么解码呢?

Java端处理URL编解码有两个类,分别是java.net.URLEncoder和java.net.URLDecoder,这两个类可将所有'%'加UTF-8码值用UTF-8解码,从而得到原始的字符。

URLEncoder受保护的特殊字符要少于JS中受保护的特殊字符。

Java端的URLEncoder和URLDecoder与前端JS对应的是encodeURIComponent和decodeURIComponent。

JS编码默认是UTF-8,,而服务端中文解码一般都是GBK或GB2312。可以使用encodeURIComponent两次编码,即encodeURIComponent(encodeURIComponent(str)),服务端默认解码之后,再使用UTF-8解码一次,以解决客户端和服务端字符编码类型不一致的问题。

 

常见问题分析

出现乱码问题都是在char到byte或byte到char转换中编码和解码的字符集不一致导致的,由于一次操作涉及多次编解码,所以出现乱码时很难查找到底是哪个环节出现了问题。

 

中文变成了看不懂的字符

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

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

 

一个汉字变成一个问号

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

将中文和中文符号经过不支持中文的ISO-8859-1编码后,所有字符变成了"?",这是因为用ISO-8859-1进行编解码时遇到不在码值范围内的字符集是地统一用3f表示,即通常所说的“黑洞”,所有ISO-8859-1不认识的字符都变成了"?"。

 

一种不正常的正确编码

在我们调用request.getParameter获取参数值时,直接调用会出现乱码,但如果用下面的方式

String value = new 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来解析。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值