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)
}
}