grpc practice: whether the cross language rpc framework is easy to use or not, just try it

Posted by Iasonic on Fri, 31 Dec 2021 23:59:51 +0100

Original link: https://mp.weixin.qq.com/s/1X...

gRPC is a great technology with strict interface constraints and high performance. It is applied in k8s and many microservice frameworks.

As a programmer, learning is right.

I've written some GRC services in Python before. Now I'm going to use Go to experience the original GRC program development.

The feature of this paper is to speak directly with code, and introduce various methods of gRPC through the complete code out of the box.

The code has been uploaded to GitHub. Let's officially start.

introduce

gRPC is a cross language open source RPC framework developed by Google based on Protobuf. gRPC is designed based on HTTP/2 protocol. It can provide multiple services based on one HTTP/2 link, which is more friendly to mobile devices.

introduction

First, let's look at the simplest gRPC service. The first step is to define the proto file, because gRPC is also a C/S architecture. This step is equivalent to clarifying the interface specification.

proto

syntax = "proto3";

package proto;

// The greeting service definition.
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
    string name = 1;
}

// The response message containing the greetings
message HelloReply {
    string message = 1;
}

Generate gRPC code using the gRPC plug-in built in protocol Gen go:

protoc --go_out=plugins=grpc:. helloworld.proto

After this command is executed, a HelloWorld. XML file will be generated in the current directory pb. Go file, which defines the interfaces of server and client respectively:

// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type GreeterClient interface {
    // Sends a greeting
    SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}

// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
    // Sends a greeting
    SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

The next step is to write the code of the server and the client to implement the corresponding interfaces respectively.

server

package main

import (
    "context"
    "fmt"
    "grpc-server/proto"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
)

type greeter struct {
}

func (*greeter) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
    fmt.Println(req)
    reply := &proto.HelloReply{Message: "hello"}
    return reply, nil
}

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    server := grpc.NewServer()
    //Register the reflection service required by grpcurl
    reflection.Register(server)
    //Register business services
    proto.RegisterGreeterServer(server, &greeter{})

    fmt.Println("grpc server start ...")
    if err := server.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

client

package main

import (
    "context"
    "fmt"
    "grpc-client/proto"
    "log"

    "google.golang.org/grpc"
)

func main() {
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    client := proto.NewGreeterClient(conn)
    reply, err := client.SayHello(context.Background(), &proto.HelloRequest{Name: "zhangsan"})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(reply.Message)
}

This completes the development of the most basic gRPC service. Next, we will enrich this "basic template" and learn more features.

Flow mode

Next, let's look at the way of streaming. As the name suggests, data can be sent and received continuously.

Flow words are divided into one-way flow and two-way flow. Here we directly take two-way flow as an example.

proto

service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
    // Sends stream message
    rpc SayHelloStream (stream HelloRequest) returns (stream HelloReply) {}
}

Add a stream function "SayHelloStream" to specify the stream characteristics through the "stream" keyword.

HelloWorld. Needs to be regenerated pb. Go file, no more here.

server

func (*greeter) SayHelloStream(stream proto.Greeter_SayHelloStreamServer) error {
    for {
        args, err := stream.Recv()
        if err != nil {
            if err == io.EOF {
                return nil
            }
            return err
        }

        fmt.Println("Recv: " + args.Name)
        reply := &proto.HelloReply{Message: "hi " + args.Name}

        err = stream.Send(reply)
        if err != nil {
            return err
        }
    }
}

Add the SayHelloStream function on the basic template. Nothing else needs to be changed.

client

client := proto.NewGreeterClient(conn)

//Stream processing
stream, err := client.SayHelloStream(context.Background())
if err != nil {
    log.Fatal(err)
}

//Send message
go func() {
    for {
        if err := stream.Send(&proto.HelloRequest{Name: "zhangsan"}); err != nil {
            log.Fatal(err)
        }
        time.Sleep(time.Second)
    }
}()

//Receive message
for {
    reply, err := stream.Recv()
    if err != nil {
        if err == io.EOF {
            break
        }
        log.Fatal(err)
    }
    fmt.Println(reply.Message)
}

The message is sent through a goroutine, and the for loop of the main program receives the message.

Executing the program will find that both the server and the client continue to have printouts.

Validator

Next is the verifier, which is a natural requirement. Because it involves requests between interfaces, it is necessary to verify the parameters properly.

Here we use protocol Gen govalidators and go grpc middleware to implement.

Install first:

go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators

go get github.com/grpc-ecosystem/go-grpc-middleware

Next, modify the proto file:

proto

import "github.com/mwitkow/go-proto-validators@v0.3.2/validator.proto";

