关于java字节码的处理,目前有很多工具,如bcel,asm。不过这些都需要直接跟虚拟机指令打交道。如果你不想了解虚拟机指令,可以采用javassist。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
    下面通过一个简单的例子,通过javassist来实现如何动态注入代码。
    假设,存在类A,如下:
public class A {
    public void method() {
        for (int i = 0; i < 1000000; i++) {
        }
        System.out.println("method1");
    }
}
测试类B如下:
public class B {
    public static void main(String[] args) {
        A a = new A();
        a.method();    
    }
}
现在想统计一下method的执行时间,
默认的实现是修改method:
 public void method() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000000; i++) {
        }
        System.out.println("method1");
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }
如果A的方法很多,统计方法的执行时间的代码就会相应的增加。为了减少工作量,通过动态注入代码的形式来实现。
修改B的main方法:
    public static void main(String[] args) throws Exception {
      //用于取得字节码类,必须在当前的classpath中,使用全称
        CtClass ctClass = ClassPool.getDefault().get("org.esoft.A");
         //需要修改的方法名称
        String mname = "method";        
        CtMethod mold = ctClass.getDeclaredMethod(mname);
         //修改原有的方法名称
        String nname = mname + "$impl";
        mold.setName(nname);
         //创建新的方法,复制原来的方法
        CtMethod mnew = CtNewMethod.copy(mold, mname, ctClass, null);
         //主要的注入代码
        StringBuffer body = new StringBuffer();
        body.append("{\nlong start = System.currentTimeMillis();\n");
        //调用原有代码,类似于method();($$)表示所有的参数
        body.append(nname + "($$);\n");
        body.append("System.out.println(\"Call to method "
                    + mname
                    + " took \" +\n (System.currentTimeMillis()-start) + "
                    + "\" ms.\");\n");
       
        body.append("}");
         //替换新方法
        mnew.setBody(body.toString());
         //增加新方法
        ctClass.addMethod(mnew);
        //类已经更改,注意不能使用A a=new A();,因为在同一个classloader中,不允许装载同一个类两次
        A a=(A)ctClass.toClass().newInstance();
        a.method();
    }
这只是简单的一个应用。javassist还提供了很多的功能,用于更改类结构。有兴趣的可以参考相关文档

posted @ 2008-10-20 16:29 jsot 阅读(706) 评论(1) 编辑
我们做java开发的一般都会遇到如何保护我们开发的代码问题。java语言由于是基于jvm上面,所以反编译class文件很很容易。假如我们做了一个web程序,并把这个web程序发布给客户。实际上,客户是很容易反编译出我们的源代码出来,包括所有的src文件和jsp文件等等。

   那么,如何保护我们的源代码,实际上,应该有几种方法可以使用:1、使用代码混淆器  2、重载应用服务器的classloader

   对于第一种方法来说,现在外面有很多开源工具可以使用,个人认为最好用的当属proguard莫属。proguard主要是易用易学。而且提供的功能也挺多。下面是个人一点使用心得

   (1)、从网上download proguard工具,proguard工具主要包含是几个jar文件和一些example,下载地址http://proguard.sourceforge.net/

   (2)、将里面的几个jar文件添加到类路径下面。当然,也可以不添加,但是下面在做混淆的时候,必须指定classpath,使在做混淆的过程中,能否访问该类

   (3)、编写一个配置文件,主要是混淆器的一些参数。比如,下面是一个例子
-injars       platform.jar
-outjars      platform_out.jar
-libraryjars  <java.home>/lib/rt.jar
-libraryjars ibatis-common-2.jar
-libraryjars ibatis-dao-2.jar
-libraryjars ibatis-sqlmap-2.jar
-libraryjars junit-3.8.1.jar
-libraryjars d:/j2ee.jar
-libraryjars struts.jar
-libraryjars commons-lang.jar
-libraryjars D:/0working/coreproject/byislib/jasperreports-0.6.1.jar
-libraryjars  commons-beanutils.jar

-printmapping proguard.map
-overloadaggressively
-defaultpackage ''
-allowaccessmodification
-dontoptimize
-keep public class *
{
public protected *;
}
-keep public class org.**
-keep public class it.**

各个参数的含义参考proguard文档,该文档非常详细,上手很容易

