admin管理员组文章数量:1033380
记一次数据编码踩坑经历
Part1引言
最近小编在一个项目的联调阶段踩坑了,排查了老半天才解决。问题是这样的:设备A上的某个服务需要将其采集到的数据发送到设备B上的某个服务,传输的数据是JSON格式。示例数据如下:
代码语言:javascript代码运行次数:0运行复制{
"osVersion":"Windows11",
"softVersion":"1.0.0",
"profileData":"010101001010"
}
其中profileData字段的值是一个proto对象序列化后的二进制数据。在设备B上无法解析出profileData中的内容。最后经过排查终于搞清了原因,本文从上述问题点出发,先系统讲述该问题涉及的知识点,最后再对问题原因进行说明。
Part2JSON序列化
计算机网络中传输的是01二进制数据,所以设备A内存中的JSON对象要传输给其他设备,必须先要进行序列化,即将其转成01010数据,然后通过网络传输给设备B。设备B接受到数据后,再通过反序列化拿到JSON对象,整个处理流程如下图。
从逻辑上来说,内存对象序列化为二进制数据分为两步。第一步是将内存对象转为JSON字符串,第二步是JSON字符串转化为二进制数据。但实际程序语言在处理时,上述两个步骤可以合在一起处理。
1内存对象转化为JSON字符串
obj是一个javascript对象,包含有姓名、年龄和城市字段。调用 JSON.stringify
函数可以将一个内存中的对象obj转为字符串,输出的字符串jsonString为{"name":"John","age":30,"city":"New York"}
var obj = {
name: "John",
age: 30,
city: "New York"
};
var jsonString = JSON.stringify(jsonObj);
同样在Go语言中,我们也可以调用标准库中的json.Marshal
方法将一个对象序列化为二进制。示例程序如下:
p := Person{
Name:"abc",
Age: 1,
Info: []byte{1,1},
}
data,_:=json.Marshal(p)
fmt.Println(string(data))
上述程序输出结果为{"name":"abc","age":1,"info":"AQE="}
,Info值是字节序列,输出的字符会被转成Base64编码(AQE=)。
2JSON字符串转化为二进制数据
JSON字符串转为二进制过程就是对JSON字符串中的每个字符,根据其在Unicode字符集中的 码点,用UTF-8进行编码表示。
下面结合Go语言中的 json.Marshal
源码进行解释说明。如果传给json.Marshal
的是一个对象,会生成一个newStructEncoder
编码器。
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
...
switch t.Kind() {
...
case reflect.Struct:
return newStructEncoder(t)
...
}
}
func newStructEncoder(t reflect.Type) encoderFunc {
se := structEncoder{fields: cachedTypeFields(t)}
return se.encode
}
真正对结构体进行编码的逻辑在下面的 encode 方法中,根据JSON定义,对象的开始和结束分别是字符 { 和 }。所以向e中写入 { 和 }。对于结构体中的每个字段,根据字段类型,调用对应的编码器进行编码。
代码语言:javascript代码运行次数:0运行复制func (se structEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
next := byte('{')
...
if next == '{' {
e.WriteString("{}")
} else {
e.WriteByte('}')
}
}
这里只对结构体中的字段是字符串类型和[]byte类型的编码器进行说明,其它类型感兴趣的可以查看对应源码。
下面代码对应的是字符串类型字段的编码器,可以看到采用的是UTF-8编码。对于单字节字符,即b的值小于 utf8.RuneSelf。UTF-8编码即为它本身值。对于多字节字符,像中文字符调用utf8.DecodeRuneInString
解码,获取对应的UTF-8编码值。该函数一次只解码一个字符。
func (e *encodeState) string(s string, escapeHTML bool) {
e.WriteByte('"')
start := 0
for i := 0; i < len(s); {
if b := s[i]; b < utf8.RuneSelf {
if htmlSafeSet[b] || (!escapeHTML && safeSet[b]) {
i++
continue
}
if start < i {
e.WriteString(s[start:i])
}
e.WriteByte('\\')
switch b {
case '\\', '"':
e.WriteByte(b)
case '\n':
e.WriteByte('n')
case '\r':
e.WriteByte('r')
case '\t':
e.WriteByte('t')
default:
e.WriteString(`u00`)
e.WriteByte(hex[b>>4])
e.WriteByte(hex[b&0xF])
}
i++
start = i
continue
}
c, size := utf8.DecodeRuneInString(s[i:])
if c == utf8.RuneError && size == 1 {
if start < i {
e.WriteString(s[start:i])
}
e.WriteString(`\ufffd`)
i += size
start = i
continue
}
if c == '\u2028' || c == '\u2029' {
if start < i {
e.WriteString(s[start:i])
}
e.WriteString(`\u202`)
e.WriteByte(hex[c&0xF])
i += size
start = i
continue
}
i += size
}
if start < len(s) {
e.WriteString(s[start:])
}
e.WriteByte('"')
}
对于 []byte 类型字段,采用如下编码器进行编码。可以看到对 []byte 内容调用 base64.StdEncoding.Encode
进行Base64编码。前面的示例中[]byte{1,1}
输出内容为 AQE= 也验证了这一点。
func encodeByteSlice(e *encodeState, v reflect.Value, _ encOpts) {
if v.IsNil() {
e.WriteString("null")
return
}
s := v.Bytes()
e.WriteByte('"')
encodedLen := base64.StdEncoding.EncodedLen(len(s))
if encodedLen <= len(e.scratch) {
dst := e.scratch[:encodedLen]
base64.StdEncoding.Encode(dst, s)
e.Write(dst)
} else if encodedLen <= 1024 {
dst := make([]byte, encodedLen)
base64.StdEncoding.Encode(dst, s)
e.Write(dst)
} else {
enc := base64.NewEncoder(base64.StdEncoding, e)
enc.Write(s)
enc.Close()
}
e.WriteByte('"')
}
Part3JSON值类型
JSON值类型共有6种,它们分别是:对象、数组、字符串、数值、布尔值、null。这里读者可能有疑问了?既然JSON值类型没有字节切片([]byte),那为啥下面代码中Info字段是[]byte类型呢?
代码语言:javascript代码运行次数:0运行复制type Person struct{
Name string `json:"name"`
Age int `json:"age"`
Info []byte `json:"info"`
}
虽然JSON不支持[]byte类型,但是Go语言的encoding/json包提供了一种与JSON兼容的方式来处理二进制数据,将[]byte数据转成了Base64编码的字符串。
Part4原因说明
回到本文开头问题,B设备上无法解析出A设备发过来数据中的profileData内容。原因是A设备采用的是Java程序序列化JSON对象,在将profileData转化二进制的时候,直接调用原生的Tobinary方法,将protobuf编码的内容转成了01010的二进制数据,所以在接收设备B上无法解析出原来的内容。✅正确的处理方法是将protobuf编码的内容用Base64编码,转成字符串放入JSON,然后序列化后发送。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2024-07-28,如有侵权请联系 cloudcommunity@tencent 删除字符串编码对象二进制数据记一次数据编码踩坑经历
Part1引言
最近小编在一个项目的联调阶段踩坑了,排查了老半天才解决。问题是这样的:设备A上的某个服务需要将其采集到的数据发送到设备B上的某个服务,传输的数据是JSON格式。示例数据如下:
代码语言:javascript代码运行次数:0运行复制{
"osVersion":"Windows11",
"softVersion":"1.0.0",
"profileData":"010101001010"
}
其中profileData字段的值是一个proto对象序列化后的二进制数据。在设备B上无法解析出profileData中的内容。最后经过排查终于搞清了原因,本文从上述问题点出发,先系统讲述该问题涉及的知识点,最后再对问题原因进行说明。
Part2JSON序列化
计算机网络中传输的是01二进制数据,所以设备A内存中的JSON对象要传输给其他设备,必须先要进行序列化,即将其转成01010数据,然后通过网络传输给设备B。设备B接受到数据后,再通过反序列化拿到JSON对象,整个处理流程如下图。
从逻辑上来说,内存对象序列化为二进制数据分为两步。第一步是将内存对象转为JSON字符串,第二步是JSON字符串转化为二进制数据。但实际程序语言在处理时,上述两个步骤可以合在一起处理。
1内存对象转化为JSON字符串
obj是一个javascript对象,包含有姓名、年龄和城市字段。调用 JSON.stringify
函数可以将一个内存中的对象obj转为字符串,输出的字符串jsonString为{"name":"John","age":30,"city":"New York"}
var obj = {
name: "John",
age: 30,
city: "New York"
};
var jsonString = JSON.stringify(jsonObj);
同样在Go语言中,我们也可以调用标准库中的json.Marshal
方法将一个对象序列化为二进制。示例程序如下:
p := Person{
Name:"abc",
Age: 1,
Info: []byte{1,1},
}
data,_:=json.Marshal(p)
fmt.Println(string(data))
上述程序输出结果为{"name":"abc","age":1,"info":"AQE="}
,Info值是字节序列,输出的字符会被转成Base64编码(AQE=)。
2JSON字符串转化为二进制数据
JSON字符串转为二进制过程就是对JSON字符串中的每个字符,根据其在Unicode字符集中的 码点,用UTF-8进行编码表示。
下面结合Go语言中的 json.Marshal
源码进行解释说明。如果传给json.Marshal
的是一个对象,会生成一个newStructEncoder
编码器。
func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc {
...
switch t.Kind() {
...
case reflect.Struct:
return newStructEncoder(t)
...
}
}
func newStructEncoder(t reflect.Type) encoderFunc {
se := structEncoder{fields: cachedTypeFields(t)}
return se.encode
}
真正对结构体进行编码的逻辑在下面的 encode 方法中,根据JSON定义,对象的开始和结束分别是字符 { 和 }。所以向e中写入 { 和 }。对于结构体中的每个字段,根据字段类型,调用对应的编码器进行编码。
代码语言:javascript代码运行次数:0运行复制func (se structEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) {
next := byte('{')
...
if next == '{' {
e.WriteString("{}")
} else {
e.WriteByte('}')
}
}
这里只对结构体中的字段是字符串类型和[]byte类型的编码器进行说明,其它类型感兴趣的可以查看对应源码。
下面代码对应的是字符串类型字段的编码器,可以看到采用的是UTF-8编码。对于单字节字符,即b的值小于 utf8.RuneSelf。UTF-8编码即为它本身值。对于多字节字符,像中文字符调用utf8.DecodeRuneInString
解码,获取对应的UTF-8编码值。该函数一次只解码一个字符。
func (e *encodeState) string(s string, escapeHTML bool) {
e.WriteByte('"')
start := 0
for i := 0; i < len(s); {
if b := s[i]; b < utf8.RuneSelf {
if htmlSafeSet[b] || (!escapeHTML && safeSet[b]) {
i++
continue
}
if start < i {
e.WriteString(s[start:i])
}
e.WriteByte('\\')
switch b {
case '\\', '"':
e.WriteByte(b)
case '\n':
e.WriteByte('n')
case '\r':
e.WriteByte('r')
case '\t':
e.WriteByte('t')
default:
e.WriteString(`u00`)
e.WriteByte(hex[b>>4])
e.WriteByte(hex[b&0xF])
}
i++
start = i
continue
}
c, size := utf8.DecodeRuneInString(s[i:])
if c == utf8.RuneError && size == 1 {
if start < i {
e.WriteString(s[start:i])
}
e.WriteString(`\ufffd`)
i += size
start = i
continue
}
if c == '\u2028' || c == '\u2029' {
if start < i {
e.WriteString(s[start:i])
}
e.WriteString(`\u202`)
e.WriteByte(hex[c&0xF])
i += size
start = i
continue
}
i += size
}
if start < len(s) {
e.WriteString(s[start:])
}
e.WriteByte('"')
}
对于 []byte 类型字段,采用如下编码器进行编码。可以看到对 []byte 内容调用 base64.StdEncoding.Encode
进行Base64编码。前面的示例中[]byte{1,1}
输出内容为 AQE= 也验证了这一点。
func encodeByteSlice(e *encodeState, v reflect.Value, _ encOpts) {
if v.IsNil() {
e.WriteString("null")
return
}
s := v.Bytes()
e.WriteByte('"')
encodedLen := base64.StdEncoding.EncodedLen(len(s))
if encodedLen <= len(e.scratch) {
dst := e.scratch[:encodedLen]
base64.StdEncoding.Encode(dst, s)
e.Write(dst)
} else if encodedLen <= 1024 {
dst := make([]byte, encodedLen)
base64.StdEncoding.Encode(dst, s)
e.Write(dst)
} else {
enc := base64.NewEncoder(base64.StdEncoding, e)
enc.Write(s)
enc.Close()
}
e.WriteByte('"')
}
Part3JSON值类型
JSON值类型共有6种,它们分别是:对象、数组、字符串、数值、布尔值、null。这里读者可能有疑问了?既然JSON值类型没有字节切片([]byte),那为啥下面代码中Info字段是[]byte类型呢?
代码语言:javascript代码运行次数:0运行复制type Person struct{
Name string `json:"name"`
Age int `json:"age"`
Info []byte `json:"info"`
}
虽然JSON不支持[]byte类型,但是Go语言的encoding/json包提供了一种与JSON兼容的方式来处理二进制数据,将[]byte数据转成了Base64编码的字符串。
Part4原因说明
回到本文开头问题,B设备上无法解析出A设备发过来数据中的profileData内容。原因是A设备采用的是Java程序序列化JSON对象,在将profileData转化二进制的时候,直接调用原生的Tobinary方法,将protobuf编码的内容转成了01010的二进制数据,所以在接收设备B上无法解析出原来的内容。✅正确的处理方法是将protobuf编码的内容用Base64编码,转成字符串放入JSON,然后序列化后发送。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2024-07-28,如有侵权请联系 cloudcommunity@tencent 删除字符串编码对象二进制数据本文标签: 记一次数据编码踩坑经历
版权声明:本文标题:记一次数据编码踩坑经历 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://it.en369.cn/jiaocheng/1748039648a2245283.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论