CSE224 - RPC

Introduction

Remote Procedure Call (RPC) is a way for a program to call a function defined on a different computer as if it were calling a local function.

Issues:

  1. A remote machine might use a different language, so it represent data types using different sizes.
  2. Use a different byte ordering (endianness).
  3. Have different data alignment requirements.
  4. Represent floating point numbers differently.

Interface Description Language (IDL)

Mechanism to pass procedure parameters and return values in a machine and language independent way.

Protobuf

Protocol Buffers (protobuf) is a language-neutral format for defining structured data by gPRC.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
syntax = "proto3";

message NumberEntry {
int64 ts = 1;
int32 number = 2;
}

service NumberService {
rpc SendNumber(NumberEntry) returns (Ack);
}

message Ack {
string status = 1;
}

.proto file defines

  1. Service APIs (rpc methods)
  2. Message formats

Then run protoc --go_out=. --go-grpc_out=. your_file.proto will generate

  1. Client stub code (for calling SendNumber)
  2. Server stub code (for implementing SendNumber)
  3. Marshalling code (for turning NumberEntry into bytes and back)

Workflow

  1. Client calls stub function
  2. Stub marshals parameters to a network message
  3. OS sends a network message to the server
  4. Server OS receives message, sends it up to stub
  5. Server stub unmarshals params, calls server function
  6. Server function runs, returns a value
  7. Server stub marshals the return value, sends msg
  8. Server OS sends the reply back across the network
  9. Client OS receives the reply and passes up to stub
  10. Client stub unmarshals return value, returns to client

Example

Building a Replicated KV Store:

  1. To run the system, run the code in each node machine.
  2. Each node is both a server and a client, server is run by a go routine, client is also run by a go routine.
  3. The server code listens to rpc calls from other nodes
  4. The client code interprets commands, and handle the command by sending rpc

Define the proto file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
syntax = "proto3";
package rkvs;

// tells the protobuf compiler where to put the generated Go code
option go_package = "/proto;pb"; // here, the package will be called pb in the /proto directory

// service defines a set of remote procedures (functions) that can be called over the network.
service RKVSService {
rpc Replicate (ReplicateRequest) returns (ReplicateResponse);
rpc Stop (StopRequest) returns (StopResponse);
}

// A message is what you send or what you receive in a network call.
message ReplicateRequest {
string key = 1;
string value = 2;
}
message ReplicateResponse{}

message StopRequest{}
message StopResponse{}

Then, protoc --go_out=. --go-grpc_out=. rkvs.proto will generate a pb package with:

  1. RKVSServiceServer interface for the server to implement
1
2
3
4
5
type RKVSServiceServer interface {
Replicate(context.Context, *ReplicateRequest) (*ReplicateResponse, error)
Stop(context.Context, *StopRequest) (*StopResponse, error)
mustEmbedUnimplementedRKVSServiceServer()
}
  1. RKVSServiceClient for calling remote methods
    1
    2
    3
    4
    type RKVSServiceClient interface {
    Replicate(ctx context.Context, in *ReplicateRequest, opts ...grpc.CallOption) (*ReplicateResponse, error)
    Stop(ctx context.Context, in *StopRequest, opts ...grpc.CallOption) (*StopResponse, error)
    }

Implement the gRPC Server in Go:

After defining a service, we need to implement rpc functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// struct server implements the RPC methods from your .proto file: Replicate and Stop
type server struct {
pb.UnimplementedRKVSServiceServer // This is part of the Go code auto-generated from your .proto file.
state map[string]string
peers []string
stopCh chan struct{}
}

// Implementing the Replicate RPC
func (s *server) Replicate(ctx context.Context, req *pb.ReplicateRequest) (*pb.ReplicateResponse, error) {
s.state[req.Key] = req.Value
return &pb.ReplicateResponse{}, nil
}

// Implementing the Stop RPC
func (s *server) Stop(ctx context.Context, req *pb.StopRequest) (*pb.StopResponse, error) {
s.stopCh <- struct{}{}
return &pb.StopResponse{}, nil
}

pb.UnimplementedRKVSServiceServer: It contains default (empty) implementations of all the methods declared in your .proto file. Since your server struct must implement all methods defined in the .proto file’s service section to satisfy the RKVSServiceServer interface. But often, you only want to implement some of them. So when you include pb.UnimplementedRKVSServiceServer, your struct now inherits those defaults, and you only need to override the ones you care about.

Start the gRPC Server

1
2
3
4
5
6
7
8
9
grpcServer := grpc.NewServer()
pb.RegisterRKVSServiceServer(grpcServer, s) // s is an object that implements all the methods of RKVSServiceServer

lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
log.Printf("Listening on %s", addr)
grpcServer.Serve(lis) // Starts the main server loop

Make gRPC Requests as a Client

1
2
3
4
5
conn, err := grpc.NewClient(peerAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) // create a connection to server
defer conn.Close()
client := pb.NewRKVSServiceClient(conn) // create a client stub

_, err = client.Replicate(context.Background(), &pb.ReplicateRequest{Key: "foo", Value: "bar"})

CSE224 - RPC
https://thiefcat.github.io/2025/05/08/CSE224/rpc/
Author
小贼猫
Posted on
May 8, 2025
Licensed under