OK,到此就完成了代码混淆,打开产生的jar包可以看到,多了好多a、b、c之类的类文件。说明混淆结果已经成功。将原jar删除、运行产生的混淆jar包,一切正常!

常见问题:使用过程中个人遇到了几个问题,开始也是找了很久才解决
   a. 内存溢出异常: 主要是proguard在做混淆的时候,吃了很多内存,因此,在运行混淆器的时候,可以增加内存,比如 java -mx512m .....
  b.栈溢出异常: 主要是proguard在做混淆的时候,会对一些代码进行优化,若遇到一些相对复杂的方法时,可能会抛出此异常。对付的办法是增加配置参数-dontoptimize,如上面的配置例子所示

对于第二种方法,重载服务器的classloader的原理是这样。 首先我们通过一定算法把class文件加密; 然后写我们自己的classloader,替换服务器的classloader。 这样,我们可以读取class文件,通过我们自己的算法反加密成正确的class,然后再次进行load。这个方式还没应用起来,这几天个人正在研究,有什么新成果会在此做一些总结。


ProGuard是一个开源的项目,主页:http://proguard.sourceforge.net/,目前最新的版本是3.3.2.。加载混淆器是非常简单的,只需要解压缩proguard3.3.2.zip,然后在 J2ME->Packing->Obfuscation 标签中选择 Proguard 的安装目录。如下图所示,在这里可以对需要在混淆过程中保留的类名进行配置,MIDlet 类的名称必须保留,以便设备的 Java 运行时环境(JRE)能够找到执行的入口点。
http://images.csdn.net/20050726/image027.jpg,It’s about the above pic.



另一篇文档
ProGuard是一款免费的Java类文件压缩器、优化器和混淆器。它能发现并删除无用类、字段(field)、方法和属性值(attribute)。它也能优化字节码并删除无用的指令。最后,它使用简单无意义的名字来重命名你的类名、字段名和方法名。经过以上操作的jar文件会变得更小,并很难进行逆向工程。这里提到了ProGuard的主要功能是压缩、优化和混淆,下面我就先介绍一下这些概念,然后再介绍ProGuard的基本使用方法。

l         什么是压缩:

Java源代码(.java文件)通常被编译为字节码(.class文件)。而完整的程序或程序库通常被压缩和发布成Java文档(.jar文件)。字节码比Java源文件更简洁,但是它仍然包含大量的无用代码,尤其它是一个程序库的时候。ProGuard的压缩程序操作能分析字节码,并删除无用的类、字段和方法。程序只保留功能上的等价,包括异常堆栈描述所需要的信息。

l         什么是混淆:

通常情况下,编译后的字节码仍然包含了大量的调试信息:源文件名,行号,字段名,方法名,参数名,变量名等等。这些信息使得它很容易被反编译和通过逆向工程获得完整的程序。有时,这是令人厌恶的。例如像ProGuard这样的混淆器就能删除这些调试信息,并用无意义的字符序列来替换所有名字,使得它很难进行逆向工程,它进一步免费的精简代码。除了异常堆栈信息所需要的类名,方法名和行号外,程序只会保留功能上的等价。通过以上的了解,你应该明白为什么需要混淆了。

l         ProGuard支持那些种类的优化:

除了在压缩操作删除的无用类,字段和方法外,ProGuard也能在字节码级提供性能优化,内部方法有:

²        常量表达式求值

²        删除不必要的字段存取

²        删除不必要的方法调用

²        删除不必要的分支

²        删除不必要的比较和instanceof验证

²        删除未使用的代码

²        删除只写字段

²        删除未使用的方法参数

²        像push/pop简化一样的各种各样的peephole优化

²        在可能的情况下为类添加static和final修饰符

²        在可能的情况下为方法添加private, static和final修饰符

²        在可能的情况下使get/set方法成为内联的

²        当接口只有一个实现类的时候,就取代它

²        选择性的删除日志代码

实际的优化效果是依赖于你的代码和执行代码的虚拟机的。简单的虚拟机比有复杂JIT编译器的高级虚拟机更有效。无论如何,你的字节码会变得更小。

仍有一些明显需要优化的技术不被支持:

²        使非final的常量字段成为内联

²        像get/set方法一样使其他方法成为内联

²        将常量表达式移到循环之外

