用Golang创建一个服务器端的gRPC流(附实例)

276 阅读3分钟

在这个例子中,我们将在Golang gRPC应用程序中创建一个服务器端的流媒体例子。这就像一对多的关系。客户端发送一个请求,服务器以零或多的方式响应。客户端进行流式传输,直到没有剩余的内容。一旦全部读完,它就退出了。欲了解更多信息,请阅读文档

我们的例子是一个简单的例子。客户端发送一个包含账户ID的请求,以便从服务器上逐一获取与之相关的所有交易。

Proto库

pkg/protobuf/bank/transaction.proto

syntax = "proto3";

package bank;

option go_package = "github.com/you/proto-lib/pkg/protobuf/bank";

import "google/protobuf/timestamp.proto";

service TransactionService {
    rpc Fetch(FetchRequest) returns (stream FetchResponse) {};
}

message FetchRequest {
    string accountId = 1;
}

message FetchResponse {
    // This could be repeated as well
    Transaction transaction = 1;
}

message Transaction {
    enum Operation {
        Credit = 0;
        Debit = 1;
    }

    google.protobuf.Timestamp time = 1;
    Operation operation = 2;
    double amount = 3;
}

制作文件

.PHONY: compile
compile:
	protoc -I pkg/protobuf/bank/ --go_out=plugins=grpc:../../.. pkg/protobuf/bank/*.proto

客户端

cmd/client/main.go

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/you/client/internal/transaction"
	"google.golang.org/grpc"
)

func main() {
	fmt.Println("client")

	conn, err := grpc.Dial(":50051", grpc.WithInsecure(), grpc.WithBlock())
	if err != nil {
		log.Fatalln(err)
	}
	defer conn.Close()

	trxClient := transaction.NewClient(conn)
	if err := trxClient.Fetch(context.Background(), "ACC-1"); err != nil {
		log.Fatalln(err)
	}
}

internal/transaction/client.go

package transaction

import (
	"context"
	"io"
	"log"

	"github.com/you/proto-lib/pkg/protobuf/bank"
	"google.golang.org/grpc"
)

type Client struct {
	client bank.TransactionServiceClient
}

func NewClient(conn grpc.ClientConnInterface) Client {
	return Client{
		client: bank.NewTransactionServiceClient(conn),
	}
}

func (c Client) Fetch(ctx context.Context, accountID string) error {
	stream, err := c.client.Fetch(ctx, &bank.FetchRequest{AccountId: accountID})
	if err != nil {
		return err
	}

	for {
		res, err := stream.Recv()
		if err != nil {
			if err == io.EOF {
				log.Println("> ALL DONE!")

				return nil
			}

			return err
		}

		trx := res.GetTransaction()
		log.Println("TIME:", trx.GetTime().String())
		log.Println("OPERATION:", trx.GetOperation().String())
		log.Println("AMOUNT:", trx.GetAmount())
		log.Println("------")
	}
}

服务器

cmd/server/main.go

package main

import (
	"fmt"
	"log"
	"net"

	"github.com/you/server/internal/transaction"
	"github.com/you/proto-lib/pkg/protobuf/bank"
	"google.golang.org/grpc"
)

func main() {
	fmt.Println("server")

	listener, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalln(err)
	}

	grpcServer := grpc.NewServer()
	trxServer := transaction.NewServer()

	bank.RegisterTransactionServiceServer(grpcServer, trxServer)

	log.Fatalln(grpcServer.Serve(listener))
}

internal/transaction/transaction.go

package transaction

import (
	"time"

	"github.com/you/proto-lib/pkg/protobuf/bank"
)

type transaction struct {
	time      time.Time
	operation bank.Transaction_Operation
	amount    float64
}

var transactions = map[string][]transaction{
	"ACC-1": {
		{
			time:      time.Now().Add(time.Second),
			operation: bank.Transaction_Credit,
			amount:    10.00,
		},
		{
			time:      time.Now().Add(time.Millisecond),
			operation: bank.Transaction_Credit,
			amount:    20.99,
		},
		{
			time:      time.Now().Add(time.Hour),
			operation: bank.Transaction_Debit,
			amount:    30.62,
		},
		{
			time:      time.Now().Add(time.Minute),
			operation: bank.Transaction_Credit,
			amount:    40,
		},
		{
			time:      time.Now().Add(time.Minute),
			operation: bank.Transaction_Debit,
			amount:    50.55,
		},
		{
			time:      time.Now().Add(time.Minute),
			operation: bank.Transaction_Debit,
			amount:    60.60,
		},
	},
}

internal/transaction/server.go

package transaction

import (
	"log"
	"math/rand"
	"time"

	"github.com/you/proto-lib/pkg/protobuf/bank"
	"github.com/golang/protobuf/ptypes"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

var sleeper = []int{0, 1, 2, 3, 4}

type Server struct {
	transactions map[string][]transaction
}

func NewServer() Server {
	return Server{
		transactions: transactions,
	}
}

func (s Server) Fetch(request *bank.FetchRequest, stream bank.TransactionService_FetchServer) error {
	rand.Seed(time.Now().UnixNano())

	log.Println("Fetching transactions for account:", request.GetAccountId())

	trxs := s.transactions[request.GetAccountId()]

	for _, trx := range trxs {
		ts, err := ptypes.TimestampProto(trx.time)
		if err != nil {
			return status.Errorf(codes.Internal, "fetch: invalid time: %v", err)
		}

		if err := stream.Send(&bank.FetchResponse{
			Transaction: &bank.Transaction{
				Time:      ts,
				Operation: trx.operation,
				Amount:    trx.amount,
			},
		}); err != nil {
			return status.Errorf(codes.Internal, "fetch: unexpected stream: %v", err)
		}

		sleep := rand.Intn(len(sleeper))
		time.Sleep(time.Duration(sleep) * time.Second)
	}

	log.Println("Completed")

	return nil
}

测试

# Run server
$ go run --race cmd/server/main.go

# Run client
$ go run --race cmd/client/main.go

测试结果

# Server
2020/10/28 17:00:58 Fetching transactions for account: ACC-1
2020/10/28 17:01:14 Completed
# Client
2020/10/28 17:00:58 TIME: seconds:1603904454 nanos:702436000
2020/10/28 17:00:58 OPERATION: Credit
2020/10/28 17:00:58 AMOUNT: 10
2020/10/28 17:00:58 ------
2020/10/28 17:00:59 TIME: seconds:1603904453 nanos:703439000
2020/10/28 17:00:59 OPERATION: Credit
2020/10/28 17:00:59 AMOUNT: 20.99
2020/10/28 17:00:59 ------
2020/10/28 17:01:01 TIME: seconds:1603908053 nanos:702440000
2020/10/28 17:01:01 OPERATION: Debit
2020/10/28 17:01:01 AMOUNT: 30.62
2020/10/28 17:01:01 ------
2020/10/28 17:01:03 TIME: seconds:1603904513 nanos:702440000
2020/10/28 17:01:03 OPERATION: Credit
2020/10/28 17:01:03 AMOUNT: 40
2020/10/28 17:01:03 ------
2020/10/28 17:01:06 TIME: seconds:1603904513 nanos:702440000
2020/10/28 17:01:06 OPERATION: Debit
2020/10/28 17:01:06 AMOUNT: 50.55
2020/10/28 17:01:06 ------
2020/10/28 17:01:10 TIME: seconds:1603904513 nanos:702440000
2020/10/28 17:01:10 OPERATION: Debit
2020/10/28 17:01:10 AMOUNT: 60.6
2020/10/28 17:01:10 ------
2020/10/28 17:01:14 > ALL DONE!