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:
- Define the service using Protobuf’s
.proto
syntax. - Generate code for Java and Python.
- Implement a gRPC server in Java.
- 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
andSubtract
. - Messages:
ArithmeticRequest
includes two integers,num1
andnum2
.ArithmeticResponse
holds a single integerresult
.
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 overrideadd
andsubtract
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 callAdd
andSubtract
. - The result is printed, just like in our Thrift scenario.
Step 5: Run and Test
-
Run the Java Server:
java -cp target/my-server.jar com.example.server.ServerApp
You should see:
gRPC server started, listening on 50051
-
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.