中文乱码

来源:互联网 发布:洋娃娃为什么恐怖 知乎 编辑:程序博客网 时间:2024/04/23 15:37
注:本文主要摘自《深入分析Java Web技术内幕》-许令波著

1、常见编码格式

1)      ASCII码

ASCII码,总共有128个,用一个字节的低7位来表示,0-31是控制字符(换行、回车等),32-126是打印字符,可以通过键盘输入并且能够显示出来。

2)      ISO-8859-1

128个字符显然是不够用的,于是ISO组织在ASCII码基础上又指定了一系列标准来扩展ASCII编码,它们是ISO-8859-1~ISO-8859-15,其中ISO-8859-1涵盖了大多数西欧语言字符,应用比较广泛。然而ISO-8859-1仍是单字节编码,可以表示256个字符。

3)      GB2312

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

4)       GBK

GBK扩展了GB2312,加入了更多的汉字,它的编码范围为8140-FEFE(去掉XX7F),总共有23940个码位,可以表示21003个汉字,并兼容GB2312。

5)      UTF-16

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

6)       UTF-8

UTF-16统一采用两个字节表示一个字符,虽然在表示上非常方便;但是也存在缺点,有很大一部分字符用一个字节就可以表示的,现在要用两个字节表示,存储空间多了一倍,在网络传输中,无疑会增大网络传输流量。UTF-8采用了一种变长技术,每个编码区域有不同的字码长度,不同类型的字符可以由1-6个字节组成,规则如下:

l  如果一个字节,最高位为0,表示这是一个ASCII字符;

l  如果一个字节,以11开头,连续的1的个数暗示这个字符的字节数,如110xxxxx表示它是双字节UTF-8字符的首字节;

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

对中文字符GB2312、GBK、UTF-16、UTF-8都可以处理,那如何选择编码格式呢?

GB2312与GBK编码规则类似,只是GBK所包含的范围更大,可以处理所有汉字。UTF-16与UTF-8都是处理Unicode编码,一个是定长编码,一个是变长编码。UTF-16为定长编码,编码效率最高,进行字符到字节的转换也简单,适合本地磁盘和内存之间使用,可以进行字节和字符的快速切换(Java的内存编码采用的是UTF-16)。UTF-16并不适合在网络中传输, UTF-16占用的空间较大;而UTF-8比较适合网络传输,在编码效率上介于GBK和UTF-16,是理想的中文编码方式。

2、Java编码时机

在JavaIO中,IO操作主要涉及字节流和字符流。当字节流和字符流相互转换时(包括磁盘IO和网络IO),需要进行编码,字节流和字符流相互转换过程如下所示:


字节到字符的转换,发生在文件读操作,InputStreamReader是字节到字符转换的桥梁,在转换过程中需要指定编码字符集,若没有指定则采用操作系统默认字符集(由于Java是跨平台的,最好指定编码字符集,否则容易出现乱码问题)。


字符到字节的转换,发生在文件写操作时,在进行转换时,同样需要指定编码格式,若没有指定则也将采用操作系统默认字符集。

在应用程序中只要涉及IO操作,只要指定统一的编码格式,一般不会出现乱码问题。特别是跨平台的应用程序,必须指定编码格式;否则,应用程序的编码格式将和操作系统环境关联起来,在其他环境很可能出现乱码问题。

此外,在内存中进行字符到字节的转换,也要指定编码格式。

 

 

3、为什么产生中文乱码

当用户从浏览器发起一个HTTP请求,存在编码的地方有URL、Cookie、Parameter。服务器端接受到HTTP请求后解析HTTP协议,其中URI、Cookie和Parameter都需要解码;此外服务端可能还要访问其他资源文件,如数据库、文本文件等,这些数据也存在编码问题。当Servlet处理完请求后,需要将数据返回给浏览器,经浏览器解析后才显示,这个过程也存在编码问题。这些过程如下所示

 

一次HTTP请求会有很多地方需要编码和解码,它们的编码、解码规则是什么呢?

3.1、URL编解码

用户提交一个URL,若存在中文,就需要对URL编码。URL是由多个部分组成的,如下


以Tomcat为例,URL的各个组成部分对应关系如下:

1)     domain对应Tomcat配置文件server.xml中Host节点

<Host appBase="webapps"name="localhost"  >

2)     port对应Tomcat配置文件server.xml中Connector节点

        <Connectorport="9090"protocol="HTTP/1.1" />

