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

来源:互联网 发布:哔哩哔哩mac客户端 编辑:程序博客网 时间:2024/05/18 03:27

几种常见的编码格式

为什么要编码

人类的语言太多,因而表示这些语言的符号太多,无法用计算机中的一个基本的存储单元——byte来表示

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

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

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

编码格式

1.ASCII码

总共128个,用一个字节的低7位表示,0~31是控制字符如换行、回车、删除等,36~126是打印字符。


2.ISO-8859-1

扩展ASCII码,涵盖了大多数西欧语言字符。仍然是单字节编码,总共能表示256个字符。


3.GB2312

双字节编码,总的编码范围是A1~F7,其中从A1~A9是符号区,总共包含682个符号,从B0~F7是汉字区,包含6763个汉字。


4.GBK

为了扩展GB2312,加入更多的汉字,它的编码范围是8410~FEFE(去掉XX7F),总共有23940个码位,他们表示21003个汉字,它的编码是和GB2312兼容的。

GB2312编码的汉字可以用GBK来解码,并且不会有乱码。


5.GB18030

与GB2312兼容。


6.UTF-16

UTF-16具体定义了Unicode字符在计算机中的存取方法。UTF-16用两个字节来表示Unicode转化格式,是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是16个bit,所以叫UTF-16。UTF-16表示字符非常方便,每两个字节表示一个字符,大大简化了字符串操作,这也是Java以UTF-16作为内存的字符存储格式的一个很重要的原因。


7.UTF-8

UTF-16统一采用两个字节表示一个字符,有很大一部分字符用一个字节就可以表示现在要用两个字节表示,存储空间扩大了一倍,增大网络传输的流量。

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


Java中需要编码的场景

I/O操作中存在的编码

涉及编码的地方一般都在字符到字节或者字节到字符的转换上。

需要这种转换的场景主要是I/O,包括磁盘I/O和网络I/O.

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

写的情况也类似,StreamEncoder类负责将字符编码成字节。

只要指定统一的编解码Charset字符集,一般不会出现乱码问题。

建议不要使用操作系统的默认编码,因为这样应用程序的编码格式就和运行环境绑定起来了,在跨环境时很可能出现乱码问题。

内存操作中的编码

在java开发中除了I/O涉及编码外,最常用的应该就是在内存中进行字符到字节的数据类型转换,java中用String表示字符串,所以String类就提供了转换到字节的方法,也支持将字节转换为字符串的构造函数。

String s = "这是一段中文字符串";

byte[] b = s.get("UTF-8");

String n = new String(b,"UTF-8");

Charset提供encode与decode分别对应char[]到byte[]的编码和byte[]到char[]的编码:

Charset charset = Charset.forName("UTF-8");

ByteBuffer byteBuffer = charset.encode(string);

CharBuffer charBuffer = charset.decode(byteBuffer);


Java中如何编解码

首先根据指定的charsetName通过Charset.forName(charsetName)找到Charset类,然后根据Charset创建CharsetEncoder对象,再调用CharsetEncoder.encode对字符串进行编码,不同的编码类型都会对应到一个类中,实际的编码过程就是在这些类中完成的。

相对来说UTF-16编码效率最高,字符到字节的相互转换更简单,进行字符串操作也更好。它适合在本地磁盘和内存之间使用,可以进行字符和字节之间的快速切换,如java的内存编码就采用UTF-16编码。但是它不适合在网络之间,因为网络传输容易损坏字节流,一旦字节流损坏将很难恢复,相比较而言UTF-8更适合网络传输。


Java Web中涉及的编解码

数据经过网络传输都是以字节为单位的,所以所有的数据都必须能够被序列化为字节。在java中数据要被序列化必须继承Serializable接口。

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


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

GET方式HTTP请求的QueryString与POST方式HTTP请求的表单参数都是作为Parameters保存的,都通过request.getParameters获取参数值。对它们的解码是在request..getParameters方法第一次被调用时进行的。

QueryString本身是通过HTTP的Header传到服务器的,并且也在URL中。

QueryString的解码字符集要么是Header中ContentType定义的Charset,要么就是默认的ISO-8859-1,要使用ContentType中定义的编码就要就要将connector的<Connector URIEncoding="UTF-8" useBodyEncodingForURI="true"/>中的useBodyEncodingForURI设置为true。


除了上面的URL外还可能会在Header中传递其他参数,如Cookie、redirectPath等。

对header中的项的进行解码也是在调用request.getHeader时进行的,如果请求的Header项没有解码则调用MessageBytes的toString方法,默认使用ISO-8859-1.而我们也不能设置header的其他解码格式,所以如果你设置的header中有非ASCII字符解码肯定会有乱码。


post表单提交的参数的解码是在第一次调用request.getParameter时发生的,post表单参数传递方式与QueryString不同,它是通过HTTP的BODY传递到服务端的

当我们在页面上单击提交按钮时浏览器首先将根据ContentType的Charset编码格式对表单填的参数进行编码,然后提交到服务端,在服务端同样也是用ContentType中的字符集进行解码的。所以通过post表单提交的参数一般不会出现问题,而且这个字符集编码是我们自己设置的,可以通过request.setCharacterEncoding(charset)来设置。

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

当用户请求的资源已经成功获取后,这些内容将通过Response返回给客户端浏览器,这个过程先要经过编码再到浏览器进行解码,编解码字符集可以通过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"




0 0
原创粉丝点击