From getting started with gRPC to giving up

Article directory

gRPC

What is gRPC

gRPCIt is a modern open source high-performance RPC framework that can run in any environment. Originally developed by Google. It uses HTTP/2 as the transport protocol.

In gRPC, clients can directly call methods of server-side applications on other machines just like calling local methods, helping you create distributed applications and services more easily. Like many RPC systems, gRPC is based on defining a service, specifying a method with parameters and return types that can be called remotely. Implement this interface in the server program and run the gRPC service to handle client calls. On the client side, there is a stub that provides the same methods as on the server side.

grpc

Why use gRPC

.protoUsing gRPC, we can define the service in one file at once and use any language that supports it to implement the client and server. In turn, they can be applied in various scenarios, from Google's server to your own tablet . Computer - gRPC helps you solve the complexity of communication between different languages ​​and environments. Other benefits include protocol buffersefficient serialization, simple IDL, and easy interface updates. In a word, using gRPC can make it easier for us to write cross-language distributed code.

IDL (Interface description language) refers to the interface description language, which is a computer language used to describe the interface of software components, and is the basis of cross-platform development. IDL describes interfaces in a neutral way so that objects running on different platforms and programs written in different languages ​​can communicate with each other; for example, one component is written in C++ and another component is written in Go.

Install gRPC

Install gRPC

Execute the following command in your project directory to obtain gRPC as a project dependency.

go get google.golang.org/grpc@latest

Install Protocol Buffers v3

Install the protocol compiler used to generate the gRPC service code, the easiest way is to download the precompiled binary file ( ) for your platform from the following link: https://github.com/protocolbuffers/protobuf/releases/ .protoc-<version>-<platform>.zip

For Windows 64-bit protoc-23.4-win64.zip

in:

  • The protoc in the bin directory is an executable file.
  • The include directory contains .protofiles defined by google, and we import "google/protobuf/timestamp.proto"import them from here.

Since the bin directory where the downloaded executable file protocis located needs to be added to the environment variables of our computer, I put it in GOPATH/bin.

install plugin

Because we use the Go language for development in this article, execute the following command to install protocthe Go plugin:

Install the go language plugin:

go install google.golang.org/protobuf/cmd/[email protected]

The plugin will .protogenerate a .pb.gofile with a suffix of , including all .prototypes defined in the file and their serialization methods.

Install the grpc plugin:

go install google.golang.org/grpc/cmd/[email protected]

The plugin generates a _grpc.pb.gofile with a suffix that contains:

  • An interface type (or stub) for clients to call service methods.
  • The interface type to be implemented by the server.

The above command will install the plug-in by default GOPATH/bin. In order for protocthe compiler to find these plug-ins, please make sure yours GOPATH/binis in the environment variable.

examine

Execute the following commands in sequence to check whether the development environment is ready.

  1. Confirm that the protoc installation is complete.

    $ protoc --version
    libprotoc 23.4
    
  2. Confirm that protoc-gen-go is installed.

    $ protoc-gen-go --version
    protoc-gen-go.exe v1.28.1
    

    If the prompt here protoc-gen-gois not an executable program, please make sure that the bin directory under your GOPATH is in the environment variable of your computer.

  3. Confirm that protoc-gen-go-grpc is installed.

    ❯ protoc-gen-go-grpc --version
    protoc-gen-go-grpc 1.2.0
    

    If the prompt here protoc-gen-go-grpcis not an executable program, please make sure that the bin directory under your GOPATH is in the environment variable of your computer.

How gRPC is developed

How many steps does it take to put an elephant in the refrigerator?

  1. Leave the refrigerator door open.
  2. Put the elephant in.
  3. Keep the refrigerator door closed.

gRPC development is also divided into three steps:

Write .protoa file definition service

Like many RPC systems, gRPC is based on the idea of ​​defining services, specifying methods that can be called remotely with parameters and return types. By default, gRPC uses protocol buffers as an interface definition language (IDL) to describe the service interface and the structure of payload messages. Other IDLs can be used instead as needed.

For example, the following defines a HelloServiceservice using protocol buffers.

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

In gRPC you can define four types of service methods.

  • Ordinary rpc, the client sends a request to the server, and then gets a response, just like a normal function call.
  rpc SayHello(HelloRequest) returns (HelloResponse);
  • Server streaming RPC, where a client sends a request to the server and gets a stream to read a sequence of messages. The client reads from the returned stream until there are no more messages. gRPC guarantees ordering of messages within a single RPC call.
  rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
  • Client-side streaming RPC, where the client writes a sequence of messages and sends them to the server, again using the provided stream. Once the client has finished writing the message, it waits for the server to read the message and return a response. Likewise, gRPC guarantees ordering of messages within a single RPC call.
  rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
  • Bidirectional streaming RPC, where both parties send a sequence of messages using a read-write stream. The two streams run independently, so clients and servers can read and write in whatever order they like: for example, the server can wait to receive all client messages before writing a response, or it can alternately read and then write messages, or other reads write combination. Messages in each stream are ordered.

Generate code for the specified language

After defining the service in .protothe file, gRPC provides a protocol buffers compiler plugin that generates client and server code.

We use these plug-ins to generate codes in Java, Go, C++, and other languages ​​as needed . PythonWe usually call these APIs on the client side and implement the corresponding APIs on the server side.

  • On the server side, the server implements the method declared by the service, and runs a gRPC server to handle the call request sent by the client. The bottom layer of gRPC decodes the incoming request, executes the called service method, and encodes the service response.
  • On the client side, the client has a local object called a stub that implements the same methods as the service. The client can then call these methods on the local object, wrapping the parameters of the call in the appropriate protocol buffers message type - which gRPC handles after sending the request to the server and returning the server's protocol buffers response.

Write business logic code

gRPC helps us solve service calls, data transmission, and message encoding and decoding in RPC, and the rest of our work is to write business logic codes.

Write business code on the server side to implement specific service methods, and call these methods on the client side as needed.

gRPC getting started example

write proto code

Protocol Buffersis a language-neutral, platform-independent extensible mechanism for serializing structured data. Use Protocol Buffersstructured data that can be defined once, then easily written and read in various languages ​​in various data streams using specially generated source code.

About Protocol Buffersthe tutorial, you can check the Protocol Buffers V3 Chinese Guide , and the following content of this article is familiar to readers by default Protocol Buffers.

syntax = "proto3"; // 版本声明,使用Protocol Buffers v3版本

option go_package = "xx";  // 指定生成的Go代码在你项目中的导入路径

package pb; // 包名


