由服务器宕机而思--再说字符编码

近段时间,Web服务器就因为一个字符乱码问题导致正则表达式匹配循环的bug(此正则表达式执行时间大约两分钟,cpu 100%)导致服务器Cpu使用率过高而引起服务器无响应宕机。其实引起问题的代码很简单,String newStr = new String (oldStr.getBytes(), "utf-8"); (新程序员缩写)

oldStr是编码为utf-8的字符串,执行此语句之后newStr乱码,之后乱码的newStr导致正则表达式匹配bug。但是在本地机器(xp)测试ok,为什么发布到linux服务器上面就出bug了呢?

原因分析:      
问题出在了String getBytes()方法,api文档如下:
byte[]     getBytes()
          使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。

平台的默认字符集的到底是什么呢?我们深入String源代码:


public byte [] getBytes () {
        return StringCoding.encode(value, offset, count);
    }

getBytes 使用 StringCoding类的encode方法来编码String,再深入:

static byte [] encode (char [] ca, int off, int len) {
       String csn = Charset. defaultCharset().name();
        try {
           return encode(csn, ca, off, len);
       } catch (UnsupportedEncodingException x) {
           warnUnsupportedCharset(csn);
       }
        try {
           return encode( "ISO-8859-1", ca, off, len);
       } catch (UnsupportedEncodingException x) {
           // If this code is hit during VM initialization, MessageUtils is
           // the only way we will be able to get any kind of error message.
           MessageUtils. err("ISO-8859-1 charset not available: "
                          + x.toString());
           // If we can not find ISO-8859-1 (a required encoding) then things
           // are seriously wrong with the installation.
           System. exit(1);
           return null ;
       }
    }

StringCoding类的encode方法通过Charset的defaultCharset方法来获系统默认字符集,我们在跟踪:

 public static Charset defaultCharset() {
        if ( defaultCharset == null ) {
           synchronized (Charset.class ) {
              java.security.PrivilegedAction pa =
                  new GetPropertyAction( "file.encoding");
              String csn = (String)AccessController.doPrivileged(pa);
              Charset cs = lookup(csn);
               if (cs != null)
                  defaultCharset = cs;
                else
                  defaultCharset = forName( "UTF-8");
            }
       }
        return defaultCharset ;
    }

到这里你明白了吧,系统默认字符集名称原来就是file.encoding系统属性,AccessController.doPrivileged()是执行特权代码的方式,是JAVA安全框架内容。以后论述。其实上面的代码等价于String csn =System. getProperty("file.encoding");
知道了系统的默认字符集之后我们来做一个测试吧,首先在Window xp系统执行以下代码:

        System.out.println(System. getProperty("file.encoding"));  
           String s = "hello!你好";
        System. out.println(s.getBytes(). length);
        System.out.println(s.getBytes("iso-8859-1" ).length );
        System. out.println(s.getBytes( "gbk").length );
        System. out.println(s.getBytes( "utf-8").length );

结果为:
UTF-8  (google许多文档说xp默认为是gbk,我的机器是utf-8,所以对xp来说,可能是utf-8 gbk gb2312)
12
8   (iso-8829-1 不支持汉字,所以编码为1个字节)
10 (gbk 汉字编码为2个字节)
12(utf-8支持汉字,每个汉字编码为3个字节)

到这里一切就清晰了,在本机执行String newStr = new String (oldStr.getBytes(), "utf-8"); 的时候,我们开发机的默认字符时utf-8,所以此变换newStr不变,而我们的linux服务器使用GBK编码(locale命令查看),所以String newStr = new String (oldStr.getBytes(), "utf-8")等价于String newStr = new String (oldStr.getBytes("gbk"), "utf-8");
变换过程: oldStr gbk的bytes用utf-8编码,肯定会导致乱码的,因为gbk和utf-8的中文字符编码不同。

所以,字符编码问题还是有必要重新给大家介绍一下:

字符编码的问题始终困扰着非英语国家的程序员,从到ASCII到ISO以及Unicode,从新手到老鸟,这个问题都可能会碰到n次。Java语言内部使用Unicode编码(最初仅仅使用了两个字节,肯定不能表示所有的语言,java5已经修正了底层的实现,支持unicode3.1的增补字符集)。
首先需要介绍几个概念:
字符(也就是文字) ,字符集, 字符编码 ,字符编码方式
用语意义字符以视觉形式表现的语言体系所用的符号字符集分配了文字编码的字符集合字符编码分配给每个文字的编码字符编码方式在计算机上表现文字编码的方式(Character Encoding Scheme)

常用字符集和字符编码

常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、GB18030字符集、Unicode字符集等。计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。
以ACSII为例:

ASCII字符集:主要包括控制字符(回车键、退格、换行键等);可显示字符(英文大小写字符、阿拉伯数字和西文符号)。
ASCII编码:将ASCII字符集转换为计算机可以接受的数字系统的数的规则。使用7位(bits)表示一个字符,共128字符;但是7位编码的字符集只能支持128个字符,为了表示更多的欧洲常用字符对ASCII进行了扩展,ASCII扩展字符集使用8位(bits)表示一个字符,共256字符。
ASCII字符集和ACSII编码以及ASCII编码方式存在的区别,但是我们通常说的ASCII字符集就包含了以上三个意思。GBK也类似。我们的所说的字符集通常也定义了字符编码