message HelloRequest {
    string name = 1 [
        (validator.field) = {regex: "^[z]{2,5}$"}
    ];
}

Here, verify the {name} parameter. Normal requests can only be made if they meet the requirements of regularity.

There are other verification rules, such as verifying the size of numbers, which will not be introduced here.

Next, generate * pb.go file:

protoc  \
    --proto_path=${GOPATH}/pkg/mod \
    --proto_path=${GOPATH}/pkg/mod/github.com/gogo/protobuf@v1.3.2 \
    --proto_path=. \
    --govalidators_out=. --go_out=plugins=grpc:.\
    *.proto

After successful execution, there will be one more HelloWorld validator. pb. Go file.

You should pay special attention here. You can't use the previous simple commands. You need to use multiple} PROTOS_ The path} parameter specifies the directory where the proto file is imported.

The official gives two kinds of dependence, one is google protobuf and the other is gogo protobuf. I use the second one here.

Even if you use the above command, you may encounter this error:

Import "github.com/mwitkow/go-proto-validators/validator.proto" was not found or had errors

But don't panic. The probability is the reference path. You must be optimistic about your installation version and the specific path in # GOPATH #.

Finally, the server code transformation:

Import package:

grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator"

Then add the verifier function during initialization:

server := grpc.NewServer(
    grpc.UnaryInterceptor(
        grpc_middleware.ChainUnaryServer(
            grpc_validator.UnaryServerInterceptor(),
        ),
    ),
    grpc.StreamInterceptor(
        grpc_middleware.ChainStreamServer(
            grpc_validator.StreamServerInterceptor(),
        ),
    ),
)

After starting the program, we use the previous client code to request, and we will receive an error:

2021/10/11 18:32:59 rpc error: code = InvalidArgument desc = invalid field Name: value 'zhangsan' must be a string conforming to regex "^[z]{2,5}$"
exit status 1

Because {name: zhangsan} does not meet the regular requirements of the server, but if you pass the parameter {name: zzz}, you can return normally.

Token authentication

Finally, it's time for authentication. Let's first look at the Token authentication method, and then introduce the certificate authentication.

First transform the server. With the experience of the verifier above, you can write an interceptor in the same way, and then inject it when initializing the server.

Authentication function:

func Auth(ctx context.Context) error {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return fmt.Errorf("missing credentials")
    }

    var user string
    var password string

    if val, ok := md["user"]; ok {
        user = val[0]
    }
    if val, ok := md["password"]; ok {
        password = val[0]
    }

    if user != "admin" || password != "admin" {
        return grpc.Errorf(codes.Unauthenticated, "invalid token")
    }

    return nil
}

metadata.FromIncomingContext: read the user name and password from the context, and then compare them with the actual data to determine whether they have passed the authentication.

Interceptor:

var authInterceptor grpc.UnaryServerInterceptor
authInterceptor = func(
    ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (resp interface{}, err error) {
    //Intercept the normal method request and verify the {Token
    err = Auth(ctx)
    if err != nil {
        return
    }
    //Continue processing request
    return handler(ctx, req)
}

initialization:

server := grpc.NewServer(
    grpc.UnaryInterceptor(
        grpc_middleware.ChainUnaryServer(
            authInterceptor,
            grpc_validator.UnaryServerInterceptor(),
        ),
    ),
    grpc.StreamInterceptor(
        grpc_middleware.ChainStreamServer(
            grpc_validator.StreamServerInterceptor(),
        ),
    ),
)

In addition to the above verifier, there is also a Token authentication interceptor called authInterceptor.

The last is the transformation of the client. The client needs to implement the {PerRPCCredentials} interface.

type PerRPCCredentials interface {
    // GetRequestMetadata gets the current request metadata, refreshing
    // tokens if required. This should be called by the transport layer on
    // each request, and the data should be populated in headers or other
    // context. If a status code is returned, it will be used as the status
    // for the RPC. uri is the URI of the entry point for the request.
    // When supported by the underlying implementation, ctx can be used for
    // timeout and cancellation.
    // TODO(zhaoq): Define the set of the qualified keys instead of leaving
    // it as an arbitrary string.
    GetRequestMetadata(ctx context.Context, uri ...string) (
        map[string]string,    error,
    )
    // RequireTransportSecurity indicates whether the credentials requires
    // transport security.
    RequireTransportSecurity() bool
}

The GetRequestMetadata method returns the necessary information required for authentication. The RequireTransportSecurity method indicates whether to enable the security link. It is generally enabled in the production environment, but it is not enabled here for testing convenience.

Implementation interface:

type Authentication struct {
    User     string
    Password string
}

func (a *Authentication) GetRequestMetadata(context.Context, ...string) (
    map[string]string, error,
) {
    return map[string]string{"user": a.User, "password": a.Password}, nil
}

func (a *Authentication) RequireTransportSecurity() bool {
    return false
}

connect:

conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithPerRPCCredentials(&auth))

