Introducing Protobuf and gRPC: Building a Java Server and Python Client

Posted on in programming

cover image for article

In our previous articles, we explored Thrift and demonstrated how to build cross-language systems with Java, Python, and even integrated Rust. Now, let’s turn our attention to Protobuf and its companion RPC framework, gRPC.

Protobuf (Protocol Buffers) focuses primarily on defining your data structures and ensuring efficient, compact serialization. For RPC capabilities, Protobuf is often paired with gRPC, which provides a high-performance, language-neutral, and platform-neutral way to define services and perform remote calls—much like Thrift, but with a different approach and ecosystem.

In this article, we’ll define a similar service using Protobuf and implement a Java-based gRPC server along with a Python client. This mirrors what we did with Thrift, setting the stage for a fair comparison between the two frameworks.

What We’re Building

Just like we did with Thrift, we’ll create a Calculator service that can perform basic arithmetic operations. Our .proto file will define the Calculator service and its methods (Add, Subtract). Then we’ll:

  1. Define the service using Protobuf’s .proto syntax.
  2. Generate code for Java and Python.
  3. Implement a gRPC server in Java.
  4. Write a Python client that makes RPC calls to the server.

Prerequisites

Before proceeding, ensure you have:

  • Protobuf Compiler (protoc): Protobuf Downloads
  • gRPC Plugins: For generating gRPC code in Java and Python.
  • Java JDK and a Build Tool (Maven/Gradle): For the Java server.
  • Python 3 and pip: For the Python client.
  • Python gRPC Packages: pip install grpcio grpcio-tools

Step 1: Define the Protobuf File

Create a file named calculator.proto:

syntax = "proto3";

package calculator;

service Calculator {
  rpc Add (ArithmeticRequest) returns (ArithmeticResponse);
  rpc Subtract (ArithmeticRequest) returns (ArithmeticResponse);
}

message ArithmeticRequest {
  int32 num1 = 1;
  int32 num2 = 2;
}

message ArithmeticResponse {
  int32 result = 1;
}

What’s happening here?

  • syntax = "proto3": We’re using the modern Protobuf syntax.
  • package calculator;: Defines a package namespace.
  • service Calculator: Defines an RPC service with two methods, Add and Subtract.
  • Messages:
    • ArithmeticRequest includes two integers, num1 and num2.
    • ArithmeticResponse holds a single integer result.

Step 2: Generate Code

You’ll need protoc and the gRPC plugins installed. Adjust paths as needed.

Generate Java Code:

protoc --java_out=gen-java --grpc-java_out=gen-java \
  --plugin=protoc-gen-grpc-java=$(which protoc-gen-grpc-java) \
  -I. calculator.proto

This produces Java files in gen-java for both Protobuf messages and gRPC stubs.

Generate Python Code:

python -m grpc_tools.protoc --python_out=gen-py --grpc_python_out=gen-py -I. calculator.proto

This creates gen-py/calculator_pb2.py and gen-py/calculator_pb2_grpc.py.

Step 3: Implement the Java gRPC Server

Create a Java project (Maven or Gradle) and include dependencies for gRPC and Protobuf in pom.xml or build.gradle.

Example pom.xml snippet:

<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-netty</artifactId>
    <version>1.54.0</version>
</dependency>
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-protobuf</artifactId>
    <version>1.54.0</version>
</dependency>
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-stub</artifactId>
    <version>1.54.0</version>
</dependency>
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.22.0</version>
</dependency>

(Adjust versions as needed.)

CalculatorServiceImpl.java:

package com.example.server;

import calculator.CalculatorGrpc;
import calculator.CalculatorOuterClass.ArithmeticRequest;
import calculator.CalculatorOuterClass.ArithmeticResponse;
import io.grpc.stub.StreamObserver;

public class CalculatorServiceImpl extends CalculatorGrpc.CalculatorImplBase {
    @Override
    public void add(ArithmeticRequest request, StreamObserver<ArithmeticResponse> responseObserver) {
        int result = request.getNum1() + request.getNum2();
        ArithmeticResponse response = ArithmeticResponse.newBuilder().setResult(result).build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }

    @Override
    public void subtract(ArithmeticRequest request, StreamObserver<ArithmeticResponse> responseObserver) {
        int result = request.getNum1() - request.getNum2();
        ArithmeticResponse response = ArithmeticResponse.newBuilder().setResult(result).build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

Server.java:

package com.example.server;

import io.grpc.Server;
import io.grpc.ServerBuilder;

public class ServerApp {
    public static void main(String[] args) throws Exception {
        Server server = ServerBuilder.forPort(50051)
                .addService(new CalculatorServiceImpl())
                .build()
                .start();

        System.out.println("gRPC server started, listening on 50051");
        server.awaitTermination();
    }
}

What’s happening here?

  • We implement CalculatorImplBase (generated by gRPC) and override add and subtract methods.
  • We create a gRPC server on port 50051 and add our service implementation.
  • Running this server will listen for RPC calls from clients.

Step 4: Implement the Python Client

Install grpcio and grpcio-tools if you haven’t:

pip install grpcio grpcio-tools

client.py:

import sys
sys.path.append('gen-py')

import grpc
import calculator_pb2
import calculator_pb2_grpc

def main():
    # Connect to the server
    channel = grpc.insecure_channel('localhost:50051')
    stub = calculator_pb2_grpc.CalculatorStub(channel)

    # Call add method
    response = stub.Add(calculator_pb2.ArithmeticRequest(num1=10, num2=5))
    print("add(10, 5) = ", response.result)

    # Call subtract method
    response = stub.Subtract(calculator_pb2.ArithmeticRequest(num1=10, num2=5))
    print("subtract(10, 5) = ", response.result)

if __name__ == "__main__":
    main()

What’s happening here?

  • We create a gRPC channel to localhost:50051.
  • We use the generated CalculatorStub to call Add and Subtract.
  • The result is printed, just like in our Thrift scenario.

Step 5: Run and Test

  1. Run the Java Server:

    java -cp target/my-server.jar com.example.server.ServerApp
    

    You should see:

    gRPC server started, listening on 50051
    
  2. Run the Python Client:

    python client.py
    

    Output:

    add(10, 5) = 15
    subtract(10, 5) = 5
    

The Java server’s console might show logs of received requests, depending on your logging configuration.

Comparing to Thrift

We now have a Protobuf/gRPC-based system that functions similarly to our Thrift example. Some key differences:

  • Service Definition: Protobuf focuses on data structures, while gRPC extends .proto to define RPC methods. Thrift integrates RPC services directly in its IDL.
  • Tooling and Ecosystem: gRPC provides streaming and advanced RPC features out of the box, fitting well into cloud-native and microservices ecosystems.
  • Language Support: Both Thrift and Protobuf support numerous languages, but their communities and integration points may differ.

Next Steps

We’ve replicated the basic client/server setup with Protobuf and gRPC in Java and Python. In the next article, we’ll bring Rust into the Protobuf ecosystem, similar to how we integrated Rust with Thrift, giving us a direct comparison of how each framework handles a third language.


Stay tuned for the next article, where we’ll integrate Rust into our Protobuf/ gRPC setup and continue our journey through multi-language, multi-framework RPC solutions.

Part 4 of the Cross-Language Client/Server Applications with Thrift and Protobuf series

Slaptijack's Koding Kraken