golang-gRPC chapter-01 (a)

abc101
6 min readJun 22, 2021

a. A simple RPC and a server-to-client stream RPC

Preparing

chapter-00: https://abc101.medium.com/golang-grpc-mac-2fe01939a29d

Create `chapter-01/product` such as,

├── LICENSE
├── README.md
├── chapter-00

├── chapter-01
│ └── product
│ ├── README.md
│ ├── client
│ │ └── main.go
│ ├── pb
│ │ └── product.proto
│ ├── sample_data
│ │ └── product_db.json
│ └── server
│ └── main.go
├── go.mod
└── go.sum

Create `chapter-01/product/pb/product.proto

syntax = "proto3";

option go_package = "lets-learn-golang-grpc/chapter-01/product/pb";

package pb;

// Interface exported by server
service ProductInfo {
// A simple RPC
rpc GetProduct(ProductId) returns (Product) {}

// A server-to-client streaming RPC
rpc ListProductsByYearRange(YearRange) returns (stream Product) {}

}

message ProductId {
int32 value = 1;
}

message Product {
ProductId id = 1; // for using proto.Equal(x, y)
string name = 2;
string description = 3;
int32 year = 4;
float price = 5;
}

message YearRange {
int32 start = 1;
int32 end = 2;
}

In a protocol buffer, we must have two messages, one is for a request and the other is for a response. We will request a product withProductId from a client and will send aProduct information from the server. TheGetProduct method will send one message. It’s a simple RPC, one response for one request.

rpc GetProduct(ProductId) returns (Product) {}

If a request message calls multiple results, RPC will send results each by each. It’s server-to-client stream RPC. For instance;

rpc ListProductByYearRange(YearRange) returns (stream Product) {}

We will compare the request message type to data id using proto.Equal(x, y) to find a target. This is an example data. We will use JSON data type.

{
"id": {
"value": 1
},
"name": "Harry Potter 1st",
"description": "Philosopher's Stone",
"year": 1997,
"price": 13.39
},

Create `chapter-01/product/server/main.go

In the GetProduct function, we will find a product with ProductId type which a client sends

func (s *productInfoServer) GetProduct(ctx context.Context, productId *pb.ProductId) (*pb.Product, error) {
for _, product := range s.savedProducts {
if proto.Equal(product.Id, productId) {
return product, nil
}
}
// No product was found, return and unnamed product
return &pb.Product{Id: productId}, nil
}

and we will send products using server-to-client stream RPC.

func (s *productInfoServer) ListProductsByYearRange(yearRange *pb.YearRange, stream pb.ProductInfo_ListProductsByYearRangeServer) error {
for _, product := range s.savedProducts {
if inRange(product.Year, yearRange) {
if err := stream.Send(product); err != nil {
return err
}
}
}
return nil
}

Now, we need a type for a server and a function for a server.

type productInfoServer struct {
pb.UnimplementedProductInfoServer
savedProducts []*pb.Product
}
func newServer() *productInfoServer {
s := &productInfoServer{}
s.loadProducts(jsonDBFile)
return s
}

This is the server code.

package main

import (
"context"
"encoding/json"
"io/ioutil"
"lets-learn-golang-grpc/chapter-01/product/pb"
"log"
"net"

"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
)

const (
port = ":50051"
jsonDBFile = "sample_data/product_db.json"
)

type productInfoServer struct {
pb.UnimplementedProductInfoServer
savedProducts []*pb.Product
}

// GetProduct returns the product infomation
func (s *productInfoServer) GetProduct(ctx context.Context, productId *pb.ProductId) (*pb.Product, error) {
for _, product := range s.savedProducts {
if proto.Equal(product.Id, productId) {
return product, nil
}
}
// No product was found, return and unnamed product
return &pb.Product{Id: productId}, nil
}

// ListProductsByYearRange lists all products published within the given year range (server-to-cient stream)
func (s *productInfoServer) ListProductsByYearRange(yearRange *pb.YearRange, stream pb.ProductInfo_ListProductsByYearRangeServer) error {
for _, product := range s.savedProducts {
if inRange(product.Year, yearRange) {
if err := stream.Send(product); err != nil {
return err
}
}
}
return nil
}

// inRange
func inRange(productYear int32, yearRange *pb.YearRange) bool {
if productYear >= yearRange.Start && productYear <= yearRange.End {
return true
}
return false
}

// loadProduccts loads products from a JSON file.
func (s *productInfoServer) loadProducts(filePath string) {
var data []byte
if filePath != "" {
var err error
data, err = ioutil.ReadFile(filePath)
if err != nil {
log.Fatalf("Failed to load default products: %v", err)
}
} else {
data = exampleData
}
if err := json.Unmarshal(data, &s.savedProducts); err != nil {
log.Fatalf("Failed to load default features: %v", err)
}
}

func newServer() *productInfoServer {
s := &productInfoServer{}
s.loadProducts(jsonDBFile)
return s
}