// 定义服务
service Greeter {
    // SayHello 方法
    rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// 请求消息
message HelloRequest {
    string name = 1;
}

// 响应消息
message HelloResponse {
    string reply = 1;
}

Write server-side Go code

We create a new hello_serverproject and execute it under the project root directory go mod init hello_server.

Create a new pbfolder and save the above proto file as hello.proto, go_packagewhich will be modified as follows.

// ...

option go_package = "hello_server/pb";

// ...

At this point, the directory structure of the project is:

hello_server
├── go.mod
├── go.sum
├── main.go
└── pb
    └── hello.proto

Execute the following command in the project root directory to hello.protogenerate the go source code file.

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb/hello.proto

The generated go source code files will be saved in the pb folder.

hello_server
├── go.mod
├── go.sum
├── main.go
└── pb
    ├── hello.pb.go
    ├── hello.proto
    └── hello_grpc.pb.go

Add the following to hello_server/main.go.

package main

import (
	"context"
	"fmt"
	"hello_server/pb"
	"net"

	"google.golang.org/grpc"
)

// hello server

type server struct {
    
    
	pb.UnimplementedGreeterServer
}

func (s *server) SayHello(_ context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
    
    
	return &pb.HelloResponse{
    
    Reply: "Hello " + in.Name}, nil
}

func main() {
    
    
	// 监听本地的8972端口
	lis, err := net.Listen("tcp", ":8972")
	if err != nil {
    
    
		fmt.Printf("failed to listen: %v", err)
		return
	}
	s := grpc.NewServer()                  // 创建gRPC服务器
	pb.RegisterGreeterServer(s, &server{
    
    }) // 在gRPC服务端注册服务
	// 启动服务
	err = s.Serve(lis)
	if err != nil {
    
    
		fmt.Printf("failed to serve: %v", err)
		return
	}
}

Compile and execute http_server:

go build
./server

Write client-side Go code

We create a new hello_clientproject and execute it under the project root directory go mod init hello_client.

Create a new pbfolder and save the above proto file as hello.proto, go_packagewhich will be modified as follows.

// ...

option go_package = "hello_client/pb";

// ...

Execute the following command in the project root directory to generate go source code files hello.protounder http_clientthe project.

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb/hello.proto

At this point, the directory structure of the project is:

http_client
├── go.mod
├── go.sum
├── main.go
└── pb
    ├── hello.pb.go
    ├── hello.proto
    └── hello_grpc.pb.go

Call the RPC service provided by http_client/main.gothe following code in the file .http_serverSayHello

package main

import (
	"context"
	"flag"
	"log"
	"time"

	"hello_client/pb"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

// hello_client

const (
	defaultName = "world"
)

var (
	addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
	name = flag.String("name", defaultName, "Name to greet")
)

func main() {
    
    
	flag.Parse()
	// 连接到server端,此处禁用安全传输
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
    
    
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// 执行RPC调用并打印收到的响应数据
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.SayHello(ctx, &pb.HelloRequest{
    
    Name: *name})
	if err != nil {
    
    
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetReply())
}

After saving it will hello_clientcompile and execute:

go build
./hello_client -name=wxy

The following output results are obtained, indicating that the RPC call is successful.

2023/08/04 14:11:19 Greeting: Hello wxy

gRPC cross-language call

Next, we demonstrate how to use gRPC to implement cross-language RPC calls.

We Pythonwrite in the language , and then send RPC requests Clientto gothe language written above.server

Install grpc under python:

pip install grpcio

Install gRPC tools:

pip install grpcio-tools

Generate Python code

Create a new py_clientproject project and hello.protosave the file to py_client/pb/the directory.

syntax = "proto3"; // 版本声明,使用Protocol Buffers v3版本

option go_package = "py_client/pb";  // 指定生成的Go代码在你项目中的导入路径

package pb; // 包名


// 定义服务
service Greeter {
    // SayHello 方法
    rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// 请求消息
message HelloRequest {
    string name = 1;
}

// 响应消息
message HelloResponse {
    string reply = 1;
}

Execute the following command in py_clientthe directory to generate the python source code file.

python -m grpc_tools.protoc -Ipb --python_out=. --grpc_python_out=. pb/hello.proto

Write a Python version of the RPC client

Save the code below to py_client/client.pya file.

from __future__ import print_function

import logging

import grpc
import hello_pb2
import hello_pb2_grpc


def run():
    # NOTE(gRPC Python Team): .close() is possible on a channel and should be
    # used in circumstances in which the with statement does not fit the needs
    # of the code.
    with grpc.insecure_channel('127.0.0.1:8972') as channel:
        stub = hello_pb2_grpc.GreeterStub(channel)
        resp = stub.SayHello(hello_pb2.HelloRequest(name='q1mi'))
    print("Greeter client received: " + resp.reply)


if __name__ == '__main__':
    logging.basicConfig()
    run()

At this time, the directory structure diagram of the project is as follows:

py_client
├── client.py
├── hello_pb2.py
├── hello_pb2_grpc.py
└── pb
    └── hello.proto

Python RPC calls

Execute the RPC service client.pythat calls the go language SayHello.

$ python client.py
Greeter client received: Hello wxy

Here we have achieved it, using the client written in Python code to call the Go language version of the server.

gRPC streaming example

In the above example, the client initiates an RPC request to the server, and the server performs business processing and returns a response to the client. This is the most basic working method of gRPC (Unary RPC). In addition, relying on HTTP2, gRPC also supports streaming RPC (Streaming RPC).

Server Streaming RPC

The client sends an RPC request, and a one-way flow is established between the server and the client. The server can write multiple response messages to the flow, and finally close the flow actively; while the client needs to monitor this flow and continuously obtain responses. until the stream is closed. Application scenario example: the client sends a stock code to the server, and the server continuously returns the real-time data of the stock to the client.

Here we write a method of greeting in multiple languages. The client sends a user name, and the server returns the greeting information multiple times.

1. Define the service

syntax = "proto3"; // 版本声明,使用Protocol Buffers v3版本

option go_package = "hello_server/pb";  // 指定生成的Go代码在你项目中的导入路径

package pb; // 包名


// 定义服务
service Greeter {
  // 服务端返回流式数据
  rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
}

// 请求消息
message HelloRequest {
  string name = 1;
}

// 响应消息
message HelloResponse {
  string reply = 1;
}

After modifying .protothe file, you need to re-use the protocol buffers compiler to generate client and server code.

Excuting an order:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb/hello.proto

2. The server needs to implement LotsOfRepliesthe method.

package main

import (
	"fmt"
	"hello_server/pb"
	"net"

	"google.golang.org/grpc"
)

// hello server

type server struct {
    
    
	pb.UnimplementedGreeterServer
}

// LotsOfReplies 返回使用多种语言打招呼
func (s *server) LotsOfReplies(in *pb.HelloRequest, stream pb.Greeter_LotsOfRepliesServer) error {
    
    
	words := []string{
    
    
		"你好",
		"hello",
		"こんにちは",
		"안녕하세요",
	}

	for _, word := range words {
    
    
		data := &pb.HelloResponse{
    
    
			Reply: word + in.GetName(),
		}
		// 使用Send方法返回多个数据
		if err := stream.Send(data); err != nil {
    
    
			return err
		}
	}
	return nil
}

func main() {
    
    
	// 监听本地的8972端口
	lis, err := net.Listen("tcp", ":8972")
	if err != nil {
    
    
		fmt.Printf("failed to listen: %v", err)
		return
	}
	s := grpc.NewServer()                  // 创建gRPC服务器
	pb.RegisterGreeterServer(s, &server{
    
    }) // 在gRPC服务端注册服务
	// 启动服务
	err = s.Serve(lis)
	if err != nil {
    
    
		fmt.Printf("failed to serve: %v", err)
		return
	}
}

3. The client calls LotsOfRepliesand prints out the received data in sequence.

package main

import (
	"context"
	"flag"
	"io"
	"log"
	"time"

	"hello_client/pb"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

// hello_client

const (
	defaultName = "world"
)

var (
	addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
	name = flag.String("name", defaultName, "Name to greet")
)

func main() {
    
    
	flag.Parse()
	// 连接到server端,此处禁用安全传输
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
    
    
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// server端流式RPC
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	stream, err := c.LotsOfReplies(ctx, &pb.HelloRequest{
    
    Name: *name})
	if err != nil {
    
    
		log.Fatalf("c.LotsOfReplies failed, err: %v", err)
	}
	for {
    
    
		// 接收服务端返回的流式数据,当收到io.EOF或错误时退出
		res, err := stream.Recv()
		if err == io.EOF {
    
    
			break
		}
		if err != nil {
    
    
			log.Fatalf("c.LotsOfReplies failed, err: %v", err)
		}
		log.Printf("got reply: %q\n", res.GetReply())
	}
}

After saving it will hello_clientcompile and execute:

go build
./hello_client -name=wxy

After executing the program, the following output will be obtained.

2023/08/04 15:08:38 got reply: "你好wxy"
2023/08/04 15:08:38 got reply: "hellowxy"
2023/08/04 15:08:38 got reply: "こんにちはwxy"
2023/08/04 15:08:38 got reply: "안녕하세요wxy"

Client Streaming RPC

The client passes in multiple request objects, and the server returns a response result. Examples of typical application scenarios: Internet of Things terminals report data to servers, big data stream computing, etc.

In this example, we write a program that sends a person's name multiple times, and the server returns a greeting message uniformly.

1. Define the service

syntax = "proto3"; // 版本声明,使用Protocol Buffers v3版本

option go_package = "hello_server/pb";  // 指定生成的Go代码在你项目中的导入路径

package pb; // 包名


// 定义服务
service Greeter {
  // 客户端发送流式数据
  rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
}

// 请求消息
message HelloRequest {
  string name = 1;
}

// 响应消息
message HelloResponse {
  string reply = 1;
}

After modifying .protothe file, you need to re-use the protocol buffers compiler to generate client and server code.

Excuting an order:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb/hello.proto

2. The implementation method of the server LotsOfGreetings.

package main

import (
	"fmt"
	"hello_server/pb"
	"io"
	"net"

	"google.golang.org/grpc"
)

// hello server

type server struct {
    
    
	pb.UnimplementedGreeterServer
}

// LotsOfGreetings 接收流式数据
func (s *server) LotsOfGreetings(stream pb.Greeter_LotsOfGreetingsServer) error {
    
    
	reply := "你好:"
	for {
    
    
		// 接收客户端发来的流式数据
		res, err := stream.Recv()
		if err == io.EOF {
    
    
			// 最终统一回复
			return stream.SendAndClose(&pb.HelloResponse{
    
    
				Reply: reply,
			})
		}
		if err != nil {
    
    
			return err
		}
		reply += res.GetName()
	}
}

func main() {
    
    
	// 监听本地的8972端口
	lis, err := net.Listen("tcp", ":8972")
	if err != nil {
    
    
		fmt.Printf("failed to listen: %v", err)
		return
	}
	s := grpc.NewServer()                  // 创建gRPC服务器
	pb.RegisterGreeterServer(s, &server{
    
    }) // 在gRPC服务端注册服务
	// 启动服务
	err = s.Serve(lis)
	if err != nil {
    
    
		fmt.Printf("failed to serve: %v", err)
		return
	}
} 

3. The client calls LotsOfGreetingsthe method, sends streaming request data to the server, receives the return value and prints it.

package main

import (
	"context"
	"flag"
	"log"
	"time"

	"hello_client/pb"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

// hello_client

const (
	defaultName = "world"
)

var (
	addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
	name = flag.String("name", defaultName, "Name to greet")
)

func main() {
    
    
	flag.Parse()
	// 连接到server端,此处禁用安全传输
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
    
    
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	// 客户端流式RPC
	stream, err := c.LotsOfGreetings(ctx)
	if err != nil {
    
    
		log.Fatalf("c.LotsOfGreetings failed, err: %v", err)
	}
	names := []string{
    
    "wxy", "Palp1tate", "沙河娜扎"}
	for _, name := range names {
    
    
		// 发送流式数据
		err := stream.Send(&pb.HelloRequest{
    
    Name: name})
		if err != nil {
    
    
			log.Fatalf("c.LotsOfGreetings stream.Send(%v) failed, err: %v", name, err)
		}
	}
	res, err := stream.CloseAndRecv()
	if err != nil {
    
    
		log.Fatalf("c.LotsOfGreetings failed: %v", err)
	}
	log.Printf("got reply: %v", res.GetReply())
}

Executing the above function will result in the following data.

2023/08/04 15:11:59 got reply: 你好:wxyPalp1tate沙河娜扎

Bidirectional streaming RPC

Two-way streaming RPC means that both the client and the server are streaming RPCs, which can send multiple request objects and receive multiple response objects. Typical application examples: chat applications, etc.

Here we still write a two-way streaming RPC example in which the client and the server conduct man-machine conversations.

1. Define the service

// 双向流式数据
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);

After modifying .protothe file, you need to re-use the protocol buffers compiler to generate client and server code.

Excuting an order:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pb/hello.proto

2. The implementation method of the server BidiHello.

package main

import (
	"fmt"
	"hello_server/pb"
	"io"
	"net"
	"strings"

	"google.golang.org/grpc"
)

// hello server

type server struct {
    
    
	pb.UnimplementedGreeterServer
}

// BidiHello 双向流式打招呼
func (s *server) BidiHello(stream pb.Greeter_BidiHelloServer) error {
    
    
	for {
    
    
		// 接收流式请求
		in, err := stream.Recv()
		if err == io.EOF {
    
    
			return nil
		}
		if err != nil {
    
    
			return err
		}

		reply := magic(in.GetName()) // 对收到的数据做些处理

		// 返回流式响应
		if err := stream.Send(&pb.HelloResponse{
    
    Reply: reply}); err != nil {
    
    
			return err
		}
	}
}

// magic 一段价值连城的“人工智能”代码
func magic(s string) string {
    
    
	s = strings.ReplaceAll(s, "吗", "")
	s = strings.ReplaceAll(s, "吧", "")
	s = strings.ReplaceAll(s, "你", "我")
	s = strings.ReplaceAll(s, "?", "!")
	s = strings.ReplaceAll(s, "?", "!")
	return s
}

func main() {
    
    
	// 监听本地的8972端口
	lis, err := net.Listen("tcp", ":8972")
	if err != nil {
    
    
		fmt.Printf("failed to listen: %v", err)
		return
	}
	s := grpc.NewServer()                  // 创建gRPC服务器
	pb.RegisterGreeterServer(s, &server{
    
    }) // 在gRPC服务端注册服务
	// 启动服务
	err = s.Serve(lis)
	if err != nil {
    
    
		fmt.Printf("failed to serve: %v", err)
		return
	}
}

3. The client invokes BidiHellothe method, while receiving the input request data from the terminal and sending it to the server, while receiving the streaming response from the server.

package main

import (
	"bufio"
	"context"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"strings"
	"time"

	"hello_client/pb"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

// hello_client

const (
	defaultName = "world"
)

var (
	addr = flag.String("addr", "127.0.0.1:8972", "the address to connect to")
	name = flag.String("name", defaultName, "Name to greet")
)

func main() {
    
    
	flag.Parse()
	// 连接到server端,此处禁用安全传输
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
    
    
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()
	// 双向流模式
	stream, err := c.BidiHello(ctx)
	if err != nil {
    
    
		log.Fatalf("c.BidiHello failed, err: %v", err)
	}
	waitc := make(chan struct{
    
    })
	go func() {
    
    
		for {
    
    
			// 接收服务端返回的响应
			in, err := stream.Recv()
			if err == io.EOF {
    
    
				// read done.
				close(waitc)
				return
			}
			if err != nil {
    
    
				log.Fatalf("c.BidiHello stream.Recv() failed, err: %v", err)
			}
			fmt.Printf("AI:%s\n", in.GetReply())
		}
	}()
	// 从标准输入获取用户输入
	reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
	for {
    
    
		cmd, _ := reader.ReadString('\n') // 读到换行
		cmd = strings.TrimSpace(cmd)
		if len(cmd) == 0 {
    
    
			continue
		}
		if strings.ToUpper(cmd) == "QUIT" {
    
    
			break
		}
		// 将获取到的数据发送至服务端
		if err := stream.Send(&pb.HelloRequest{
    
    Name: cmd}); err != nil {
    
    
			log.Fatalf("c.BidiHello stream.Send(%v) failed: %v", cmd, err)
		}
	}
	stream.CloseSend()
	<-waitc
}

By running both the server and client codes, a simple dialogue program can be realized.

image-20230804152125944

metadata

Metadata ( metadata ) refers to information that is required in the process of processing RPC requests and responses but does not belong to specific services (such as authentication details), in the form of a list of key-value pairs, where the key is the type, and the value is usually the stringtype []string. But it can also be binary data. The metadata in gRPC is similar to our key-value pairs in HTTP headers. Metadata can include authentication tokens, request identifiers, and monitoring tags.

Keys in metadata are case-insensitive and consist of letters, numbers, and special characters -, _, .and cannot grpc-start with (gRPC reserves for their own use). The key name of a binary value must -binend with .

Metadata is invisible to gRPC itself, we usually deal with metadata in application code or middleware, we don't need to .protospecify metadata in files.

How metadata is accessed depends on the programming language used. In the Go language, we use the library google.golang.org/grpc/metadata to manipulate metadata.

The metadata type is defined as follows:

type MD map[string][]string

Metadata can be read like a normal map. Note that the value type of this map is []string, so users can append multiple values ​​with one key.

create new metadata

There are two commonly used methods for creating MDs.

The first way is to use a function to create metadata Newbased on :map[string]string

md := metadata.New(map[string]string{
    
    "key1": "val1", "key2": "val2"})

Another way is to use Pairs. Values ​​with the same key will be combined into one list:

md := metadata.Pairs(
    "key1", "val1",
    "key1", "val1-2", // "key1"的值将会是 []string{"val1", "val1-2"}
    "key2", "val2",
)

Note: All keys will be automatically converted to lowercase, so "kEy1" and "Key1" will be the same key and their values ​​will be merged into the same list. This applies to Newand Pair.

Store binary data in metadata

In metadata, keys are always strings. But values ​​can be strings or binary data. To store binary data values ​​in metadata, simply add the "-bin" suffix to the key. When creating metadata, values ​​with "-bin" suffixed keys are encoded:

md := metadata.Pairs(
    "key", "string value",
    "key-bin", string([]byte{
    
    96, 102}), // 二进制数据在发送前会进行(base64) 编码
                                        // 收到后会进行解码
)

Get metadata from request context

FromIncomingContextMetadata can be obtained from the context of an RPC request using :

func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) {
    
    
    md, ok := metadata.FromIncomingContext(ctx)
    // do something with metadata
}

Send and receive metadata - client

send metadata

There are two ways to send metadata to the server. The recommended way is to AppendToOutgoingContextattach the kv pair to the context using This method can be used regardless of whether there is already metadata in the context. If there is no metadata before, add metadata; if metadata already exists in the context, merge the kv pair into it.

// 创建带有metadata的context
ctx := metadata.AppendToOutgoingContext(ctx, "k1", "v1", "k1", "v2", "k2", "v3")

// 添加一些 metadata 到 context (e.g. in an interceptor)
ctx := metadata.AppendToOutgoingContext(ctx, "k3", "v4")

// 发起普通RPC请求
response, err := client.SomeRPC(ctx, someRequest)

// 或者发起流式RPC请求
stream, err := client.SomeStreamingRPC(ctx)

Alternatively, NewOutgoingContextmetadata can be attached to the context using However, this will replace any existing metadata in the context, so care must be taken to preserve existing metadata (if desired). This method AppendToOutgoingContextis slower than using . An example of this is as follows:

// 创建带有metadata的context
md := metadata.Pairs("k1", "v1", "k1", "v2", "k2", "v3")
ctx := metadata.NewOutgoingContext(context.Background(), md)

// 添加一些metadata到context (e.g. in an interceptor)
send, _ := metadata.FromOutgoingContext(ctx)
newMD := metadata.Pairs("k3", "v3")
ctx = metadata.NewOutgoingContext(ctx, metadata.Join(send, newMD))

// 发起普通RPC请求
response, err := client.SomeRPC(ctx, someRequest)

// 或者发起流式RPC请求
stream, err := client.SomeStreamingRPC(ctx)

Receive metadata

The metadata that the client can receive includes header and trailer.

Trailer can be used when the server wants to send any content to the client after processing the request. For example, in streaming RPC, the load information can only be calculated after all the results have flowed to the client. At this time, headers cannot be used (the header is before the data. , the trailer is after the data).

Extension: HTTP trailer

normal call

You can use the Header and Trailer functions in CallOption to get the header and trailer sent by ordinary RPC calls:

var header, trailer metadata.MD // 声明存储header和trailer的变量
r, err := client.SomeRPC(
    ctx,
    someRequest,
    grpc.Header(&header),    // 将会接收header
    grpc.Trailer(&trailer),  // 将会接收trailer
)

// do something with header and trailer
streaming call

Streaming calls include:

  • client streaming
  • server-side streaming
  • two-way streaming

Using the and functions in the interface ClientStream , you can receive Header and Trailer from the returned stream:HeaderTrailer

stream, err := client.SomeStreamingRPC(ctx)

// 接收 header
header, err := stream.Header()

// 接收 trailer
trailer := stream.Trailer()

Sending and receiving metadata - server side

Receive metadata

To read the metadata sent by the client, the server needs to retrieve it from the RPC context. If it's a normal RPC call, you can use the RPC handler's context. For stream calls, the server needs to get the context from the stream.

normal call
func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) {
    
    
    md, ok := metadata.FromIncomingContext(ctx)
    // do something with metadata
}
streaming call
func (s *server) SomeStreamingRPC(stream pb.Service_SomeStreamingRPCServer) error {
    
    
    md, ok := metadata.FromIncomingContext(stream.Context()) // get context from stream
    // do something with metadata
}

send metadata

normal call

In normal calls, the server can call the SendHeader and SetTrailer functions in the grpc module to send headers and trailers to the client. These two functions take context as the first parameter. It should be the RPC handler's context or a context derived from it:

func (s *server) SomeRPC(ctx context.Context, in *pb.someRequest) (*pb.someResponse, error) {
    
    
    // 创建和发送 header
    header := metadata.Pairs("header-key", "val")
    grpc.SendHeader(ctx, header)
    // 创建和发送 trailer
    trailer := metadata.Pairs("trailer-key", "val")
    grpc.SetTrailer(ctx, trailer)
}
streaming call

For streaming calls, you can use the and functions in the interface ServerStream to send headers and trailers:SendHeaderSetTrailer

func (s *server) SomeStreamingRPC(stream pb.Service_SomeStreamingRPCServer) error {
    
    
    // 创建和发送 header
    header := metadata.Pairs("header-key", "val")
    stream.SendHeader(header)
    // 创建和发送 trailer
    trailer := metadata.Pairs("trailer-key", "val")
    stream.SetTrailer(trailer)
}

Common RPC call metadata example

Metadata operations on the client side

The following code snippet demonstrates how to set and get metadata on the client side.

// unaryCallWithMetadata 普通RPC调用客户端metadata操作
func unaryCallWithMetadata(c pb.GreeterClient, name string) {
    
    
	fmt.Println("--- UnarySayHello client---")
	// 创建metadata
	md := metadata.Pairs(
		"token", "app-test-q1mi",
		"request_id", "1234567",
	)
	// 基于metadata创建context.
	ctx := metadata.NewOutgoingContext(context.Background(), md)
	// RPC调用
	var header, trailer metadata.MD
	r, err := c.SayHello(
		ctx,
		&pb.HelloRequest{
    
    Name: name},
		grpc.Header(&header),   // 接收服务端发来的header
		grpc.Trailer(&trailer), // 接收服务端发来的trailer
	)
	if err != nil {
    
    
		log.Printf("failed to call SayHello: %v", err)
		return
	}
	// 从header中取location
	if t, ok := header["location"]; ok {
    
    
		fmt.Printf("location from header:\n")
		for i, e := range t {
    
    
			fmt.Printf(" %d. %s\n", i, e)
		}
	} else {
    
    
		log.Printf("location expected but doesn't exist in header")
		return
	}
   // 获取响应结果
	fmt.Printf("got response: %s\n", r.Reply)
	// 从trailer中取timestamp
	if t, ok := trailer["timestamp"]; ok {
    
    
		fmt.Printf("timestamp from trailer:\n")
		for i, e := range t {
    
    
			fmt.Printf(" %d. %s\n", i, e)
		}
	} else {
    
    
		log.Printf("timestamp expected but doesn't exist in trailer")
	}
}

Server-side metadata operation

The following code snippet demonstrates how to set and get metadata on the server side.

// UnarySayHello 普通RPC调用服务端metadata操作
func (s *server) UnarySayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
    
    
	// 通过defer中设置trailer.
	defer func() {
    
    
		trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))
		grpc.SetTrailer(ctx, trailer)
	}()

	// 从客户端请求上下文中读取metadata.
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
    
    
		return nil, status.Errorf(codes.DataLoss, "UnarySayHello: failed to get metadata")
	}
	if t, ok := md["token"]; ok {
    
    
		fmt.Printf("token from metadata:\n")
		if len(t) < 1 || t[0] != "app-test-q1mi" {
    
    
			return nil, status.Error(codes.Unauthenticated, "认证失败")
		}
	}

	// 创建和发送header.
	header := metadata.New(map[string]string{
    
    "location": "BeiJing"})
	grpc.SendHeader(ctx, header)

	fmt.Printf("request received: %v, say hello...\n", in)

	return &pb.HelloResponse{
    
    Reply: in.Name}, nil
}

