【2】Protobuf VS JSON
阿新 • • 發佈:2020-10-08
需要結合上節測試,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檔案(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
時,我們將節省大量頻寬。而且由於體積較小,因此運輸速度也更快。這就是二進位制協議的優點。