Managing character sets and encodings

全世界有很多的语言,它们使用不同的字符集。同时存在不同的方法,将这些字符集编码为字节。这一章主要考虑这方面的问题。

Introduction

早期的电脑是在英语开发的,所以只是拉丁字母,加上数字,标点字符,及少量其它的字符。编码方式使用ASCII和EBCDIC。

对于只有256个的英语来说,它的处理机制是:文本文件和I/O都是由一系例的字节组成,每一个字节表示单个的字符。字符串的比较可以通过匹配字节来完成。从大写字符到小写的转换,可以只对单个字节进行操作。

在世界上存在有6,000多种语言. 只有少部分语言会使用到"english"中的字符。拉丁语系的语言,字符还带不同的修饰,比如法语,你可以写为"j'ai arrêté", 它有两个重读的元字符。相同的,德语有额外的字符'ß', 英国的英文字符没有被包含在标准的ASCII字符集中:英镑符号'£'和'€'

而且,全世界不都是使用拉丁字母表。泰国有它自已的字母表,比如"ภาษาไทย". 日本甚至会有两个字母表,平假名和片假名

还有一个象形语言,比如中文"百度一下,你就知道"

从技术的角度来看,全世界只使用ASCII是最好的。可是用户只想使用自己熟悉的语言。如果你建立了一个应用程序,它面向的是不同的国家,用户肯定会想使用自己的语言。对于分布式的系统,系统的不同组件都会需要使用不同的语言和字符。

Internationalisation(i18n)会告诉你写出国际化的应用程序。Localisation (l10n)是处理你国际化时,特有的部分,比如货币,时间等。

i18n和l10n是一个非常大的主题,比如,颜色的问题:白色中西方文化中表示纯洁,而在中文中,表示死亡,埃及表示欢乐。 在这一章中,我们仅关注字符处理的问题。

Definitions

character

一个字符是一个"信息的基本单元,大致为自然语言中的一个grapheme(写出来的符号), 比如字母,数字,或者标点符号(Wikipedia)". 一个字符是"书面语言的最小组件,它含有语义值(Unicode)". 包含的字母为'a' and 'À',数字可以为'2',标点符号可以为',', 以及其它的不同符号,比如英镑'£'.

从概念上讲,字符还包含控制字符,它们不出现在自然语言的字符符号中,只是用于处理语言的文本信息。

一个字符并没有特定的外观,只是我们使用外观来帮助我们认识字符。但这种外观在不同的上下文中,有不同的含义,在数学中,如果你看到符号π (pi),表示圆周率。如果你是在读希腊语,它是字母表中的第16个字符:“προσ” 表示希腊的单词"with",而不是3.14159...

Character repertoire/character set

Character repertoire是由不同的字符组成的集合。比如拉丁字符。并且没有特定的顺序。在英语中,虽然我们说'a'比'z'在字母表中要靠前,但我们不能说'a'要比'z'小。比如电话本中的"McPhee"排在"MacRea"前,这就表示字符顺序并没有起到决定性作用。

一个字元指定了一个字符的名字。通常是一个字符看起来的样本。e.g. 字符'a'可以看起来为'a', 以及italic 'a'和bold 'a'. 一个字元可能会区分大小写字符,比如'a', 'A'.但它们也可以认为是相同的,只是不同的样本表现(就像有些语言会区分大写和小写,比如Go语言,但有的语言不会).从另一个方面讲,一个字元可能包含不同的字符,只要这些字符有相同的样本外观:对于希腊字元,数学家有两个不同的字符外观π.这也称为非编码字符集。

Character code

一个字符编码(Character code)是将一个字符映射到整型。一个字符集的映射过程也称为编码字符集或者编码集。每一个字符的映射值通常称为字符码(code point). ASCII是一个code set(编码集). 'a'的codepoint为 97,'A'的为65

字符编码是一个抽像的过程,你在文本文件或者TCP数据包中是看不到的。可是它将人类可读的东西,映射到了数字。

Character encoding

为了通信或者储存一个字符,你需要对它进行编码。为了发送一个字符串,你需要对字符串中所有的字符进行编码。这一过程可以使用不同的编码集(code set).

比如,对于7-bit的ASCII字符码(code point)可以被直接编码为8-bit的一个字节。所以ASCII 'A'(codepoint的为65)被编码为8-bit的,则为01000001. 但是,一个不同的编码会保留最高位做奇偶校验。e.g.对于奇校验,ASCII 'a'为11000001(偶数个1,奇校验位为1)。有一些协议,比如Sun's XDR使用32-bit的等宽编码。ASCII 'A'被编码为 00000000 00000000 00000000 01000001.

字符的编码是在我们程序级别的功能。我们的程序处理编码的字符。显然对于8-bit的字符,带奇偶校验跟不带奇偶校验有不同的处理,或者32-bit字符。

这种编码可以扩展为字符串。使用word-length(等宽,32位)的偶校验,"ABC"为00000000(最高位表示偶校验位)11000011(C)01000010(B)01000001(A 在最低位)

Transport encoding