Streaming RPC call metadata example

Here, two-way streaming RPC is used as an example to demonstrate how the client and server perform metadata operations.

Metadata operations on the client side

The following code snippet demonstrates how the client side sets and gets metadata in the server-side streaming RPC mode.

// bidirectionalWithMetadata 流式RPC调用客户端metadata操作
func bidirectionalWithMetadata(c pb.GreeterClient, name string) {
    
    
	// 创建metadata和context.
	md := metadata.Pairs("token", "app-test-q1mi")
	ctx := metadata.NewOutgoingContext(context.Background(), md)

	// 使用带有metadata的context执行RPC调用.
	stream, err := c.BidiHello(ctx)
	if err != nil {
    
    
		log.Fatalf("failed to call BidiHello: %v\n", err)
	}

	go func() {
    
    
		// 当header到达时读取header.
		header, err := stream.Header()
		if err != nil {
    
    
			log.Fatalf("failed to get header from stream: %v", err)
		}
		// 从返回响应的header中读取数据.
		if l, ok := header["location"]; ok {
    
    
			fmt.Printf("location from header:\n")
			for i, e := range l {
    
    
				fmt.Printf(" %d. %s\n", i, e)
			}
		} else {
    
    
			log.Println("location expected but doesn't exist in header")
			return
		}

		// 发送所有的请求数据到server.
		for i := 0; i < 5; i++ {
    
    
			if err := stream.Send(&pb.HelloRequest{
    
    Name: name}); err != nil {
    
    
				log.Fatalf("failed to send streaming: %v\n", err)
			}
		}
		stream.CloseSend()
	}()

	// 读取所有的响应.
	var rpcStatus error
	fmt.Printf("got response:\n")
	for {
    
    
		r, err := stream.Recv()
		if err != nil {
    
    
			rpcStatus = err
			break
		}
		fmt.Printf(" - %s\n", r.Reply)
	}
	if rpcStatus != io.EOF {
    
    
		log.Printf("failed to finish server streaming: %v", rpcStatus)
		return
	}

	// 当RPC结束时读取trailer
	trailer := stream.Trailer()
	// 从返回响应的trailer中读取metadata.
	if t, ok := trailer["timestamp"]; ok {
    
    
		fmt.Printf("timestamp from trailer:\n")
		for i, e := range t {
    
    
			fmt.Printf(" %d. %s\n", i, e)
		}
	} else {
    
    
		log.Printf("timestamp expected but doesn't exist in trailer")
	}
}

