乱码问题

  路边逮一美国IT佬,问其乱码问题,愕然相对,你这是闹哪样,全然不知。相反,如遇国内IT民工,如临大敌。诚然,在国内做软件,像我这种初级程序员,遇到字符乱码,往往会不知所措,直冒汗。第一台计算机诞生不久,就有了ASCII编码,后来因ASCII不能满足现下的字符,就由ISO组织扩展成为ISO-8859-1。计算机的普及,各个国家都有了自己的编码,目的可以在计算机上可以显示它们的语言。比如GBK编码来表示中文。但这也产生了编码不一致的问题,后来unicode统一了全世界的语言的编码规则,它可以表示全世界的语言。那为何美国人就不会遇到字符乱码的问题?美国人使用的是英文,而中国人使用的是中文。原因是全世界的字符编码对英文的编码规则是一致的,都是以一个字节来保存英文的。而中文不同,有些编码根本不支持中文,比如ISO-8859-1,有些编码对中文的编码规则不一致,比如GBK以2个字节,而UTF-8是以3个字节保存中文。

一、为什么需要字符编码

  了解此问题前,首先得理解几个过程

  • 编码过程:将非二进制字符转换成二进制("string".getBytes(String encoding))
  • 解码过程:将二进制转换为字符(new String(byte[] c,String encoding))
  • 存储过程:计算机是一个字节一个字节存储的,比如"中文"通过GBK编码后为d6d0 cec4,然后计算机拆分为d6 d0 ce c4以一个字节一个字节将信息存储

计算机只识别二进制即1010,由1和0组成的序列集,当需计算机识别其他字符时,就必须将其转换成二进制存储到计算机中。当需从计算机中读取信息以某种字符形式表示时,就需从中读取二进制信息,然后以特定的字符编码将二进制转换为字符。字符编码指将字符转换为二进制的规则。比较常见的字符编码ISO-8859-1(常用于网络传输),GBK,UTF-8(unicode的一种实现)。

二、为什么会出现乱码

  在我们沟通过程中,经常也会出现"乱码问题"。小明想表达的意思是A,说出来的意思是B,小芳接收到的意思是C,小芳理解的意思是D。A=D时,证明此沟通成功。但往往沟通过程中,没那么顺畅,出现A!=B,B!=C,C!=D,其中任何一个环节出错,都会造成A!=D的情况。A是二进制信息,B是编码后的字符,C是通过某种途径传输B后的字符,D是

解码后的字符。传输过程中如果没有信息丢失,B=C。所以问题往往会出现A!=B和C!=D的情况,这两种情况就是编码和解码不一致导致的,这也是产生乱码的根本原因。

三、乱码问题的情景

  细心的童靴会留意到前面沟通B->C过程中,是要将表达意思传递给小芳。当系统需要从外部资源读写数据时,外部资源可以是文件(数据库)、网络及内存。这里有两个过程,数据传输过程接收端发送端编码和解码的过程。因为发送端需要将数据编码成二进制,由计算机通过某种载体传输到接收端,接收端接收到二进制数据,就需要将数据解码。当出现乱码问题时,我们首先确定2个端的字符编码方式,然后统一2个端的编码方式即可。乱码问题的情景有二种,A!=B和B!=D(假设B=C),A!=B是编译阶段,B!=D是系统从外部资源读写数据阶段。总结一下乱码问题的情景:

  • Java 编译阶段(A!=B)
    • Java文件
    • JSP文件
  • 从外部资源读写数据阶段(B!=D)
    • WEB交互
      • 表单提交get/post
      • 超链接
      • XMLHttpRequest异步提交get/post
      • 直接在浏览器输入URL
    • 数据库
    • 文件
      • 显式的操作文件,I/O流
      • 编写代码阶段

四、解决乱码问题

  • 文件

  编写代码阶段,eclipse平台上编写完代码后,需要保存到文件,普通的Java文件,通常会根据当前操作系统的默认字符编码来保存Java文件,比如在中文环境下通常是GBK。而在jsp中,由<%@page pageEncoding="gbk"%>pageEncoding来指定页面的字符编码。

  在使用I/O流操作文件时,有字节流和字符流2种方式。当使用字节流时固然是没有问题的,但当使用字符流时,请看下面源码。

 1 public class FromOutsideData {
 2     private final static String FILE_SEPARATOR= File.separator;
 3     
 4     public static void main(String[] args) throws IOException {
 5         String path = "D:" + FILE_SEPARATOR + "1.txt";
 6         File file = new File(path);
 7         write(file, "utf-8");
 8         read(file); // 如果去读以utf-8编码后的文件,就会出现乱码。
 9         read(file, "utf-8"); // 指定utf-8去读取文件,正常。
10     }
11     
12     public static void read(File file) throws IOException {
13         // 使用字符流的原理是先使用字节流每次读2个字节,然后根据当前操作系统的默认字符编码来解码成字符
14         BufferedReader br = new BufferedReader(new FileReader(file));
15         String line = null;
16         StringBuilder sb = new StringBuilder();
17         while ((line = br.readLine()) != null) {
18             sb.append(line);
19         }
20         System.out.println(sb.toString());
21     }
22     
23     public static void read(File file, String charset) throws IOException {
24         // 使用这种方式可以显式的指定字符编码来解码
25         BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), charset));
26         String line = null;
27         StringBuilder sb = new StringBuilder();
28         while ((line = br.readLine()) != null) {
29             sb.append(line);
30         }
31         System.out.println(sb.toString());
32     }
33     
34     /**
35      * 根据charset的字符编码写文件
36      */
37     public static void write(File file, String charset) throws IOException {
38         BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, true), charset));
39         bw.write("is 最牛x轰轰滴");
40         bw.flush();
41         bw.close();
42     }
43 }
View Code
  • WEB交互

                