一个字符编码只能满足于在单个应用程序中处理字符。可以,一旦你开始在应用程序之间发送文本,则有更深的一个问题,如何传输字节,这里可能会用于gzip,或者base64.

如果我们知道字符和传输用到的编码, 然后就可以在编程时管理字符和字符串。如果我们不知道字符和传输用到的编码。则只能猜测。

在互联网中传送文本信息时,有一个约定: 文本信息的头包含了编码的信息,比如,HTTP header会包含以下的行

Content-Type: text/html; charset=ISO-8859-4
Content-Encoding: gzip

它告诉我们字符集为ISO-8859-4, 而且使用了gzip压缩。

但是你如何阅读这个头信息?它是否被有被编码?这是否又是鸡和蛋的问题?当然不是。这个约定中的头信息都是ASCII编码的。所以程序可以读取这个头,然后调整它对文档之后的编码。

ASCII

ASCII的字元包含英文字符加上数字,标点符号和其它控制字符。它的字符码如下表所示

Oct Dec Hex Char    Oct  Dec Hex Char
------------------------------------------------------------
000 0   00  NUL'\0' 100  64  40 @
001 1   01  SOH     101  65  41 A
002 2   02  STX     102  66  42 B
003 3   03  ETX     103  67  43 C
004 4   04  EOT     104  68  44 D
005 5   05  ENQ     105  69  45 E
006 6   06  ACK     106  70  46 F
007 7   07  BEL'\a' 107  71  47 G
010 8   08  BS'\b'  110  72  48 H
011 9   09  HT'\t'  111  73  49 I
012 10  0A  LF'\n'  112  74  4A J
013 11  0B  VT'\v'  113  75  4B K
014 12  0C  FF'\f'  114  76  4C L
015 13  0D  CR'\r'  115  77  4D M
016 14  0E  SO      116  78  4E N
017 15  0F  SI      117  79  4F O

以上的字符集是US ASCII,使用的是7位的字节的代码点, 西欧的一些标点符号及重音字符都被省略掉了。

ISO 8859

现在8位是标准的字节大小。它允许ASCII扩展128个代码点。ISO 8859有一系列的字符集,用来捕获不同的西欧语言。ISO 8859-1,也可以称为Latin-1.它覆盖了很多西欧国家的语言。ISO 8859中的其它系列则覆盖欧洲其于的语言,甚至包括希伯利亚语,阿拉伯语和泰语。比如,ISO 8859-5斯拉夫语(Cyrillic),它主要在俄罗斯。而ISO 8859-8包含了希伯利亚语(Hebrew).

这些字符符都是使用的8-bit. 比如字符'Á'在ISO-8859-1中的字符码为193, 所有的ISO 8859字符集系列中,128以下的都是ASCII,所以ASCII字符在所有的ISO-8859系列中都是一样的。

HTML推荐使用ISO 8859-1字符集。HTML 3.2是最后一个使用ISO 8859-1的版本。在HTML4.0中推荐使用Unicode.在2010 Google统计的页面中,有20%依然使用ISO 8859格式,而20%依然使用ASCII, Unicode将进50%.

Unicode

ASCII和ISO 8859都不能覆盖象形语言,中文估计有20,000个独立的字符。5,000个常用字符。它们超过了一个字节所可以表示的数量。通常需要有两个字节。这里有许多两个字节的字符集:Big5, EUC-TW, GB2312 and GBK/GBX for chinese, JIS x 02 for Japanese。这些编码集互相之间是不兼容的。

Unicode是旨在覆盖所有主要的字符集,使其统一为一个标准的字符集。这包括European, Asian, Indian。现在的版本是5.2它覆盖了107,000个字符. 字符码超过65,536个。这已经超过了2 ^ 16(两个字节)。

Unicode中的前256个字符码跟ISO 8859-1相匹配. 同样前128个为ASCII. 但这对于其它字符集,却并不适用:比如,虽然Big5字符都包含在Unicode. 但是字符码却不是相同的。可以查看http://moztw.org/docs/big5/table/unicode1.1-obsolete.txt演示了Big5到Unicode.

为了在计算机系统中表示Unicode characters.必须使用一个编码。UCS是其中的一个编码,它使用双字节。虽然有很多Unicode字符符合2个字节,但这个编码已经过时了,不在被使用。替代为:

  • UTF-32是一个4字节编码,但不经常使用,HTML5已经明确提出,反对使用它
  • UTF-16将常见的字符编码为2个字节和2个字节的“overflow". ASCII和ISO8859-1含有相同的值
  • UTF-8中每个字符1到4个字节,ASCII的编码后的值是一样的,但与ISO8859-1编码后的值不一样
  • UTF-7 有时会使用,但不会经常使用
  • UTF-8, Go and runes

UTF-8是最常使用的编码,Google评估有50%的页面是使用UTF-8. ASCII字符集编码后的值跟UTF-8前128个字符编码后的字节是一样的。所以UTF-8阅读器可以阅读ASCII字符以及所有的Unicode字符集。

Go使用UTF-8编码字符串中的字符。每一个字符的类型为rune.它的int32的别名,对于一个Unicode字符,它在utf-8中可以为1,2,4字节。从字符的角度来理解,一个字符串是一个rune类型的数组。

