2、關於網路中接受的資料如何序列化和反序列化的思考以及實現

畫個一樣的我發表於2023-10-28

1、背景介紹

因工作接觸到半導體行業,主要負責 EAP 相關的東西,其中需要實現 SECS/GEM 協議,訊息協議使用的是 SECS-II ,其中有一種資料型別是 A 型別,表示字串型別。需要將接收到的 SECS 指令記錄在日誌中,以及反解析 SECS 指令。

我們知道,網路中接受到的資料都是 byte,需要自己根據規則來進行序列化和反序列化,像平常我們使用 HTTP 通訊時,都是使用第三方通訊庫,底層的實現已經封裝好了。

比如我們規定現在傳輸過來的資料表示的是ASCII碼,那此時byte = 65 表示的是 字元A,我們把這一步叫做序列化,現在需要把 字元A 轉換為byte = 65 ,就叫做反序列化。

知道上面的規則定義後,來看看現在的具體需求,需求蠻簡單,就是在收到資料後,將其序列化後儲存在日誌後,方便後續的維護以及排查問題等,還有一個功能是可以將日誌中的 SECS 指令匯入到模擬器中(其實就是將 日誌中的 SECS 指令反序列化為 byte 資料)。

2、資料如何序列化和反序列化的思考

知道需求後,就開始想想怎麼來實現。

因為使用的是 golang 作為開發語言,所以接下來講解都是基於 go ,不過不同語言關於這塊應該大差不差。

最簡單的想法就是使用 golang 的格式化字串中的 %S 來序列化資料,這樣簡單也方便,其實大多數情況下是可以的,不過有一個問題就是,當遇到ASCII中的控制字元時,這種方式就不太行了。

比如 ASCII=10 (LF) - 換行:\n 格式化後,記錄日誌時就直接換行了,日誌會亂七八糟的換行,這種可能還可以接受,如果接收到的資料是6 (ACK) - 應答:不常用 這樣的時候,使用%s 得到的結果是看不錯效果的,那更加談不上反序列化了。

那怎麼解決呢?

對於 go 有一種簡便方式,使用 go 中的格式化方式 %q該值對應的單引號括起來的go語法字元字面值,必要時會採用安全的轉義表示.。是不是看著一頭霧水,其實就是使用特殊的字串代替 ASCII 中的控制字元,接下來看看是如何表示的。

控制字元採用以下方式:

ascii: 0 <---> "\x00"
ascii: 1 <---> "\x01"
ascii: 2 <---> "\x02"
ascii: 3 <---> "\x03"
ascii: 4 <---> "\x04"
ascii: 5 <---> "\x05"
ascii: 6 <---> "\x06"
ascii: 7 <---> "\a"  
ascii: 8 <---> "\b"  
ascii: 9 <---> "\t"
ascii: 10 <---> "\n"
ascii: 11 <---> "\v"
ascii: 12 <---> "\f"
ascii: 13 <---> "\r"
ascii: 14 <---> "\x0e"
ascii: 15 <---> "\x0f"
ascii: 16 <---> "\x10"
ascii: 17 <---> "\x11"
ascii: 18 <---> "\x12"
ascii: 19 <---> "\x13"
ascii: 20 <---> "\x14"
ascii: 21 <---> "\x15"
ascii: 22 <---> "\x16"
ascii: 23 <---> "\x17"
ascii: 24 <---> "\x18"
ascii: 25 <---> "\x19"
ascii: 26 <---> "\x1a"
ascii: 27 <---> "\x1b"
ascii: 28 <---> "\x1c"
ascii: 29 <---> "\x1d"
ascii: 30 <---> "\x1e"
ascii: 31 <---> "\x1f"
ascii: 127 <---> "\x7f"

ascii: 92 <---> "\\"

知道了上面的規定後,那後面的編碼就簡單很多了,下面貼下自己的程式碼。

3、程式碼實現

3.1、byte轉字串的方式

package main

import (
	"fmt"
	"unicode"
)

var controlCharters = make([]byte, 33)
var controlCharterMaps = make(map[byte]byte)

func init() {
	for i := 0; i <= 127; i++ {
		if unicode.IsControl(rune(i)) {
			controlCharters = append(controlCharters, byte(i))
			controlCharterMaps[byte(i)] = byte(i)
		}
	}
}
func main() {
	var recvData []byte
	for i := 0; i <= 127; i++ {
		recvData = append(recvData, byte(i))
	}

	fmt.Println(fmt.Sprintf("%q", string(recvData)))
}

輸出結果:

\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f

3.2、字串轉byte的方式

package main

import (
	"errors"
	"fmt"
	"strings"
	"unicode"
)

var controlCharters = make([]byte, 33)
var controlCharterMaps = make(map[byte]byte)

func init() {
	for i := 0; i <= 127; i++ {
		if unicode.IsControl(rune(i)) {
			controlCharters = append(controlCharters, byte(i))
			controlCharterMaps[byte(i)] = byte(i)
		}
	}
}
func main() {
	s := "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f"
	unescaped, err := replaceControlCharacters(s)
	if err != nil {
		fmt.Printf("err: %s", err)
		return
	}

	fmt.Println([]byte(unescaped))
}

// replaceControlCharacters 將 sml 中表示為控制字元以及跳脫字元(\) 轉換為 ascii 字元
func replaceControlCharacters(input string) (string, error) {

	controlCharMap := map[string]string{
		"\\\\":  "\\",
		"\\n":   "\n",
		"\\t":   "\t",
		"\\r":   "\r",
		"\\b":   "\b",
		"\\a":   "\a",
		"\\f":   "\f",
		"\\v":   "\v",
		"\\x00": "\x00",
		"\\x01": "\x01",
		"\\x02": "\x02",
		"\\x03": "\x03",
		"\\x04": "\x04",
		"\\x05": "\x05",
		"\\x06": "\x06",
		"\\x0e": "\x0e",
		"\\x0f": "\x0f",
		"\\x10": "\x10",
		"\\x11": "\x11",
		"\\x12": "\x12",
		"\\x13": "\x13",
		"\\x14": "\x14",
		"\\x15": "\x15",
		"\\x16": "\x16",
		"\\x17": "\x17",
		"\\x18": "\x18",
		"\\x19": "\x19",
		"\\x1a": "\x1a",
		"\\x1b": "\x1b",
		"\\x1c": "\x1c",
		"\\x1d": "\x1d",
		"\\x1e": "\x1e",
		"\\x1f": "\x1f",
		"\\x7f": "\x7f",
	}

	var output strings.Builder
	i := 0

	for i < len(input) {
		if input[i] == '\\' {
			escaped := false
			for key, value := range controlCharMap {
				if strings.HasPrefix(input[i:], key) {
					output.WriteString(value)
					i += len(key)
					escaped = true
					break
				}
			}
			if !escaped {
				return "", errors.New(fmt.Sprintf("illegal control character: %s, origin string: %s", string(input[i]), input))
			}
		} else {
			output.WriteByte(input[i])
			i++
		}
	}

	return output.String(), nil
}

輸出方式:

[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127]

從上面可以看出,方式還是很簡單的,其實還有很多其他方式可以達到序列化以及反序列化的方式,比如將收到的資料使用 base64 等方式,這樣可以的。只不過因為我們的日誌還需要給到客戶看,使用 base64 並不友好(自己平時看也不方便),所以需要使用上面這種方式。

相關文章