Unicode字符集和字符编码方式
 
  Unicode的码空间从U+0000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符. Unicode的码空间可以划分为17个平面(plane),每个平面包含216(65,536)个码位。每个平面的码位可表示为从U+xx0000到U+xxFFFF, 其中xx表示十六进制值从0016 到1016,共计17个平面。第一个平面成为基本多文种平面(Basic Multilingual Plane, BMP),或稱第零平面(Plane 0)。其他平面称为辅助平面(Supplementary Planes)。基本多语言平面內,從U+D800到U+DFFF之間的码位區段是永久保留不映射到字符,因此UTF-16利用保留下来的0xD800-0xDFFF区段的码位來對輔助平面的字符的码位進行編碼。


字符编码方式包括UTF-8,TF-16, UTF-32等。
UTF-8 有一下特性:

    UCS 字符 U+0000 到 U+007F (ASCII) 被编码为字节 0x00 到 0x7F (ASCII 兼容). 这意味着只包含 7 位 ASCII 字符的文件在 ASCII 和 UTF-8 两种编码方式下是一样的.
    所有 >U+007F 的 UCS 字符被编码为一个多个字节的串, 每个字节都有标记位集. 因此, ASCII 字节 (0x00-0x7F) 不可能作为任何其他字符的一部分.
    表示非 ASCII 字符的多字节串的第一个字节总是在 0xC0 到 0xFD 的范围里, 并指出这个字符包含多少个字节. 多字节串的其余字节都在 0x80 到 0xBF 范围里.   这使得重新同步非常容易, 并使编码无国界, 且很少受丢失字节的影响.
    可以编入所有可能的 231个 UCS 代码
    UTF-8 编码字符理论上可以最多到 6 个字节长, 然而 16 位 BMP 字符最多只用到 3 字节长.
    Bigendian UCS-4 字节串的排列顺序是预定的.
    字节 0xFE 和 0xFF 在 UTF-8 编码中从未用到.

下列字节串用来表示一个字符. 用到哪个串取决于该字符在 Unicode 中的序号.
U-00000000 - U-0000007F:      0xxxxxxx
U-00000080 - U-000007FF:      110xxxxx 10xxxxxx
U-00000800 - U-0000FFFF:      1110xxxx 10xxxxxx 10xxxxxx
U-00010000 - U-001FFFFF:      11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U-00200000 - U-03FFFFFF:      111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U-04000000 - U-7FFFFFFF:      1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-16

1,从U+0000至U+D7FF以及从U+E000至U+FFFF的码位

第一个Unicode平面(码位从U+0000至U+FFFF)包含了最常用的字符。该平面被称为基本多语言平面,缩写为BMP. UTF-16与UCS-2编码这个范围内的码位为单个16比特长的码元,数值等价于对应的码位. BMP中的这些码位是仅有的码位可以在UCS-2被表示.

2, 从U+10000到U+10FFFF的码位

辅助平面(Supplementary Planes)中的码位,在UTF-16中被编码为一对16比特长的码元(即32bit,4Bytes),称作 code units called a 代理对(surrogate pair), 具体方法是:
UTF-16解碼
 lead \ trail      DC00      DC01         …         DFFF
D800      10000      10001      …      103FF
D801      10400      10401      …      107FF
  ⋮      ⋮      ⋮      ⋱      ⋮
DBFF      10FC00      10FC01      …      10FFFF

    码位减去0x10000, 得到的值的范围为20比特长的0..0xFFFFF.
    高位的10比特的值(值的范围为0..0x3FF)被加上0xD800得到第一个码元或称作高位代理(high surrogate), 值的范围是0xD800..0xDBFF. 由于高位代理比低位代理的值要小,所以为了避免混淆使用,Unicode标准现在称高位代理为前导代理(lead surrogates).
    低位的10比特的值(值的范围也是0..0x3FF)被加上0xDC00得到第二个码元或称作低位代理(low surrogate), 现在值的范围是0xDC00..0xDFFF. 由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode标准现在称低位代理为后尾代理(trail surrogates).

由于高位代理、低位代理、BMP中的有效字符的码位,三者互不重叠,搜索是简单的: 一个字符编码的一部分不可能与另一个字符编码的不同部分相重叠。这意味着UTF-16是自同步(self-synchronizing): 可以通过仅检查一个码元就可以判定给定字符的下一个字符的起始码元. UTF-8也有类似优点,但许多早期的编码模式就不是这样,必须从头开始分析文本才能确定不同字符的码元的边界.



参考:
http://www.cnblogs.com/kenkofox/archive/2010/04/23/1719009.html
http://blog.stariy.org/2011-03/java_encoding.html

http://blog.csdn.net/qinysong/article/details/1179480

字符集和字符编码(Charset & Encoding)
http://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html

Java 平台中的增补字符
http://java.sun.com/developer/technicalArticles/Intl/Supplementary/index_zh_CN.html
使用java语言unicode代理编程
http://www.ibm.com/developerworks/cn/java/j-unicode/


你可能感兴趣的:(String,正则表达式,服务器,byte,encoding,initialization)