func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
var opts []grpc.ServerOption
grpcServer := grpc.NewServer(opts...)
pb.RegisterProductInfoServer(grpcServer, newServer())
grpcServer.Serve(lis)
}

var exampleData = []byte(`[
{
"id": {
"value": 1
},
"name": "Harry Potter 1st",
"description": "Philosopher's Stone",
"year": 1997,
"price": 13.39
},
{
"id": {
"value": 2
},
"name": "Harry Potter 2nd",
"description": "Chamber of Secrets",
"year": 1998,
"price": 14.59
},
{
"id": {
"value": 3
},
"name": "Harry Potter 3rd",
"description": "Prisoner of Azkaban",
"year": 1999,
"price": 12.79
},
{"id": {
"value": 4
},
"name": "Harry Potter 4th",
"description": "Goblet of Fire",
"year": 2000,
"price": 15.49
},
{"id": {
"value": 5
},
"name": "Harry Potter 5th",
"description": "Order of the Phoenix",
"year": 2003,
"price": 13.79
},
{"id": {
"value": 6
},
"name": "Harry Potter 6th",
"description": "Half-Blood Prince",
"year": 2005,
"price": 14.29
},
{"id": {
"value": 7
},
"name": "Harry Potter 7th",
"description": "Deathly Hallows",
"year": 2007,
"price": 15.99
}
]`)

Create `chapter-01/product/client/main.go`

In the client, we will send a ProdctId type to the server. If there is no response within 10 seconds, it will cancel.

func printProduct(client pb.ProductInfoClient, productId *pb.ProductId) {
log.Printf("Getting product for product id: %v", productId.Value)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

product, err := client.GetProduct(ctx, productId)
if err != nil {
log.Fatal(err)
}
log.Println(product)
}

To receive stream data, we will wait until we get the EOF(End Of File). That’s all.

func printProductsByYearRange(client pb.ProductInfoClient, yearRange *pb.YearRange) {
log.Printf("Looking for products %v", yearRange)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
stream, err := client.ListProductsByYearRange(ctx, yearRange)
if err != nil {
log.Fatalf("%v.ListProductByYearRange(_) = _, %v", client, err)
}
for {
product, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("%v.ListProductsByYearRange(_) = _, %v", client, err)
}
log.Printf("Product: name: %q, year: %v", product.GetName(), product.GetYear())
}
}

This is the client code.

package main

import (
"context"
"io"
"lets-learn-golang-grpc/chapter-01/product/pb"
"log"
"time"

"google.golang.org/grpc"
)

const (
serverAddr = "localhost:50051"
)

func printProduct(client pb.ProductInfoClient, productId *pb.ProductId) {
log.Printf("Getting product for product id: %v", productId.Value)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

product, err := client.GetProduct(ctx, productId)
if err != nil {
log.Fatal(err)
}
log.Println(product)
}

func printProductsByYearRange(client pb.ProductInfoClient, yearRange *pb.YearRange) {
log.Printf("Looking for products %v", yearRange)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
stream, err := client.ListProductsByYearRange(ctx, yearRange)
if err != nil {
log.Fatalf("%v.ListProductByYearRange(_) = _, %v", client, err)
}
for {
product, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("%v.ListProductsByYearRange(_) = _, %v", client, err)
}
log.Printf("Product: name: %q, year: %v", product.GetName(), product.GetYear())
}
}

func main() {
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
opts = append(opts, grpc.WithBlock())

conn, err := grpc.Dial(serverAddr, opts...)
if err != nil {
log.Fatalf("failed to dial: %v", err)
}
defer conn.Close()
client := pb.NewProductInfoClient(conn)

// Looking for a valid product (Unary)
printProduct(client, &pb.ProductId{Value: 1})

// Product missing
printProduct(client, &pb.ProductId{Value: 0})

// Looking for products between 1999 and 2004 (Stream)
printProductsByYearRange(client, &pb.YearRange{
Start: 1999,
End: 2004,
})
}

Let’s compile the `product.proto`

> cd chapter-01/product
> protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb/product.proto

Run the server

> go run ./chapter-01/product/server/main.go

Run the client

> go run ./chapter-01/product/client/main.go

In the client code, I put sample requests, id = 1, id = 0 and, yearRange from 1994 to 2004. It will return data such as

2021/06/22 11:23:21 Getting product for product id: 1
2021/06/22 11:23:21 id:{value:1} name:"Harry Potter 1st" description:"Philosopher's Stone" year:1997 price:13.39
2021/06/22 11:23:21 Getting product for product id: 0
2021/06/22 11:23:21 id:{}
2021/06/22 11:23:21 Looking for products start:1999 end:2004
2021/06/22 11:23:21 Product: name: "Harry Potter 3rd", year: 1999
2021/06/22 11:23:21 Product: name: "Harry Potter 4th", year: 2000
2021/06/22 11:23:21 Product: name: "Harry Potter 5th", year: 2003

--

--