深入分析Java Web中的编码问题

时间:2021-04-23 08:55:51

编码问题一直困扰着我,每次遇到乱码或者编码问题,网上一查,问题解决了,但是实际的原理并没有搞懂,每次遇到,都是什么头疼。

决定彻彻底底的一次性解决编码问题。

1.为什么要编码

计算机的基本单元是字节,一个字节是8bit。一个字节的范围是0~255。

人类要表示的符号肯定比256个多,所以无法用一个字节来表示这个多的符号。

你想想,光汉字就有几千个。

要解决这个矛盾,有了一个新的数据结构:char。char也就是字符,最长16bit,最短为8bit。一个字符的最大长度是16bit。一个字符的范围是0~2的16次方。

所以一个char型的数据结构,可以表示这个地球上的所有字符。

但是计算机它不会为你改变啊,它的基本单元还是字节,8bit。所以如何把一个最长为16bit的char字符表示成为一个或者若干个8bit长的byte字节,

这个过程就需要编码。

2.如何翻译

各种语言交流,都需要经过翻译。一个长为16bit的char字符表示成为一个或者若干个8bit长的byte字节,

计算机中提供了很多翻译方式:ASCII,ISO-8859-1,GB2312,GBK,UTF-8,UTF-16

上面的几种编码方式都可以看成是字典,它们规定了转化的规则,按照这个规则就可以让计算机正确通过字节的来表示我们自己定义的字符char这种数据类型。

ASCII码:

ASCII码,总共128个。由于ASCII码只有128个,所以可以用一个8bit长的字节表示,也可以用一个16bit长的字符表示。

0~31表示的控制字符如换行,回车,删除等,32~126是打印字符,如大写字母,小写字母等。

ASCII码是单字节编码,也就是一个8bit长的char字符或者一个8bit的byte字符用一个字节来编码。

ISO-8859-1:

128个字符对于只使用英语的国家来说是够用了,但是对于那么西欧国家的语言来说就不够用了,因为他们国家的语言中,除了英语字母,还有其他的字符。

于是ISO组织在ASCII码基础上制定了一系列的标准来扩展ASCII码,它们是ISO-8859-1至ISO-8859-15。其中ISO-8859-1涵盖了大多数的西欧语言字符,

所以应用的最为广泛。

ISO-8859-1也是单字节编码,也就是一个8bit长的char字符用一个字节来编码。也就是扩展ASCII码之后的ISO-8859-1,总共可以表示256个字符。

GB2312:

GB2312的全称是《信息技术 中文编码字符集》,GB2312是双字节编码,也就是一个16bit长的char字符用两个字节来编码。

GB2312可以表示682个符号和6763个汉字。

GBK:

GBK的全称是《汉字内码扩展规范》,它的出现是为了扩展GB2312,以表示更多的汉字。

GBK是双字节编码,也就是一个16bit长的char字符用两个字节来编码。

GBK可以表示21003个汉字。

UTF-16:

说到UTF必须提到Unicode,ISO试图创建一个全新的超语言字典,世界上所有的语言都可以通过这个字典来相互翻译,而这就是Unicode。

UTF-16以及下面提到的UTF-8都是Unicode的不同实现形式。

UTF-16是双字节编码,也就是一个16bit长的char字符用两个字节来编码。

UTF-8:

UTF-16统一使用两个字节来表示一个字符,虽然在表示上非常简单,方便,但是也有缺点,有很多的字符用一个字节就可以表示了,但是使用两个字节来表示,造成了

存储空间的浪费。UTF-8则采用了一种变长的表示技术,每个编码区域有不同的字符长度。不同类型的字符可以由1~6个字节来表示。

UTF-8有如下的编码规则:

如果是一个字节长度的byte或者8bit长的char,最高位为0,则表示这是一个ASCII字符,UTF-8用单字节来表示。

如果一个字节以11开头,则连续的1的个数暗示这个字符的字节数。110XXXXX则表示这是char字符的第一个字节,这个char字符由两个字节组成,这个char字符16bit长,

UTF-8需要用两个字节或者更多字节来编码这个字符。

如果一个字节以10开头,表名这个字节不是char字符的第一个字节,前一个字节是这个char字符的第一个字节,也暗示这个char字符由两个字节组成,这个char字符16bit长,

UTF-8需要使用两个或者更多的字节来编码这个字符。

由上面可知一个char字符可以是8bit的,如英文字母,符号,西欧字符,也可以是16bit的,如汉字,汉语中的符号。对于8bit的char字符,除了UTF-16用两个字节来编码这个字符,