WEB交互,这里特指浏览器与服务器基于http协议进行数据传输的过程。http报头的首部contentType指定了数据传输过程中的字符编码和内容编码。字符编码不等同于内容编码,像MIME指的是内容编码,它规定了内容的格式。像text/html、application/x-www-form-urlencoded、application/vnd ms-excel等等,例如contentType:"text/html;charset=utf-8",指浏览器告诉服务器,传递的数据的内容格式是html,字符编码是utf-8。同样服务器返回的数据,也是通过contentType来告诉浏览器数据的内容编码和字符编码。比如,servlet可以通过response.setContentType("text/html;charset=utf-8");所以我们只要正确的指定contentType的字符编码就可以避免WEB交互的乱码问题。在jsp页面有2个部分可以指定contentType的,第一是<%@page contentType="text/html;charset=utf-8"%>,第二是<meta http-equiv="Content-Type" content="text/html; charset=utf-8">,但前者的优先级高于后者,浏览器确定页面字符编码有4个步骤,首先看<%@page%>有没有指定,然后是自动检测,而后会meta,最后会按ISO-8859-1默认编码来编码。服务器设置字符解码的地方有两个部分:第一是WEB服务器的配置文件指定,第二是request.setCharacterEncoding("utf-8");知道了这些后,我们再来看看WEB交互的乱码问题。

浏览器发送数据到服务器的字符编码

  1. 超链接
foo://example.com:8042/over/there?name=ferret#nose
   \_/ \______________/ \________/\_________/ \__/
    |         |              |         |        |
scheme     authority        path     query   fragment    前面是URL的组成成分,path部分的编码会比较麻烦,这部分会由浏览器语言版本决定,如果是中文则以GBK编码,如果英文环境则以ISO-5589-1编码。所以我们需要用js的方法encodeURIComponent(s,enc)来统一编码path部分,而query部分由contentType决定的。
  2.  直接在浏览器输入URL(由浏览器的语言版本决定)
  3.  表单提交get/post(由contentType决定)
  4.  XMLHttpRequest异步提交get/post(不由任何决定,一定是utf-8编码)

服务器对数据解码的字符编码
在1,2和3中的get提交产生的都是get请求,实质就是拼接url。服务器对其设置对url解码的字符编码,对于tomcat服务器,该文件是server.xml
<Connector port="8080" protocol="HTTP/1.1" 
maxThreads="150" connectionTimeout="20000" 
redirectPort="8443" URIEncoding="GBK"/>
URIEncoding告诉服务器servlet解码URL时采用的编码。而resin服务器可以直接通过request.setCharacterEncoding("gbk")指定;
3.form表单post请求由request.setContentType("gbk")来指定编码;
4.如果请求中指定了contentType:'application/x-www-form-urlencoded;charset=utf-8'(注意这里是分号,不是逗号,如果是逗号,在firefox会报500),服务器会优先按charset指定的编码来解码,然后是request.setCharacterEncoding("utf-8");那为什么在firefox不加contentType报头也不会乱码呢,因为它会自动加上这个头。而ie和chrome不会。

服务器返回数据给浏览器的字符编码
服务器也是通过指定contentType响应头来告诉浏览器,比如,servlet的response.setCharacterEncoding("GBK");如果服务器没有指定contentType,如果返回的是html,那么浏览器首先会自己探测,如果遇到meta有contentType报头就会以其指定的编码解析。如果返回的是json数据就必须指定,不然如果默认的字符编码,比如ISO-8859-1,不支持中文的话,就会出现乱码问题。
  • Java 编译阶段

当我们编写完源码后,通常会运行javac xxx, 将其编译为*.class文件,编译的时候会读取源码,那么是以什么编码来读取呢,默认是按操作系统的语言环境的,中文环境默认是gbk,如我们需要指定,可以 javac xxx -encoding utf-8。

  • 数据库
数据库设置编码会有2个过程,第一,当我们应用连接数据库时,告诉数据库用什么字符编码来解码。第二,数据库以什么字符编码来保存数据。前者可使用jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=UTF-8;后者由数据库决定,可到数据库去设置。
五、总结
  现存的字符编码环境对中文的支持是各不相同的,如果端与端之间采用了不同的字符编码,就会出现乱码问题。同时介绍了乱码问题出现的情景及其解决方案。由于个人文字功底略差,很多想表达的东西都未能表达。在文章的整体措词方面略显粗糙,欢迎拍砖。
posted @ 2013-12-16 22:12  pp_wen  阅读(2830)  评论(15编辑  收藏  举报