Metadata operation on the server side

The following code snippet demonstrates setting and manipulating metadata on the server side in server-side streaming RPC mode.

// BidirectionalStreamingSayHello 流式RPC调用客户端metadata操作
func (s *server) BidirectionalStreamingSayHello(stream pb.Greeter_BidiHelloServer) error {
    
    
	// 在defer中创建trailer记录函数的返回时间.
	defer func() {
    
    
		trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))
		stream.SetTrailer(trailer)
	}()

	// 从client读取metadata.
	md, ok := metadata.FromIncomingContext(stream.Context())
	if !ok {
    
    
		return status.Errorf(codes.DataLoss, "BidirectionalStreamingSayHello: failed to get metadata")
	}

	if t, ok := md["token"]; ok {
    
    
		fmt.Printf("token from metadata:\n")
		for i, e := range t {
    
    
			fmt.Printf(" %d. %s\n", i, e)
		}
	}

	// 创建和发送header.
	header := metadata.New(map[string]string{
    
    "location": "X2Q"})
	stream.SendHeader(header)

	// 读取请求数据发送响应数据.
	for {
    
    
		in, err := stream.Recv()
		if err == io.EOF {
    
    
			return nil
		}
		if err != nil {
    
    
			return err
		}
		fmt.Printf("request received %v, sending reply\n", in)
		if err := stream.Send(&pb.HelloResponse{
    
    Reply: in.Name}); err != nil {
    
    
			return err
		}
	}
}