²        Optimizations that require escape analysis



    ProGuard是一个命令行工具,并提供了图形化用户界面,它也可以结合Ant或J2ME Wireless Toolkit使用。通过ProGuard得到的更精简的jar文件意味着只需要更小的存储空间;网络传输更省时;装载速度更快和占用更小的内存空间。另外,ProGuard非常快速和高效,它仅仅只花费几秒钟和几兆的内存在处理程序。它处理的顺序是先压缩,然后优化,最后才进行混淆。The results section presents actual figures for a number of applications.与其他Java混淆器相比,ProGuard的主要优势可能是它的基于模版文件的简单配置。一些直观的命令行选项或一个简单的配置文件已经足够了。例如,下面的配置选项保护了jar文件里的所有applets:

-keep public class * extends java.applet.Applet

用户指南里说明了所有可用的选项,并以大量的例子为你演示这些功能强大的配置选项。



       上面谈到了ProGuard的很多好处,现在我们就来看看如何在程序中使用ProGuard吧,之前也提到了ProGuard可以用命令行、图形界面、Ant等来执行和处理程序,同时也提到了配置文件,下面我们一起来看如何使用:

用命令行执行ProGuard的命令如下:

java –jar proguard.jar options……

具体的选项可以参考ProGuard的用户指南,你也可以把这些属性写在配置文件里;运行时,我们只需要指定这个配置文件就行了,例如:

java –jar proguard.jar @config.pro

而配置文件的格式也是要按照ProGuard提供的格式来写的,这个可以参考ProGuard例子里的配置文件来配置适合你的应用系统的ProGuard配置文件。ProGuard提供了图形界面的配置和运行程序,你可以在界面上配置你想要的参数,然后运行即可。前面提到的要手动写的配置文件也可以用图形界面来配置和生成。

如果你要在Ant里运行ProGuard,只需要添加一一个如下的target即可:

<target name="proguard" depends="init">

       <taskdef resource="proguard/ant/task.properties" classpath="${lib.dir}/proguard/proguard.jar" />

       <proguard configuration="${src.dir}/config.pro" />

</target>

你只需要制定lib.dir和src.dir属性就行了,同样的,这里也用了proguard配置文件,跟上面提到的是一样的。建议大家把ProGuardGUI当成一个生成配置文件的向导来使用,这样我们只需要修改配置文件而不用重新写一个配置文件。

如果你觉得ProGuard还不错,那就快把它加入你的项目里吧。





第三文档
这是一个不应该在开源社区出现的东西,但它的的确确是一个开源的项目,正像它的名字一样,Proguard,即Program Guard(程序卫士),它代表了开源的相对面--代码保护。
  作为JAVA这样的高级语言,编译的产物只是相对源代码的一个概念而已,字节码虽然不像源代码那样易懂,但绝不是不可能进行反编译的,针对JAVA的反编译产品很多,如CAVAJ,JAD等等。面对反编译产品的不断出现,将代码视为财富的那些开发者,又何去何从。
  混淆器正是在这种背景下应运而生,既然不可能完全地将拒绝反编译,那就让他们去反编译吧,只要反编译的结果别人不能直接使用不就行了吗?只要将代码搞混,让别人拿到了反编译的结果也看不懂,甚至不能编译。
  混淆的方法有很多,主要是以下几方面。
更名,将私有类,私有的成员,方法体内部的变量名改名,改成a,b,c等等,甚至1,2,3(代码中不允许不等于成果物中不允许)
改变逻辑的流向,如将if条件取反,if/else对换
等价代码,如将循环改成GOTO
无效代码,插入不可及的无用代码
  Proguard是一个非常优秀的开源的JAVA混淆器,可以在http://proguard.sourceforge.net/下载到,现在就让我一起来看一下Proguard.
  以3.2版为例,释放压缩包,我们看到,作为开源项目就有docs,lib,src,sample文件夹,在此就不一一介绍了。
  进入lib目录,内有proguard.jar,如果要自己有混淆器的外壳,或作ANT插件的话,会用到它,详细情况可以参考Proguard的文档。
  我们要看的是proguardgui.jar,这是Proguard的图形界面,我们使用JDK打开,注意是JDK,不是JRE。

点选Input/Output标签,选择要混淆的JAR包(注意是JAR包),输出JAR包,以及用到的所有类库。
点选Obfuscation标签,选中不需要混淆的类(要被反射的类绝对不能被混淆)
点选Process标签,Process按钮,等着看结果吧。
Proguard中还包括了代码优化和代码整理的功能,不是本文讨论范围,有兴趣的就自己研究吧)
只混淆方面的选项