其他编码规则都是用一个字节来编码的。而对于16bit的char字符各个编码规则,则有不同字节来编码。

3.Java中涉及编码的场景:

在I/O操作中存在编码问题

我们知道在涉及编码的地方一般都在从字符到字节或者字节到字符的转换上,而涉及这种转换的场景主要是I/O。

我们站在内存的角度上,读操作是从硬盘文件上将内容传入的内存,写操作是将内容从内存传入到硬盘文件。

硬盘文件中保存的byte字节数据。(我们之所以打开文件可以看见字符,是因为文本编辑器对这些byte字节进行了解码,形成了char字符)

而内存中(此处的内存可以理解为我们的程序)的数据是char字符。也就是读操作是将硬盘文件中的byte字节转化为程序中的char字符的解码过程。

写操作就是将程序中的char字符转化为硬盘文件中byte字节的编码过程。

InputStrean,OutputStream是面向字节的输入和输出流 (父类)。这个两个流的子类用来关联存储byte字节数据的硬盘文件。

Reader,Writer是面向字符的输入和输出流 (父类)。这两个流的子类用来关联内存(程序)中的char字符数据。

而这两种流之间的编码,解码的转化由OutputStreamWriter,InputStreamReader这两个类来完成,也就是OutputStreamWriter,InputStreamReader关联了字符流和字节流,是桥梁,

这个编码和解码的过程可以指定编解码格式。如果没有指定编码,解码格式,则将使用本地环境中的默认字符集,如在中文环境中将使用GBK编码。

在Java中    一个String字符串,我们可以调用这个字符串的getBytes(编码规则),来得到这个字符串用指定编码规则编码之后的byte字节数组。

我们也可以使用 new  String(byte[]   byteArray ,String  编码规则),来得到一个byte字节数组,通过指定编码规则进行解码之后的字符串。

4.在Java Web中涉及的编解码

上面Java中涉及的编码是磁盘I/O,而对于Java Web而言涉及的I/O是网络I/O。通过网络I/O传输都是以字节为单位的,这就涉及到编码。

用户从浏览器端发起一个Http请求,对于这个请求,需要编码的地方有URL , Cookie ,Post表单参数。

URL的编解码:

先来区分什么是URL,什么是URI。

http://localhost:8080/examples/servlets/servlet/啦啦?name=小明

完整的URL是?前面的所有 ,也就是说name=小明这个不是URL的一部分 ,这个部分称为查询字符串

完整的URI是去掉域名http://localhost:8080之后到?之前的内容,也就是说URI是URL的一部分。

在Java中可以通过request.getRequestURL()和request.getRequestURI来分别获取一个请求的URL和URI。

我们知道请求的方式分为get和post,对于post方式提交查询字符串是通过表单的方式提交的服务器的。

对于get方式来说,虽然URL和查询字符串写在一起,浏览器却对于 它们实行了不一样的编码 ,对于URL实行了UTF-8编码,对于查询字符串实行了GBK编码。

对于post方式,对于URL也是实行UTF-8编码,由于查询字符串通过表达提交,在将对post表单编码的时候再说。

提交一个请求之后,tomcat需要对URL进行解码成字符串,才能进行匹配这个请求是请求哪个servlet。对于URL中URI的解码规则是在connector的<Connector URIEncoding="UTF-8">中指定的,

如果没有定义,那么将以默认编码ISO-8859-1来解析。那么问题就来了,如果URL中含有中文,浏览器通过UTF-8对URL进行编码,我们知道UTF-8可以编码中文使信息不丢失,但是如果tomcat没有指定<Connector URIEncoding="UTF-8">,

那么tomcat用ISO-8859-1来解码,就不行了。如果URL中没有中文,只是英文和/,我们知道UTF-8对于英文字符和特殊字符,是用一个字节来编码的,ISO-8859-1也是用一个字节来编码的,所以对于URL是英文的情况,tomcat指定解码规则是

UTF-8还是使用默认的ISO-8859-1来解码,都是可以的。所以在URL中最好不要出现中文。

对于get方式提交的查询字符串的编码格式,是在这个请求的header中的ContentType中指定的,那么在服务端使用request.getParamters("name")进行解码时(假设我们没有使用过滤器request.serCharacterEncoding()来指定解码规则),是通过

