路边逮一美国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流
- 编写代码阶段
- WEB交互
四、解决乱码问题
- 文件
编写代码阶段,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 }
- 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交互的乱码问题。
浏览器发送数据到服务器的字符编码
- 超链接
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决定的。
<Connector port="8080" protocol="HTTP/1.1"
maxThreads="150" connectionTimeout="20000"
redirectPort="8443" URIEncoding="GBK"/>
URIEncoding告诉服务器servlet解码URL时采用的编码。而resin服务器可以直接通过request.setCharacterEncoding("gbk")指定;
- Java 编译阶段
当我们编写完源码后,通常会运行javac xxx, 将其编译为*.class文件,编译的时候会读取源码,那么是以什么编码来读取呢,默认是按操作系统的语言环境的,中文环境默认是gbk,如我们需要指定,可以 javac xxx -encoding utf-8。
- 数据库