Могу ли я повторно использовать существующий двоичный файл protobuf при маршалинге сообщения, включающего его? (protobuf3)

Определения Protobuf такие:

syntax = "proto3"

message HugeMessage {
    // omitted
}

message Request {
    string name = 1;
    HugeMessage payload = 2;
}

В ситуации я получил HugeMessage от кого-то, и я хочу упаковать его дополнительными полями, а затем передать сообщение кому-то еще. Так что мне нужно распаковать бинарник HugeMessage в структуру Go, упаковать его в Request и снова маршалировать. Из-за большого размера HugeMessage стоимость Unmarshal и Marshal недоступна. так могу ли я повторно использовать двоичный файл HugeMessage без изменения определений protobuf?

func main() {
    // receive it from file or network, not important.
    bins, _ := os.ReadFile("hugeMessage.dump")
    var message HugeMessage
    _ = proto.Unmarshal(bins, &message) // slow
    request := Request{
        name: "xxxx",
        payload: message,
    }
    requestBinary, _ := proto.Marshal(&request) // slow
    // send it.
    os.WriteFile("request.dump", requestBinary, 0644)
}

Шаблоны Angular PrimeNg
Шаблоны Angular PrimeNg
Как привнести проверку типов в наши шаблоны Angular, использующие компоненты библиотеки PrimeNg, и настроить их отображение с помощью встроенной...
Создайте ползком, похожим на звездные войны, с помощью CSS и Javascript
Создайте ползком, похожим на звездные войны, с помощью CSS и Javascript
Если вы веб-разработчик (или хотите им стать), то вы наверняка гик и вам нравятся "Звездные войны". А как бы вы хотели, чтобы фоном для вашего...
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
Начала с розового дизайна
Начала с розового дизайна
Pink Design - это система дизайна Appwrite с открытым исходным кодом для создания последовательных и многократно используемых пользовательских...
Шлюз в PHP
Шлюз в PHP
API-шлюз (AG) - это сервер, который действует как единая точка входа для набора микросервисов.
14 Задание: Типы данных и структуры данных Python для DevOps
14 Задание: Типы данных и структуры данных Python для DevOps
проверить тип данных используемой переменной, мы можем просто написать: your_variable=100
0
0
79
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Короткий ответ: нет, простого или стандартного способа добиться этого не существует.

Наиболее очевидная стратегия — сделать то, что вы делаете сейчас: разархивировать HugeMessage, установить его в Request, а затем снова упорядочить. Поверхность API golang protobuf на самом деле не предоставляет средств для чего-то большего — и на то есть веские причины.

Тем не менее, есть способы достичь того, что вы хотите сделать. Но они не обязательно безопасны или надежны, поэтому вам нужно сопоставить эту стоимость со стоимостью того, что у вас есть сейчас.

Один из способов избежать немаршалирования — это воспользоваться способом, которым сообщение обычно сериализуется;

message Request {
    string name = 1;
    HugeMessage payload = 2;
}

.. эквивалентно

message Request {
    string name = 1;
    bytes payload = 2;
}

.. где payload содержит результат вызова Marshal(...) для некоторого HugeMessage.

Итак, если у нас есть следующие определения:

syntax = "proto3";

message HugeMessage {
  bytes field1 = 1;
  string field2 = 2;
  int64 field3 = 3;
}

message Request {
  string name = 1;
  HugeMessage payload = 2;
}

message RawRequest {
  string name = 1;
  bytes payload = 2;
}

Следующий код:

req1, err := proto.Marshal(&pb.Request{
    Name: "name",
    Payload: &pb.HugeMessage{
        Field1: []byte{1, 2, 3},
        Field2: "test",
        Field3: 948414,
    },
})
if err != nil {
    panic(err)
}

huge, err := proto.Marshal(&pb.HugeMessage{
    Field1: []byte{1, 2, 3},
    Field2: "test",
    Field3: 948414,
})
if err != nil {
    panic(err)
}

req2, err := proto.Marshal(&pb.RawRequest{
    Name:    "name",
    Payload: huge,
})
if err != nil {
    panic(err)
}

fmt.Printf("equal? %t\n", bytes.Equal(req1, req2))

выходы equal? true

