深入分析编码问题

Posted by ShiYu on 2017-12-22

为什么要编码

现实生活中有很多种语言,表示这些语言的符号太多,但是计算机存储的基本单元----字节(byte)却只有8个bit,每个bit可以有0和1两种状态,也就是最多只能表示256个字符,远远不够表示组成世界众多语言的字符,比如说常用的汉字就有3500个,单纯靠计算机存储基本单元表示连常用的汉字都表示不了,要解决这个问题必须要有一个新的数据结构char,而从char到byte必须通过编码。

编码场景

I/O中存在的编码

涉及编码的地方一般都是字符到字节或者字节到字符的转换上,这种转换的主要场景是I/O,I/O包括磁盘I/O和网络I/O,网络I/O主要体现在WEB应用上,下图是java中处理I/O问题的接口:
java中处理I/O问题的接口

Reader类是读字符的父类,Inputstream类是读字节的父类,InputStreamReader是字节到字符的适配器类,它负责I/O处理过程中读取字节到字符的转换,而对具体字节到字符的解码实现,它又委托StreamDecoder去做,在StreamDecoder解码使用用户指定的Charset编码格式,如果没有指定Charset,则将使用本地环境中的默认字符集,如在中文环境中将使用GBK编码。

写的情况比较类似,是由字符转换成字节,中间使用了StreamEncoder进行编码,编码过程也需要指定Charset。

我们在程序中设计I/O时,编码和解码的Charset必须一致,否则就会出现乱码问题.

内存操作中的编码

java开发中,经常会在内存中进行从字符到字节的数据类型转换,java中,String表示字符串,所以String类就提供了转换到字节的方法,也支持将字节转换位字符的构造器,代码如下:

1
2
3
4

String str="我是史禹,我想与整个世界谈谈"
byte[] b=str.getBytes("utf-8");
String s=new String(b,"utf-8");

java web中涉及的编解码

一次HTTP请求需要在以下地方进行编码:

URL的编解码

用户提交一个URL,URL中可能存在中文,因为此需要进行编码,URL由下图几个部分组成:

URL组成

以Tomcat为例,将他们分别对应到配置文件中,Port对应在server.xml中中,ContextPath在<Contextpath="/test/" />中配置,Servlet Path在Web应用的web.xml中 中配置,PathInfo是我们请求的具体Servlet,QueryString是要传递的参数,由于是这几在浏览器中输入该url,所以是通过Get方式请求的。

1
2
3
4
<servlet-mapping>
<servlet-name>shiyu</servlet-name>
<url-pattern>/servlets/servlet/*</url-pattern>
</servlet-mapping>

许令波的《深入分析JavaWeb技术内幕》一书中,指出通过GET方式请求时,PathInfo和QueryString在浏览器发起请求前的编码不同,PathInfo是UTF-8编码,QueryString是GBK编码,但是经过个人实践,在CHROME、SAFAIR、FIREFOX三个浏览器中两者采用的编码都是UTF-8,可能是由于版本不同导致的实践结果不一致,总而言之,当前浏览器对于PathInfo和QueryString采用的编码都是UTF-8。

浏览器发送请求之前的编码我们已经了解了,现在来分析一下服务器是如何进行解码的,以Tomcat为例,Tomcat对URL中uri部分进行解码的字符集是通过读取server.xml中 中的URIEncoding来决定的,如果没有定义,默认就会采用ISO-8859-1。对于QueryString,是作为Parameters保存的,是通过request.getParameter来获取参数值,对于它的解码是在request.getParameter方法第一次调用时进行的,QueryString的解码字符集本身是通过HTTP的header传递到服务端的,QueryString的解码字符集要么是Header中ContentType定义的charset,要么是默认的ISO-8859-1,想要使用ContentType中定义的charset,就需要配置server.xml中 userBodyEncodingForURI为true.

总结:GET请求的URL编解码需要配置中的URIEncoding和useBodyEncodingForURI,其中URIEncoding是用来指定URI的解码字符集,userBodyEncodingForURI是用来指定QueryString的解码字符集使用header中ContentType生命的charset。

HTTP Header的编解码

当客户端发起请求时,会向服务端传递类似Cookie、redirectPath等信息,针对通过Header传递的信息的解码统一使用的是ISO-8859-1,没有地方可以指定其他的字符集进行解码,所以Header中不能传递非ASCII字符,如果一定要传递,需要先用org.apache.catalina.util.URLEncoder编码,然后再添加到Header中,这样可以避免传递过程中信息丢失,我们要使用时再按照编码时指定的字符集进行解码即可。

Post表单的编码

Post表单提交的参数的解码也是在第一次调用request.getParameter时发生的,post表单参数传递的方式与queryString不同,它是通过HTTP的Body传递到服务端的,它首先会根据ContentType的Charset编码,然后提交的服务端,服务端同样通过ContentType中的字符集进行解码,所以默认情况下Post表单提交的参数不会出现乱码问题,另外,POST解码的字符集我们可以通过request.setCharacterEncoding(charset)手动指定。

JS中编码问题

外部引入js文件

如果引入的js文件的编码和当前页面的编码不一致,就需要声明js文件的编码:

1
<script src="a.js" charset="utf-8" />

JS的URL编码

escape()

该函数功能是将ASCII编码之外的其他字符转化成Unicode编码,并在编码值前加上“%u",通过将特殊字符转换成unicode编码值,可以防止黑洞发生,即信息丢失,解码时通过unescape()解码即可。

encodeURI

该函数是用来对URL编码的函数,可以将整个URL中字符(特殊字符除外,如”!“”#“”¥“”&“等)进行UTF8编码,每个码值前加上”%“,通过decodeURI()函数进行解码。

encodeURIComponent()

该函数除了对”!“、”’“、”(“、”)“、”*“、”-“、”.“、”_"、“~”、“0-9”、“a-zA-Z"这几个字符不编码,对其他所有字符都编码,该函数通常用于将一个url当做参数放在另一个url中,该函数可以解决后面url中”&“影响前面url完成性的问题,通过decodeURIComponent()进行解码。

java与js编解码

通过js编码的字符传递到服务端通过java如何解码?

java中处理URL主要是java.net.URLEncoder和java.net.URLDecoder。这两个类可以将所有”%“加utf8码值用urf8解码,从而得到原始字符,java端的URLEncoder和URLDecoder对应js端的encodeURIComonent和decodeURIComponent。

如果前端用js编码后传递到后端出现乱码,肯定是因为连段编码类型不一致,前端用的是UTF8,但是服务端解码一般是GBK或者GB2313,解决方法是利用encodeURIComponent两次编码,这样在java端通过request.getParamter()用gbk解码后取得的就是UTF8编码的字符串,如果java端需要使用这个字符串,就再用UTF8解码一次。

常见问题

中文变成看不懂字符

原因一般都是因为解码所用字符集与编码所用字符集不一致。

一个汉字变成一个问号

原因是采用了不支持中文的ISO-8859-1编码导致的,ISO-8859-1进行编解码时,对于不在码值范围内的字符会统一用3f表示,3f解码后就是问号。

一个汉字变成两个问号

这种情况比较复杂,中文经过了多次编码,但是其中有一次编码或解码不对有时就好出现问号,要仔细查看编码环节,找出编码错误地方。