使用此种方式,如果a-z使用过,会转向aa.class,如下图配置界面
1,4,6,9,10,11,12

源代码
package org.zwm.pub;

public class Bru {

/**
* @param args
*/

public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(showMsg());
}
public static String showMsg() {
return "You are my sun";
}
}
反编译后的代码
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)

package org.zwm.pub;

import java.io.PrintStream;

public class Bru
{

    public Bru()
    {
    }

    public static void main(String args[])
    {
        System.out.println(PK0304140008000800fZ());
    }

    public static String PK0304140008000800fZ()
    {
        return "You are my sun";
    }
}




类名不变化,方法名混淆。
posted @ 2008-10-20 16:27 jsot 阅读(4382) 评论(0) 编辑

计算机中的字是如何处理的?

 

    如果你用放大镜看一下,可以看出屏幕上的字是由一个一个的像素点组成的,每一个字符用一组像素点拼接出来,这些像素点组成一幅图像,变成了我们的文字,计算机又是如何将我们的文字保存起来的呢?是用一个个的点组成的图像将文字保存起来的吗?当然不是,让我们从英文开始,由于英文是拼音文字,实际上所有的英文字符和符号加起来也不超过100个,在我们的文字中存在着如此大量的重复符号,这就意味着保存每个字符的图像会有大量的重复,比如 e 就是出现最多的符号等等。所以在计算机中,实际上不会保存字符的图像。

 

什么是字符编码?

 

    由于我们的文字中存在着大量的重复字符,而计算机天生就是用来处理数字的,为了减少我们需要保存的信息量,我们可以使用一个数字编码来表示每一个字符,通过对每一个字符规定一个唯一的数字代号,然后,对应每一个代号,建立其相对应的图形,这样,在每一个文件中,我们只需要保存每一个字符的编码就相当于保存了文字,在需要显示出来的时候,先取得保存起来的编码,然后通过编码表,我们可以查到字符对应的图形,然后将这个图形显示出来,这样我们就可以看到文字了,这些用来规定每一个字符所使用的代码的表格,就称为编码表。编码就是对我们日常使用字符的一种数字编号。

 

第一个编码表 ASCII

 

在最初的时候,美国人制定了第一张编码表 《美国标准信息交换码》,简称 ASCII,它总共规定了 128 个符号所对应的数字代号,使用了 7 位二进制的位来表示这些数字。其中包含了英文的大小写字母、数字、标点符号等常用的字符,数字代号从 0 至 127,ASCII 的表示内容如下:

0 – 31         控制符号

32              空格

33-47           常用符号

48-57           数字

58-64           符号

65-90           大写字母

91-96           符号

97-127          小写字母

 

注意,32 表示空格,虽然我们再纸上写字时,只要手腕动一下,就可以流出一个空格,但是,在计算机上,空格与普通得字符一样也需要用一个编码来表示,33-127 共95个编码用来表示符号,数字和英文的大写和小写字母。比如数字 1 所对应的数字代号为 49,大写字母 A 对应的代号为 65, 小写字母 a 对应的代号为 97。所以,我们所写的代码 hello, world 保存在文件中时,实际上是保存了一组数字 104 101 108 108 111 44 32 119 111 114 108 100。我们再程序中比较英文字符串的大小时,实际上也是比较字符对应的 ASCII 的编码大小。

    由于 ASCII 出现最早,因此各种编码实际上都受到了它的影响,并尽量与其相兼容。

 

扩展 ASCII 编码 ISO8859

 

    美国人顺利解决了字符的问题,可是欧洲的各个国家还没有,比如法语中就有许多英语中没有的字符,因此 ASCII 不能帮助欧洲人解决编码问题。

为了解决这个问题,人们借鉴 ASCII 的设计思想,创造了许多使用 8 位二进制数来表示字符的扩充字符集,这样我们就可以使用256种数字代号了,表示更多的字符了。在这些字符集中,从 0 - 127 的代码与 ASCII 保持兼容,从 128 到 255 用于其它的字符和符号,由于有很多的语言,有着各自不同的字符,于是人们为不同的语言制定了大量不同的编码表,在这些码表中,从 128 - 255 表示各自不同的字符,其中,国际标准化组织的 ISO8859 标准得到了广泛的使用。

