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 len int cap int}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 { ID uint32 Desc string}
起因在于构造体的长度是变长的
然而上面这种构造体是能够反对的(因为构造体中的字节数组的长度固定是3)
type Data struct { ID uint32 Timestamp uint64 Value int16 Desc [3]byte}
因为binary包的这个个性,因而它比拟适宜于做一些简略的封装,如果咱们须要封装蕴含有变长的构造,那么咱们还须要本人一一字段进行封装(而不能应用binary提供的Read和Write办法一次性将构造体进行封包和解包)
- 示例1:固定长度构造体
type message struct { Id int32 Len int32 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 = 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的时候会报错
- 可变长构造体
如果构造体中蕴含可变长的字段,那么就须要咱们手动进行解决
type message struct { id int32 len int32 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 = 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 Name string}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的一些操作函数
示例
//marshalfunc main() { type ColorGroup struct { ID int Name string 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)}//unmarshalfunc main() { var jsonBlob = []byte(`[ {"Name": "Platypus", "Order": "Monotremata"}, {"Name": "Quoll", "Order": "Dasyuromorphia"}]`) type Animal struct { Name string 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
6. golang standard library gob package