golang网络数据交换

1. 问题描述 在C/C++中处理结构体在网络上传输的处理
1.1 直接发送二进制的结构体数据

struct DataFormat { long arg1; long arg2; }; struct Result { long sum; }; int main(int argc, char **argv) {...char sendline[MAXLINE]; struct DataFormat args; struct Result result; args.arg1 = 1; args.arg2 = 2; write(sockfd, &args, sizeof(args)); if (readn(sockfd, &result, sizeof(result)) != 0) printf("%ld\n", result.sum); ... }

这种做法有许多缺陷,包括:
  • 发送的多字节类型(int,long等)在不同架构的机器上的大小端方式可能不同,造成解析错误
  • 即使是大小端一致的机器上,可能由于int、long等类型的机器字长不同而出现错误
  • 结构体在不同的编译器中的对其方式可能有所不同,这也会造成解析错误
1.2 处理方式
针对以上3个问题,采用的解决思路:
  • 对于多字节类型,采用转换成统一的网络字节序进行处理,使用htons htonl和ntohs ntohl
  • 对于类型字长不一致的情况,统一采用固定长度的类型,比如uint8, uint16等类型
  • 结构体的对其方式都设置成packed,这样是的成员直接没有任何对其(在不同的编译器中表示方式不一样,在gcc中使用__attribute__((packed)))
struct DataFormat { int32_t arg1; int32_t arg2; }__attribute__((packed)); struct Result { int32_t sum; }__attribute__((packed)); int main(int argc, char **argv) {...char sendline[MAXLINE]; struct DataFormat args; struct Result result; args.arg1 = htonl(1); args.arg2 = htonl(2); write(sockfd, &args, sizeof(args)); if (readn(sockfd, &result, sizeof(result)) != 0) printf("%ld\n", ntohl(result.sum)); ... }