error handling

gRPC code

Similar to HTTP defining a set of response status codes, gRPC also defines some status codes. In the Go language, this status code is defined by codes , which is essentially a uint32.

type Code uint32

The package needs to be imported when using it google.golang.org/grpc/codes.

import "google.golang.org/grpc/codes"

The currently defined status codes are as follows.

Code value meaning
OK 0 successful request
Canceled 1 Operation canceled
Unknown 2 unknown mistake. An instance of this error may be returned if a status value received from another address space belongs to an error space that is unknown in that address space. Errors raised by APIs that do not return sufficient error information may also be converted to this error
InvalidArgument 3 Indicates that the parameter specified by the client is invalid. Note that this is not the same as FailedPrecondition. It indicates a problematic parameter (for example, a malformed filename) regardless of the state of the system.
DeadlineExceeded 4 Indicates that the operation expired before completion. For operations that change the state of the system, this error may be returned even if the operation completed successfully. For example, a successful response from the server may have been delayed long enough for the deadline to expire.
NotFound 5 Indicates that some requested entity (for example, a file or directory) was not found.
AlreadyExists 6 An attempt to create an entity failed because the entity already exists.
PermissionDenied 7 Indicates that the caller does not have permission to perform the specified operation. It cannot be used to reject caused by exhausting some resource (use ResourceExhausted). It also cannot be used if the caller cannot be identified (use Unauthenticated).
ResourceExhausted 8 Indicates that some resource has been exhausted, perhaps a per-user quota, or that the entire filesystem is out of space
FailedPrecondition 9 Indicates that the operation was rejected because the system is not in the state required for the operation to perform. For example, the directory to be removed may be non-empty, the rmdir operation is applied to a non-directory, etc.
Aborted 10 Indicates that the operation was aborted, usually due to a concurrency issue such as a failed sorter check, transaction aborted, etc.
OutOfRange 11 Indicates that an operation outside the valid range was attempted.
Unimplemented 12 Indicates that the operation is not implemented or not supported/enabled in this service.
Internal 13 Means that some invariants expected by the underlying system have been broken. If you see this error, the problem is serious.
Unavailable 14 Indicates that the service is currently unavailable. This is most likely a temporary condition and can be corrected with a fallback and retry. Note that it is not always safe to retry non-idempotent operations.
DataLoss 15 Indicates irrecoverable data loss or corruption
Unauthenticated 16 Indicates that the request did not have valid authentication credentials for the operation
_maxCode 17 -