在 ISO8859 的编码表中,编号 0 – 127 与 ASCII 保持兼容,编号128 – 159 共32个编码保留给扩充定义的 32 个扩充控制码,160 为空格, 161 -255 的 95 个数字用于新增加的字符代码。编码的布局与 ASCII 的设计思想如出一辙,由于在一张码表中只能增加 95 种字符的代码,所以 ISO8859 实际上不是一张码表,而是一系列标准,包括 14 个字符码表。例如,西欧的常用字符就包含在 ISO8859-1字符表中。在 ISO8859-7种则包含了 ASCII 和现代希腊语字符。

 

问题出现了!

 

    ISO 的8859标准解决了大量的字符编码问题,但也带来了新的问题,比如说,没有办法在一篇文章中同时使用 ISO8859-1 和 ISO8859-7,也就是说,在同一篇文章中不能同时出现希腊文和法文,因为他们的编码范围是重合的。例如:在 ISO8859-1 中 217号编码表示字符Ù ,而在 ISO8859-7中则表示希腊字符Ω,这样一篇使用 ISO8859-1 保存的文件,在使用 ISO8859-7编码的计算机上打开时,将看到错误的内容。为了同时处理一种以上的文字,甚至还出现了一些同时包含原来不属于同一张码表的字符的新码表。

 

大字符集的烦恼

 

    不管如何,欧洲的拼音文字都还可以用一个字节来保存,一个字节由8个二进制的位组成,用来表示无符号的整数的话,范围正好是 0 – 255。

但是,更严重的问题出现在东方,中国,朝鲜和日本的文字包含大量的符号。例如,中国的文字不是拼音文字,汉字的个数有数万之多,远远超过区区 256 个字符,因此 ISO 的 8859 标准实际上不能处理中文的字符。

通过借鉴 ISO8859 的编码思想,中国的专家灵巧的解决了中文的编码问题。

既然一个字节的 256 种字符不能表示中文,那么,我们就使用两个字节来表示一个中文,在每个字符的 256 种可能中,低于 128 的为了与 ASCII 保持兼容,我们不使用,借鉴 ISO8859的设计方案,只使用从 160 以后的 96 个数字,两个字节分成高位和低位,高位的取值范围从 176-247 共72个,低位从 161 – 254共94这样,两个字节就有 72 * 94 = 6768种可能,也就是可以表示 6768 种汉字,这个标准我们称为 GB2312-80。

6768 个汉字显然不能表示全部的汉字,但是这个标准是在1980年制定的,那时候,计算机的处理能力,存储能力都还很有限,所以在制定这个标准的时候,实际上只包含了常用的汉字,这些汉字是通过对日常生活中的报纸,电视,电影等使用的汉字进行统计得出的,大概占常用汉字的 99%。因此,我们时常会碰到一些名字中的特殊汉字无法输入到计算机中的问题,就是由于这些生僻的汉字不在 GB2312 的常用汉字之中的缘故。

由于 GB2312 规定的字符编码实际上与 ISO8859 是冲突的,所以,当我们在中文环境下看一些西文的文章,使用一些西文的软件的时候,时常就会发现许多古怪的汉字出现在屏幕上,实际上就是因为西文中使用了与汉字编码冲突的字符,被我们的系统生硬的翻译成中文造成的。

不过,GB2312 统一了中文字符编码的使用,我们现在所使用的各种电子产品实际上都是基于 GB2312 来处理中文的。

GB2312-80 仅收汉字6763个,这大大少于现有汉字,随着时间推移及汉字文化的不断延伸推广,有些原来很少用的字,现在变成了常用字,例如:朱镕基的“镕”字,未收入GB2312-80,现在大陆的报业出刊只得使用(金+容)、(金容)、(左金右容)等来表示,形式不一而同,这使得表示、存储、输入、处理都非常不方便,而且这种表示没有统一标准。

为了解决这些问题,全国信息技术化技术委员会于1995年12月1日《汉字内码扩展规范》。GBK向下与GB2312完全兼容,向上支持ISO 10646国际标准,在前者向后者过渡过程中起到的承上启下的作用。GBK 亦采用双字节表示,总体编码范围为8140-FEFE之间,高字节在81-FE之间,低字节在40-FE之间,不包括7F。在 GBK 1.0 中共收录了 21886个符号,汉字有21003个。

