数据序列化
在客户端和服务器之间的通信是要求交换数据。这些数据可能是高度结构化的,但又需要序列化之后在传输。这一章将看看序列化基础,然后在考虑Go API支持对序列化的支持。
Introduction
客户端和服务器需要通过message交换信息。TCP和UDP提供了传输的机制来做这件事情。在一台计算机的两个进程之间也需要一种协议,那样消息的交换才有意义。
信息通过网络发送是一系列的字节,它们没有结构。我们将在下一章中会学习到解决不同的消息和协议定义,在这一章中,我们只关心消息的组件——数据如何被传递。
一个程序通常建立在复杂的数据结构之上,用来保存当前程序的状态。在与远程的客户端或服务器通信时,程序将尝试跨网络传输这些数据结构——可是这超出了应用程序的能力范围。
程序语言使用到的数据结构如下
- records/structures
- Variant records
- array-fixed size or varying
- string-fixed size or varying * table -e.g. arrays of records
- non-linear structures such as
- circular linked list
- binary tree
- objects with reference to other object
IP, TCP, UDP数据包都不知道这些数据类型。这些数据的相同点是,它们都包含一系列的字节,因此一个应用程序可以序列化任何数据并写入到一个字节流。然后从字节流中反序列化回合适的数据结构。这两个操作称为marshalling and unmarshalling.
比如,考虑发送下面这个可变长度的table,它两列的字符串长度也是可变的
fred | programmer |
---|---|
liping | analyst |
sureerat | manager |
可以有不同的方法达到这个目的。举例来说,我们假设数据是一个2例的表,但行数不定,所以marshalled格式为
3 // 3 rows, 2 columns assumed
4 fred // 4 char string,col 1
10 programmer // 10 char string,col 2
6 liping // 6 char string, col 1
7 analyst // 7 char string, col 2
8 sureerat // 8 char string, col 1
7 manager // 7 char string, col 2
字符串可变长度可以通过终止符表示,比如字符串的'\0'
3
fred\0
programmer\0
liping\0
analyst\0
sureerat\0
manager\0
或者我们可以知道数据的表格是3行2列,而每列的字符串长度8或者10,则序列化的数据可以为
fred\0\0\0\0
programmer
liping\0\0
analyst\0\0\0
sureerat
manager\0\0\0
以上的这些格式都可以——但是message交换协议必须指定使用哪一个,或者在程序运行时确定使用哪一个。
Mutual agreement
在上面一切,简单介绍了一下数据序列化的问题,在实际中,要考虑的细节更加复杂。举例,考虑上面的第一种情况,编码一个table数据到字节流
3
4 fred
10 programmer
6 liping
7 analyst
8 sureerat
7 manager
这就有许多问题出现,比如,一个表的行数使用多大的整数来表示,如果是255以下,则可以使用一个字切,但如果有更多的行,则需要一个short, integer or long. 对于每个字符串的长度来说也是一样。对于字符来说,它们属于哪个字符集,7位的ASCII还是16位的Unicode? 对于字符集的长度在后面的章节讨论。
所以上面的序列化是不透明或者隐式的。如果数据编码(marshalled)使用上面的格式,则没有说明如何解码(Unmarshalled)这些序列化的数据。在数据解码端需要知道数据是如何编码的,才能进行正确的解码。比如,如果行数是一个8位的整数,但如果解码的时候使用的16位整数,则会出现问题。
最早已知的编码方法是XDR(external data representation)使用在Sun的RPC, 后来ONC(Open Network Computing). XDR被定义在RFC 1832。但XDR天生的是类型不安全的(type-unsafe), 即序列化的数据没有类型信息。
Go语言中不支持对不透明序列化数据的编码和解码。Go的RPC包也不是使用XDR,而是使用"gob" serialisation. 这在本章的后面讲到
Self-describing data
Self-describing data(自描述数据)携带数据和数据的类型信息。比如,这上的数据可能得到的编码为
table
uint8 3
uint 2
string
uint8 4
[]byte fred
string
uint8 10
[]byte programmer
string
uint8 6
[]byte liping
string
uint8 7
[]byte analyst
string
uint8 8
[]byte sureerat
string
uint8 7
[]byte manager
当然,在实际的编码中,不会像我们的例子那样笨重和冗长:用一个小的整型作为类型标记而整个数据会尽可能的塞进一个小的字节数据。所以原则是,编码器将在序列化的数据中生成类型信息。而解码器将知道类型的生成规则(type-generation),所以可以使用它来构建正确的数据结构。
ASN.1
抽像语法标记(Abstract Syntax Notation One)最初是在1984年为电信行业设计的。ASN.1是一个复杂的标准,Go的"asn1"支持它的一个子集。它可以从一个复杂的数据结构创建自描述序列数据。它主要使用在当前的网络系统中,X.509证书的编码,x.509大量用于认证系统。所以在Go语言中,asn1 package用于读写x.509证书.
以下两个函数允许我们marshal和unmarshal数据
func Marshal(val interface{}) ([]byte, os.Error)
func Unmarshal(b []byte, val interface{}) (rest []byte, err error)
第一个函数将一个数据值编组为序列化的字节数组,第二个函数对它进行解组。可是,第一个参数的类型是interface{}, 所以要进行进一步的检测。对于编组来说,可以直接传递任何一个值给第一个函数,而对于解码来说,我们需要跟序列化数据匹配的类型相匹配的变量,即这个变量有分配此类型相对应的内存,这样在实际解组中,可以将值写入到这块内存中。稍后会讨论更具体的细节。
我们以一个简单的例子来举例说明,编组和编出一个整型。我们可以传递一个整型值给Marshal,并返回一个字节数组,然后在将个字节数组编出到一个整型。
func main() {
mdata, err := asn1.Marshal((13))
checkError(err)
var n int
fmt.Println(mdata)
_, err = asn1.Unmarshal(mdata, &n)
checkError(err)
fmt.Println("After marshal/unmarshal: ", n)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
解组后的值依然为13.
当然事情都不是这么简单,在了解更复杂数据类型时,我们先看看ASN.1支持哪些数据结构,并且了解Go又是如何支持ASN.1的。
任何的序列化方法都支持一部分的数据类型,而不支持别一部分。所以为了确定是否能用ASN.1来做序列化,就需要先看看你应用程序的数据类型是否与ASN.1匹配。可以从http://www.obj-sys.com/asn1tutorial/node4.html了解ASN1支持的类型。
简单的类型如下
- BOOLEAN: 两个状态的变量值 * INTEGER: 整型变量值
- BIT STRING: 任意长度的二进制数据
- OCTET STRING: 二进制数据,但这经的长度为8的倍数 * NULL: 表示无效的元素序列
- OBJECT IDENTIFIER:对像的名字信息
- REAL: 实数,浮点数变量值
- ENUMERATED:枚举类型,三种以上的状态值
- CHARACTER STRING:字符字符串
字符字符串可以是以下字符集
- NumericString: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
- PrintableString: Upper and lower case letters, digits, space, apostrphe(省略号),left/right parenthesis, lus sign, comma, hyphen, full stop, solidus, colon, equal sign, question mark
- TeletexString: The Teletex character set in CCITT's T61, space, and delete
- VideotexString: The Videotex character set in CCITT's T.100 and T.101, space, and delete VisibleString (ISO646String): Printing character sets of international ASCII, and space IA5String: International Alphabet 5 (International ASCII) * GraphicString 25 All registered G sets, and space GraphicString
以下是结构化类型
- SEQUENCE: Models an ordered collection of variables of different type
- SEQUENCE OF: Models an ordered collection of variables of the same type
- SET: Model an unordered collection of variables of different types
- SET OF: Model an unordered collection of variables of the same type
- CHOICE: Specify a collection of distinct types from which to choose one type
- SELECTION: Select a component type from a specified CHOICE type
- ANY: Enable an application to specify the type Note: ANY is a deprecated ASN.1 Structured Type. It has been replaced with X.680 Open Type.
Go语言并不支持所以的数据类型,以下是支持的数据类型
An ASN.1 INTEGER can be written to an int or int64. If the encoded value does not fit in the Go type, Unmarshal returns a parse error.
An ASN.1 BIT STRING can be written to a BitString(asn1中定义的类型)
- An ASN.1 OCTET STRING can be written to a []byte.
An ASN.1 OBJECT IDENTIFIER can be written to an ObjectIdentifier(asn1中定义的类型).
An ASN.1 ENUMERATED can be written to an Enumerated(asn1中定义的类型)
- An ASN.1 UTCTIME or GENERALIZEDTIME can be written to a *time.Time.
- An ASN.1 PrintableString or IA5String can be written to a string.
Any of the above ASN.1 values can be written to an interface{}. The value stored in the interface has the corresponding Go type. For integers, that type is int64.
An ASN.1 SEQUENCE OF x or SET OF x can be written to a slice if an x can be written to the slice's element type.
- An ASN.1 SEQUENCE or SET can be written to a struct if each of the elements in the sequence can be written to the corresponding element in the struct.
Go对ASN.1中的浮点数real的位数有限制.同时ASN.1中允许整型是任意大小,最Go只允许最大为有符号的64-bit整型。另一方面,Go区分带符号和无符号类型,而ASN.1没有。因此,传输一个无符号uint64,而它大于最大的int64位时,会发生失败。
同样的,ASN.1允许多个不同的字符集。Go只支持可打印PrintableString and IA5String(ASCII). ASN.1不支持Unicode字符(需要使用到ASN.1的扩展BMPString). 而Go不支持基础Unicode字符集(ASN编码). 如果需要传输Unicode characters,则需要使用到UTF-7. 这些字符集编码都会在之后的章节中讨论。
我们在之前的例子中简单的编组和解组了一个整型,其它的基础类型,比如booleans和reals可以相似的处理, 对于字符串,如果整个字符串都是ASCII则可以编组和解组,而如果字符串为"hello\u00bc",包含非ASCII字符, 则会发生错误"ASN.1 structure error:PrintableString contains invalid character". 以下是一个字符串的例子
s := "hello"
mdata, _ := asn1.Marshal(s)
var newstr string
asn1.Unmarshal(mdata, &newstr)
ASN.1也包含一些有用的类型,而没有在上面的例表中出现,比如UTC time. Go支持UTC time类型,这意味着你可以直接传递时间值。
t := time.Now()
mdata, err := asn1.Marshal(t)
checkError(err)
newtime := new(time.Time)
_, err = asn1.Unmarshal(mdata, newtime)
checkError(err)
fmt.Println(newtime)
通常,你可能想要对struct进行编组和解组。但不能对指针进行编组,会报unknown Go type,而Unmarshal的第二个参数必须是一个地址,并组这个结构的字段必须是导出的,否则发生panic reflect异常. 因为Golang 使用reflect包来marshal/unmarshal structures. 所以必须检查所有的字段是可导出的,而以下这的struct字段不能被marshed
type T struct {
Field1 int
field2 int // not exportable
}
ASN.1仅能处理数据类型,它不会考虑struct的字段名字,所以以下面的T1 可以marshelled/unmarshelled to T2,只需要相应的字段的类型一样
type T1 struct {
F1 int
F2 string
}
type T2 struct {
FF1 int
FF2 string
}
不仅仅字段的类型要一样,字段的数量也必须相同,以下的两个struct不能相互编组
type T1 struct {
F1 int
}
type T2 struct {
F1 int
F2 string // too many fields
}
以下是一个实际的例子
type name struct {
First string
Second string
}
var a name
a = name{"b", "b"}
fmt.Println(a)
mdata1, err := asn1.Marshal(a)
checkError(err)
my := new(name)
asn1.Unmarshal(mdata1, my)
fmt.Println(my)
ASN.1 daytime client and server
现在让我们使用ASN.1来跨网络传输数据
我们可以写一个TCP服务器,以ASN.1的Time类型来传递当前时间,代码如下
//DaytimeServer
func main() {
service := ":1200"
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
}
daytime := time.Now()
mdata, _ := asn1.Marshal(daytime)
conn.Write(mdata)
conn.Close()
}
}
//DaytimeClient
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("tcp4", service)
checkError(err)
result, err := readFully(conn)
checkError(err)
var newtime time.Time
_, err1 := asn1.Unmarshal(result, &newtime)
checkError(err1)
fmt.Println("After marshal/unmarshal", newtime.String())
os.Exit(0)
}
func readFully(conn net.Conn) ([]byte, error) {
defer conn.Close()
result := bytes.NewBuffer(nil)
var buf [512]byte
for {
n, err := conn.Read(buf[0:])
result.Write(buf[0:n])
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
}
return result.Bytes(), nil
}
JSON
JSON表示的是Javascript Object Notation. 它设计上是轻量的,用于在Javascript系统之间传递数据。它是基于文本的格式,并且成为多种程序语言通用的序列化方法。
JSON可以序列化对像,数组和基础值。基础值包括string, number, boolean 和 null value. 数组是以逗号分隔的列表,表示不同语言中的arrays, vectors, lists or sequences. "[...]"是数组的分界符。 对像表示的是一系列的"field:value"对,它们包含在"{...}".
举个例子,之前关于雇员的表,JSON表示为
[
{Name: fred, Occupation: programmer},
{Name: liping, Occupation: analyst},
{Name: sureerat, Occupation: manager}
]
它不支持复杂的数据类型,比如dates, 而且在数字类型之间没有明显的区别,没有递归的类型(recursive types),etc. JSON是一个非常简单的语言,但却非常有用。因为它是基础文本格式,所以使得人们用起来非常简单,尽使存在处理字符串时会产生开销。
从Go JSON包的参数说明中,我们知道编组使用下面的类型依赖进行默认编码。
- Boolean 值编码为JSON booleans
- 浮点数和整型值编码为JSON Number
- String值编码为JSON string, 对于每一个无效的UTF-8序列,使用Unicode字符U+FFFD替换
- Array and Slice的值编码为JSON arrays, 除了[]byte编码为base64-encoded string.
Struct值编码为一个JSON objects. Struct中的每一个字段成为对像的成员。默认情况下,对像的键的名字就是struct字段的名字转化为小写后的值。如果一个struct字段含有标签,则这个标签将被用于对像的名字,比如字段格式如下 FieldName string "json:username"
Map值被编码为一个JSON对像, map中的key类型必须为string; JSON对像中的key将直接跟map中的key对应。
- 指针值将被编码为它指向的值(注意:这里允许树,不能用于图). 一个空的指针则为null的JSON object.
- Interface值被编码为接口包含的值,一个空的接口指向null JSON object.
- Channel, complex and function值不能被编码为JSON. 如果尝试编码这些值会引发一个InvalidTypeEroor.
- JSON 不能表示的数据结构,Marshal将不能处理它们。如果传递一个循环的结构,将导致无限递归。
- 以下这个程序将JSON serialiased data保存到一个文件
//SaveJson
package main
import (
"encoding/json"
"fmt"
"os"
)
type Name struct {
Family string
Personal string
}
type Email struct {
Kind string `json:"type"`
Address string
}
type Person struct {
Name Name
Email []Email
}
func main() {
person := Person{
Name{"Newmarch", "Jan"},
[]Email{
{"home", "[email protected]"},
{"work", "[email protected]"},
}}
saveJson("person.json", &person)
fmt.Printf("%#v", person)
}
func saveJson(fileName string, key interface{}) {
outFile, err := os.Create(fileName)
checkError(err)
encoder := json.NewEncoder(outFile)
err = encoder.Encode(key)
checkError(err)
outFile.Close()
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal Error:%s ", err.Error())
os.Exit(1)
}
}
/*
{"Name":{"Family":"Newmarch","Personal":"Jan"},"Email":[{"type":"home","Address":"[email protected]"},{"type":"work","Address":"[email protected]"}]}
*/
package main
import (
"encoding/json"
"fmt"
"os"
)
type Name struct {
Family string
Personal string
}
type Email struct {
Kind string `json:"type"`
Address string
}
type Person struct {
Name Name
Email []Email
}
func (p Person) String() string {
s := p.Name.Personal + " " + p.Name.Family
for _, v := range p.Email {
s += "\n" + v.Kind + ": " + v.Address
}
return s
}
func main() {
var person Person
LoadJSON("person.json", &person)
fmt.Println("Person", person)
}
func LoadJSON(fileName string, key interface{}) {
inFile, err := os.Open(fileName)
checkError(err)
decoder := json.NewDecoder(inFile)
err = decoder.Decode(key)
checkError(err)
inFile.Close()
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal Error:%s ", err.Error())
os.Exit(1)
}
}
The gob package
Gob序列化技术是Go语言特有的。它被设计用于编码Go特有的数据类型,其它语言是不支持Gob的。 Go支持所有的Go数据类型,除了channels, function 和接口,同时支持所有的整型类型和大小(ASN.1只支持),String and Booleans, structs,arrays and slices. 到目前为循环结构还有问题,比台rings,但这会随着时间推移得到改善。
Gob会将类型信息编码到序,它支持的类型要比X.509中支持的类型,比XML文档中的类型信息更有效。Gob的类型信息对于每一块数据只包含一次.
类型信息使得Gob编组和解组时变得强键,或者这样理解,编组器和解组器(针对的数据结构发生了不同)
struct T {
a int
b int
}
上面编组后的二进制可以解组到不同的结构中
struct { A, B int } // the same
struct { B, A int } // ordering doesn't matter; matching is by name
struct { A, B, C int } // extra field (C) ignored
struct { B int } // missing field (A) ignored; data will be dropped
struct { B, C int } // missing field (A) ignored; extra field (C) ignored.
字段的顺序发生变化, 则根据字段名匹配。它可以应对缺失字段(后struct比前struct多出来的字段,则忽略后struct中的这个字段) or 额外字段(后struct中的字段不变)。它也可以处理为指针类型。所以上面的struct可以解组为
struct T {
*a int
**b int
}
在某种程度上,它可以应付强制类型转换,所以int字段可以扩展为int64, 但不能是不同的类型,比如int and uint.
使用Gob编组一个日期,你首先需要创建一个Encoder.它接会一个Writer作为参数。Encoder有一个方法encode, 它将编码后的值写入到Writer中。这个方法可以在不同的数据块中调用多次。但是类型信息只会写入一次。
你使用Decoder解组一个序列化的流,它接受一个Reader,每一次read返回一个unmarshalled数据值。
以下这个程序保存gob序列化数据到一个文件中
//SaveGob
package main
import (
"encoding/gob"
"fmt"
"os"
)
type Name struct {
Family string
Personal string
}
type Email struct {
Kind string `json:"type"`
Address string
}
type Person struct {
Name Name
Email []Email
}
func main() {
person := Person{
Name{"Newmarch", "Jan"},
[]Email{{"home", "[email protected]"},
{"work", "[email protected]"}}}
saveGob("person.gob", &person)
}
func saveGob(fileName string, person *Person) {
outFile, err := os.Create(fileName)
checkError(err)
encoder := gob.NewEncoder(outFile)
err = encoder.Encode(person)
checkError(err)
outFile.Close()
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal Error:%s ", err.Error())
os.Exit(1)
}
}
//LoadGob
package main
import (
"encoding/gob"
"fmt"
"os"
)
type Name struct {
Family string
Personal string
}
type Email struct {
Kind string `json:"type"`
Address string
}
type Person struct {
Name Name
Email []Email
}
func (p Person) String() string {
s := p.Name.Personal + " " + p.Name.Family
for _, v := range p.Email {
s += "\n" + v.Kind + ":" + v.Address
}
return s
}
func main() {
var person Person
loadGob("person.gob", &person)
fmt.Println("Person", person)
}
func loadGob(fileName string, key interface{}) {
inFile, err := os.Open(fileName)
checkError(err)
decoder := gob.NewDecoder(inFile)
err = decoder.Decode(key)
checkError(err)
inFile.Close()
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal Error:%s ", err.Error())
os.Exit(1)
}
}
Encoding binary data as strings
先前,传输8-bits的数据会发生错误。经常是由于串行线路噪声导致的(串行线路网际协议,常用于电话线拔号上网时)数据损毁。另一方面,7位的数据传输变得更加可靠。这是因为使用第8位进行数字校验。比如,使用"奇偶校验"模式,前7位为有奇数个1,则第设置第8位为0,或者前7位有偶数个1,则设置第8位为1。这样就能够通过一个位来检测一个字节。
ASCII是7位的字符集,它带的检测模式要比简单的奇偶校验要复杂。但这涉及到将8位转换到7位ASCII的操作,本质上讲,有的8位的数据是从7位的字节上扩展出来的。
在HTTP请求和响应中传输的二进制经常会翻译成ASCII的形式。使得我们可以文本的方式检查HTTP消息,而不用担心显示8位的字节。
一个常见的字符串格式是Base64位。Go还支持很多其它的binary-to-text格式。
以下两个函数用于Base64位的编码和解码
func NewEncoder(enc *Encoding, w io.Writer) io.WriteCloser
func NewDecoder(enc *Encoding, r io.Reader) io.Reader
以下是一个简单的例子
/**
* Base64
*/
package main
import (
"bytes"
"encoding/base64"
"fmt"
)
func main() {
eightBitData := []byte{1, 2, 3, 4, 5, 6, 7, 8}
bb := &bytes.Buffer{}
encoder := base64.NewEncoder(base64.StdEncoding, bb)
encoder.Write(eightBitData)
encoder.Close()
fmt.Println(bb)
dbuf := make([]byte, 12)
decoder := base64.NewDecoder(base64.StdEncoding, bb)
decoder.Read(dbuf)
for _, ch := range dbuf {
fmt.Print(ch)
}