Featured image of post Protobuf: The Not-So-Secret Sauce Behind Efficient Data Serialization

Protobuf: The Not-So-Secret Sauce Behind Efficient Data Serialization

Code Samples in Python, C#, and Go Language

Protobuf: The Not-So-Secret Sauce Behind Efficient Data Serialization

Ever tried sending a massive JSON file over the network and thought, “There has to be a better way”?

What’s Protobuf, Anyway?

Think of Protobuf as the minimalist’s answer to data serialization.

Developed by Google, it’s a method to encode structured data in a compact binary format. Imagine packing your suitcase efficiently for a trip—Protobuf does that for your data.

It’s like JSON or XML but on a serious diet.

A Brief Stroll Down Memory Lane

Back in the early 2000s, Google faced a challenge: they needed a fast and efficient way to serialize data for communication between their ever-growing number of services.

XML was too bulky, and JSON, while better, still wasn’t cutting it. So, they rolled up their sleeves and created Protobuf, which they open-sourced in 2008.

Since then, it’s become a staple for developers needing efficient data interchange formats.

Why Should You Care?

Great question! Here’s why Protobuf might just become your new best friend:

  • Efficiency: Protobuf’s binary format means smaller message sizes and faster parsing compared to text-based formats like JSON or XML. This efficiency is particularly beneficial in performance-critical applications where bandwidth and speed are paramount.

  • Language-Agnostic: Define your data structures once using Protobuf’s interface description language, and then generate code in multiple languages. Whether you’re working in C#, Python, Go, or others, Protobuf has got you covered.

  • Forward and Backward Compatibility: Need to update your data structures? No problem. Protobuf allows you to add or remove fields without breaking existing deployments, making it easier to evolve your applications over time.

How Does Protobuf Stack Up Against the Competition?

Let’s take a look at how Protobuf compares to other serialization formats:

FeatureProtobufJSONXMLThriftAvro
Serialization FormatBinaryTextTextBinaryBinary
Schema DefinitionRequiredNoNoRequiredRequired
ReadabilityLowHighHighLowLow
PerformanceHighMediumLowHighHigh
Language SupportMultipleMultipleMultipleMultipleMultiple

Note: This table provides a general comparison. Specific use cases may influence the choice of serialization format.

Show Me the Code!

Below are examples in C#, Python, and Go for both client and server.

1. Define Your Protobuf Schema

First, create a person.proto file:

1
2
3
4
5
6
7
syntax = "proto3";

message Person {
    string name = 1;
    int32 id = 2;
    string email = 3;
}

Compile this .proto file using the Protobuf compiler (protoc) to generate code for your target languages.

2. C# Example

Server:

 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
using System;
using System.IO;
using Google.Protobuf;
using Grpc.Core;

public class PersonService : Person.PersonBase
{
    public override Task<PersonResponse> GetPerson(PersonRequest request, ServerCallContext context)
    {
        var person = new Person
        {
            Name = "Alice",
            Id = request.Id,
            Email = "alice@example.com"
        };
        return Task.FromResult(new PersonResponse { Person = person });
    }
}

public class Program
{
    const int Port = 50051;
    public static void Main(string[] args)
    {
        var server = new Server
        {
            Services = { Person.BindService(new PersonService()) },
            Ports = { new ServerPort("localhost", Port, ServerCredentials.Insecure) }
        };
        server.Start();
        Console.WriteLine("Server listening on port " + Port);
        Console.ReadKey();
        server.ShutdownAsync().Wait();
    }
}

Client:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
using System;
using Grpc.Net.Client;

public class Program
{
    public static void Main(string[] args)
    {
        var channel = GrpcChannel.ForAddress("http://localhost:50051");
        var client = new Person.PersonClient(channel);
        var response = client.GetPerson(new PersonRequest { Id = 1 });
        Console.WriteLine($"Name: {response.Person.Name}, Email: {response.Person.Email}");
    }
}

3. Python Example

Server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from concurrent import futures
import grpc
import person_pb2
import person_pb2_grpc

class PersonService(person_pb2_grpc.PersonServicer):
    def GetPerson(self, request, context):
        person = person_pb2.Person(
            name="Alice",
            id=request.id,
            email="alice@example.com"
        )
        return person_pb2.PersonResponse(person=person)

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    person_pb2_grpc.add_PersonServicer_to_server(PersonService(), server)
    server.add_insecure_port('[::]:50051')
    server.start()
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

Client:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import grpc
import person_pb2
import person_pb2_grpc

def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = person_pb2_grpc.PersonStub(channel)
    response = stub.GetPerson(person_pb2.PersonRequest(id=1))
    print(f"Name: {response.person.name}, Email: {response.person.email}")

if __name__ == '__main__':
    run()

4. Go Example

Server:

 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
package main

import (
    "context"
    "log"
    "net"

    pb "path/to/your/proto"
    "google.golang.org/grpc"
)

type server struct {
    pb.UnimplementedPersonServer
}

func (s *server) GetPerson(ctx context.Context, in *pb.PersonRequest) (*pb.PersonResponse, error) {
    person := &pb.Person{
        Name:  "Alice",
        Id:    in.Id,
        Email: "alice@example.com",
    }
    return &pb.PersonResponse{Person: person}, nil
}

func main() {
    lis, err = net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v",