GBK 共收入21886个汉字和图形符号,包括:

* GB2312 中的全部汉字、非汉字符号。

* BIG5 中的全部汉字。

* 与ISO 10646相应的国家标准GB13000中的其它CJK汉字,以上合计20902个汉字。

* 其它汉字、部首、符号,共计984个。

 

微软公司自Windows 95 简体中文版开始支持GBK代码,但目前的许多软件都不能很好地支持GBK汉字。

GBK 编码区分三部分:

* 汉字区 包括

GBK/2 :OXBOA1-F7FE, 收录GB2312汉字6763个,按原序排列;

GBK/3 :OX8140-AOFE,收录CJK汉字6080个;

GBK/4 :OXAA40-FEAO,收录CJK汉字和增补的汉字8160个。

* 图形符号区 包括

GBK/1 :OXA1A1-A9FE,除GB2312的符号外,还增补了其它符号

GBK/5 :OXA840-A9AO,扩除非汉字区。

* 用户自定义区

即GBK区域中的空白区,用户可以自己定义字符。

 

GB18030 是最新的汉字编码字符集国家标准, 向下兼容 GBK 和 GB2312 标准。 GB18030 编码是一二四字节变长编码。 一字节部分从 0x0~0x7F与 ASCII 编码兼容。二字节部分, 首字节从 0x81~0xFE, 尾字节从 0x40~0x7E 以及 0x80~0xFE, 与 GBK标准基本兼容。 四字节部分, 第一字节从 0x81~0xFE, 第二字节从 0x30~0x39, 第三和第四字节的范围和前两个字节分别相同。

 

不一样的中文

 

中文的问题好像也解决了,且慢,新的问题又来了。

中国的台湾省也在使用中文,但是由于历史的原因,那里没有使用大陆的简体中文,还在使用着繁体的中文,并且他们自己也制定了一套表示繁体中文的字符编码,称为 BIG5,不幸的是,虽然他们的也使用两个字节来表示一个汉字,但他们没有象我们兼容 ASCII 一样兼容大陆的简体中文,他们使用了大致相同的编码范围来表示繁体的汉字。天哪! ISO8859 的悲剧又出现在同样使用汉字的中国人身上了,同样的编码在大陆和台湾的编码中实际上表示不同的字符,大陆的玩家在玩台湾的游戏时,经常会遇到乱码的问题,问题根源就在于,大陆的计算机默认字符的编码就是 GB2312, 当碰到台湾使用 BIG5 编码的文字时,就会作出错误的转换。

 

由于历史和文化的原因,日文和韩文中也包含许多的汉字,象汉字一样拥有大量的字符,不幸的是,他们的字符编码也同样与中文编码有着冲突,日文的游戏在大陆上一样也会出现无法理解的乱码。《中文之星》,《南极星》,《四通利方》就是用于在这些编码中进行识别和转换的专用软件。

 

互联的时代

 

在二十世纪八十年代后期,互联网出现了,一夜之间,地球村上的人们可以直接访问远在天边的服务器,电子文件在全世界传播,在一切都在数字化的今天,文件中的数字到底代表什么字?这可真是一个问题。

 

UNICODE

 

实际上问题的根源在于我们有太多的编码表。

 

如果整个地球村都使用一张统一的编码表,那么每一个编码就会有一个确定的含义,就不会有乱码的问题出现了。

实际上,在80年代就有了一个称为 UNICODE 的组织,这个组织制定了一个能够覆盖几乎任何语言的编码表,在 Unicode3.0.1中就包含了 49194 个字符,将来,Unicode 中还会增加更多的字符。Unicode 的全称是 Universal Multiple-Octet Coded Character Set ,简称为 UCS。

由于要表示的字符如此之多,所以一开始的 Unicode1.0编码就使用连续的两个字节也就是一个WORD 来表示编码,比如“汉”的UCS 编码就是 6C49。这样在 Unicode 的编码中就可以表示 256*256 = 65536 种符号了。

直接使用一个WORD 相当于两个字节来保存编码可能是最为自然的 Unicode 编码的方式,这种方式被称为 UCS-2,也被称为 ISO 10646,,在这种编码中,每一个字符使用两个字节来进行表示,例如,“中” 使用 11598 来编码,而大写字母 A 仍然使用 65 表示,但它占用了两个字节,高位用 0 来进行补齐。

 