gRPC Status

The gRPC Status used by the Go language is defined at google.golang.org/grpc/status and needs to be imported when using it.

import "google.golang.org/grpc/status"

Methods of the RPC service should return nilor from status.Statusan error of type. Clients can access errors directly.

create error

The method function of the gRPC service should create one when an error is encountered status.Status. Normally we would use status.Newa function and pass in an appropriate status.Codeand error description to generate one status.Status. Calling status.Erra method turns one status.Statusinto errora type. There also exists an easy status.Errormethod to generate directly error. Below is a comparison of the two methods.

// 创建status.Status
st := status.New(codes.NotFound, "some description")
err := st.Err()  // 转为error类型

// vs.

err := status.Error(codes.NotFound, "some description")

Add additional details to the error

In some cases, it may be necessary to add details for specific errors on the server side. status.WithDetailsIt exists for this purpose, it can add any number proto.Message, we can use google.golang.org/genproto/googleapis/rpc/errdetailsthe definition in or custom error details.

st := status.New(codes.ResourceExhausted, "Request limit exceeded.")
ds, _ := st.WithDetails(
	// proto.Message
)
return nil, ds.Err()

Clients can then read these details by first errorconverting the plain type back status.Status, and then using .status.Details

s := status.Convert(err)
for _, d := range s.Details() {
    
    
	// ...
}

code example

We are now going to helloset access restrictions for the service, each method namecan only be called once , and if this limit is exceeded, a request exceeding the limit error will be returned.SayHello

Server

Use map to store the number of requests for each name, and return an error if it exceeds 1, and record the error details.

package main

import (
	"context"
	"fmt"
	"hello_server/pb"
	"net"
	"sync"

	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// grpc server

type server struct {
    
    
	pb.UnimplementedGreeterServer
	mu    sync.Mutex     // count的并发锁
	count map[string]int // 记录每个name的请求次数
}

// SayHello 是我们需要实现的方法
// 这个方法是我们对外提供的服务
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
    
    
	s.mu.Lock()
	defer s.mu.Unlock()
	s.count[in.Name]++ // 记录用户的请求次数
	// 超过1次就返回错误
	if s.count[in.Name] > 1 {
    
    
		st := status.New(codes.ResourceExhausted, "Request limit exceeded.")
		ds, err := st.WithDetails(
			&errdetails.QuotaFailure{
    
    
				Violations: []*errdetails.QuotaFailure_Violation{
    
    {
    
    
					Subject:     fmt.Sprintf("name:%s", in.Name),
					Description: "限制每个name调用一次",
				}},
			},
		)
		if err != nil {
    
    
			return nil, st.Err()
		}
		return nil, ds.Err()
	}
	// 正常返回响应
	reply := "hello " + in.GetName()
	return &pb.HelloResponse{
    
    Reply: reply}, nil
}