Неясно, является ли эта «причуда» полностью надежной, и нет никаких гарантий, что она будет работать бесконечно долго. И, очевидно, тип RawRequest должен полностью отражать тип Request, что не идеально.

Другой альтернативой является создание сообщения более ручным способом, то есть с использованием пакета protowire — опять же, наугад, рекомендуется соблюдать осторожность.

Вы правы, я думаю. Тот же вывод из этого поста: Быстрое кодирование с использованием пре-сериализации.

NutwallTinker 22.11.2022 10:33

Вкратце это можно сделать с помощью protowire, и это не очень сложно, если повторно используемая структура не сложна.

Я задавал этот вопрос не так давно и, наконец, решил его, вдохновленный постом @nj_ . Согласно главе кодирования protobuf, сообщение буфера протокола представляет собой серию пар поле-значение, и порядок этих пар не имеет значения. Мне приходит в голову очевидная идея: просто работает как компилятор protoc, вручную формирует встроенное поле и добавляет его в конец запроса.

В этой ситуации мы хотим повторно использовать HugeMessage в Request, поэтому пара ключ-значение поля будет 2:{${HugeMessageBinary}}. Таким образом, код (немного другой) может быть:

func binaryEmbeddingImplementation(messageBytes []byte, name string) (requestBytes []byte, err error) {
    // 1. create a request with all ready except the payload. and marshal it.
    request := protodef.Request{
        Name: name,
    }
    requestBytes, err = proto.Marshal(&request)
    if err != nil {
        return nil, err
    }
    // 2. manually append the payload to the request, by protowire.
    requestBytes = protowire.AppendTag(requestBytes, 2, protowire.BytesType) //  embedded message is same as a bytes field, in wire view.
    requestBytes = protowire.AppendBytes(requestBytes, messageBytes)
    return requestBytes, nil
}

Сообщите номер поля, тип поля и байты. Вот и все. Обычный способ такой.

func commonImplementation(messageBytes []byte, name string) (requestBytes []byte, err error) {
    // receive it from file or network, not important.
    var message protodef.HugeMessage
    _ = proto.Unmarshal(messageBytes, &message) // slow
    request := protodef.Request{
        Name:    name,
        Payload: &message,
    }
    return proto.Marshal(&request) // slow
}

Некий эталон.

$ go test -bench=a -benchtime 10s ./pkg/                               
goos: darwin
goarch: arm64
pkg: pbembedding/pkg
BenchmarkCommon-8             49         288026442 ns/op
BenchmarkEmbedding-8         201         176032133 ns/op
PASS
ok      pbembedding/pkg 80.196s

package pkg

import (
    "github.com/stretchr/testify/assert"
    "golang.org/x/exp/rand"
    "google.golang.org/protobuf/proto"
    "pbembedding/pkg/protodef"
    "testing"
)

var hugeMessageSample = receiveHugeMessageFromSomewhere()

func TestEquivalent(t *testing.T) {
    requestBytes1, _ := commonImplementation(hugeMessageSample, "xxxx")
    requestBytes2, _ := binaryEmbeddingImplementation(hugeMessageSample, "xxxx")
    // They are not always equal int bytes. you should compare them in message view instead of binary from
    // due to: https://developers.google.com/protocol-buffers/docs/encoding#implications
    // I'm Lazy.
    assert.NotEmpty(t, requestBytes1)
    assert.Equal(t, requestBytes1, requestBytes2)
    var request protodef.Request
    err := proto.Unmarshal(requestBytes1, &request)
    assert.NoError(t, err)
    assert.Equal(t, "xxxx", request.Name)
}

// actually mock one.
func receiveHugeMessageFromSomewhere() []byte {
    buffer := make([]byte, 1024*1024*1024)
    _, _ = rand.Read(buffer)
    message := protodef.HugeMessage{
        Data: buffer,
    }
    res, _ := proto.Marshal(&message)
    return res
}

func BenchmarkCommon(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := commonImplementation(hugeMessageSample, "xxxx")
        if err != nil {
            panic(err)
        }
    }
}

func BenchmarkEmbedding(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := binaryEmbeddingImplementation(hugeMessageSample, "xxxx")
        if err != nil {
            panic(err)
        }
    }
}

Другие вопросы по теме