由于每个WORD 表示一个字符,但是在不同的计算机上,实际上对 WORD 有两种不同的处理方式,高字节在前,或者低字节在前,为了在UCS-2编码的文档中,能够区分到底是高字节在前,还是低字节在前,使用 UCS-2 的文档使用了一组不可能在UCS-2种出现的组合来进行区分,通常情况下,低字节在前,高字节在后,通过在文档的开头增加 FFFE 来进行表示。高字节在前,低字节在后,称为大头在前,即Big Endian,使用 FFFE 来进行表示。这样,程序可以通过文档的前两个字节,立即判断出该文档是否高字节在前。

Endian 这个词出自 《格列佛游记》,小人国的内战就源于吃鸡蛋时要先吃大头 big endian 还是小头 little-endian,并由此发生了内战。

 

理想与现实

 

UCS-2 虽然理论上可以统一编码,但仍然面临着现实的困难。

首先,UCS-2 不能与现有的所有编码兼容,现有的文档和软件必须针对 Unicode 进行转换才能使用。即使是英文也面临着单字节到双字节的转换问题。

其次,许多国家和地区已经以法律的形式规定了其所使用的编码,更换为一种新的编码不现实。比如在中国大陆,就规定 GB2312 是大陆软件、硬件编码的基础。

第三,现在还有使用中的大量的软件和硬件是基于单字节的编码实现的,UCS-2 的双字节表示的字符不能可靠的在其上工作。

 

新希望 UTF-8

 

为了尽可能与现有的软件和硬件相适应,美国人又制定了一系列用于传输和保存Unicode 的编码标准 UTF,这些编码称为UCS 传输格式码,也就是将 UCS 的编码通过一定的转换,来达到使用的目的。常见的有 UTF-7,UTF-8,UTF-16等。

其中 UTF-8 编码得到了广泛的应用,UTF-8 的全名是UCS Transformation Format 8, 即 UCS 编码的8位传输格式,就是使用单字节的方式对 UCS 进行编码,使 Unicode 编码能够在单字节的设备上正常进行处理。

UTF-8 编码是变长的编码,对不同的 Unicode 可能编成不同的长度

 

UCS-2                           UTF-8

0000-007F    0- 127            0xxxxxxx

0080-07FF 128- 2047            110xxxxx 10xxxxxx

    0800-FFFF 2048-65535            1110xxxx 10xxxxxx 10xxxxxx

 

 例如 1 的Unicode 编码是 31 00,在 0-127之间,所以转换后即为 31,而“中”字的UTF-8 Unicode 编码为 11598,转换成 UTF-8则为 e4 b8 ad。

 

实际上,ASCII 字符用 UTF-8 来表示后,与 ASCII 是完全一样的,美国人又近水楼台的把自己的问题解决了。但其他的编码就没有这么幸运了。

 

突破障碍 - Unicode 与 本地编码的转换

 

UTF-8 编码解决了字符的编码问题,又可以在现有的设备上通行,因此,得到了广泛的使用,

 

在人间

 

    XML 中的问题

    XML 的设计目标是实现跨网络,跨国界的信息表示,所以,在XML 设计之初,就规定 XML 文件的默认编码格式就是 UTF-8,也就是说,如果没有特殊的说明,XML文件将被视为 UTF-8 编码。

然而,大部分的中文编辑软件,是根据操作系统来决定编码的方式的,所以,在写字板中直接输入并保存的文件,将被保存为 GB2312 编码,所以,在读出 XML文件内容时,往往就会出现文件错误的提示了。这种情况会出现在文件中有中文出现的时候,如果没有中文,只有英文信息,就不会出现问题。原因很简单,有中文时,因为中文的编码并不是UTF-8 编码,所以会造成冲突,没有中文时,英文的编码在GB2312 中与ASCII是兼容的,而ASCII 与UTF-8 是完全一致的,所以不会出现问题。这种情况也包括 UltraEdit 软件。

但时,专业的 XML编辑软件会自动将内容保存为 UTF-8 编码,不会有问题。

在通过DOM或XSLT保存 XML 文件时也有着同样的问题。

默认情况下,XML 的处理程序一般会将内容作为 UTF-8 编码进行处理,所以保存下来的 XML 文件必须要用可以识别 UTF-8 的软件来进行查看,如Windows 的记事本。

 

    Java 的处理

