在这个例子中,我们将创建一个gRPC客户端和服务器应用程序。客户端将发送一个复杂的多维JSON请求消息,并从服务器接收一个简单的响应消息。一些字段将有它们的proto消息,一些将是完全未知/随机的字段。
我们在这里使用两个重要的包。protobufprotobuf包用于处理复杂的字段。jsonpb包用于处理JSON marshalling和unmarshalling。滚动到底部查看链接。
请求
这就是我们要发送的东西。它将从一个文件中读取,以保持博客的简短。它以client/request.json 的形式存储在应用程序中。
{
"name": "The Premier League",
"founded_at": "1992-02-200T22:40:36Z",
"is_active": true,
"budget": 1234567890.99,
"mascot": "Lion",
"stadiums": {
"City Ground": "Nottingham Forest",
"Manchester United": "Old Trafford",
"National": "Wembley Stadium"
},
"sponsors": [
"EA Sports",
"Coca Cola",
"Nike"
],
"awards": [
{
"type": "Trophy",
"name": "The champion"
},
{
"type": "Medal",
"name": "The runner up"
}
],
"address": {
"line1": "Brunel Building",
"line2": "57 North Wharf Road",
"line3": "",
"postcode": "W2 1HQ",
"county": "London"
},
"other": {
"concacaf": false,
"confederation": "UEFA",
"founder": null,
"random_array": [
"one",
2,
true,
false,
null
],
"random_json": {
"key_1": "value",
"key_2": 2,
"key_3": true,
"key_4": false,
"key_5": null
},
"uefa": true,
"world_ranking": 4
},
"plain": "{\"key_1\":\"value\",\"key_2\":2,\"key_3\":true,\"key_4\":false,\"key_5\":null}"
}
结构
├── Makefile
├── client
│ ├── main.go
│ └── request.json
├── football
│ ├── client.go
│ └── server.go
├── go.mod
├── pkg
│ └── protobuf
│ └── football
│ ├── league
│ │ ├── address.pb.go
│ │ ├── address.proto
│ │ ├── award.pb.go
│ │ ├── award.proto
│ │ ├── league.pb.go
│ │ └── league.proto
│ ├── response.pb.go
│ ├── response.proto
│ ├── service.pb.go
│ └── service.proto
└── server
└── main.go
文件
制作文件
运行make compile ,生成*.pb.go 文件。
.PHONY: compile
compile:
protoc --go_out=plugins=grpc:. --go_opt=paths=source_relative pkg/protobuf/football/league/*.proto
protoc --go_out=plugins=grpc:. --go_opt=paths=source_relative pkg/protobuf/football/*.proto
.PHONY: client
client:
go run --race client/main.go
.PHONY: server
server:
go run --race server/main.go
go.mod
module github.com/inanzzz/sport
go 1.15
require (
github.com/golang/protobuf v1.4.2
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f // indirect
golang.org/x/text v0.3.3 // indirect
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d // indirect
google.golang.org/grpc v1.31.1
google.golang.org/protobuf v1.25.0
)
pkg/protobuf/football/league/address.proto
syntax = "proto3";
package football;
option go_package = "github.com/inanzzz/sport/pkg/protobuf/football/league";
message Address {
string line1 = 1;
string line2 = 2;
string line3 = 3;
string postcode = 4;
string county = 5;
}
pkg/protobuf/football/league/award.proto
syntax = "proto3";
package football;
option go_package = "github.com/inanzzz/sport/pkg/protobuf/football/league";
message Award {
enum Type {
None = 0;
Trophy = 1;
Medal = 2;
}
Type type = 1;
string name = 2;
}
pkg/protobuf/football/league/league.proto
syntax = "proto3";
package football;
option go_package = "github.com/inanzzz/sport/pkg/protobuf/football/league";
import "pkg/protobuf/football/league/address.proto";
import "pkg/protobuf/football/league/award.proto";
import "google/protobuf/struct.proto";
// You could import "well-known" google/protobuf/timestamp.proto package and
// replace string with google.protobuf.Timestamp type below for founded_at.
message CreateLeagueRequest {
string name = 1;
string founded_at = 2;
bool is_active = 3;
double budget = 4;
string mascot = 5;
map <string, string> stadiums = 6;
repeated string sponsors = 7;
repeated Award awards = 8;
Address address = 9;
google.protobuf.Struct other = 10; // an unknown set of json key/value pairs
google.protobuf.Value plain = 11; // an unknown json.RawMessage string
}
//message UpdateLeagueRequest {}
//message FindLeagueRequest {}
//message DeleteLeagueRequest {}
pkg/protobuf/football/response.proto
syntax = "proto3";
package football;
option go_package = "github.com/inanzzz/sport/pkg/protobuf/football";
// You could import "well-known" google/protobuf/any.proto package and
// replace bytes with google.protobuf.Any type below.
message Response {
enum Result {
SUCCESS = 0;
ERROR = 1;
}
message Success {
bytes data = 1;
}
message Error {
string message = 1;
bytes errors = 2;
}
Result result = 1;
Success success = 2;
Error error = 3;
}
pkg/protobuf/football/service.proto
syntax = "proto3";
package football;
option go_package = "github.com/inanzzz/sport/pkg/protobuf/football";
import "pkg/protobuf/football/response.proto";
import "pkg/protobuf/football/league/league.proto";
service FootballService {
rpc CreateLeague(CreateLeagueRequest) returns (Response) {}
// rpc UpdateLeague(UpdateLeagueRequest) returns (Response) {}
// rpc FindLeague(FindLeagueRequest) returns (Response) {}
// rpc DeleteLeague(DeleteLeagueRequest) returns (Response) {}
}
client/main.go
package main
import (
"bytes"
"context"
"io/ioutil"
"log"
"time"
"github.com/inanzzz/sport/football"
"google.golang.org/grpc"
)
func main() {
log.Println("client")
conn, err := grpc.Dial(":50051", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalln(err)
}
defer conn.Close()
json, err := ioutil.ReadFile("client/request.json")
if err != nil {
log.Fatalln(err)
}
footballClient := football.NewClient(conn, time.Second)
err = footballClient.CreateLeague(context.Background(), bytes.NewBuffer(json))
log.Println("ERR:", err)
}
football/client.go
package football
import (
"context"
"fmt"
"io"
"log"
"time"
"github.com/inanzzz/sport/pkg/protobuf/football"
"github.com/inanzzz/sport/pkg/protobuf/football/league"
"github.com/golang/protobuf/jsonpb"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)
type Client struct {
footballClient football.FootballServiceClient
timeout time.Duration
}
func NewClient(conn grpc.ClientConnInterface, timeout time.Duration) Client {
return Client{
footballClient: football.NewFootballServiceClient(conn),
timeout: timeout,
}
}
func (c Client) CreateLeague(ctx context.Context, json io.Reader) error {
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(c.timeout))
defer cancel()
req := league.CreateLeagueRequest{}
if err := jsonpb.Unmarshal(json, &req); err != nil {
return fmt.Errorf("client create league: unmarshal: %w", err)
}
res, err := c.footballClient.CreateLeague(ctx, &req)
if err != nil {
if er, ok := status.FromError(err); ok {
return fmt.Errorf("client create league: code: %s - msg: %s", er.Code(), er.Message())
}
return fmt.Errorf("client create league: %w", err)
}
log.Println("RESULT:", res.Result)
log.Println("RESPONSE:", res)
return nil
}
server/main.go
package main
import (
"log"
"net"
"github.com/inanzzz/sport/football"
"google.golang.org/grpc"
protofootball "github.com/inanzzz/sport/pkg/protobuf/football"
)
func main() {
log.Println("server")
listener, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalln(err)
}
grpcServer := grpc.NewServer()
footballServer := football.NewServer()
protofootball.RegisterFootballServiceServer(grpcServer, footballServer)
log.Fatalln(grpcServer.Serve(listener))
}
football/server.go
package football
import (
"bytes"
"context"
"log"
"github.com/inanzzz/sport/pkg/protobuf/football"
"github.com/inanzzz/sport/pkg/protobuf/football/league"
"github.com/golang/protobuf/jsonpb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type Server struct {
football.UnimplementedFootballServiceServer
}
func NewServer() Server {
return Server{}
}
func (s Server) CreateLeague(ctx context.Context, req *league.CreateLeagueRequest) (*football.Response, error) {
json := bytes.Buffer{}
// OrigName uses the actual field names from the proto files rather than casting them to camelCase.
// EmitDefaults prevents discarding empty/nullable fields and keeps zero values.
mars := jsonpb.Marshaler{OrigName: true, EmitDefaults: true}
if err := mars.Marshal(&json, req); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "server create league: marshal: %v", err)
}
log.Println("REQUEST:", json.String())
return &football.Response{
Result: football.Response_SUCCESS,
Success: &football.Response_Success{
Data: []byte("good job"),
},
}, nil
}
测试
服务器
make server
客户端
make client
结果
当你发送开头所示的JSON文件时,服务器将输出完全相同的JSON内容。如果你发送{} 或所有的null 值,结果应该如下图所示。
# Request 1
{}
# Request 2
{
"name": null,
"founded_at": null,
"is_active": null,
"budget": null,
"mascot": null,
"stadiums": null,
"sponsors": null,
"awards": null,
"address": null,
"other": null,
"plain": null
}
你可以看到,所有的字段都被设置为零值:
# Result
{
"name": "",
"founded_at": "",
"is_active": false,
"budget": 0,
"mascot": "",
"stadiums": {
},
"sponsors": [
],
"awards": [
],
"address": null,
"other": null,
"plain": null
}