在gRPC客户端和服务器Golang应用程序中处理复杂的JSON请求

313 阅读3分钟

在这个例子中,我们将创建一个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
}