一个字符串也可以是一个字节的数组。但你必须小心:仅有ASCII的子集中,一个字节才对应一个字符。其它的字符,则可能是2,3,4个字节。所以字符(rune)的长度跟字节数组的长度是不一样的。

以下是一个例子

str := "百度一下,你就知道"
println("String length", len([]rune(str)))
println("Byte length", len(str))

prints

String length 9
Byte length 27

UTF-8 客户端和服务器

对于UTF-8来说,你不需要详细的处理UTF-8的文本。UTF-8字符串的底层数据类型是一个字节数据。Go在需要的时候会将字符串看成为1,2,3,4的字节数组。这个字符串的长度就是字节数组的长度。所以你write任何UTF-8字符串时,它会写为一个字节数组。

相同的,在读取一个字符串时,你仅需要读取一个字节数组,然后使用string([]byte)将数组转换为一个字符串。如果Go不能解码字节到Unicode字符。则给写一个Unicode替代的字符,比如\uFFFD.

所以在之前章节中的clients和Severs例子中,都是utf-8编码的文本

ASCII客户端和服务器

ASCII字符跟UTF-8是一样的,所以使用UTF-8的字符处理同样可以读写ASCII的字符

UTF-16 and Go

utf-16用于处理16位无符号整型数组。package utf16用于管理这些数组。为了将一个正常的Go字符串,UTF-8字符串转换为一个UTF-16.你首先提取出字符串的代码点到一个[]rune数组,然后使用utf16.Encode生成一个uint16的数组。

相同的,将一个utf-16的无符号short数组解码为字符串。你使用utf16.Decode将它转换为了一个字符码数组[]rune.然后在转换为一个字符串。如下所示

str := "百度一下,你就知道"
runes := utf16.Encode([]rune(str))
ints := utf16.Decode(runes)
str = string(ints)

这些类型转换在客户端和服务器都是需要处理。

Little-endian and big-endian

不幸的是,UTF-16暗藏一个问题,基本上是将一个字符编码为16-bit的short整型。最大的问题是:对于每一个short, 它如何写成两个字节?高位还是低位?每一个都可以,只要接收端和发送端采用相同的方式。

Unicode为了解决这个问题是使用BOM(byte order marker). 这是一个zero-width非打印字符,所以你在文本中不能看到它。

  • big-endian sytems it is 0xFFFE
  • little-endian system it is 0xFEFF

BOM会作为文本的第一个字符,在读取文本时,可以测试这两个字节,以决定使用哪一个endian方式.

UTF-16 client and server

/* UTF16 Server
*/
package main
import (
    "fmt"
    "net"
    "os"
    "unicode/utf16"
)
const BOM = '\ufffe'
func main() {
    service := "0.0.0.0:1210"
    tcpAddr, err := net.ResolveTCPAddr("tcp", service)
    checkError(err)
    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)
    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        str := "j'ai arrêté"
        shorts := utf16.Encode([]rune(str))
        writeShorts(conn, shorts)
        conn.Close() // we're finished
    }
}
func writeShorts(conn net.Conn, shorts []uint16) {
    var bytes [2]byte
    // send the BOM as first two bytes
    bytes[0] = BOM >> 8
    bytes[1] = BOM & 255
    _, err := conn.Write(bytes[0:])
    if err != nil {
        return
    }
    for _, v := range shorts {
        bytes[0] = byte(v >> 8)
        bytes[1] = byte(v & 255)
        _, err = conn.Write(bytes[0:])
        if err != nil {
            return
        }
    }
}
func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}
/* UTF16 Client
*/
package main
import (
    "fmt"
    "net"
    "os"
    "unicode/utf16"
)
const BOM = '\ufffe'
func main() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "host:port")
        os.Exit(1)
    }
    service := os.Args[1]
    conn, err := net.Dial("tcp", service)
    checkError(err)
    shorts := readShorts(conn)
    ints := utf16.Decode(shorts)
    str := string(ints)
    fmt.Println(str)
    os.Exit(0)
}
func readShorts(conn net.Conn) []uint16 {
    var buf [512]byte
    // read everything into the buffer
    n, err := conn.Read(buf[0:2])
    for true {
        m, err := conn.Read(buf[n:])
        if m == 0 || err != nil {
            break
        }
        n += m
    }
    checkError(err)
    var shorts []uint16
    shorts = make([]uint16, n/2)
    if buf[0] == 0xff && buf[1] == 0xfe {
        // big endian
        for i := 2; i < n; i += 2 {
            shorts[i/2] = uint16(buf[i])<<8 + uint16(buf[i+1])
        }
    } else if buf[1] == 0xff && buf[0] == 0xfe {
    // little endian
        for i := 2; i < n; i += 2 {
            shorts[i/2] = uint16(buf[i+1])<<8 + uint16(buf[i])
        }
    } else {
        // unknown byte order
        fmt.Println("Unknown order")
    }
    return shorts
}
func checkError(err error) {
    if err != nil {
        fmt.Println("Fatal error ", err.Error())
        os.Exit(1)
    }
}