1.3 处理方式2
还有一种更好的处理方式: 我们在两端发送和接收的时候都使用字符串进行,因为字符只有一个字节,在传输过程中不存在大小端的问题,另外字符的表示方法在各平台基本是一致的,也没有所谓的对齐问题,因此这种方案具有最大的通用性
struct DataFormat { int32_t arg1; int32_t arg2; }; struct Result { int32_t sum; }; int main(int argc, char **argv) {//...char sendline[1024]; struct DataFormat args; struct Result result; snprintf(sendline, sizeof(sendline), "%d%d\n", args.arg1,args.arg2); write(sockfd, sendline, strlen(sendline)); //... }

传输的过程是按照结构体中成员的顺序依次进行的,对端解析的时候也需要按照这个顺序进行处理
2. golang中的处理 在查阅了一些资料之后总结如下:
  • 使用unsafe包,使用类似于C/C++的做法(不推荐)
  • 使用encoding/binary包进行字节的封包和解包
  • 使用更高层级的marshal和unmarshal的做法
2.1 使用unsafe包
这种方式由于涉及到底层许多C和golang互操作的细节,我暂时没有兴趣和需求了解,先略过
贴一个简单的示例,可以感受基本上和C/C++指针方式类似
package mainimport ( "fmt" "unsafe" )type TestStructTobytes struct { data int64 } type SliceMock struct { addr uintptr lenint capint }func main() { var testStruct = &TestStructTobytes{100} Len := unsafe.Sizeof(*testStruct) testBytes := &SliceMock{ addr: uintptr(unsafe.Pointer(testStruct)), cap:int(Len), len:int(Len), } data := *(*[]byte)(unsafe.Pointer(testBytes)) fmt.Println("[]byte is : ", data)var ptestStruct *TestStructTobytes = *(**TestStructTobytes)(unsafe.Pointer(&data)) fmt.Println("ptestStruct.data is : ", ptestStruct.data) }

2.2 使用encoding/binary
在阅读文档之后了解到,encoding/binary库有一些缺陷:
  • 仅支持定长变量和结构体的encoding和decoding
  • 支持的数据类型是Numeric类型的(也就是整数型、浮点型)
于是下面这样的结构体是不支持的
type Info struct { IDuint32 Desc string }

原因在于结构体的长度是变长的
但是下面这种结构体是可以支持的(因为结构体中的字节数组的长度固定是3)
type Data struct { IDuint32 Timestamp uint64 Value int16 Desc[3]byte }

由于binary包的这个特性,因此它比较适合于做一些简单的封装,如果我们需要封装包含有变长的结构,那么我们还需要自己逐个字段进行封装(而不能使用binary提供的Read和Write方法一次性将结构体进行封包和解包)
  1. 示例1:固定长度结构体
type message struct { Idint32 Lenint32 Data [4]byte }func unpack(data []byte) *message { msg := &message{} dataio := bytes.NewReader(data)binary.Read(dataio, binary.LittleEndian, msg)//上面这一行 binary.Read(dataio, binary.LittleEndian, msg)可以 //用下面的3行替换,效果是一样的 // binary.Read(dataio, binary.LittleEndian, &msg.Id) // binary.Read(dataio, binary.LittleEndian, &msg.Len) // binary.Read(dataio, binary.LittleEndian, &msg.Data)return msg } func pack(msg *message) []byte {databufio := bytes.NewBuffer([]byte{})binary.Write(databufio, binary.LittleEndian, msg)//上面这一行binary.Write(databufio, binary.LittleEndian, msg) //可以用下面3行替换 // binary.Write(databufio, binary.LittleEndian, msg.Id) // binary.Write(databufio, binary.LittleEndian, msg.Len) // binary.Write(databufio, binary.LittleEndian, msg.Data) return databufio.Bytes() }func main() { bindata := []byte{}msg := &message{ Id:1, Len:4, Data: [4]byte{'h', 'a', 'h', 'h'}, }bindata = https://www.it610.com/article/pack(msg) fmt.Println("struct-to-byte array") fmt.Println(bindata)msg2 := unpack(bindata) fmt.Println("byte-array-to-struct =") fmt.Printf("%v\n", msg2) }

有一些需要注意的点:
  • 如果是单独的一个整型值,可以使用PutUvarint等直接转换即可,不需要使用Read和Write,后者主要用于对结构体进行转换
  • 在结构体中的成员必须使用首字母大写的方式(导出),否则在Read和Write的时候会报错
  1. 可变长结构体
如果结构体中包含可变长的字段,那么就需要我们手动进行处理
type message struct { idint32 lenint32 data []byte }func unpackHead(byteValue []byte) *message { msg := &message{} dataio := bytes.NewReader(byteValue) binary.Read(dataio, binary.LittleEndian, &msg.id) binary.Read(dataio, binary.LittleEndian, &msg.len) return msg }func pack(msg *message) []byte {databufio := bytes.NewBuffer([]byte{})binary.Write(databufio, binary.LittleEndian, msg.id) binary.Write(databufio, binary.LittleEndian, msg.len) binary.Write(databufio, binary.LittleEndian, msg.data) return databufio.Bytes() }func main() {msg := &message{ id:1, len:4, data: []byte{'h', 'a', 'h', 'h'}, }bindata := pack(msg) fmt.Println("packed size:", len(bindata)) fmt.Println("struct-to-byte-array", bindata)msg2 := unpackHead(bindata) beginIndex := unsafe.Sizeof(msg2.id) + unsafe.Sizeof(msg2.len) msg2.data = https://www.it610.com/article/make([]byte, 4) binary.Read(bytes.NewReader(bindata[beginIndex:]), binary.LittleEndian, &msg2.data) fmt.Println("byte-array-to-strcut") fmt.Printf("%v\n", msg2) }

以上仅仅是处理了一个结构体的情形,可想而知如果是一个可变长的结构体的slice,那么处理的复杂度会提升非常多,因此不太建议使用这种方式来处理复杂的类型
3 更高层级的做法
数据在网络传输中是一些二进制的数据流而已,从发送端将程序中定义的数据结构体转换成字节流,接收端接收到数据流之后需要反向转换回原来的数据结构,这一过程一般称之为Serialization和Deserialization,在golang中一般称之为 marshalling 和 unmarshalling
golang提供了多种marshling和unmarshaling的方法,包括
  • GOB(encoding/gob包,仅golang语言可用)
  • JSON(JavaScript Object Notation)(encoding/json包)
  • ASN.1 (Abstract Syntax Notation One) (encoding/asn1包)
  • 其他...
3.1 GOB
(1)特点:
  • golang specific,不可以跨语言
  • 支持golang内置的绝大部分类型(除channel、function、interface外)
  • 支持接收方数据结构的一些兼容字段转换
(2)使用方法
发送端申请一个Encoder,接收方申请一个Decoder
(3)扩展自定义类型
如果自定义类型需要处理,可以实现BinaryMarshaler接口(MarshalBinary)和BinaryUnmarshaler(UnmarshalBinary)接口
示例程序:
type P struct { X, Y, Z int Namestring }type Q struct { X, Y *int32 Name string }func main() { var network bytes.Buffer enc := gob.NewEncoder(&network) dec := gob.NewDecoder(&network)// Encode (send) some values. err := enc.Encode(P{3, 4, 5, "Pythagoras"}) if err != nil { log.Fatal("encode error:", err) } // Decode (receive) and print the values. var q Q err = dec.Decode(&q) if err != nil { log.Fatal("decode error 1:", err) } fmt.Printf("%q: {%d, %d}\n", q.Name, *q.X, *q.Y) }

可以看到gob相比之前的binary的不仅支持的数据类型丰富,而且编码方式简单太多了,推荐使用这种方式在进程间传输数据
3.2 JSON
JSON这种方式也gob有点类似,只不过它是给予JSON这种格式规范来进行处理Encode和Decode,由于JSON格式的一些限制(比如对象的名称只能是字符串等)因此相对来说它提供的支持类型比gob要少一些,但是它提供了对于json的一些操作函数
示例
//marshal func main() { type ColorGroup struct { IDint Namestring Colors []string } group := ColorGroup{ ID:1, Name:"Reds", Colors: []string{"Crimson", "Red", "Ruby", "Maroon"}, } b, err := json.Marshal(group) if err != nil { fmt.Println("error:", err) } os.Stdout.Write(b) }//unmarshal func main() { var jsonBlob = []byte(`[ {"Name": "Platypus", "Order": "Monotremata"}, {"Name": "Quoll","Order": "Dasyuromorphia"} ]`) type Animal struct { Namestring Order string } var animals []Animal err := json.Unmarshal(jsonBlob, &animals) if err != nil { fmt.Println("error:", err) } fmt.Printf("%+v", animals) }

3.3 ASN.1
ASN.1是一个1984年推出来的通信领域的协议,也是用于数据的交换,它定义的规则相对比较复杂,golang中使用在X.509 certificates中,golang中的asn1包主要提供以下两个函数来进行封包和解包
func Marshal(val interface{}) ([]byte, os.Error) func Unmarshal(val interface{}, b []byte) (rest []byte, err os.Error)

简单的使用示例
func main() { mdata, err := asn1.Marshal(13) checkError(err)var n int _, err1 := asn1.Unmarshal(mdata, &n) checkError(err1)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) } }

更多内容请参考golang标准库
4. 参考资料 1.打造自己的字节序转换函数(16位、32位和64位)
2.packing struct in golang in bytes to talk with C application
3.Equivalent of C++ reinterpret_cast a void* to a struct in Golang
4.Go语言结构体与二进制数组转换
5.decoding binary data when structures include strings
【golang网络数据交换】6. golang standard library gob package

    推荐阅读