3)     context path对应Tomcat配置文件server.xml的Host节点的子节点Context

<Context docBase="jzt"path="/jzt"  />

4)     servlet path对应web应用w配置文件web.xml中的url-pattern节点

5)     path info对应请求的servlet

6)     parameter为对应GET请求的参数

在上面的请求中pathinfo和parameter中都出现了中文,那么在浏览器中输入URL时,浏览器是如何编码这个URL呢?最终浏览器(ChromeVersion 44.0.2403.157 Mac版)将URL编码为:

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

经过测试pathinfo和parameter中“君山”的编码为UTF-8编码。为什么会有%,URL编码规范RFC3986可知,浏览器编码URL将非ASCII字符按照某种编码格式编码成16进制后,将每个16进制表示的字节前加上“%”。

虽然上面pathinfo和parameter中的中文采用的是相同的编码,但不同浏览器对pathinfo的编码可能并不相同,对服务器的解码造成了很大困难。所以在实际应用中pathinfo一般并不采用中文。

Tomcat对URI部分进行解码的字符集是在connect中定义的,<ConnectorURIEncoding="UTF-8"port="9090"protocol="HTTP/1.1"/>,如果没有定义将以默认编码ISO-8859-1来解析。所以有中文URI时最好将URIEncoding设置为UTF-8编码。

服务器对请求的参数是如何解析呢?GET请求的参数和POST请求的参数在服务端都是通过HttpRequest对象的getParameter方法获取。对请求参数的解码是第一次调用getParameter时进行的(以Tomcat为例,详情可参考Tomcat源码)。但是对GET请求和POST请求解析参数时,使用的字符集可能并不相同。那么GET请求参数和POST请求参数的解码字符集是在哪里定义的呢?

GET请求参数的解码字符集要么是Header中ContentType定义的Charset,要么就是默认的ISO-8859-1,要使用ContentType中定义的编码解码就需要设置Tomcat配置文件server.xml中connector的useBodyEncodingForURI属性为true,<ConnectorURIEncoding="UTF-8"useBodyEncodingForURI="true" …/>

useBodyEncodingForURI为true,将会对GET请求参数采用BodyEncoding(HTML页面中的contentType属性中定义的字符集)进行解码,而并不是对URI整体采用BodyEncoding进行解码。

URL编码和解码是比较复杂的,在应用程序中尽量避免使用非ASCII字符,不然很可能会遇到乱码问题。此外,服务器最好在<Connector/>设置URIEncoding和useBodyEncodingForURI两个参数。

 

3.2、HTTP Header编解码

当客户端发起一个HTTP请求时,除了URL外还可能会在Headr中传递其他参数,如Cookie等,这些参数很可能也会存在编码问题。

Tomcat对Header中参数解码也是调用HttpRequest对象的getHeader方法时进行的。如果请求的Header中没有解码字符集,将采用默认编码ISO-8859-1对参数进行解码,如果请求Header中有非ASCII字符则很可能会产生乱码。

当向服务器返回数据时,如果需要添加头信息,如Cookie,最好不要在Header中传递非ASCII码。如果一定要传递,可以先将这些字符采用URLEncoder进行编码,再添加到Header中。这样浏览器将Cookie信息发送到服务器时就可可以采用同样的字符集解码就好了。

 

3.3、POST参数编解码

POST请求的参数是通过HTTP的BODY传递到服务器端的。浏览器是如何对POST请求的参数进行编码呢?浏览器将根据请求页面的contentType中的charset编码格式对参数进行编码,然后提交到服务器。在服务器端,服务器也是根据contentType中的charset编码对其解码的(服务如何获取该值呢?),所以POST请求参数一般不会出现乱码。

注意;Tomcat在解析请求参数前会先获取Header中的contentt-type请求头,并检查这个content-type中的charset值,默认情况下浏览器在提交form表单时,提交的content-type中是不会含有charset信息的。所以如果没有调用requet.setCharacterEncoding方法设置解析编码集,那么表单的数据会按照系统的默认编码方式解析。注意一定要在第一次调用getParameter方法之前设置,否则POST请求参数也可能出现乱码。

此外,对multipart/form-data类型的参数,也就上传文件的编码,同样也使用ContentType中的字符集。上传文件是用字节流的方式传输到服务器的本地临时目录,这个过程并没有涉及字符编码。当将文件内容添加到parameters中时,将会进行编码,如果没有编码集可用,将采用默认编码ISO-8859-1。

