gRPC is a great technology with tight interface constraints and high performance, and is used in k8s and many microservices frameworks.
as a programmer, you learned it right.
Having written some gRPC services in Python before, I’m ready to use Go to get a feel for the original gRPC program development.
This article features speaking directly in code, with out-of-the-box complete code to introduce various ways to use gRPC.
The code has been uploaded to GitHub, and the following is officially started.
introduce
gRPC is a cross-language, open source RPC framework developed by Google Based on Protobuf. Designed based on the HTTP/2 protocol, gRPC can provide multiple services based on a single HTTP/2 link, making it more mobile-friendly.
entry
The first step to a simple gRPC service is to define the proto file, because gRPC is also a C/S architecture, which is equivalent to clarifying the interface specification.
therefore
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;
}
copy the code
Generate gRPC code using the gRPC plugin built into protoc-gen-go:
protoc --go_out=plugins=grpc:. helloworld.proto
copy the code
after executing this command, a helloworld.pb.go file is generated in the current directory, which defines the interfaces of the server and the 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)
}
copy the code
the next step is to write the code of the server and the client, and 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()
// 注册 grpcurl 所需的 reflection 服务
reflection.Register(server)
// 注册业务服务
proto.RegisterGreeterServer(server, &greeter{})
fmt.Println("grpc server start ...")
if err := server.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
copy the code
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)
}
copy the code
This completes the development of the most basic gRPC service, and then we continue to enrich this “basic template” and learn more features.
stream mode
next, look at how streams are flowing, and as the name suggests, data can be sent and received in a steady stream.
the flow is divided into one-way flow and two-way flow, here we directly through the two-way flow to give an example.
therefore
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
// Sends stream message
rpc SayHelloStream (stream HelloRequest) returns (stream HelloReply) {}
}
copy the code
add a stream function to specify the stream attributes by keyword.SayHelloStream
stream
the helloworld.pb.go file needs to be regenerated, and i won’t go into more detail 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
}
}
}
copy the code
add functions to the “base template”, and nothing else needs to be changed.SayHelloStream
client
client := proto.NewGreeterClient(conn)
// 流处理
stream, err := client.SayHelloStream(context.Background())
if err != nil {
log.Fatal(err)
}
// 发送消息
go func() {
for {
if err := stream.Send(&proto.HelloRequest{Name: "zhangsan"}); err != nil {
log.Fatal(err)
}
time.Sleep(time.Second)
}
}()
// 接收消息
for {
reply, err := stream.Recv()
if err != nil {
if err == io.EOF {
break
}
log.Fatal(err)
}
fmt.Println(reply.Message)
}
copy the code
sending a message through a goroutine, the loop of the main program receives the message.for
the executor will find that both the server and the client have printouts.
validator
next is the validator, which is a natural requirement that comes to mind, because when it comes to requests between interfaces, it is necessary to perform proper validation of the parameters.
here we use protoc-gen-govalidators and go-grpc-middleware to implement this.
install first:
go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators
go get github.com/grpc-ecosystem/go-grpc-middleware
copy the code
next modify the proto file:
therefore
import "github.com/mwitkow/go-proto-validators@v0.3.2/validator.proto";
message HelloRequest {
string name = 1 [
(validator.field) = {regex: "^[z]{2,5}$"}
];
}
copy the code
the parameters are checked here, and the requirements of the regular rule need to be met before they can be requested normally.name
there are other validation rules, such as verifying the size of the number, which are not covered here.
next, generate the *.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
copy the code
after successful execution, there will be one more helloworld.validator.pb.go file in the directory.
it is important to note here that it is not possible to use the previous simple command, and you need to use multiple parameters to specify the directory where to import the proto file.proto_path
the official gives two dependency cases, one is google protobuf and the other is gogo protobuf. i’m using the second one here.
even with the above command, you may encounter this error:
Import "github.com/mwitkow/go-proto-validators/validator.proto" was not found or had errors
copy the code
but don’t panic, the probability is the problem of referencing the path, be sure to look at your own installation version, as well as the specific path in.GOPATH
finally, there is the server-side code transformation:
ingest package:
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator"
copy the code
then add the authenticator function during initialization:
server := grpc.NewServer(
grpc.UnaryInterceptor(
grpc_middleware.ChainUnaryServer(
grpc_validator.UnaryServerInterceptor(),
),
),
grpc.StreamInterceptor(
grpc_middleware.ChainStreamServer(
grpc_validator.StreamServerInterceptor(),
),
),
)
copy the code
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
copy the code
because it does not meet the requirements of the service-side regularity, but if the parameter is passed, it can be returned normally.name: zhangsan
name: zzz
Token authentication
Finally to the authentication link, first look at the Token authentication method, and then introduce the certificate certification.
first transform the server, with the experience of the above validators, then you can use the same way, write an interceptor, 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
}
copy the code
metadata.FromIncomingContext
the user name and password are read from the context, and then compared with the actual data to determine whether it is authenticated.
interceptors:
var authInterceptor grpc.UnaryServerInterceptor
authInterceptor = func(
ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (resp interface{}, err error) {
//拦截普通方法请求,验证 Token
err = Auth(ctx)
if err != nil {
return
}
// 继续处理请求
return handler(ctx, req)
}
copy the code
initialize:
server := grpc.NewServer(
grpc.UnaryInterceptor(
grpc_middleware.ChainUnaryServer(
authInterceptor,
grpc_validator.UnaryServerInterceptor(),
),
),
grpc.StreamInterceptor(
grpc_middleware.ChainStreamServer(
grpc_validator.StreamServerInterceptor(),
),
),
)
copy the code
In addition to the validators above, there are more Token authentication interceptors.authInterceptor
finally, there is the client transformation, which needs to implement the interface.PerRPCCredentials
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
}
copy the code
GetRequestMetadata
the method returns the necessary information required for authentication, the method indicates whether to enable safe link, which is generally enabled in a production environment, but for the convenience of testing, it is not enabled here for the time being.RequireTransportSecurity
implement the 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
}
copy the code
connect:
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure(), grpc.WithPerRPCCredentials(&auth))
copy the code
Okay, now our service has token authentication. If the username or password is incorrect, the client receives:
2021/10/11 20:39:35 rpc error: code = Unauthenticated desc = invalid token
exit status 1
copy the code
if the user name and password are correct, you can return normally.
one-way certificate authentication
there are two ways to authenticate a certificate:
- one-way authentication
- two-way authentication
first, let’s take a look at the one-way authentication method:
generate a certificate
Start by generating a self-signed SSL certificate through the openssl tool.
1. generate a private key:
openssl genrsa -des3 -out server.pass.key 2048
copy the code
2. remove the password from the private key:
openssl rsa -in server.pass.key -out server.key
copy the code
3. generate a 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"
copy the code
4. generate certificate:
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
copy the code
To say a little more, let’s look at the three files that an X.509 certificate contains: key, csr, and crt.
- key: a private key file on the server that is used to encrypt data sent to the client and decrypt data received from the client.
- csr: A certificate signing request file that is submitted to a certificate authority (CA) for signing the certificate.
- crt: A certificate signed by a certificate authority (CA), or a developer-signed certificate that contains information about the certificate holder, the holder’s public key, and the signer’s signature.
gRPC code
once the certificate is available, all that’s left is to transform the program, starting with the server-side code.
// 证书认证-单向认证
creds, err := credentials.NewServerTLSFromFile("keys/server.crt", "keys/server.key")
if err != nil {
log.Fatal(err)
return
}
server := grpc.NewServer(grpc.Creds(creds))
copy the code
there are only a few lines of code to modify, and it’s simple, followed by the client.
since it is a one-way authentication, you do not need to generate a separate certificate for the client, you only need to copy the crt file on the server side to the corresponding directory of the client.
// 证书认证-单向认证
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))
copy the code
well, now our service supports one-way certificate authentication.
but before it’s over, 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
copy the code
The reason is that Go 1.15 began to deprecate CommonName, and SAN certificates are recommended. If you want to be compatible with the previous way, you can support it by setting the environment variables, as follows:
export GODEBUG="x509ignoreCN=0"
copy the code
Note, however, that as of Go 1.17, environment variables no longer take effect and must be san-styled. Therefore, in order to upgrade the subsequent 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 A CERTIFICATE WITH A SAN
IT’S STILL A CERTIFICATE, BUT THIS TIME IT’S A LITTLE DIFFERENT, WE NEED TO GENERATE A CERTIFICATE WITH SAN EXTENSIONS.
WHAT IS A SAN?
A SAN (Subject Alternative Name) is an extension defined in the SSL standard x509. An SSL certificate that uses a SAN field extends the domain names supported by this certificate so that a single certificate can support the resolution of multiple different domain names.
Copies the default OpenSSL configuration file to the current directory.
Linux systems in:
/etc/pki/tls/openssl.cnf
copy the code
Mac systems in:
/System/Library/OpenSSL/openssl.cnf
copy the code
modify the temporary configuration file, locate the paragraph, and then remove the comments for the following statement.[ req ]
req_extensions = v3_req # The extensions to add to a certificate request
copy the code
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
copy the code
[ alt_names ]
the location can be configured with multiple domain names, such as:
[ alt_names ]
DNS.1 = www.example.grpcdev.cn
DNS.2 = www.test.grpcdev.cn
copy the code
for the convenience of testing, only one domain name is configured here.
1. generate a 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-side 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
# 签名证书
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
copy the code
3. generate a client 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
# 签名证书
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
copy the code
gRPC code
next, start modifying the code, first looking at the server:
// 证书认证-双向认证
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert, _ := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")
// 创建一个新的、空的 CertPool
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
// 尝试解析所传入的 PEM 编码的证书。如果解析成功会将其加到 CertPool 中,便于后面的使用
certPool.AppendCertsFromPEM(ca)
// 构建基于 TLS 的 TransportCredentials 选项
creds := credentials.NewTLS(&tls.Config{
// 设置证书链,允许包含一个或多个
Certificates: []tls.Certificate{cert},
// 要求必须校验客户端的证书。可以根据实际情况选用以下参数
ClientAuth: tls.RequireAndVerifyClientCert,
// 设置根证书的集合,校验方式使用 ClientAuth 中设定的模式
ClientCAs: certPool,
})
copy the code
look at the client:
// 证书认证-双向认证
// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert, _ := tls.LoadX509KeyPair("cert/client.pem", "cert/client.key")
// 创建一个新的、空的 CertPool
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile("cert/ca.pem")
// 尝试解析所传入的 PEM 编码的证书。如果解析成功会将其加到 CertPool 中,便于后面的使用
certPool.AppendCertsFromPEM(ca)
// 构建基于 TLS 的 TransportCredentials 选项
creds := credentials.NewTLS(&tls.Config{
// 设置证书链,允许包含一个或多个
Certificates: []tls.Certificate{cert},
// 要求必须校验客户端的证书。可以根据实际情况选用以下参数
ServerName: "www.example.grpcdev.cn",
RootCAs: certPool,
})
copy the code
home and dry.
Python client
As mentioned earlier, gRPC is cross-language, so at the end of this article we will 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 original “base 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;
}
copy the code
similarly, the pb.py file needs to be generated from the command line:
python3 -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. ./*.proto
copy the code
after successful execution, two files, helloworld_pb2.py and helloworld_pb2_grpc.py, are generated in the directory.
this process may also report errors:
ModuleNotFoundError: No module named 'grpc_tools'
copy the code
don’t panic, it is missing packages, install it:
pip3 install grpcio
pip3 install grpcio-tools
copy the code
最后看一下 Python 客户端代码:
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 Go-enabled server-side services through the Python client.
summary
This article uses the actual combat perspective and directly speaks in code to illustrate some applications of gRPC.
Content includes simple gRPC services, stream processing patterns, validators, token authentication, and certificate authentication.
IN ADDITION TO THIS, THERE ARE OTHER THINGS WORTH INVESTIGATING, SUCH AS TIMEOUT CONTROL, REST INTERFACES, AND LOAD BALANCING. IN THE FUTURE, WE WILL FIND TIME TO CONTINUE TO IMPROVE THE REST OF THE CONTENT.
The code in this article has been tested and verified, can be executed directly, and has been uploaded to GitHub, and small partners can read the source code again and against the content of the article to learn.