func main() {
    
    
	// 启动服务
	l, err := net.Listen("tcp", ":8972")
	if err != nil {
    
    
		fmt.Printf("failed to listen, err:%v\n", err)
		return
	}
	s := grpc.NewServer() // 创建grpc服务
	// 注册服务,注意初始化count
	pb.RegisterGreeterServer(s, &server{
    
    count: make(map[string]int)})
	// 启动服务
	err = s.Serve(l)
	if err != nil {
    
    
		fmt.Printf("failed to serve,err:%v\n", err)
		return
	}
}

client

When the server returns an error, try to get detail information from the error.

package main

import (
	"context"
	"flag"
	"fmt"
	"google.golang.org/grpc/status"
	"hello_client/pb"
	"log"
	"time"

	"google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

// grpc 客户端
// 调用server端的 SayHello 方法

var name = flag.String("name", "七米", "通过-name告诉server你是谁")

func main() {
    
    
	flag.Parse() // 解析命令行参数

	// 连接server
	conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
    
    
		log.Fatalf("grpc.Dial failed,err:%v", err)
		return
	}
	defer conn.Close()
	// 创建客户端
	c := pb.NewGreeterClient(conn) // 使用生成的Go代码
	// 调用RPC方法
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	resp, err := c.SayHello(ctx, &pb.HelloRequest{
    
    Name: *name})
	if err != nil {
    
    
		s := status.Convert(err)        // 将err转为status
		for _, d := range s.Details() {
    
     // 获取details
			switch info := d.(type) {
    
    
			case *errdetails.QuotaFailure:
				fmt.Printf("Quota failure: %s\n", info)
			default:
				fmt.Printf("Unexpected type: %s\n", info)
			}
		}
		fmt.Printf("c.SayHello failed, err:%v\n", err)
		return
	}
	// 拿到了RPC响应
	log.Printf("resp:%v\n", resp.GetReply())
}

encryption or authentication

No encryption authentication

In the examples above, we have not configured encryption or authentication for our gRPC, which is an insecure connection.

Client side:

conn, _ := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
client := pb.NewGreeterClient(conn)

Server side:

s := grpc.NewServer()
lis, _ := net.Listen("tcp", "127.0.0.1:8972")
// error handling omitted
s.Serve(lis)

Use Server Authentication SSL/TLS

gRPC has built-in support for SSL/TLS, and can establish a secure connection through an SSL/TLS certificate to encrypt the transmitted data.

Here we demonstrate how to use a self-signed certificate for server-side encryption.

generate certificate

generate private key

Execute the following command to generate a private key file - server.key.

openssl ecparam -genkey -name secp384r1 -out server.key

The ECC private key is generated here, of course you can also use RSA.

Generate a self-signed certificate

After Go1.15, x509 abandons Common Name and uses SANs instead.

When the following errors occur, you need to provide SANs information.

transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0

In order to add SANs information in the certificate, we save the following custom configuration to server.cnfa file.

[ req ]
default_bits       = 4096
default_md		= sha256
distinguished_name = req_distinguished_name
req_extensions     = req_ext

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = BEIJING
localityName                = Locality Name (eg, city)
localityName_default        = BEIJING
organizationName            = Organization Name (eg, company)
organizationName_default    = DEV
commonName                  = Common Name (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = liwenzhou.com

[ req_ext ]
subjectAltName = @alt_names

[alt_names]
DNS.1   = localhost
DNS.2   = liwenzhou.com
IP      = 127.0.0.1

Execute the following command to generate a self-signed certificate -- server.crt.

openssl req -nodes -new -x509 -sha256 -days 3650 -config server.cnf -extensions 'req_ext' -key server.key -out server.crt

establish a secure connection

The server side uses credentials.NewServerTLSFromFilefunctions to load certificates server.certand secret keys respectively server.key.

creds, _ := credentials.NewServerTLSFromFile(certFile, keyFile)
s := grpc.NewServer(grpc.Creds(creds))
lis, _ := net.Listen("tcp", "127.0.0.1:8972")
// error handling omitted
s.Serve(lis)

The client side uses the certificate file generated in the previous step server.certto establish a secure connection.

creds, _ := credentials.NewClientTLSFromFile(certFile, "")
conn, _ := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(creds))
// error handling omitted
client := pb.NewGreeterClient(conn)
// ...

In addition to this self-signed certificate method, a trusted CA certificate is usually required for external communication in the production environment.

interceptor (middleware)

gRPC provides some simple APIs for implementing and installing interceptors on a per ClientConn/Server basis. Interceptors intercept the execution of each RPC call. Users can use interceptors for logging, authentication/authorization, metrics collection, and many other features that can be shared across RPCs.

In gRPC, interceptors can be divided into two types according to the type of RPC calls intercepted. The first is a normal interceptor (unary interceptor), which intercepts normal RPC calls. The other is a stream interceptor, which handles streaming RPC calls. Both the client and the server have their own common interceptor and stream interceptor types. So in total there are four different types of interceptors in gRPC.

client interceptor

Common Interceptor/Unary Interceptor

UnaryClientInterceptor is the type of client-side unary interceptor, and its function is preceded by the following:

func(ctx context.Context, method string, req, reply interface{
    
    }, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error

The implementation of a unary interceptor can usually be divided into three parts: before calling the RPC method (preprocessing), calling the RPC method (RPC call) and after calling the RPC method (after calling).

  • Preprocessing: Users can get information about the current RPC call by inspecting the incoming parameters such as RPC context, method string, request to send, and CallOptions configuration.
  • RPC call: After the preprocessing is completed, invokerthe RPC call can be executed by executing.
  • Post-call: Once the caller returns a reply and an error, the user can post-process the RPC call. Generally, it's about handling the returned responses and errors. To ClientConninstall unary interceptors on , use the configuration DialOptionWithUnaryInterceptorof DialOptionDial .

stream interceptor

StreamClientInterceptor is the type of client stream interceptor. Its function signature is

func(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, streamer Streamer, opts ...CallOption) (ClientStream, error)

Implementations of stream interceptors typically include preprocessing and interception of stream operations.

  • Preprocessing: Similar to the unary interceptor above.
  • Stream operation interception: The stream interceptor does not perform RPC method calls and post-processing afterwards, but intercepts user operations on the stream. First, the interceptor calls the passed in streamerto get ClientStream, then wraps ClientStreamand overloads its methods with interception logic. Finally, the interceptor returns the wrapped ClientStreamto the user for operation.

To ClientConninstall a stream interceptor, WithStreamInterceptorconfigure the Dial with the DialOption.

server-side interceptor

Server-side interceptors are similar to client-side interceptors, but provide slightly different information.

Common Interceptor/Unary Interceptor

UnaryServerInterceptor is a unary interceptor type on the server side, and its function signature is

func(ctx context.Context, req interface{
    
    }, info *UnaryServerInfo, handler UnaryHandler) (resp interface{
    
    }, err error)

The specific implementation details of the server-side unary interceptor are similar to those of the client-side version.

To install unary interceptors for the server, use the configuration UnaryInterceptorof .ServerOptionNewServer

stream interceptor

StreamServerInterceptor is a type of server-side streaming interceptor, and its signature is as follows:

func(srv interface{
    
    }, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error

The implementation details are similar to the client stream interceptor section.

To install a stream interceptor for the server, use the configuration StreamInterceptorof .ServerOptionNewServer

Interceptor example

A complete interceptor example will be demonstrated below, we add interceptors for both unary RPC and streaming RPC services.

We first define a validvalidation function called .

// valid 校验认证信息.
func valid(authorization []string) bool {
    
    
	if len(authorization) < 1 {
    
    
		return false
	}
	token := strings.TrimPrefix(authorization[0], "Bearer ")
	// 执行token认证的逻辑
	// 这里是为了演示方便简单判断token是否与"some-secret-token"相等
	return token == "some-secret-token"
}

Client Interceptor Definition

unary interceptor
// unaryInterceptor 客户端一元拦截器
func unaryInterceptor(ctx context.Context, method string, req, reply interface{
    
    }, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    
    
	var credsConfigured bool
	for _, o := range opts {
    
    
		_, ok := o.(grpc.PerRPCCredsCallOption)
		if ok {
    
    
			credsConfigured = true
			break
		}
	}
	if !credsConfigured {
    
    
		opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(&oauth2.Token{
    
    
			AccessToken: "some-secret-token",
		})))
	}
	start := time.Now()
	err := invoker(ctx, method, req, reply, cc, opts...)
	end := time.Now()
	fmt.Printf("RPC: %s, start time: %s, end time: %s, err: %v\n", method, start.Format("Basic"), end.Format(time.RFC3339), err)
	return err
}

Among them, grpc.PerRPCCredentials()the function specifies the credentials used for each RPC request, and it receives a credentials.PerRPCCredentialsparameter of interface type. credentials.PerRPCCredentialsThe interface is defined as follows:

type PerRPCCredentials interface {
    
    
	// GetRequestMetadata 获取当前请求的元数据,如果需要则会设置token。
	// 传输层在每个请求上调用,并且数据会被填充到headers或其他context。
	GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
	// RequireTransportSecurity 指示该 Credentials 的传输是否需要需要 TLS 加密
	RequireTransportSecurity() bool
}

In the sample code, oauth.NewOauthAccess()a function provided by the built-in oauth package is used to return the URL containing the given token PerRPCCredentials.

// NewOauthAccess constructs the PerRPCCredentials using a given token.
func NewOauthAccess(token *oauth2.Token) credentials.PerRPCCredentials {
    
    
	return oauthAccess{
    
    token: *token}
}

func (oa oauthAccess) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
    
    
	ri, _ := credentials.RequestInfoFromContext(ctx)
	if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
    
    
		return nil, fmt.Errorf("unable to transfer oauthAccess PerRPCCredentials: %v", err)
	}
	return map[string]string{
    
    
		"authorization": oa.token.Type() + " " + oa.token.AccessToken,
	}, nil
}

func (oa oauthAccess) RequireTransportSecurity() bool {
    
    
	return true
}
stream interceptor

Customize a ClientStreamtype.

type wrappedStream struct {
    
    
	grpc.ClientStream
}

wrappedStreamOverride grpc.ClientStreamthe interface's RecvMsgand SendMsgmethods.

func (w *wrappedStream) RecvMsg(m interface{
    
    }) error {
    
    
	logger("Receive a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
	return w.ClientStream.RecvMsg(m)
}

func (w *wrappedStream) SendMsg(m interface{
    
    }) error {
    
    
	logger("Send a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
	return w.ClientStream.SendMsg(m)
}

func newWrappedStream(s grpc.ClientStream) grpc.ClientStream {
    
    
	return &wrappedStream{
    
    s}
}

Here, the interface type wrappedStreamis embedded grpc.ClientStream, and then grpc.ClientStreamthe method of the interface is re-implemented.

The following defines a streaming interceptor, and finally returns the one defined above wrappedStream.

// streamInterceptor 客户端流式拦截器
func streamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
    
    
	var credsConfigured bool
	for _, o := range opts {
    
    
		_, ok := o.(*grpc.PerRPCCredsCallOption)
		if ok {
    
    
			credsConfigured = true
			break
		}
	}
	if !credsConfigured {
    
    
		opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(&oauth2.Token{
    
    
			AccessToken: "some-secret-token",
		})))
	}
	s, err := streamer(ctx, desc, cc, method, opts...)
	if err != nil {
    
    
		return nil, err
	}
	return newWrappedStream(s), nil
}

Server Interceptor Definition

unary interceptor

The server defines a unary interceptor to authorizationverify what is obtained from the request metadata.

// unaryInterceptor 服务端一元拦截器
func unaryInterceptor(ctx context.Context, req interface{
    
    }, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{
    
    }, error) {
    
    
	// authentication (token verification)
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
    
    
		return nil, status.Errorf(codes.InvalidArgument, "missing metadata")
	}
	if !valid(md["authorization"]) {
    
    
		return nil, status.Errorf(codes.Unauthenticated, "invalid token")
	}
	m, err := handler(ctx, req)
	if err != nil {
    
    
		fmt.Printf("RPC failed with error %v\n", err)
	}
	return m, err
}

stream interceptor

Also define a streaming interceptor for streaming RPC that gets authentication information from metadata.

// streamInterceptor 服务端流拦截器
func streamInterceptor(srv interface{
    
    }, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
    
    
	// authentication (token verification)
	md, ok := metadata.FromIncomingContext(ss.Context())
	if !ok {
    
    
		return status.Errorf(codes.InvalidArgument, "missing metadata")
	}
	if !valid(md["authorization"]) {
    
    
		return status.Errorf(codes.Unauthenticated, "invalid token")
	}

	err := handler(srv, newWrappedStream(ss))
	if err != nil {
    
    
		fmt.Printf("RPC failed with error %v\n", err)
	}
	return err
}

register interceptor

Client registration interceptor

conn, err := grpc.Dial("127.0.0.1:8972",
	grpc.WithTransportCredentials(creds),
	grpc.WithUnaryInterceptor(unaryInterceptor),
	grpc.WithStreamInterceptor(streamInterceptor),
)

Server registration interceptor

s := grpc.NewServer(
	grpc.Creds(creds),
	grpc.UnaryInterceptor(unaryInterceptor),
	grpc.StreamInterceptor(streamInterceptor),
)

go-grpc-middleware

There are many open source and common grpc middleware in the community - go-grpc-middleware , you can choose to use it according to your needs.

References:https://www.liwenzhou.com/posts/Go/golang-menu/

Guess you like

Origin blog.csdn.net/m0_63230155/article/details/132107368