哪个规则来进行解码的?答案是要么使用默认的ISO-8859-1来解码,要么使用ContentType中的编码规则来解码,而且要使用ContentType中的编码规则来解码,需要在<Connector URIEncoding="UTF-8" useBodyEncodingForURI="true"/>

useBodyEncodingForURI="true",这个名字可能会让人混淆,不是对于整个URI使用BodyEncoding的编码规则来解码(BodyEncoding的编码就是ContentType中的编码规则),而是只对查询字符串使用BodyEncoing也就是ContentType中指定的

编码规则来解码。

可见如果使用get方式提交请求,查询字符串中出现了中文,查询字符串的编码规则在header中的ContentType中指定,而解码规则要和编码规则对应,则需要在Connector中进行设置(如果不使用request.serCharacterEncoding()来指定解码规则)。

而且如果使用request.serCharacterEncoding()指定解码规则,查询字符串中每个含有中文的参数都要单独指定解码规则,而不是像post那样,参数通过一个整体body提交,只要指定一次解码规则就行了  。

所以用get方式提交请求,查询字符串中出现中文,编解码都比较麻烦,所以我们应该尽量在通过Get方式提交的请求中,不再查询字符串中出现中文 。

Http header的编解码:

当客户端发起一个http请求时,除了上面的URL外在这个请求的header中传递的其他参数,如Cookie,redirectPath,也存在编解码的问题。

请求的header的编解码,默认都是ISO-8859-1,所以我们在添加header中参数的值时,不要在header中传递非ASCII字符。

Post表单的编解码:

post表单提交参数的方式和get方式通过查询字符串方式不同。get方式的查询字符串的编码方式是ContentType中的Charset指定的编码规则,post的方式的参数的的编码方式也是ContentType中的Charset指定的编码规则,但是不同的是我们可以

通过request.setCharacterEncoding(charset)的方式来设定post方式参数的编码规则  ,解码规则则使用ContentType中设定的编码规则。

http body的编解码:

对于响应的编解码,可以使用response.setCharacterEncoding()来设置header的ContentType的编码规则,也可直接使用response.setContentType()来设置,浏览器接到响应会按照ContentType中的编码规则去解码。

如果我们没有在响应中设置编码规则,那么浏览器会根据页面的<meta http-equiv="Content-Type" content="text/html" ;charset=GBK>中charset来解码,如果这个<meta>标签也没有设置,浏览器将会以默认的编码格式来解码。

上面所说的请求编解码,响应编解码,均是浏览器来进行处理,与页面没有关系。也就是说页面的编解码规则,与我们在这个页面上发送的请求的编码规则无关,请求的编码规则是在浏览器的设置中,每个浏览器对于请求的编码规则可能略有不同。

则向上面所说的。

5.在JS中的编码问题

外部引入JS文件

<html>
    <meta charset="utf-8">

<script src="static/js/script.js"  charset="gbk"></script>

这里的<script/>标签中的charset指明我们将以gbk的解码规则去解码引入的js文件。如果我们没有在这里指定<script/>中的charset。将会以这个页面<meta/>标签中的字符集规则去解码。

那么这个时候如果引入的JS文件是gbk编码,没有设置解码规则,按照页面的utf-8去解码,就有可能出现乱码的情况。

JS中的URL编码:

我们知道在页面上我们直接点击一个链接来发起请求,这个链接的编码是有浏览器的设置来进行编码的。通过JS来发起异步的请求,对于URL的编码也是默认是浏览器的设置。如果我们使用了JS框架,那么不同框架对于URL的编码也有可能不同。

实际上,我们可以主动去编码URL,一般来说URL中都是英文字母和特殊字符,无论哪种编码都是用单字节来编码这些ASCII的,那么无论用哪一种编码来解码都会解码正确。

JS中处理URL编码的函数有三个:

escape(args)

将传入的args参数,除ASCII字符之外的其他字符,按照UTF-16进行编码,并且在编码值前加上“%u”

encodeURI(args)

将传入的args参数,除除ASCII字符之外的其他字符,按照UTF-8进行编码,并且在每个编码值前加上“%”

encodeURIComponent()

比encodeURI还彻底的编码函数。

上面我们对于URL在JS中完成了编码,那么这个编码的URL,由于ASCII字符,无论哪种编码和解码都会正确,所以可以根据URL来找到正确的servlet。

而对于这个URL中的非ASCII字符的字符,我们就需要使用Java的URLDecoder类来进行解码了,URLDecoder的解码可以将“%”加utf-8编码的非ASCII字符,使用utf-8进行解码。

从而的到正确的未编码前的URL。