Java 的设计目标是一次编写,到处运行,所以在 Java 的内部对字符的处理采用了 UCS 来处理,因此 Java 的字符类型不再是 C++ 中的一个字节,而使用两个字节来保存一个字符。

但是,我们会发现,在 Java 的文件流中保存为文件后,我们可以直接使用记事本或 UltraEdit 打开察看。

在这里,Java 采用了一个灵巧的默认转换机制,当需要将内容中的字符保存到文件中时,Java 会自动的查看一下系统的本地编码,系统的本地编码可以在控制面板中查到,然后,自动将 UCS 编码的字符转换为本地编码,并进行保存。当需要从系统的文件系统中读入一个文件时,Java 通过查看系统的本地编码来决定如何识别文件的内容。这样,Java 就可以在内部使用 UCS, 但用户可以直接使用本地编码的文件了。

Java 在相应的方法中,提供了额外的参数,可以让用户自己来指定文件的编码。

 

    .Net 的处理

    在微软的 .Net 内部,同样使用 UCS 编码,但是,在对文件进行处理的时候,与Java 有一些区别,.Net 不查询系统的本地编码,而是直接使用磨人的 UTF-8 编码进行文件的处理,所以,你保存的中文内容,在 UltraEdit 中可能就是乱码,但是,如果你使用记事本打开的话,就不会有问题,因为 Windows 的记事本可以识别 UTF-8 的编码。

.Net 软件的配置文件使用 XML 格式,默认的编码一样是 UTF-8 ,所以,必须使用可以识别 UTF-8 的软件进行处理,如:vs.net,记事本等。

.Net 中,网页默认处理编码就是 UTF-8。

 

    Web 中的问题

网页的编码问题主要有两点,一是网页是如何编码的,二是如何告诉浏览器如何编码的。

第一个问题,又可以分成静态页面和动态页面两个问题。

对静态页面,网页的编码要看你保存文件时的编码选项,多数的网页编辑软件可以让你选择编码的类型,默认为本地编码,为了使网页减少编码的问题,最好保存为 UTF-8 编码格式。

对动态页面,如 Servlet 生成的页面,在 HttpServletResponse 类中有一个方法 setContentType,可以通过参数来指定生成的页面的类型和编码,例如:response.setContentType("text/html; charset=utf-8");来指定生成的页面的编码类型。

jsp 页面可以通过 <%@ page contentType="text/html;charset=gb2312" %> 来指定生成的页面的编码及类型。

第二个问题,如何通知浏览器网页的编码类型。

浏览器收到只是一个字节流,它并不知道页面是如何编码的,因此,需要一个机制来告诉浏览器页面的编码类型,标准的机制是使用 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 来指定页面的编码,当浏览器读取页面遇到这样的指示时,将使用这里制定的编码方式重新加载页面。

否则的话,浏览器将会试图猜出页面的编码类型。

 

    Tomcat 中的中文问题

 

在 Tomcat 中,经常遇到取回客户端提交的信息是乱码的问题。

当提交表单的时候,HTML页面的Form标签会使情况变得更为复杂。浏览器的编码方式取决于当前页面的编码设定,对Form标签也照此处理。这意味着如果ASCII格式的HTML页面用ISO-8859-1编码,那么用户在此页面中将不能提交中文字符。所以,如果你的页面使用的是 utf-8,那么 POST 的时候,也将使用 utf-8 。

由于 Tomcat 是美国人设计的,Tomcat 默认使用ISO8859-1 编码队客户端返回的内容进行解码,由于编码与内容不一致,就会出现乱码的 ??? 出现,根据以上的分析,在服务器端读取客户端回送的内容时,需要首先设定回送内容的编码,然后再进行信息的读取,通过使用 HttpServletRequest 的方法 setCharacterEncoding("utf-8")先行设定信息的编码类型。然后,就可以正确读取内容了。

 

总结

 

编码问题是信息处理的基本问题,但是由于历史和政治的问题,事实上存在着大量不统一的编码方式,造成在信息处理过程中的信息丢失,转换错误等问题,UCS 为问题的解决提供了一个很好的方向,但是,在现在的软件环境中,还没有达到全面地使用。在实际中工作中应尽量采用统一的编码格式,减少编码问题的发生

posted @ 2008-10-20 15:15 jsot 阅读(196) 评论(0) 编辑