3.4、HTTP BODY编解码

当用户请求成功后,请求的资源将通过Response返回给浏览器。这个过程先要经过编码再到浏览器进行解码,编码字符集可以通过response.serCharacterEncoding方法来设置,并通过Header的Content-Type返回给客户端。浏览器接收到响应后将根据Content-Type中的charset来解码。如果返回的HTTPHeader中没有charset,那么浏览器将根据HTML的

<metahttp-equiv="Content-Type"content="text/html; charset=UTF-8">

中charset来解码;如果也没有定义,那么浏览器将采用默认的编码来解码。

3.5、JS编码问题

在HTML页面或JSP页面引入外部JS文件时,外部文件可能存在中文。这时,如果没有在<script>标签内设置charset,浏览器就会以当前页面的默认的字符集来解析js文件。所以在引入外部js文件时,需要在<script>标签内添加charset属性,如下

     <scripttype="text/javascript"src=""charset="utf-8"/>

此外,通过js发起异步调用的URL默认编码也是受浏览器影响的,如果使用原始的ajax调用,URL的默认编码,随浏览器的不同而不同;而且,不同的js框架对URL编码的处理也不同。为了解决该问题,在发送异步请求时可以调用JS的encodeURI方法来对URL中的字符进行UTF-8编码;解码时可以通过decodeURI。此外encodeURIComponent方法比encodeURI编码还要彻底,该函数通常用于将一个URL当作一个惨呼放在另一个URL中;解码可以通过decodeURIComponent。

当经过编码的URL传递到服务器时,需要使用java.net.URLDecoder来对参数进行解码,或将结果经编码后返回到浏览器可以通过java.net.URLEncoder。(服务器端的URLEncoder和URLDecoder与JS的encodeURIComponent和decodeURIComponent对应)。

 

3.6、其他编码问题

1)     数据库编码

访问数据库时一般都是通过JDBC驱动来完成的,用JDBC来存取数据要和数据的内置编码一致。在创建数据库时(以mysql为例)要显示指定字符集,如下

CREATE DATABASE IF NOT EXISTS db DEFAULT CHARSET utf8 COLLATEutf8_general_ci;

在连接数据库通过URL指定字符编码,如

url=”jdbc:mysql://localhost:3306/DB?useUnicode=true&characterEncoding=UTF-8”。

2)     下载文件

当我们下载文件时,若资源名为中文,有可能会出现乱码,此时我们可以先将资源名,采用URLEncoding进行编码,再将其写入到相应头中,如下

public void doPost(HttpServletRequest request, HttpServletResponse response)

              throwsServletException,IOException

    {

            

             ServletContext context=this.getServletContext();

             String path= context.getRealPath("/res/许嵩.bmp");

             /*获取下载的资源*/

             File file =newFile(path);

             /*构造文件的输入流*/

             InputStream is=newFileInputStream(file);

            

             /*写:输出流*/

             /*文件的下载头信息*/

             response.setHeader("content-disposition","attachment;filename="+URLEncoder.encode(file.getName(),"UTF-8"));

             OutputStream os= response.getOutputStream();

            

             bytebuffer[]=newbyte[1024];

             int len=0;

             while((len=is.read(buffer))!=-1){

              os.write(buffer,0,len);

}

 

3)     xml、jsp等编码格式

xml编码格式:<?xmlversion="1.0"encoding="UTF-8"?>

    jsp编码格式: <%@ page language="java"contentType="text/html; charset=UTF-8"pageEncoding="UTF-8"%>

 

3.7、一种不正常的解码

当我们使用request.getParameter获取参数后,若存在乱码,我们可能会采用下面的方式转码:

String(request.getParameter(name).getBytes(“ISO-8859-1”),”GBK”)

转码后可能会得到正确的汉字。由于ISO-8859-1字符集的编码范围为0000-00FF,正好和一个字节的编码范围对应。这种特性保证了使用ISO-8859-1进行编码和解码时可以保持编码数值“不变”。虽然中文字符经过网络传输时,被错误地“拆分”为两个欧洲字符,但由于在服务端第一次解析时也采用默认字符集IOS-8859-1进行解码,被错误拆分的两个字符又被合并在一起,从而组成一个正确的汉字。不建议使用该种解码方式,会增加额外的编码与解码过程。

 

0 0
原创粉丝点击