Well, now our service has the function of Token authentication. If the user name or password is incorrect, the client will receive:

2021/10/11 20:39:35 rpc error: code = Unauthenticated desc = invalid token
exit status 1

If the user name and password are correct, you can return normally.

One way certificate authentication

There are two ways of certificate authentication:

  1. One way authentication
  2. Two way authentication

Let's first look at the one-way authentication method:

Generate certificate

First, a self signed SSL certificate is generated through the openssl tool.

1. Generate private key:

openssl genrsa -des3 -out server.pass.key 2048

2. Remove password from private key:

openssl rsa -in server.pass.key -out server.key

3. Generate csr file:

openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=beijing/L=beijing/O=grpcdev/OU=grpcdev/CN=example.grpcdev.cn"

4. Generate certificate:

openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt

One more word, let's introduce the three files contained in the X.509 certificate: key, csr and crt.

  • Key: the private key file on the server, which is used to encrypt the data sent to the client and decrypt the data received from the client.
  • csr: Certificate Signing Request file, which is used to submit to the certification authority (CA) to sign the certificate.
  • crt: a certificate signed by a certification authority (CA), or a developer's self signed certificate, including the information of the certificate holder, the holder's public key, and the signer's signature.

gRPC code

After having the certificate, the rest is to transform the program. The first is the server code.

//Certificate authentication - one way authentication
creds, err := credentials.NewServerTLSFromFile("keys/server.crt", "keys/server.key")
if err != nil {
    log.Fatal(err)
    return
}

server := grpc.NewServer(grpc.Creds(creds))

Only a few lines of code need to be modified. It's very simple. Next is the client.

Because it is one-way authentication, there is no need to generate a certificate for the client separately, just copy the crt file of the server to the corresponding directory of the client.

//Certificate authentication - one way authentication
creds, err := credentials.NewClientTLSFromFile("keys/server.crt", "example.grpcdev.cn")
if err != nil {
    log.Fatal(err)
    return
}
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))

Well, now our service supports one-way certificate authentication.

But it's not over yet. There may be a problem here:

2021/10/11 21:32:37 rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0"
exit status 1

The reason is that Go 1.15 began to abandon CommonName, and SAN certificate is recommended. If you want to be compatible with the previous method, you can support it by setting environment variables, as follows:

export GODEBUG="x509ignoreCN=0"

However, it should be noted that since Go 1.17, environment variables are no longer effective and must be implemented through SAN. Therefore, in order to upgrade the follow-up Go version, it is better to support it as soon as possible.

Two way certificate authentication

Finally, let's look at two-way certificate authentication.

Generate certificate with SAN

It's still a certificate, but this time it's a little different. We need to generate a certificate with SAN extension.

What is SAN?

SAN (Subject Alternative Name) is an extension defined in the SSL standard x509. The SSL certificate using the SAN field can extend the domain name supported by this certificate, so that a certificate can support the resolution of multiple different domain names.

Copy the default OpenSSL configuration file to the current directory.

Linux system:

/etc/pki/tls/openssl.cnf

Mac system on:

/System/Library/OpenSSL/openssl.cnf

Modify the temporary configuration file, find the [req] paragraph, and then remove the comments of the following statements.

req_extensions = v3_req # The extensions to add to a certificate request

Then add the following configuration:

[ v3_req ]
# Extensions to add to a certificate request

basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = www.example.grpcdev.cn

Multiple domain names can be configured at the [alt_names] location, such as:

[ alt_names ]
DNS.1 = www.example.grpcdev.cn
DNS.2 = www.test.grpcdev.cn

For the convenience of testing, only one domain name is configured here.

1. Generate ca certificate:

openssl genrsa -out ca.key 2048

openssl req -x509 -new -nodes -key ca.key -subj "/CN=example.grpcdev.com" -days 5000 -out ca.pem

2. Generate server certificate:

#Generate certificate
openssl req -new -nodes \
    -subj "/C=CN/ST=Beijing/L=Beijing/O=grpcdev/OU=grpcdev/CN=www.example.grpcdev.cn" \
    -config <(cat openssl.cnf \
        <(printf "[SAN]\nsubjectAltName=DNS:www.example.grpcdev.cn")) \
    -keyout server.key \
    -out server.csr

#Signing certificate
openssl x509 -req -days 365000 \
    -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
    -extfile <(printf "subjectAltName=DNS:www.example.grpcdev.cn") \
    -out server.pem

