记一次数据编码踩坑经历

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

代码语言:javascript代码运行次数:0运行复制
var obj = {
  name: "John",
  age: 30,
  city: "New York"
};
 
var jsonString = JSON.stringify(jsonObj);

同样在Go语言中,我们也可以调用标准库中的json.Marshal方法将一个对象序列化为二进制。示例程序如下:

代码语言:javascript代码运行次数:0运行复制
   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编码器。

代码语言:javascript代码运行次数:0运行复制
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编码值。该函数一次只解码一个字符。

代码语言:javascript代码运行次数:0运行复制
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= 也验证了这一点。

代码语言:javascript代码运行次数:0运行复制
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 删除字符串编码对象二进制数据