【2】Protobuf VS JSON
需要结合上节测试,proto
文件在深入学习 Protocol Buffers。
一、生成随机的protobuf消息
现在,让我们创建一个sample
程序包以生成一些随机笔记本电脑数据。我喜欢使用随机数据,因为它在编写单元测试时非常有用。每次调用它将返回不同的值,并且数据看起来非常自然且接近实际。
laptop
├── proto
├── pb
├── sample
│ ├── generator.go
│ └── random.go
├── go.mod
└── Makefile
这里不多bb,直接上代码:
generator.go
package sample
import (
"github.com/golang/protobuf/ptypes"
"laptop/pb"
)
func NewKeyboard() *pb.Keyboard {
keyboard := &pb.Keyboard{
Layout: randomKeyboardLayout(),
Backlit: randomBool(),
}
return keyboard
}
func NewCPU() *pb.CPU {
brand := randomCPUBrand()
name := randomCPUName(brand)
numberCores := randomCPUNumberCores()
minGhz := randomFloat64(2.0, 3.5)
maxGhz := randomFloat64(minGhz, 5.0)
cpu := &pb.CPU{
Brand: brand,
Name: name,
NumberCores: uint32(numberCores),
NumberThreads: uint32(numberCores) * 2,
MinGhz: minGhz,
MaxGhz: maxGhz,
}
return cpu
}
func NewGPU() *pb.GPU {
brand := randomGPUBrand()
name := randomGPUName(brand)
minGhz := randomFloat64(1.0, 1.5)
maxGhz := randomFloat64(minGhz, 2.0)
memory := &pb.Memory{
Value: uint64(randomInt(2, 6)),
Unit: pb.Memory_GIGABYTE,
}
gpu := &pb.GPU{
Brand: brand,
Name: name,
MinGhz: minGhz,
MaxGhz: maxGhz,
Memory: memory,
}
return gpu
}
func NewRAM() *pb.Memory {
ram := &pb.Memory{
Value: uint64(randomInt(4, 64)),
Unit: pb.Memory_GIGABYTE,
}
return ram
}
func NewSSD() *pb.Storage {
ssd := &pb.Storage{
Driver: pb.Storage_SSD,
Memory: &pb.Memory{
Value: uint64(randomInt(128, 1024)),
Unit: pb.Memory_GIGABYTE,
},
}
return ssd
}
func NewHDD() *pb.Storage {
hdd := &pb.Storage{
Driver: pb.Storage_HDD,
Memory: &pb.Memory{
Value: uint64(randomInt(1, 6)),
Unit: pb.Memory_TERABYTE,
},
}
return hdd
}
func NewScreen() *pb.Screen {
sizeInch := randomFloat32(13, 17)
panel := randomScreenPanel()
resolution := randomScreenResolution()
screen := &pb.Screen{
SizeInch: sizeInch,
Resolution: resolution,
Panel: panel,
Multitouch: randomBool(),
}
return screen
}
func NewLaptop() *pb.Laptop {
brand := randomLaptopBrand()
name := randomLaptopName(brand)
laptop := &pb.Laptop{
Id: randomID(),
Brand: brand,
Name: name,
Cpu: NewCPU(),
Ram: NewRAM(),
Gpus: []*pb.GPU{NewGPU()},
Storages: []*pb.Storage{NewHDD(), NewSSD()},
Screen: NewScreen(),
Keyboard: NewKeyboard(),
Weight: &pb.Laptop_WeightKg{WeightKg: randomFloat64(1.0, 3.0)},
Price: randomFloat64(1500.0, 3000.0),
ReleaseYear: uint32(randomInt(2016, 2020)),
UpdatedAt: ptypes.TimestampNow(),
}
return laptop
}
random.go
package sample
import (
"github.com/google/uuid"
"laptop/pb"
"math/rand"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func randomKeyboardLayout() pb.Keyboard_Layout {
switch rand.Intn(3) {
case 1:
return pb.Keyboard_QWERTY
case 2:
return pb.Keyboard_QWERTZ
default:
return pb.Keyboard_AZERTY
}
}
func randomBool() bool {
return rand.Intn(2) == 1
}
func randomCPUBrand() string {
return randomStringFromSet("Intel", "AMD")
}
func randomStringFromSet(a ...string) string {
n := len(a)
if n == 0 {
return ""
}
return a[rand.Intn(n)]
}
func randomCPUName(brand string) string {
if brand == "Intel" {
return randomStringFromSet(
"Xeon E-2286M",
"Core i9-9980HK",
"Core i7-9750H",
"Core i5-9400F",
"Core i3-1005G1",
)
}
return randomStringFromSet(
"Ryzen 7 PRO 2700U",
"Ryzen 5 PRO 3500U",
"Ryzen 3 PRO 3200GE",
)
}
func randomCPUNumberCores() int {
cores := []int{2, 4, 6, 8}
return cores[rand.Intn(4)]
}
func randomFloat64(min float64, max float64) float64 {
return min + rand.Float64()*(max-min)
}
func randomGPUBrand() string {
return randomStringFromSet("Intel", "AMD")
}
func randomGPUName(brand string) string {
if brand == "Intel" {
return randomStringFromSet(
"RTX 2060",
"RTX 2070",
"GTX 1660-Ti",
"GTX 1070",
)
}
return randomStringFromSet(
"RX 590",
"RX 580",
"RX 5700-XT",
"RX Vega-56",
)
}
func randomInt(min int, max int) int {
return min + rand.Intn(max-min+1)
}
func randomFloat32(min float32, max float32) float32 {
return min + rand.Float32()*(max-min)
}
func randomScreenPanel() pb.Screen_Panel {
if rand.Intn(2) == 1 {
return pb.Screen_IPS
}
return pb.Screen_OLED
}
func randomScreenResolution() *pb.Screen_Resolution {
height := randomInt(1080, 4320)
width := height * 16 / 9
resolution := &pb.Screen_Resolution{
Width: uint32(width),
Height: uint32(height),
}
return resolution
}
func randomID() string {
return uuid.New().String()
}
func randomLaptopBrand() string {
return randomStringFromSet("Apple", "Dell", "Lenovo")
}
func randomLaptopName(brand string) string {
switch brand {
case "Apple":
return randomStringFromSet("Macbook Air", "Macbook Pro")
case "Dell":
return randomStringFromSet("Latitude", "Vostro", "XPS", "Alienware")
default:
return randomStringFromSet("Thinkpad X1", "Thinkpad P1", "Thinkpad P53")
}
}
二、序列化protobuf消息
现在,我们将创建一个新serializer
包并编写一些函数以将笔记本电脑对象序列化为文件。因此,让我们file.go
在这里创建一个文件。
laptop
├── proto
├── pb
├── sample
│ ├── generator.go
│ └── random.go
├── serializer
│ ├── file.go
│ ├── file_test.go
│ └── json.go
├── tmp
├── go.mod
└── Makefile
将protobuf消息写入二进制文件(file.go)
func WriteProtobufToBinaryFile(message proto.Message, filename string) error {
data, err := proto.Marshal(message)
if err != nil {
return fmt.Errorf("cannot marshal proto message to binary: %w", err)
}
err = ioutil.WriteFile(filename, data, 0644)
if err != nil {
return fmt.Errorf("cannot write binary data to file: %w", err)
}
return nil
}
从二进制文件读取protobuf消息(file.go)
func ReadProtobufFromBinaryFile(filename string, message proto.Message) error {
data, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("cannot read binary data from file: %w", err)
}
err = proto.Unmarshal(data, message)
if err != nil {
return fmt.Errorf("cannot unmarshal binary data to proto message: %w", err)
}
return nil
}
将protobuf消息写入JSON文件
file.go
func WriteProtobufToJSONFile(message proto.Message, filename string) error {
data, err := ProtobufToJSON(message)
if err != nil {
return fmt.Errorf("cannot marshal proto message to JSON: %w", err)
}
err = ioutil.WriteFile(filename, []byte(data), 0644)
if err != nil {
return fmt.Errorf("cannot write JSON data to file: %w", err)
}
return nil
}
json.go
package serializer
import (
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
)
func ProtobufToJSON(message proto.Message) (string, error) {
marshaler := jsonpb.Marshaler{
EnumsAsInts: false, // 是否将枚举值呈现为整数或字符串
EmitDefaults: true, // 是否使用默认值呈现字段
Indent: " ", // 使用什么缩进
OrigName: true, // 是否要像原始文件中一样使用原始字段名称
}
return marshaler.MarshalToString(message)
}
编写单元测试(file_test.go)
package serializer
import (
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/require"
"laptop/pb"
"laptop/sample"
"testing"
)
func TestFileSerializer(t *testing.T) {
t.Parallel()
binaryFile := "../tmp/laptop.bin"
jsonFile := "../tmp/laptop.json"
laptop1 := sample.NewLaptop()
err := WriteProtobufToBinaryFile(laptop1, binaryFile)
require.NoError(t, err)
laptop2 := &pb.Laptop{}
err = ReadProtobufFromBinaryFile(binaryFile, laptop2)
require.NoError(t, err)
require.True(t, proto.Equal(laptop1, laptop2))
err = WriteProtobufToJSONFile(laptop1, jsonFile)
require.NoError(t, err)
}
三、比较二进制文件和JSON文件的大小
执行上面的单元测试,即可看到tmp
文件夹下生成了laptop.bin
和laptop.json
两个文件。
可以看到JSON
文件的大小大约是二进制文件的5倍。
因此,当使用Protobuf
而不是普通的JSON
时,我们将节省大量带宽。而且由于体积较小,因此运输速度也更快。这就是二进制协议的优点。