3. Generate client certificate:

#Generate certificate
openssl req -new -nodes \
    -subj "/C=CN/ST=Beijing/L=Beijing/O=grpcdev/OU=grpcdev/CN=www.example.grpcdev.cn" \
    -config <(cat openssl.cnf \
        <(printf "[SAN]\nsubjectAltName=DNS:www.example.grpcdev.cn")) \
    -keyout client.key \
    -out client.csr

#Signing certificate
openssl x509 -req -days 365000 \
    -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
    -extfile <(printf "subjectAltName=DNS:www.example.grpcdev.cn") \
    -out client.pem

gRPC code

Next, start to modify the code. First look at the server:

//Certificate authentication - two way authentication
//Read and parse the information from the certificate related files to obtain the certificate public key and key pair
cert, _ := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")
//Create a new, empty CertPool
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
//An attempt was made to resolve the incoming PEM encoded certificate. If the parsing is successful, it will be added to CertPool for later use
certPool.AppendCertsFromPEM(ca)
//Build the # TransportCredentials # option based on # TLS #
creds := credentials.NewTLS(&tls.Config{
    //Set the certificate chain to allow one or more certificates to be included
    Certificates: []tls.Certificate{cert},
    //The client's certificate must be verified. The following parameters can be selected according to the actual situation
    ClientAuth: tls.RequireAndVerifyClientCert,
    //Set the collection of root certificates. The verification method uses the mode set in ClientAuth
    ClientCAs: certPool,
})

Look at the client:

//Certificate authentication - two way authentication
//Read and parse the information from the certificate related files to obtain the certificate public key and key pair
cert, _ := tls.LoadX509KeyPair("cert/client.pem", "cert/client.key")
//Create a new, empty CertPool
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
//An attempt was made to resolve the incoming PEM encoded certificate. If the parsing is successful, it will be added to CertPool for later use
certPool.AppendCertsFromPEM(ca)
//Build the # TransportCredentials # option based on # TLS #
creds := credentials.NewTLS(&tls.Config{
    //Set the certificate chain to allow one or more certificates to be included
    Certificates: []tls.Certificate{cert},
    //The client's certificate must be verified. The following parameters can be selected according to the actual situation
    ServerName: "www.example.grpcdev.cn",
    RootCAs:    certPool,
})

be accomplished.

Python client

As mentioned earlier, gRPC is cross language. At the end of this article, we write a client in Python to request the Go server.

Use the simplest way to achieve:

The proto file uses the proto file of the initial "basic template":

syntax = "proto3";

package proto;

// The greeting service definition.
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
    // Sends stream message
    rpc SayHelloStream (stream HelloRequest) returns (stream HelloReply) {}
}

// The request message containing the user's name.
 message HelloRequest {
    string name = 1;
}

// The response message containing the greetings
message HelloReply {
    string message = 1;
}

Similarly, you also need to generate Pb from the command line Py file:

python3 -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. ./*.proto

After successful execution, HelloWorld will be generated in the directory_ pb2. Py and helloworld_pb2_grpc.py two files.

This process may also report errors:

ModuleNotFoundError: No module named 'grpc_tools'

Don't panic. The package is missing. Just install it:

pip3 install grpcio
pip3 install grpcio-tools

Finally, take a look at the Python client code:

import grpc

import helloworld_pb2
import helloworld_pb2_grpc

def main():
    channel = grpc.insecure_channel("127.0.0.1:50051")
    stub = helloworld_pb2_grpc.GreeterStub(channel)
    response = stub.SayHello(helloworld_pb2.HelloRequest(name="zhangsan"))
    print(response.message)

if __name__ == '__main__':
    main()

In this way, you can request the server-side service started by Go through the Python client.

summary

This paper explains some applications of gRPC from the perspective of actual combat and directly speaking with code.

The content includes simple gRPC service, stream processing mode, verifier, Token authentication and certificate authentication.

In addition, there are other contents worth studying, such as timeout control, REST interface and load balancing. I will take time to continue to improve the REST of this part in the future.

The code in this article has been tested and verified, can be executed directly, and has been uploaded to GitHub. Partners can look at the source code and learn by comparing the content of the article again and again.

Source address:


WeChat public number [programmer Huang Xiaoxie] is the former engineer of ant Java, who focuses on sharing Java technology dry cargo and job search experience. It is not limited to BAT interview, algorithm, computer basis, database, distributed official account, spring family bucket, micro service, high concurrency, JVM, Docker container, ELK, big data, etc. [book] get 20 selected high-quality e-books necessary for Java interview.

Topics: Java